From 5fdae490a40c6d594d89bd532f1296a8b4b0ec54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ignaszewski?= Date: Tue, 30 May 2017 17:34:25 +0200 Subject: [PATCH 01/23] ngrx/store + actions and effects --- demo/backend/index.js | 95 +++-- demo/src/app/app.component.html | 2 +- demo/src/app/app.component.ts | 4 - demo/src/app/app.module.ts | 2 +- package.json | 6 +- src/configuration/IUrlConfiguration.ts | 1 + .../fileManagerConfiguration.service.ts | 15 +- src/configuration/tree.service.ts | 9 +- src/crop/crop.component.ts | 6 +- src/decorators/logFunction.decorator.ts | 2 +- src/dropdown/dropdown.component.ts | 34 +- src/dropdown/dropdown.html | 19 + src/filemanager.component.ts | 362 +++++++----------- src/filemanager.html | 23 +- src/filemanager.module.ts | 38 +- src/filesList/file.model.ts | 18 +- src/filesList/fileManagerUploader.service.ts | 6 +- src/filesList/files.service.ts | 2 +- src/filesList/filesList.component.ts | 57 ++- src/filesList/interface/IFileModel.ts | 1 + src/main.less | 10 + src/preview/preview.component.ts | 47 ++- src/preview/preview.html | 16 + src/services/fileTypeFilter.service.ts | 21 + src/services/searchFilter.service.ts | 19 + src/store/fileManagerActions.service.ts | 102 +++++ src/store/fileManagerDispatcher.service.ts | 34 ++ src/store/fileManagerEffects.service.ts | 85 ++++ src/store/fileManagerReducer.ts | 51 +++ .../fileTypeFilter.component.html | 6 + .../fileTypeFilter.component.ts | 40 ++ .../searchFile/searchFile.component.html | 8 + .../searchFile/searchFile.component.ts | 22 ++ src/toolbar/toolbar.component.ts | 56 +-- src/toolbar/toolbar.html | 20 +- tslint.json | 4 +- 36 files changed, 778 insertions(+), 465 deletions(-) create mode 100644 src/dropdown/dropdown.html create mode 100644 src/preview/preview.html create mode 100644 src/services/fileTypeFilter.service.ts create mode 100644 src/services/searchFilter.service.ts create mode 100644 src/store/fileManagerActions.service.ts create mode 100644 src/store/fileManagerDispatcher.service.ts create mode 100644 src/store/fileManagerEffects.service.ts create mode 100644 src/store/fileManagerReducer.ts create mode 100644 src/toolbar/fileTypeFilter/fileTypeFilter.component.html create mode 100644 src/toolbar/fileTypeFilter/fileTypeFilter.component.ts create mode 100644 src/toolbar/searchFile/searchFile.component.html create mode 100644 src/toolbar/searchFile/searchFile.component.ts diff --git a/demo/backend/index.js b/demo/backend/index.js index 6f6bf53..0c764c6 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'}); } - }); @@ -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..84e174b 100644 --- a/demo/src/app/app.component.html +++ b/demo/src/app/app.component.html @@ -6,5 +6,5 @@

Filemanager

Use multiselection - Loading... + Loading... diff --git a/demo/src/app/app.component.ts b/demo/src/app/app.component.ts index 0f84a3a..1323479 100644 --- a/demo/src/app/app.component.ts +++ b/demo/src/app/app.component.ts @@ -14,8 +14,4 @@ export class AppComponent { public toggleMultiSelection() { this.isMultiSelection = !this.isMultiSelection; } - - public selectFile(data: ISelectFile) { - console.log(data); - } } diff --git a/demo/src/app/app.module.ts b/demo/src/app/app.module.ts index e4305a5..47261f6 100644 --- a/demo/src/app/app.module.ts +++ b/demo/src/app/app.module.ts @@ -18,7 +18,7 @@ import {FileManagerModule} from "../../../src/filemanager.module"; HttpModule ], providers: [ - {provide: 'fileManagerUrls', useValue: {foldersUrl: '/api/folder', filesUrl: '/api/files'}} + {provide: 'fileManagerUrls', useValue: {foldersUrl: '/api/folder', filesUrl: '/api/files', folderMoveUrl: '/api/folder/move'}} ], bootstrap: [AppComponent] }) diff --git a/package.json b/package.json index f1366cf..3bc8917 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,10 @@ "@angular/platform-browser": "^2.4.0", "@angular/platform-browser-dynamic": "^2.4.0", "@angular/router": "^3.4.0", - "@rign/angular2-tree": "^0.8.1", + "@rign/angular2-tree": "git://github.com/qjon/angular2-tree.git#1729f93dabc3b7797f51e1130f87a0de985e0943", + "@ngrx/core": "^1.2.0", + "@ngrx/effects": "^2.0.3", + "@ngrx/store": "^2.2.2", "angular2-bootstrap-confirm": "^1.0.4", "angular2-notifications": "^0.4.53", "bootstrap": "^3.3.7", @@ -42,6 +45,7 @@ "devDependencies": { "@angular/cli": "1.0.0-beta.32.3", "@angular/compiler-cli": "^2.4.0", + "@ngrx/store-devtools": "^3.2.4", "@types/jasmine": "2.5.38", "@types/node": "^6.0.42", "codelyzer": "~2.0.0-beta.4", diff --git a/src/configuration/IUrlConfiguration.ts b/src/configuration/IUrlConfiguration.ts index bd2f3e9..f2460ff 100644 --- a/src/configuration/IUrlConfiguration.ts +++ b/src/configuration/IUrlConfiguration.ts @@ -1,4 +1,5 @@ export class IUrlConfiguration { filesUrl: string; foldersUrl: string; + folderMoveUrl: string; } diff --git a/src/configuration/fileManagerConfiguration.service.ts b/src/configuration/fileManagerConfiguration.service.ts index 619877a..1a791d7 100644 --- a/src/configuration/fileManagerConfiguration.service.ts +++ b/src/configuration/fileManagerConfiguration.service.ts @@ -1,14 +1,14 @@ -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/main'; +import {Injectable, Inject} from '@angular/core'; +import {IFileTypeFilter} from '../toolbar/interface/IFileTypeFilter'; +import {ICropSize} from '../crop/ICropSize'; +import {IUrlConfiguration} from './IUrlConfiguration'; @Injectable() export class FileManagerConfiguration { public contextMenuItems: IContextMenu[] = []; - public isMultiSelection: boolean = false; + public isMultiSelection = false; public fileTypesFilter: IFileTypeFilter[] = [ { @@ -44,7 +44,7 @@ export class FileManagerConfiguration { } ]; - public fileUrl: string = '/api/files'; + public fileUrl = '/api/files'; public allowedCropSize: ICropSize[] = [ { @@ -61,7 +61,6 @@ export class FileManagerConfiguration { constructor(@Inject('fileManagerUrls') urls: IUrlConfiguration) { - this.fileUrl = urls.filesUrl; } } diff --git a/src/configuration/tree.service.ts b/src/configuration/tree.service.ts index 7c83498..9250333 100644 --- a/src/configuration/tree.service.ts +++ b/src/configuration/tree.service.ts @@ -1,7 +1,7 @@ -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/main'; +import {Http} from '@angular/http'; +import {IUrlConfiguration} from './IUrlConfiguration'; @Injectable() export class TreeService extends NodeService { @@ -13,6 +13,7 @@ export class TreeService extends NodeService { getUrl: urls.foldersUrl, updateUrl: urls.foldersUrl, removeUrl: urls.foldersUrl, + moveUrl: urls.folderMoveUrl }; } } diff --git a/src/crop/crop.component.ts b/src/crop/crop.component.ts index db44309..d24c898 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"; @@ -78,7 +78,7 @@ export class CropComponent { ngAfterContentInit() { this.updateCropSize(this.cropSizeList[0]); - } + }; public cropImage() { let bounds: ICropBounds = { @@ -98,8 +98,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..b9e42f6 100644 --- a/src/filemanager.component.ts +++ b/src/filemanager.component.ts @@ -1,23 +1,41 @@ -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, Input, OnChanges, EventEmitter, Output +} from '@angular/core'; +import { + TreeComponent, + NodeService, + IContextMenu, + IOuterNode, + ITreeData, + ITreeState, + IConfiguration, + TreeModel, + TreeActionsService, + NodeDispatcherService +} 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 {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 {TreeService} from './configuration/tree.service'; +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'; @Component({ - selector: 'filemanager', + selector: 'ri-filemanager', providers: [NodeService, FilesService, NotificationsService], styleUrls: ['./main.less'], templateUrl: './filemanager.html' @@ -29,34 +47,40 @@ export class FileManagerComponent implements OnInit, OnChanges { @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; @@ -79,145 +103,113 @@ 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: TreeService, + private filesService: FilesService, + private notifications: NotificationsService, + private configuration: FileManagerConfiguration, + private fileManagerDispatcher: FileManagerDispatcherService, + private fileTypeFilter: FileTypeFilterService, + private searchFilterService: SearchFilterService, + private fileManagerEffects: FileManagerEffectsService) { this.menu = configuration.contextMenuItems; } 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(); - } + ngOnChanges() { + this.configuration.isMultiSelection = this.multiSelection; } - @log - public onSelect(event: ITreeItemEvent) { - if (event.status) { - this.loadFiles(event.node.id); - this.currentFolderId = event.node.id; - } else { - this.loadFiles(''); - this.currentFolderId = null; - } + + get currentSelectedFolderId(): string | null { + const value = this.treeModel.currentSelectedNode$.getValue(); + + return value ? value.id : null; } + @log + 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; @@ -232,21 +224,7 @@ export class FileManagerComponent implements OnInit, OnChanges { @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'); - } - ); + this.fileManagerDispatcher.cropFile(event.file, event.bounds); } @log @@ -262,11 +240,11 @@ 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); @@ -283,18 +261,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 +280,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..6952b64 100644 --- a/src/filemanager.html +++ b/src/filemanager.html @@ -2,39 +2,26 @@
- - + + >
diff --git a/src/filemanager.module.ts b/src/filemanager.module.ts index f43812b..279d21e 100644 --- a/src/filemanager.module.ts +++ b/src/filemanager.module.ts @@ -2,45 +2,69 @@ 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 {TreeModule, treeReducer, TreeEffectsService} from "@rign/angular2-tree/main"; +import {NotificationsService, 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 {FilesListComponent} 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 {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 {FilesService} from './filesList/files.service'; +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'; @NgModule({ imports: [ BrowserModule, ConfirmModule, + EffectsModule.run(FileManagerEffectsService), FormsModule, FileUploadModule, HttpModule, ReactiveFormsModule, SimpleNotificationsModule, + StoreModule.provideStore({files: fileManagerReducer, trees: treeReducer}), + StoreDevtoolsModule.instrumentOnlyWithExtension({}), TreeModule ], declarations: [ FileManagerComponent, + FileTypeFilterComponent, Toolbar, - FilesList, - Dropdown, + FilesListComponent, + DropdownComponent, PreviewComponent, CropComponent, - ImageCropperComponent + ImageCropperComponent, + SearchFileComponent ], entryComponents: [ImageCropperComponent], providers: [ + FileManagerActionsService, FileManagerConfiguration, + FileManagerDispatcherService, + FileManagerEffectsService, FileManagerUploader, + FilesService, + FileTypeFilterService, + NotificationsService, + SearchFilterService, TreeService ], exports: [FileManagerComponent], diff --git a/src/filesList/file.model.ts b/src/filesList/file.model.ts index b89ad05..74da4f5 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; @@ -39,6 +39,10 @@ export class FileModel implements IFileModel { this.name = data.name; } + public toJSON() { + return this._orgData; + } + public getId() { return this._orgData.id; } @@ -71,6 +75,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..55f41f4 100644 --- a/src/filesList/fileManagerUploader.service.ts +++ b/src/filesList/fileManagerUploader.service.ts @@ -1,6 +1,6 @@ -import {Injectable, Inject} from "@angular/core"; -import {FileUploader} from "ng2-file-upload"; -import {IUrlConfiguration} from "../configuration/IUrlConfiguration"; +import {Injectable, Inject} from '@angular/core'; +import {FileUploader} from 'ng2-file-upload'; +import {IUrlConfiguration} from '../configuration/IUrlConfiguration'; @Injectable() export class FileManagerUploader { diff --git a/src/filesList/files.service.ts b/src/filesList/files.service.ts index 036d21c..7faa8fc 100644 --- a/src/filesList/files.service.ts +++ b/src/filesList/files.service.ts @@ -14,7 +14,7 @@ export class FilesService { this.url = configuration.fileUrl; } - public crop(file: IFileModel, bounds: ICropBounds): Observable { + public crop(file: IFileModel, bounds: ICropBounds): Observable { return this.http.put(this.url, {id: file.getId(), bounds: bounds}) .map((res: Response) => { let body = res.json(); diff --git a/src/filesList/filesList.component.ts b/src/filesList/filesList.component.ts index 7f61fae..98179b3 100644 --- a/src/filesList/filesList.component.ts +++ b/src/filesList/filesList.component.ts @@ -1,30 +1,40 @@ -import {Component, Input, Output, EventEmitter} from "@angular/core"; -import {FileModel} from "./file.model"; -import {IFileEvent} from "./interface/IFileEvent"; -import {IFileModel} from "./interface/IFileModel"; -import {ConfirmOptions, Position} from "angular2-bootstrap-confirm"; -import {Positioning} from "angular2-bootstrap-confirm/position"; -import {FileManagerConfiguration} from "../configuration/fileManagerConfiguration.service"; +import {Component, Input, Output, EventEmitter} from '@angular/core'; +import {FileModel} from './file.model'; +import {IFileEvent} from './interface/IFileEvent'; +import {IFileModel} from './interface/IFileModel'; +import {ConfirmOptions, Position} from 'angular2-bootstrap-confirm'; +import {Positioning} from 'angular2-bootstrap-confirm/position'; +import {FileManagerConfiguration} from '../configuration/fileManagerConfiguration.service'; +import {FileManagerActionsService, IFileManagerAction} from '../store/fileManagerActions.service'; +import {FileManagerDispatcherService} from '../store/fileManagerDispatcher.service'; +import {NotificationsService} from 'angular2-notifications'; +import {Actions} from '@ngrx/effects'; @Component({ - selector: 'files-list', + selector: 'ri-files-list', templateUrl: './files.html', providers: [ConfirmOptions, {provide: Position, useClass: Positioning}], styleUrls: ['./files-list.less'] }) -export class FilesList { +export class FilesListComponent { @Input() files: FileModel[]; - @Output() onRenameFile = new EventEmitter(); - @Output() onDeleteFile = new EventEmitter(); @Output() onPreviewFile = new EventEmitter(); @Output() onCropFile = new EventEmitter(); @Output() onSelectFile = new EventEmitter(); - public removeTitle: string = 'Remove file'; + public removeTitle = 'Remove file'; - public constructor(public configuration: FileManagerConfiguration) { + public constructor(public configuration: FileManagerConfiguration, + private fileManagerDispatcher: FileManagerDispatcherService, + notifications: NotificationsService, + actions$: Actions) { + + actions$.ofType(FileManagerActionsService.FILEMANAGER_DELETE_FILE_SUCCESS) + .subscribe((action: IFileManagerAction) => { + notifications.success('File delete', `${action.payload.file.name} has been deleted`); + }); } /** @@ -33,36 +43,19 @@ export class FilesList { * @param file */ public deleteFile(file: IFileModel) { - this.onDeleteFile.emit({ - name: 'onDeleteFile', - file: file - }); + this.fileManagerDispatcher.deleteFile(file); } public getRemoveMessage(file: IFileModel) { return 'You are try to delete ' + file.name + '. Are you sure?'; } - /** - * Fired when clicked on button "rename file name" - * - * @param file - */ - public renameFile(file: IFileModel) { - let fileEvent: IFileEvent = { - eventName: 'onRenameFile', - file: file - }; - - this.onRenameFile.emit(fileEvent); - } - /** * Select or unselect all files * * @param selected */ - public allFilesSelection(selected: boolean = true) { + public allFilesSelection(selected = true) { this.files.map((file) => file.selected = selected); } diff --git a/src/filesList/interface/IFileModel.ts b/src/filesList/interface/IFileModel.ts index 9780e38..809b107 100644 --- a/src/filesList/interface/IFileModel.ts +++ b/src/filesList/interface/IFileModel.ts @@ -5,6 +5,7 @@ export interface IFileModel { url: string; selected: boolean; fromJSON(data: IOuterFile): void; + toJSON(): IOuterFile; getId(): string|number; getMime(): string; getWidth(): number; diff --git a/src/main.less b/src/main.less index f120823..d6610b5 100644 --- a/src/main.less +++ b/src/main.less @@ -2,6 +2,16 @@ .filemanager-container { width: 990px; height: 700px; + + .tree { + .dropdown { + position: relative; + } + + .dropdown-menu { + position: fixed !important; + } + } } .fm-main-box { diff --git a/src/preview/preview.component.ts b/src/preview/preview.component.ts index 02c039b..91f23c7 100644 --- a/src/preview/preview.component.ts +++ b/src/preview/preview.component.ts @@ -1,39 +1,38 @@ -import {Component, Input, OnChanges, HostListener} from "@angular/core"; -import {IFileModel} from "../filesList/interface/IFileModel"; +import {Component, Input, OnChanges, HostListener} from '@angular/core'; +import {IFileModel} from '../filesList/interface/IFileModel'; +import {FileModel} from '../filesList/file.model'; @Component({ - selector: 'file-preview', - template: ` -
- -
- ` + selector: 'ri-file-preview', + templateUrl: './preview.html' }) export class PreviewComponent implements OnChanges { + /** + * Collection of files + */ @Input() files: IFileModel[]; + + /** + * Current viewed file + */ @Input() file: IFileModel; - public currentIndex: number = 0; + /** + * Current index + * @type {number} + */ + public currentIndex = 0; - private length: number = 0; + private length = 0; ngOnChanges() { this.length = this.files.length; - this.currentIndex = this.files.indexOf(this.file); + + const selectedFiles = this.files + .filter((file: FileModel) => file.getId() === this.file.getId()); + + this.currentIndex = selectedFiles.length === 1 ? this.files.indexOf(selectedFiles[0]) : -1; } public next() { diff --git a/src/preview/preview.html b/src/preview/preview.html new file mode 100644 index 0000000..157b9d5 --- /dev/null +++ b/src/preview/preview.html @@ -0,0 +1,16 @@ +
+ +
diff --git a/src/services/fileTypeFilter.service.ts b/src/services/fileTypeFilter.service.ts new file mode 100644 index 0000000..8fe5f58 --- /dev/null +++ b/src/services/fileTypeFilter.service.ts @@ -0,0 +1,21 @@ +import {Injectable} from '@angular/core'; +import {BehaviorSubject} from 'rxjs/BehaviorSubject'; +import {IFileTypeFilter} from '../toolbar/interface/IFileTypeFilter'; + +@Injectable() +export class FileTypeFilterService { + + /** + * File type filter + * @type {BehaviorSubject} + */ + public filter$: BehaviorSubject = new BehaviorSubject(null); + + public getValue(): IFileTypeFilter | null { + return this.filter$.getValue(); + } + + public setValue(value: IFileTypeFilter | null) { + this.filter$.next(value); + } +} diff --git a/src/services/searchFilter.service.ts b/src/services/searchFilter.service.ts new file mode 100644 index 0000000..c984d53 --- /dev/null +++ b/src/services/searchFilter.service.ts @@ -0,0 +1,19 @@ +import {Injectable} from '@angular/core'; +import {BehaviorSubject} from 'rxjs/BehaviorSubject'; + +@Injectable() +export class SearchFilterService { + /** + * File type filter + * @type {BehaviorSubject} + */ + public filter$: BehaviorSubject = new BehaviorSubject(''); + + public getValue(): string { + return this.filter$.getValue(); + } + + public setValue(value: string) { + this.filter$.next(value); + } +} diff --git a/src/store/fileManagerActions.service.ts b/src/store/fileManagerActions.service.ts new file mode 100644 index 0000000..0d6d78f --- /dev/null +++ b/src/store/fileManagerActions.service.ts @@ -0,0 +1,102 @@ +import {Injectable} from '@angular/core'; +import {Action} from '@ngrx/store'; +import {IOuterFile} from '../filesList/interface/IOuterFile'; +import {IFileModel} from '../filesList/interface/IFileModel'; +import {ICropBounds} from '../crop/ICropBounds'; + +export interface IFileManagerPayloadData { + folderId?: string; + files?: IOuterFile[]; + file?: IFileModel; + bounds?: ICropBounds; +} + +export interface IFileManagerAction extends Action { + payload: IFileManagerPayloadData; +} + +@Injectable() +export class FileManagerActionsService { + static FILEMANAGER_CROP_FILE = 'FILEMANAGER_CROP_FILE'; + static FILEMANAGER_CROP_FILE_SUCCESS = 'FILEMANAGER_CROP_FILE_SUCCESS'; + static FILEMANAGER_DELETE_FILE = 'FILEMANAGER_DELETE_FILE'; + static FILEMANAGER_DELETE_FILE_SUCCESS = 'FILEMANAGER_DELETE_FILE_SUCCESS'; + static FILEMANAGER_LOAD_FILES = 'FILEMANAGER_LOAD_FILES'; + static FILEMANAGER_LOAD_FILES_SUCCESS = 'FILEMANAGER_LOAD_FILES_SUCCESS'; + static FILEMANAGER_UPLOAD_FILE_ERROR = 'FILEMANAGER_UPLOAD_FILE_ERROR'; + static FILEMANAGER_UPLOAD_FILE_SUCCESS = 'FILEMANAGER_UPLOAD_FILE_SUCCESS'; + + public cropFile(file: IFileModel, bounds: ICropBounds): IFileManagerAction { + return { + type: FileManagerActionsService.FILEMANAGER_CROP_FILE, + payload: { + file: file, + bounds: bounds + } + } + } + + public cropFileSuccess(file: IFileModel): IFileManagerAction { + return { + type: FileManagerActionsService.FILEMANAGER_CROP_FILE_SUCCESS, + payload: { + file: file + } + } + } + + public deleteFile(file: IFileModel): IFileManagerAction { + return { + type: FileManagerActionsService.FILEMANAGER_DELETE_FILE, + payload: { + file: file + } + } + } + + public deleteFileSuccess(file: IFileModel): IFileManagerAction { + return { + type: FileManagerActionsService.FILEMANAGER_DELETE_FILE_SUCCESS, + payload: { + file: file + } + } + } + + public loadFiles(folderId: string): IFileManagerAction { + return { + type: FileManagerActionsService.FILEMANAGER_LOAD_FILES, + payload: { + folderId: folderId + } + } + } + + public loadFilesSuccess(folderId: string, files: IOuterFile[]): IFileManagerAction { + return { + type: FileManagerActionsService.FILEMANAGER_LOAD_FILES_SUCCESS, + payload: { + folderId: folderId, + files: files + } + } + } + + public uploadSuccess(file: IOuterFile): IFileManagerAction { + return { + type: FileManagerActionsService.FILEMANAGER_UPLOAD_FILE_SUCCESS, + payload: { + files: [file] + } + } + } + + public uploadError(file: IOuterFile): IFileManagerAction { + return { + type: FileManagerActionsService.FILEMANAGER_UPLOAD_FILE_ERROR, + payload: { + files: [file] + } + } + } +} diff --git a/src/store/fileManagerDispatcher.service.ts b/src/store/fileManagerDispatcher.service.ts new file mode 100644 index 0000000..457fe25 --- /dev/null +++ b/src/store/fileManagerDispatcher.service.ts @@ -0,0 +1,34 @@ +import {Injectable} from '@angular/core'; +import {Store} from '@ngrx/store'; +import {IFileManagerState} from './fileManagerReducer'; +import {FileManagerActionsService} from './fileManagerActions.service'; +import {IFileModel} from '../filesList/interface/IFileModel'; +import {IOuterFile} from '../filesList/interface/IOuterFile'; +import {ICropBounds} from '../crop/ICropBounds'; + +@Injectable() +export class FileManagerDispatcherService { + + constructor(private store: Store, private fileManagerActions: FileManagerActionsService) { + } + + public cropFile(file: IFileModel, bounds: ICropBounds): void { + this.store.dispatch(this.fileManagerActions.cropFile(file, bounds)); + } + + public deleteFile(file: IFileModel): void { + this.store.dispatch(this.fileManagerActions.deleteFile(file)); + } + + public loadFiles(folderId: string | null): void { + this.store.dispatch(this.fileManagerActions.loadFiles(folderId)); + } + + public uploadError(file: IOuterFile) { + this.store.dispatch(this.fileManagerActions.uploadError(file)); + } + + public uploadSuccess(file: IOuterFile) { + this.store.dispatch(this.fileManagerActions.uploadSuccess(file)); + } +} diff --git a/src/store/fileManagerEffects.service.ts b/src/store/fileManagerEffects.service.ts new file mode 100644 index 0000000..41979d7 --- /dev/null +++ b/src/store/fileManagerEffects.service.ts @@ -0,0 +1,85 @@ +import {Injectable} from '@angular/core'; +import {Actions, Effect} from '@ngrx/effects'; +import {FilesService} from '../filesList/files.service'; +import {FileManagerActionsService, IFileManagerAction} from './fileManagerActions.service'; +import {IOuterFile} from '../filesList/interface/IOuterFile'; +import {Observable} from 'rxjs/Observable'; +import {IFileModel} from '../filesList/interface/IFileModel'; +import {NotificationsService} from 'angular2-notifications'; +import {ICropBounds} from '../crop/ICropBounds'; + +@Injectable() +export class FileManagerEffectsService { + + constructor(private actions$: Actions, + private filesService: FilesService, + private fileManagerActions: FileManagerActionsService, + private notificationService: NotificationsService) { + } + + @Effect() + public loadFiles$ = this.actions$ + .ofType(FileManagerActionsService.FILEMANAGER_LOAD_FILES) + .switchMap((action: IFileManagerAction) => this.loadFiles(action.payload.folderId) + .map((files: IOuterFile[]): IFileManagerAction => { + return this.fileManagerActions.loadFilesSuccess(action.payload.folderId, files); + }) + .catch(() => Observable.of(this.onLoadFilesError(action.payload.folderId))) + ); + + @Effect() + public cropFile$ = this.actions$ + .ofType(FileManagerActionsService.FILEMANAGER_CROP_FILE) + .switchMap((action: IFileManagerAction) => this.cropFile(action.payload.file, action.payload.bounds) + .map((result: IOuterFile): IFileManagerAction => { + this.notificationService.success('Crop Image', 'Image has been cropped'); + return this.fileManagerActions.cropFileSuccess(action.payload.file); + }) + .catch(() => Observable.of(this.onCropFileError(action.payload.file))) + ); + + @Effect() + public deleteFile$ = this.actions$ + .ofType(FileManagerActionsService.FILEMANAGER_DELETE_FILE) + .switchMap((action: IFileManagerAction) => this.deleteFile(action.payload.file) + .map((result: boolean): IFileManagerAction => { + return this.fileManagerActions.deleteFileSuccess(action.payload.file); + }) + .catch(() => Observable.of(this.onDeleteFileError(action.payload.file))) + ); + + public uploadError$ = this.actions$ + .ofType(FileManagerActionsService.FILEMANAGER_UPLOAD_FILE_ERROR) + .map((action: IFileManagerAction) => { + this.notificationService.alert('File upload', `${action.payload.file.name} exists on the server in this directory`); + }); + + public cropFileSuccess$ = this.actions$ + .ofType(FileManagerActionsService.FILEMANAGER_CROP_FILE_SUCCESS); + + + protected cropFile(file: IFileModel, bounds: ICropBounds): Observable { + return this.filesService.crop(file, bounds); + } + + protected deleteFile(file: IFileModel): Observable { + return this.filesService.remove(file); + } + + protected loadFiles(folderId: string | null): Observable { + return this.filesService.load(folderId); + } + + protected onCropFileError(file: IFileModel): void { + console.warn('[FILEMANAGER] Can not crop file' + file.name); + this.notificationService.error('Crop Image', 'Image has not been cropped'); + } + + protected onDeleteFileError(file: IFileModel): void { + console.warn('[FILEMANAGER] Can not delete file' + file.name); + } + + protected onLoadFilesError(folderId: string): void { + console.warn('[FILEMANAGER] Can not load files for folder ' + folderId); + } +} diff --git a/src/store/fileManagerReducer.ts b/src/store/fileManagerReducer.ts new file mode 100644 index 0000000..d496574 --- /dev/null +++ b/src/store/fileManagerReducer.ts @@ -0,0 +1,51 @@ +import {IOuterFile} from '../filesList/interface/IOuterFile'; +import {FileManagerActionsService, IFileManagerAction} from './fileManagerActions.service'; + +export interface IFileManagerState extends Array { +} + + +function cropFile(state: IFileManagerState, action: IFileManagerAction): IFileManagerState { + let newState = [...state]; + + for (let i in state) { + if (newState[i].id === action.payload.file.getId()) { + newState[i] = action.payload.file.toJSON(); + } + } + return newState; +} + +function loadFiles(state: IFileManagerState, action: IFileManagerAction): IFileManagerState { + return [...action.payload.files]; +} + +function removeFile(state: IFileManagerState, action: IFileManagerAction): IFileManagerState { + let id = action.payload.file.getId(); + + return [...state.filter((file: IOuterFile) => file.id !== id)] +} + +function uploadFiles(state: IFileManagerState, action: IFileManagerAction): IFileManagerState { + return [...state, ...action.payload.files]; +} + +export function fileManagerReducer(state: IFileManagerState = [], action: IFileManagerAction): IFileManagerState { + switch (action.type) { + case FileManagerActionsService.FILEMANAGER_CROP_FILE_SUCCESS: + return cropFile(state, action); + case FileManagerActionsService.FILEMANAGER_DELETE_FILE_SUCCESS: + return removeFile(state, action); + case FileManagerActionsService.FILEMANAGER_LOAD_FILES_SUCCESS: + return loadFiles(state, action); + case FileManagerActionsService.FILEMANAGER_UPLOAD_FILE_SUCCESS: + return uploadFiles(state, action); + case FileManagerActionsService.FILEMANAGER_CROP_FILE: + case FileManagerActionsService.FILEMANAGER_DELETE_FILE: + case FileManagerActionsService.FILEMANAGER_LOAD_FILES: + return state; + default: + return state; + } + +} diff --git a/src/toolbar/fileTypeFilter/fileTypeFilter.component.html b/src/toolbar/fileTypeFilter/fileTypeFilter.component.html new file mode 100644 index 0000000..30c6689 --- /dev/null +++ b/src/toolbar/fileTypeFilter/fileTypeFilter.component.html @@ -0,0 +1,6 @@ +
+ +
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..a721e91 100644 --- a/src/toolbar/toolbar.component.ts +++ b/src/toolbar/toolbar.component.ts @@ -5,10 +5,9 @@ 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 {FileManagerDispatcherService} from '../store/fileManagerDispatcher.service'; @Component({ selector: 'toolbar', @@ -19,18 +18,10 @@ import {FileManagerUploader} from "../filesList/fileManagerUploader.service"; export class Toolbar 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 +43,23 @@ 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; + if (status === 200) { + this.fileManagerDispatcher.uploadSuccess(JSON.parse(response)); + } else { + this.fileManagerDispatcher.uploadError(JSON.parse(response)); } - }); + }; } public ngOnChanges() { @@ -102,22 +80,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/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, From dd2088552bb3f5fc598954f8d95f221e5dfd58fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ignaszewski?= Date: Tue, 30 May 2017 17:34:25 +0200 Subject: [PATCH 02/23] ngrx/store + actions and effects --- demo/backend/index.js | 95 +++-- demo/src/app/app.component.html | 2 +- demo/src/app/app.component.ts | 4 - demo/src/app/app.module.ts | 2 +- index.d.ts | 1 + package.json | 8 +- src/configuration/IUrlConfiguration.ts | 1 + .../fileManagerConfiguration.service.ts | 15 +- src/configuration/tree.service.ts | 9 +- src/crop/crop.component.ts | 6 +- src/decorators/logFunction.decorator.ts | 2 +- src/dropdown/dropdown.component.ts | 34 +- src/dropdown/dropdown.html | 19 + src/filemanager.component.ts | 362 +++++++----------- src/filemanager.html | 23 +- src/filemanager.module.ts | 66 +++- src/filesList/file.model.ts | 18 +- src/filesList/fileManagerUploader.service.ts | 6 +- src/filesList/files.service.ts | 2 +- src/filesList/filesList.component.ts | 57 ++- src/filesList/interface/IFileModel.ts | 1 + src/main.less | 10 + src/preview/preview.component.ts | 47 ++- src/preview/preview.html | 16 + src/services/fileTypeFilter.service.ts | 21 + src/services/searchFilter.service.ts | 19 + src/store/fileManagerActions.service.ts | 102 +++++ src/store/fileManagerDispatcher.service.ts | 34 ++ src/store/fileManagerEffects.service.ts | 85 ++++ src/store/fileManagerReducer.ts | 51 +++ .../fileTypeFilter.component.html | 6 + .../fileTypeFilter.component.ts | 40 ++ .../searchFile/searchFile.component.html | 8 + .../searchFile/searchFile.component.ts | 22 ++ src/toolbar/toolbar.component.ts | 56 +-- src/toolbar/toolbar.html | 20 +- tslint.json | 4 +- 37 files changed, 794 insertions(+), 480 deletions(-) create mode 100644 index.d.ts create mode 100644 src/dropdown/dropdown.html create mode 100644 src/preview/preview.html create mode 100644 src/services/fileTypeFilter.service.ts create mode 100644 src/services/searchFilter.service.ts create mode 100644 src/store/fileManagerActions.service.ts create mode 100644 src/store/fileManagerDispatcher.service.ts create mode 100644 src/store/fileManagerEffects.service.ts create mode 100644 src/store/fileManagerReducer.ts create mode 100644 src/toolbar/fileTypeFilter/fileTypeFilter.component.html create mode 100644 src/toolbar/fileTypeFilter/fileTypeFilter.component.ts create mode 100644 src/toolbar/searchFile/searchFile.component.html create mode 100644 src/toolbar/searchFile/searchFile.component.ts diff --git a/demo/backend/index.js b/demo/backend/index.js index 6f6bf53..0c764c6 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'}); } - }); @@ -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..84e174b 100644 --- a/demo/src/app/app.component.html +++ b/demo/src/app/app.component.html @@ -6,5 +6,5 @@

Filemanager

Use multiselection - Loading... + Loading... diff --git a/demo/src/app/app.component.ts b/demo/src/app/app.component.ts index 0f84a3a..1323479 100644 --- a/demo/src/app/app.component.ts +++ b/demo/src/app/app.component.ts @@ -14,8 +14,4 @@ export class AppComponent { public toggleMultiSelection() { this.isMultiSelection = !this.isMultiSelection; } - - public selectFile(data: ISelectFile) { - console.log(data); - } } diff --git a/demo/src/app/app.module.ts b/demo/src/app/app.module.ts index e4305a5..47261f6 100644 --- a/demo/src/app/app.module.ts +++ b/demo/src/app/app.module.ts @@ -18,7 +18,7 @@ import {FileManagerModule} from "../../../src/filemanager.module"; HttpModule ], providers: [ - {provide: 'fileManagerUrls', useValue: {foldersUrl: '/api/folder', filesUrl: '/api/files'}} + {provide: 'fileManagerUrls', useValue: {foldersUrl: '/api/folder', filesUrl: '/api/files', folderMoveUrl: '/api/folder/move'}} ], bootstrap: [AppComponent] }) 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/package.json b/package.json index f1366cf..f6b3a97 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "file", "manager" ], - "main": "main.ts", + "main": "main", "author": "Rafal Ignaszewski http://ignaszewski.pl", "dependencies": { "@angular/common": "^2.4.0", @@ -27,7 +27,10 @@ "@angular/platform-browser": "^2.4.0", "@angular/platform-browser-dynamic": "^2.4.0", "@angular/router": "^3.4.0", - "@rign/angular2-tree": "^0.8.1", + "@rign/angular2-tree": "git://github.com/qjon/angular2-tree.git#6b99173269e9c3b988846ff76cbfef72d20271ae", + "@ngrx/core": "^1.2.0", + "@ngrx/effects": "^2.0.3", + "@ngrx/store": "^2.2.2", "angular2-bootstrap-confirm": "^1.0.4", "angular2-notifications": "^0.4.53", "bootstrap": "^3.3.7", @@ -42,6 +45,7 @@ "devDependencies": { "@angular/cli": "1.0.0-beta.32.3", "@angular/compiler-cli": "^2.4.0", + "@ngrx/store-devtools": "^3.2.4", "@types/jasmine": "2.5.38", "@types/node": "^6.0.42", "codelyzer": "~2.0.0-beta.4", diff --git a/src/configuration/IUrlConfiguration.ts b/src/configuration/IUrlConfiguration.ts index bd2f3e9..f2460ff 100644 --- a/src/configuration/IUrlConfiguration.ts +++ b/src/configuration/IUrlConfiguration.ts @@ -1,4 +1,5 @@ export class IUrlConfiguration { filesUrl: string; foldersUrl: string; + folderMoveUrl: string; } diff --git a/src/configuration/fileManagerConfiguration.service.ts b/src/configuration/fileManagerConfiguration.service.ts index 619877a..47de2bf 100644 --- a/src/configuration/fileManagerConfiguration.service.ts +++ b/src/configuration/fileManagerConfiguration.service.ts @@ -1,14 +1,14 @@ -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 {IUrlConfiguration} from './IUrlConfiguration'; @Injectable() export class FileManagerConfiguration { public contextMenuItems: IContextMenu[] = []; - public isMultiSelection: boolean = false; + public isMultiSelection = false; public fileTypesFilter: IFileTypeFilter[] = [ { @@ -44,7 +44,7 @@ export class FileManagerConfiguration { } ]; - public fileUrl: string = '/api/files'; + public fileUrl = '/api/files'; public allowedCropSize: ICropSize[] = [ { @@ -61,7 +61,6 @@ export class FileManagerConfiguration { constructor(@Inject('fileManagerUrls') urls: IUrlConfiguration) { - this.fileUrl = urls.filesUrl; } } diff --git a/src/configuration/tree.service.ts b/src/configuration/tree.service.ts index 7c83498..fd18597 100644 --- a/src/configuration/tree.service.ts +++ b/src/configuration/tree.service.ts @@ -1,7 +1,7 @@ -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 {IUrlConfiguration} from './IUrlConfiguration'; @Injectable() export class TreeService extends NodeService { @@ -13,6 +13,7 @@ export class TreeService extends NodeService { getUrl: urls.foldersUrl, updateUrl: urls.foldersUrl, removeUrl: urls.foldersUrl, + moveUrl: urls.folderMoveUrl }; } } diff --git a/src/crop/crop.component.ts b/src/crop/crop.component.ts index db44309..d24c898 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"; @@ -78,7 +78,7 @@ export class CropComponent { ngAfterContentInit() { this.updateCropSize(this.cropSizeList[0]); - } + }; public cropImage() { let bounds: ICropBounds = { @@ -98,8 +98,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..2a66688 100644 --- a/src/filemanager.component.ts +++ b/src/filemanager.component.ts @@ -1,23 +1,41 @@ -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, Input, OnChanges, EventEmitter, Output +} from '@angular/core'; +import { + TreeComponent, + NodeService, + IContextMenu, + IOuterNode, + ITreeData, + ITreeState, + IConfiguration, + TreeModel, + TreeActionsService, + NodeDispatcherService +} from '@rign/angular2-tree'; +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 {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 {TreeService} from './configuration/tree.service'; +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'; @Component({ - selector: 'filemanager', + selector: 'ri-filemanager', providers: [NodeService, FilesService, NotificationsService], styleUrls: ['./main.less'], templateUrl: './filemanager.html' @@ -29,34 +47,40 @@ export class FileManagerComponent implements OnInit, OnChanges { @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; @@ -79,145 +103,113 @@ 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: TreeService, + private filesService: FilesService, + private notifications: NotificationsService, + private configuration: FileManagerConfiguration, + private fileManagerDispatcher: FileManagerDispatcherService, + private fileTypeFilter: FileTypeFilterService, + private searchFilterService: SearchFilterService, + private fileManagerEffects: FileManagerEffectsService) { this.menu = configuration.contextMenuItems; } 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(); - } + ngOnChanges() { + this.configuration.isMultiSelection = this.multiSelection; } - @log - public onSelect(event: ITreeItemEvent) { - if (event.status) { - this.loadFiles(event.node.id); - this.currentFolderId = event.node.id; - } else { - this.loadFiles(''); - this.currentFolderId = null; - } + + get currentSelectedFolderId(): string | null { + const value = this.treeModel.currentSelectedNode$.getValue(); + + return value ? value.id : null; } + @log + 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; @@ -232,21 +224,7 @@ export class FileManagerComponent implements OnInit, OnChanges { @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'); - } - ); + this.fileManagerDispatcher.cropFile(event.file, event.bounds); } @log @@ -262,11 +240,11 @@ 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); @@ -283,18 +261,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 +280,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..6952b64 100644 --- a/src/filemanager.html +++ b/src/filemanager.html @@ -2,39 +2,26 @@
- - + + >
diff --git a/src/filemanager.module.ts b/src/filemanager.module.ts index f43812b..aff5c10 100644 --- a/src/filemanager.module.ts +++ b/src/filemanager.module.ts @@ -1,46 +1,70 @@ -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} from '@angular/core'; +import {BrowserModule} from '@angular/platform-browser'; +import {HttpModule} from '@angular/http'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {TreeModule, treeReducer, TreeEffectsService} from '@rign/angular2-tree'; +import {NotificationsService, SimpleNotificationsModule} from 'angular2-notifications'; +import {ConfirmModule} from 'angular2-bootstrap-confirm'; +import {FileManagerComponent} from './filemanager.component'; +import {Toolbar} 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 {FilesService} from './filesList/files.service'; +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'; @NgModule({ imports: [ BrowserModule, ConfirmModule, + EffectsModule.run(FileManagerEffectsService), FormsModule, FileUploadModule, HttpModule, ReactiveFormsModule, SimpleNotificationsModule, + StoreModule.provideStore({files: fileManagerReducer, trees: treeReducer}), + StoreDevtoolsModule.instrumentOnlyWithExtension({}), TreeModule ], declarations: [ FileManagerComponent, + FileTypeFilterComponent, Toolbar, - FilesList, - Dropdown, + FilesListComponent, + DropdownComponent, PreviewComponent, CropComponent, - ImageCropperComponent + ImageCropperComponent, + SearchFileComponent ], entryComponents: [ImageCropperComponent], providers: [ + FileManagerActionsService, FileManagerConfiguration, + FileManagerDispatcherService, + FileManagerEffectsService, FileManagerUploader, + FilesService, + FileTypeFilterService, + NotificationsService, + SearchFilterService, TreeService ], exports: [FileManagerComponent], diff --git a/src/filesList/file.model.ts b/src/filesList/file.model.ts index b89ad05..74da4f5 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; @@ -39,6 +39,10 @@ export class FileModel implements IFileModel { this.name = data.name; } + public toJSON() { + return this._orgData; + } + public getId() { return this._orgData.id; } @@ -71,6 +75,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..55f41f4 100644 --- a/src/filesList/fileManagerUploader.service.ts +++ b/src/filesList/fileManagerUploader.service.ts @@ -1,6 +1,6 @@ -import {Injectable, Inject} from "@angular/core"; -import {FileUploader} from "ng2-file-upload"; -import {IUrlConfiguration} from "../configuration/IUrlConfiguration"; +import {Injectable, Inject} from '@angular/core'; +import {FileUploader} from 'ng2-file-upload'; +import {IUrlConfiguration} from '../configuration/IUrlConfiguration'; @Injectable() export class FileManagerUploader { diff --git a/src/filesList/files.service.ts b/src/filesList/files.service.ts index 036d21c..7faa8fc 100644 --- a/src/filesList/files.service.ts +++ b/src/filesList/files.service.ts @@ -14,7 +14,7 @@ export class FilesService { this.url = configuration.fileUrl; } - public crop(file: IFileModel, bounds: ICropBounds): Observable { + public crop(file: IFileModel, bounds: ICropBounds): Observable { return this.http.put(this.url, {id: file.getId(), bounds: bounds}) .map((res: Response) => { let body = res.json(); diff --git a/src/filesList/filesList.component.ts b/src/filesList/filesList.component.ts index 7f61fae..98179b3 100644 --- a/src/filesList/filesList.component.ts +++ b/src/filesList/filesList.component.ts @@ -1,30 +1,40 @@ -import {Component, Input, Output, EventEmitter} from "@angular/core"; -import {FileModel} from "./file.model"; -import {IFileEvent} from "./interface/IFileEvent"; -import {IFileModel} from "./interface/IFileModel"; -import {ConfirmOptions, Position} from "angular2-bootstrap-confirm"; -import {Positioning} from "angular2-bootstrap-confirm/position"; -import {FileManagerConfiguration} from "../configuration/fileManagerConfiguration.service"; +import {Component, Input, Output, EventEmitter} from '@angular/core'; +import {FileModel} from './file.model'; +import {IFileEvent} from './interface/IFileEvent'; +import {IFileModel} from './interface/IFileModel'; +import {ConfirmOptions, Position} from 'angular2-bootstrap-confirm'; +import {Positioning} from 'angular2-bootstrap-confirm/position'; +import {FileManagerConfiguration} from '../configuration/fileManagerConfiguration.service'; +import {FileManagerActionsService, IFileManagerAction} from '../store/fileManagerActions.service'; +import {FileManagerDispatcherService} from '../store/fileManagerDispatcher.service'; +import {NotificationsService} from 'angular2-notifications'; +import {Actions} from '@ngrx/effects'; @Component({ - selector: 'files-list', + selector: 'ri-files-list', templateUrl: './files.html', providers: [ConfirmOptions, {provide: Position, useClass: Positioning}], styleUrls: ['./files-list.less'] }) -export class FilesList { +export class FilesListComponent { @Input() files: FileModel[]; - @Output() onRenameFile = new EventEmitter(); - @Output() onDeleteFile = new EventEmitter(); @Output() onPreviewFile = new EventEmitter(); @Output() onCropFile = new EventEmitter(); @Output() onSelectFile = new EventEmitter(); - public removeTitle: string = 'Remove file'; + public removeTitle = 'Remove file'; - public constructor(public configuration: FileManagerConfiguration) { + public constructor(public configuration: FileManagerConfiguration, + private fileManagerDispatcher: FileManagerDispatcherService, + notifications: NotificationsService, + actions$: Actions) { + + actions$.ofType(FileManagerActionsService.FILEMANAGER_DELETE_FILE_SUCCESS) + .subscribe((action: IFileManagerAction) => { + notifications.success('File delete', `${action.payload.file.name} has been deleted`); + }); } /** @@ -33,36 +43,19 @@ export class FilesList { * @param file */ public deleteFile(file: IFileModel) { - this.onDeleteFile.emit({ - name: 'onDeleteFile', - file: file - }); + this.fileManagerDispatcher.deleteFile(file); } public getRemoveMessage(file: IFileModel) { return 'You are try to delete ' + file.name + '. Are you sure?'; } - /** - * Fired when clicked on button "rename file name" - * - * @param file - */ - public renameFile(file: IFileModel) { - let fileEvent: IFileEvent = { - eventName: 'onRenameFile', - file: file - }; - - this.onRenameFile.emit(fileEvent); - } - /** * Select or unselect all files * * @param selected */ - public allFilesSelection(selected: boolean = true) { + public allFilesSelection(selected = true) { this.files.map((file) => file.selected = selected); } diff --git a/src/filesList/interface/IFileModel.ts b/src/filesList/interface/IFileModel.ts index 9780e38..809b107 100644 --- a/src/filesList/interface/IFileModel.ts +++ b/src/filesList/interface/IFileModel.ts @@ -5,6 +5,7 @@ export interface IFileModel { url: string; selected: boolean; fromJSON(data: IOuterFile): void; + toJSON(): IOuterFile; getId(): string|number; getMime(): string; getWidth(): number; diff --git a/src/main.less b/src/main.less index f120823..d6610b5 100644 --- a/src/main.less +++ b/src/main.less @@ -2,6 +2,16 @@ .filemanager-container { width: 990px; height: 700px; + + .tree { + .dropdown { + position: relative; + } + + .dropdown-menu { + position: fixed !important; + } + } } .fm-main-box { diff --git a/src/preview/preview.component.ts b/src/preview/preview.component.ts index 02c039b..91f23c7 100644 --- a/src/preview/preview.component.ts +++ b/src/preview/preview.component.ts @@ -1,39 +1,38 @@ -import {Component, Input, OnChanges, HostListener} from "@angular/core"; -import {IFileModel} from "../filesList/interface/IFileModel"; +import {Component, Input, OnChanges, HostListener} from '@angular/core'; +import {IFileModel} from '../filesList/interface/IFileModel'; +import {FileModel} from '../filesList/file.model'; @Component({ - selector: 'file-preview', - template: ` -
- -
- ` + selector: 'ri-file-preview', + templateUrl: './preview.html' }) export class PreviewComponent implements OnChanges { + /** + * Collection of files + */ @Input() files: IFileModel[]; + + /** + * Current viewed file + */ @Input() file: IFileModel; - public currentIndex: number = 0; + /** + * Current index + * @type {number} + */ + public currentIndex = 0; - private length: number = 0; + private length = 0; ngOnChanges() { this.length = this.files.length; - this.currentIndex = this.files.indexOf(this.file); + + const selectedFiles = this.files + .filter((file: FileModel) => file.getId() === this.file.getId()); + + this.currentIndex = selectedFiles.length === 1 ? this.files.indexOf(selectedFiles[0]) : -1; } public next() { diff --git a/src/preview/preview.html b/src/preview/preview.html new file mode 100644 index 0000000..157b9d5 --- /dev/null +++ b/src/preview/preview.html @@ -0,0 +1,16 @@ +
+ +
diff --git a/src/services/fileTypeFilter.service.ts b/src/services/fileTypeFilter.service.ts new file mode 100644 index 0000000..8fe5f58 --- /dev/null +++ b/src/services/fileTypeFilter.service.ts @@ -0,0 +1,21 @@ +import {Injectable} from '@angular/core'; +import {BehaviorSubject} from 'rxjs/BehaviorSubject'; +import {IFileTypeFilter} from '../toolbar/interface/IFileTypeFilter'; + +@Injectable() +export class FileTypeFilterService { + + /** + * File type filter + * @type {BehaviorSubject} + */ + public filter$: BehaviorSubject = new BehaviorSubject(null); + + public getValue(): IFileTypeFilter | null { + return this.filter$.getValue(); + } + + public setValue(value: IFileTypeFilter | null) { + this.filter$.next(value); + } +} diff --git a/src/services/searchFilter.service.ts b/src/services/searchFilter.service.ts new file mode 100644 index 0000000..c984d53 --- /dev/null +++ b/src/services/searchFilter.service.ts @@ -0,0 +1,19 @@ +import {Injectable} from '@angular/core'; +import {BehaviorSubject} from 'rxjs/BehaviorSubject'; + +@Injectable() +export class SearchFilterService { + /** + * File type filter + * @type {BehaviorSubject} + */ + public filter$: BehaviorSubject = new BehaviorSubject(''); + + public getValue(): string { + return this.filter$.getValue(); + } + + public setValue(value: string) { + this.filter$.next(value); + } +} diff --git a/src/store/fileManagerActions.service.ts b/src/store/fileManagerActions.service.ts new file mode 100644 index 0000000..0d6d78f --- /dev/null +++ b/src/store/fileManagerActions.service.ts @@ -0,0 +1,102 @@ +import {Injectable} from '@angular/core'; +import {Action} from '@ngrx/store'; +import {IOuterFile} from '../filesList/interface/IOuterFile'; +import {IFileModel} from '../filesList/interface/IFileModel'; +import {ICropBounds} from '../crop/ICropBounds'; + +export interface IFileManagerPayloadData { + folderId?: string; + files?: IOuterFile[]; + file?: IFileModel; + bounds?: ICropBounds; +} + +export interface IFileManagerAction extends Action { + payload: IFileManagerPayloadData; +} + +@Injectable() +export class FileManagerActionsService { + static FILEMANAGER_CROP_FILE = 'FILEMANAGER_CROP_FILE'; + static FILEMANAGER_CROP_FILE_SUCCESS = 'FILEMANAGER_CROP_FILE_SUCCESS'; + static FILEMANAGER_DELETE_FILE = 'FILEMANAGER_DELETE_FILE'; + static FILEMANAGER_DELETE_FILE_SUCCESS = 'FILEMANAGER_DELETE_FILE_SUCCESS'; + static FILEMANAGER_LOAD_FILES = 'FILEMANAGER_LOAD_FILES'; + static FILEMANAGER_LOAD_FILES_SUCCESS = 'FILEMANAGER_LOAD_FILES_SUCCESS'; + static FILEMANAGER_UPLOAD_FILE_ERROR = 'FILEMANAGER_UPLOAD_FILE_ERROR'; + static FILEMANAGER_UPLOAD_FILE_SUCCESS = 'FILEMANAGER_UPLOAD_FILE_SUCCESS'; + + public cropFile(file: IFileModel, bounds: ICropBounds): IFileManagerAction { + return { + type: FileManagerActionsService.FILEMANAGER_CROP_FILE, + payload: { + file: file, + bounds: bounds + } + } + } + + public cropFileSuccess(file: IFileModel): IFileManagerAction { + return { + type: FileManagerActionsService.FILEMANAGER_CROP_FILE_SUCCESS, + payload: { + file: file + } + } + } + + public deleteFile(file: IFileModel): IFileManagerAction { + return { + type: FileManagerActionsService.FILEMANAGER_DELETE_FILE, + payload: { + file: file + } + } + } + + public deleteFileSuccess(file: IFileModel): IFileManagerAction { + return { + type: FileManagerActionsService.FILEMANAGER_DELETE_FILE_SUCCESS, + payload: { + file: file + } + } + } + + public loadFiles(folderId: string): IFileManagerAction { + return { + type: FileManagerActionsService.FILEMANAGER_LOAD_FILES, + payload: { + folderId: folderId + } + } + } + + public loadFilesSuccess(folderId: string, files: IOuterFile[]): IFileManagerAction { + return { + type: FileManagerActionsService.FILEMANAGER_LOAD_FILES_SUCCESS, + payload: { + folderId: folderId, + files: files + } + } + } + + public uploadSuccess(file: IOuterFile): IFileManagerAction { + return { + type: FileManagerActionsService.FILEMANAGER_UPLOAD_FILE_SUCCESS, + payload: { + files: [file] + } + } + } + + public uploadError(file: IOuterFile): IFileManagerAction { + return { + type: FileManagerActionsService.FILEMANAGER_UPLOAD_FILE_ERROR, + payload: { + files: [file] + } + } + } +} diff --git a/src/store/fileManagerDispatcher.service.ts b/src/store/fileManagerDispatcher.service.ts new file mode 100644 index 0000000..457fe25 --- /dev/null +++ b/src/store/fileManagerDispatcher.service.ts @@ -0,0 +1,34 @@ +import {Injectable} from '@angular/core'; +import {Store} from '@ngrx/store'; +import {IFileManagerState} from './fileManagerReducer'; +import {FileManagerActionsService} from './fileManagerActions.service'; +import {IFileModel} from '../filesList/interface/IFileModel'; +import {IOuterFile} from '../filesList/interface/IOuterFile'; +import {ICropBounds} from '../crop/ICropBounds'; + +@Injectable() +export class FileManagerDispatcherService { + + constructor(private store: Store, private fileManagerActions: FileManagerActionsService) { + } + + public cropFile(file: IFileModel, bounds: ICropBounds): void { + this.store.dispatch(this.fileManagerActions.cropFile(file, bounds)); + } + + public deleteFile(file: IFileModel): void { + this.store.dispatch(this.fileManagerActions.deleteFile(file)); + } + + public loadFiles(folderId: string | null): void { + this.store.dispatch(this.fileManagerActions.loadFiles(folderId)); + } + + public uploadError(file: IOuterFile) { + this.store.dispatch(this.fileManagerActions.uploadError(file)); + } + + public uploadSuccess(file: IOuterFile) { + this.store.dispatch(this.fileManagerActions.uploadSuccess(file)); + } +} diff --git a/src/store/fileManagerEffects.service.ts b/src/store/fileManagerEffects.service.ts new file mode 100644 index 0000000..41979d7 --- /dev/null +++ b/src/store/fileManagerEffects.service.ts @@ -0,0 +1,85 @@ +import {Injectable} from '@angular/core'; +import {Actions, Effect} from '@ngrx/effects'; +import {FilesService} from '../filesList/files.service'; +import {FileManagerActionsService, IFileManagerAction} from './fileManagerActions.service'; +import {IOuterFile} from '../filesList/interface/IOuterFile'; +import {Observable} from 'rxjs/Observable'; +import {IFileModel} from '../filesList/interface/IFileModel'; +import {NotificationsService} from 'angular2-notifications'; +import {ICropBounds} from '../crop/ICropBounds'; + +@Injectable() +export class FileManagerEffectsService { + + constructor(private actions$: Actions, + private filesService: FilesService, + private fileManagerActions: FileManagerActionsService, + private notificationService: NotificationsService) { + } + + @Effect() + public loadFiles$ = this.actions$ + .ofType(FileManagerActionsService.FILEMANAGER_LOAD_FILES) + .switchMap((action: IFileManagerAction) => this.loadFiles(action.payload.folderId) + .map((files: IOuterFile[]): IFileManagerAction => { + return this.fileManagerActions.loadFilesSuccess(action.payload.folderId, files); + }) + .catch(() => Observable.of(this.onLoadFilesError(action.payload.folderId))) + ); + + @Effect() + public cropFile$ = this.actions$ + .ofType(FileManagerActionsService.FILEMANAGER_CROP_FILE) + .switchMap((action: IFileManagerAction) => this.cropFile(action.payload.file, action.payload.bounds) + .map((result: IOuterFile): IFileManagerAction => { + this.notificationService.success('Crop Image', 'Image has been cropped'); + return this.fileManagerActions.cropFileSuccess(action.payload.file); + }) + .catch(() => Observable.of(this.onCropFileError(action.payload.file))) + ); + + @Effect() + public deleteFile$ = this.actions$ + .ofType(FileManagerActionsService.FILEMANAGER_DELETE_FILE) + .switchMap((action: IFileManagerAction) => this.deleteFile(action.payload.file) + .map((result: boolean): IFileManagerAction => { + return this.fileManagerActions.deleteFileSuccess(action.payload.file); + }) + .catch(() => Observable.of(this.onDeleteFileError(action.payload.file))) + ); + + public uploadError$ = this.actions$ + .ofType(FileManagerActionsService.FILEMANAGER_UPLOAD_FILE_ERROR) + .map((action: IFileManagerAction) => { + this.notificationService.alert('File upload', `${action.payload.file.name} exists on the server in this directory`); + }); + + public cropFileSuccess$ = this.actions$ + .ofType(FileManagerActionsService.FILEMANAGER_CROP_FILE_SUCCESS); + + + protected cropFile(file: IFileModel, bounds: ICropBounds): Observable { + return this.filesService.crop(file, bounds); + } + + protected deleteFile(file: IFileModel): Observable { + return this.filesService.remove(file); + } + + protected loadFiles(folderId: string | null): Observable { + return this.filesService.load(folderId); + } + + protected onCropFileError(file: IFileModel): void { + console.warn('[FILEMANAGER] Can not crop file' + file.name); + this.notificationService.error('Crop Image', 'Image has not been cropped'); + } + + protected onDeleteFileError(file: IFileModel): void { + console.warn('[FILEMANAGER] Can not delete file' + file.name); + } + + protected onLoadFilesError(folderId: string): void { + console.warn('[FILEMANAGER] Can not load files for folder ' + folderId); + } +} diff --git a/src/store/fileManagerReducer.ts b/src/store/fileManagerReducer.ts new file mode 100644 index 0000000..d496574 --- /dev/null +++ b/src/store/fileManagerReducer.ts @@ -0,0 +1,51 @@ +import {IOuterFile} from '../filesList/interface/IOuterFile'; +import {FileManagerActionsService, IFileManagerAction} from './fileManagerActions.service'; + +export interface IFileManagerState extends Array { +} + + +function cropFile(state: IFileManagerState, action: IFileManagerAction): IFileManagerState { + let newState = [...state]; + + for (let i in state) { + if (newState[i].id === action.payload.file.getId()) { + newState[i] = action.payload.file.toJSON(); + } + } + return newState; +} + +function loadFiles(state: IFileManagerState, action: IFileManagerAction): IFileManagerState { + return [...action.payload.files]; +} + +function removeFile(state: IFileManagerState, action: IFileManagerAction): IFileManagerState { + let id = action.payload.file.getId(); + + return [...state.filter((file: IOuterFile) => file.id !== id)] +} + +function uploadFiles(state: IFileManagerState, action: IFileManagerAction): IFileManagerState { + return [...state, ...action.payload.files]; +} + +export function fileManagerReducer(state: IFileManagerState = [], action: IFileManagerAction): IFileManagerState { + switch (action.type) { + case FileManagerActionsService.FILEMANAGER_CROP_FILE_SUCCESS: + return cropFile(state, action); + case FileManagerActionsService.FILEMANAGER_DELETE_FILE_SUCCESS: + return removeFile(state, action); + case FileManagerActionsService.FILEMANAGER_LOAD_FILES_SUCCESS: + return loadFiles(state, action); + case FileManagerActionsService.FILEMANAGER_UPLOAD_FILE_SUCCESS: + return uploadFiles(state, action); + case FileManagerActionsService.FILEMANAGER_CROP_FILE: + case FileManagerActionsService.FILEMANAGER_DELETE_FILE: + case FileManagerActionsService.FILEMANAGER_LOAD_FILES: + return state; + default: + return state; + } + +} diff --git a/src/toolbar/fileTypeFilter/fileTypeFilter.component.html b/src/toolbar/fileTypeFilter/fileTypeFilter.component.html new file mode 100644 index 0000000..30c6689 --- /dev/null +++ b/src/toolbar/fileTypeFilter/fileTypeFilter.component.html @@ -0,0 +1,6 @@ +
+ +
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..a721e91 100644 --- a/src/toolbar/toolbar.component.ts +++ b/src/toolbar/toolbar.component.ts @@ -5,10 +5,9 @@ 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 {FileManagerDispatcherService} from '../store/fileManagerDispatcher.service'; @Component({ selector: 'toolbar', @@ -19,18 +18,10 @@ import {FileManagerUploader} from "../filesList/fileManagerUploader.service"; export class Toolbar 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 +43,23 @@ 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; + if (status === 200) { + this.fileManagerDispatcher.uploadSuccess(JSON.parse(response)); + } else { + this.fileManagerDispatcher.uploadError(JSON.parse(response)); } - }); + }; } public ngOnChanges() { @@ -102,22 +80,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/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, From d4b6ba4dbe4307cb4e6b23dd776895da415b43e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ignaszewski?= Date: Sun, 2 Jul 2017 10:43:47 +0200 Subject: [PATCH 03/23] New package versions --- angular-cli.json | 5 ++- demo/src/app/app.component.spec.ts | 34 ----------------- demo/src/tsconfig.json | 10 +++-- package.json | 59 +++++++++++++++++------------- src/configuration/tree.service.ts | 2 +- 5 files changed, 44 insertions(+), 66 deletions(-) delete mode 100644 demo/src/app/app.component.spec.ts diff --git a/angular-cli.json b/angular-cli.json index 2e5935e..605383c 100644 --- a/angular-cli.json +++ b/angular-cli.json @@ -1,7 +1,7 @@ { "$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": [ @@ -16,8 +16,9 @@ "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": [ 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/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/package.json b/package.json index f6b3a97..f46db1c 100644 --- a/package.json +++ b/package.json @@ -19,53 +19,60 @@ "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": "git://github.com/qjon/angular2-tree.git#6b99173269e9c3b988846ff76cbfef72d20271ae", + "@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", + "@rign/angular2-tree": "^2.0.0", "@ngrx/core": "^1.2.0", "@ngrx/effects": "^2.0.3", + "core-js": "^2.4.1", "@ngrx/store": "^2.2.2", "angular2-bootstrap-confirm": "^1.0.4", + "angular2-contextmenu": "^0.7.7", "angular2-notifications": "^0.4.53", + "angular2-uuid": "^1.1.1", "bootstrap": "^3.3.7", - "core-js": "^2.4.1", - "font-awesome": "^4.7.0", + "font-awesome": "^4.6.3", + "ng2-dnd": "^2.2.2", "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" + "zone.js": "^0.8.4" }, "devDependencies": { - "@angular/cli": "1.0.0-beta.32.3", - "@angular/compiler-cli": "^2.4.0", + "@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.38", - "@types/node": "^6.0.42", - "codelyzer": "~2.0.0-beta.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/configuration/tree.service.ts b/src/configuration/tree.service.ts index fd18597..a232cbb 100644 --- a/src/configuration/tree.service.ts +++ b/src/configuration/tree.service.ts @@ -5,7 +5,7 @@ import {IUrlConfiguration} from './IUrlConfiguration'; @Injectable() export class TreeService extends NodeService { - public constructor(http: Http, @Inject('fileManagerUrls') urls: IUrlConfiguration) { + public constructor(protected http: Http, @Inject('fileManagerUrls') urls: IUrlConfiguration) { super(http); this.apiConfig = { From 5796fa2c11708c91b5a213a9953cfb063b5c27b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ignaszewski?= Date: Sun, 2 Jul 2017 20:51:39 +0200 Subject: [PATCH 04/23] API --- demo/src/app/app.module.ts | 2 +- src/filemanager.component.ts | 3 +- src/filemanager.module.ts | 2 + src/filesList/interface/IOuterFile.ts | 3 +- src/store/IFileManagerApi.ts | 14 +++ src/store/fileManagerApi.service.ts | 141 ++++++++++++++++++++++++ src/store/fileManagerEffects.service.ts | 6 +- 7 files changed, 166 insertions(+), 5 deletions(-) create mode 100644 src/store/IFileManagerApi.ts create mode 100644 src/store/fileManagerApi.service.ts diff --git a/demo/src/app/app.module.ts b/demo/src/app/app.module.ts index 47261f6..10ec28c 100644 --- a/demo/src/app/app.module.ts +++ b/demo/src/app/app.module.ts @@ -4,7 +4,7 @@ import {FormsModule} from '@angular/forms'; import {HttpModule} from '@angular/http'; import {AppComponent} from './app.component'; -import {FileManagerModule} from "../../../src/filemanager.module"; +import {FileManagerModule} from '../../../main'; @NgModule({ diff --git a/src/filemanager.component.ts b/src/filemanager.component.ts index 2a66688..6a1e618 100644 --- a/src/filemanager.component.ts +++ b/src/filemanager.component.ts @@ -33,6 +33,7 @@ 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"; @Component({ selector: 'ri-filemanager', @@ -106,7 +107,7 @@ export class FileManagerComponent implements OnInit, OnChanges { public constructor(private store: Store, private treeActions: TreeActionsService, private nodeDispatcherService: NodeDispatcherService, - private treeService: TreeService, + private treeService: FileManagerApiService, private filesService: FilesService, private notifications: NotificationsService, private configuration: FileManagerConfiguration, diff --git a/src/filemanager.module.ts b/src/filemanager.module.ts index aff5c10..15b7784 100644 --- a/src/filemanager.module.ts +++ b/src/filemanager.module.ts @@ -28,6 +28,7 @@ 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'; @NgModule({ imports: [ @@ -57,6 +58,7 @@ import {SearchFileComponent} from './toolbar/searchFile/searchFile.component'; entryComponents: [ImageCropperComponent], providers: [ FileManagerActionsService, + FileManagerApiService, FileManagerConfiguration, FileManagerDispatcherService, FileManagerEffectsService, diff --git a/src/filesList/interface/IOuterFile.ts b/src/filesList/interface/IOuterFile.ts index 0a8c0ca..9931857 100644 --- a/src/filesList/interface/IOuterFile.ts +++ b/src/filesList/interface/IOuterFile.ts @@ -1,5 +1,6 @@ export interface IOuterFile { - id: string|number; + id: string | number; + folderId?: string; name: string; thumbnailUrl: string; url: string; diff --git a/src/store/IFileManagerApi.ts b/src/store/IFileManagerApi.ts new file mode 100644 index 0000000..c4a2ee2 --- /dev/null +++ b/src/store/IFileManagerApi.ts @@ -0,0 +1,14 @@ +import {Observable} from 'rxjs/Observable'; +import {IOuterNode} from '@rign/angular2-tree'; +import {IOuterFile} from '../filesList/interface/IOuterFile'; + + +export interface IFileManagerApi { + add(node: IOuterNode, parentNodeId: string): Observable; + load(nodeId: string): Observable; + move(srcNode: IOuterNode, targetNode: IOuterNode | null): Observable; + update(node: IOuterNode): Observable; + remove(nodeId: string): Observable; + + loadFiles(nodeId: string): Observable; +} diff --git a/src/store/fileManagerApi.service.ts b/src/store/fileManagerApi.service.ts new file mode 100644 index 0000000..93889ff --- /dev/null +++ b/src/store/fileManagerApi.service.ts @@ -0,0 +1,141 @@ +import {Injectable} from '@angular/core'; +import {IOuterNode} from '@rign/angular2-tree'; +import {Observable} from 'rxjs/Observable'; +import {UUID} from 'angular2-uuid'; +import {IFileManagerApi} from './IFileManagerApi'; +import {IOuterFile} from "../filesList/interface/IOuterFile"; + +@Injectable() +export class FileManagerApiService implements IFileManagerApi { + protected treeName = 'fileManagerTree'; + protected fileManagerName= 'fileManagerFiles'; + + + protected nodes: IOuterNode[]; + protected files: IOuterFile[]; + + public load(nodeId = ''): Observable { + if (!this.nodes) { + this.nodes = this.getAllDataFromLocalStorage(); + } + + const nodes = this.getChildren(nodeId); + + return Observable.of(nodes); + } + + public add(node: IOuterNode, parentNodeId: string = null): Observable { + node.parentId = parentNodeId; + node.id = UUID.UUID(); + + this.nodes.push(node); + + this.saveNodes(); + + return Observable.of(node); + } + + public move(srcNode: IOuterNode, targetNode: IOuterNode | null): Observable { + const srcId = srcNode.id; + const targetId = targetNode ? targetNode.id : null; + + const index = this.findIndexByNodeId(srcId); + + this.nodes[index].parentId = targetId; + this.saveNodes(); + + return Observable.of(this.nodes[index]); + } + + public update(node: IOuterNode): Observable { + const index = this.findIndexByNodeId(node.id); + + this.nodes[index] = node; + this.saveNodes(); + + return Observable.of(node); + } + + public remove(nodeId: string): Observable { + const index = this.findIndexByNodeId(nodeId); + const node = this.nodes[index]; + + const hasChildren = this.getChildren(nodeId).length > 0; + + if (!hasChildren) { + this.nodes.splice(index, 1); + + this.saveNodes(); + + return Observable.of(node); + } else { + return Observable.throw('Node is not empty'); + } + + } + + + public loadFiles(nodeId = ''): Observable { + + if (!this.files) { + this.files = this.getAllFileDataFromLocalStorage(); + } + + const files = this.getFilesFromFolder(nodeId); + + return Observable.of(files); + } + + + private findIndexByNodeId(nodeId: string): number { + return this.nodes.findIndex((node) => { + return node.id === nodeId; + }); + } + + private getChildren(nodeId: string): IOuterNode[] { + return this.nodes.filter((node: IOuterNode) => node.parentId === nodeId); + } + + private getFilesFromFolder(nodeId: string): IOuterFile[] { + return this.files.filter((file: IOuterFile) => file.folderId === nodeId); + } + + protected getAllDataFromLocalStorage(): IOuterNode[] { + try { + const data = localStorage.getItem(this.treeName); + + if (data) { + return JSON.parse(data); + } + + return []; + + } catch (e) { + return []; + } + } + + protected getAllFileDataFromLocalStorage(): IOuterFile[] { + try { + const data = localStorage.getItem(this.fileManagerName) + + if (data) { + return JSON.parse(data); + } + + return []; + + } catch (e) { + return []; + } + } + + private saveNodes() { + try { + localStorage.setItem(this.treeName, JSON.stringify(this.nodes)); + } catch (e) { + console.warn('State not save'); + } + } +} diff --git a/src/store/fileManagerEffects.service.ts b/src/store/fileManagerEffects.service.ts index 41979d7..4f80857 100644 --- a/src/store/fileManagerEffects.service.ts +++ b/src/store/fileManagerEffects.service.ts @@ -7,6 +7,7 @@ import {Observable} from 'rxjs/Observable'; import {IFileModel} from '../filesList/interface/IFileModel'; import {NotificationsService} from 'angular2-notifications'; import {ICropBounds} from '../crop/ICropBounds'; +import {FileManagerApiService} from "./fileManagerApi.service"; @Injectable() export class FileManagerEffectsService { @@ -14,7 +15,8 @@ export class FileManagerEffectsService { constructor(private actions$: Actions, private filesService: FilesService, private fileManagerActions: FileManagerActionsService, - private notificationService: NotificationsService) { + private notificationService: NotificationsService, + private fileManagerApiService: FileManagerApiService) { } @Effect() @@ -67,7 +69,7 @@ export class FileManagerEffectsService { } protected loadFiles(folderId: string | null): Observable { - return this.filesService.load(folderId); + return this.fileManagerApiService.loadFiles(folderId); } protected onCropFileError(file: IFileModel): void { From 043424393dc0cc38a1cf382814bff4b9c1bb5f79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ignaszewski?= Date: Mon, 10 Jul 2017 21:21:16 +0200 Subject: [PATCH 05/23] Change example --- src/filemanager.module.ts | 2 + src/filesList/fileManagerUploader.service.ts | 6 +- src/services/extendedFileUplaoder.service.ts | 28 ++++++ src/services/imageDataConverter.service.ts | 94 ++++++++++++++++++++ 4 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 src/services/extendedFileUplaoder.service.ts create mode 100644 src/services/imageDataConverter.service.ts diff --git a/src/filemanager.module.ts b/src/filemanager.module.ts index 15b7784..dfd0613 100644 --- a/src/filemanager.module.ts +++ b/src/filemanager.module.ts @@ -29,6 +29,7 @@ import {FileManagerDispatcherService} from './store/fileManagerDispatcher.servic 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"; @NgModule({ imports: [ @@ -65,6 +66,7 @@ import {FileManagerApiService} from './store/fileManagerApi.service'; FileManagerUploader, FilesService, FileTypeFilterService, + ImageDataConverter, NotificationsService, SearchFilterService, TreeService diff --git a/src/filesList/fileManagerUploader.service.ts b/src/filesList/fileManagerUploader.service.ts index 55f41f4..3aa58ce 100644 --- a/src/filesList/fileManagerUploader.service.ts +++ b/src/filesList/fileManagerUploader.service.ts @@ -1,14 +1,14 @@ import {Injectable, Inject} from '@angular/core'; -import {FileUploader} from 'ng2-file-upload'; import {IUrlConfiguration} from '../configuration/IUrlConfiguration'; +import {ExtendedFileUploader} from '../services/extendedFileUplaoder.service'; @Injectable() export class FileManagerUploader { - public uploader: FileUploader; + public uploader: ExtendedFileUploader; public constructor(@Inject('fileManagerUrls') urls: IUrlConfiguration) { - this.uploader = new FileUploader({url: urls.filesUrl}); + this.uploader = new ExtendedFileUploader({url: null}); } public clear() { diff --git a/src/services/extendedFileUplaoder.service.ts b/src/services/extendedFileUplaoder.service.ts new file mode 100644 index 0000000..10d5eed --- /dev/null +++ b/src/services/extendedFileUplaoder.service.ts @@ -0,0 +1,28 @@ +import {FileItem, FileUploader, FileUploaderOptions} from 'ng2-file-upload'; +import {IImageDataProperties, ImageDataConverter} from "./imageDataConverter.service"; + +export class ExtendedFileUploader extends FileUploader { + + public uploadItem(value: FileItem): void { + if (this.options.url) { + super.uploadItem(value); + } else { + let imageDataConverter = new ImageDataConverter(); + + if (this.isUploading) { + return; + } + + this.isUploading = true; + + imageDataConverter.getProperties(value._file) + .subscribe((file: IImageDataProperties) => { + console.log(file); + this.isUploading = false; + + value._onComplete('', 200, {}); + value._onSuccess('', 200, {}); + }) + } + } +} diff --git a/src/services/imageDataConverter.service.ts b/src/services/imageDataConverter.service.ts new file mode 100644 index 0000000..9e4c02a --- /dev/null +++ b/src/services/imageDataConverter.service.ts @@ -0,0 +1,94 @@ +import {Observable} from 'rxjs/Observable'; +import {Injectable} from "@angular/core"; + +export interface IFileDataProperties { + id: string; + name: string; + size: number; + data: string; + type: string; +} + +export interface IImageDataProperties extends IFileDataProperties { + width?: number; + height?: number; +} + +export interface IImageDimensions { + width: number; + height: number; +} + +@Injectable() +export class ImageDataConverter { + public getProperties(file: File): Observable { + let properties: IImageDataProperties = { + id: '12', + name: file.name, + size: file.size, + type: file.type, + data: '' + }; + + let reader = this.getBase64FromFile(file); + + return reader + .concatMap((data: string) => { + properties.data = data; + + if (properties.type.indexOf('image') === 0) { + return this.getImageDimensions(data); + } else { + return Observable.of({width: 0, height: 0}); + } + }) + .map((dimensions: IImageDimensions) => { + properties.width = dimensions.width; + properties.height = dimensions.height; + + return properties; + }) + } + + /** + * Create observable which return image as base64 data + * + * @param file + * @return Observable + */ + private getBase64FromFile(file: File): Observable { + let reader = new FileReader(); + reader.readAsDataURL(file); + + + return Observable.fromEvent(reader, 'load') + .map(() => { + console.log(reader); + return reader.result; + }); + }; + + /** + * Create observable which return dimensions of the image + * + * @param data + * @returns {Observable} + */ + private getImageDimensions(data: string): Observable { + let image = new Image(); + image.src = data; + image.style.display = 'none'; + + const loadImage = Observable.fromEvent(image, 'load') + .map(() => { + return { + width: image.naturalWidth, + height: image.naturalHeight + } + }); + + document.body.appendChild(image); + + return loadImage; + } +} From 92af2a184473f6e9b752df0b53a0a3504d8caf88 Mon Sep 17 00:00:00 2001 From: Rafal Ignaszewski Date: Tue, 11 Jul 2017 08:59:17 +0200 Subject: [PATCH 06/23] Upload file to memory --- demo/src/app/app.module.ts | 2 +- src/configuration/IUrlConfiguration.ts | 2 +- src/filemanager.module.ts | 4 +++- src/filesList/fileManagerUploader.service.ts | 8 ++++---- src/services/extendedFileUplaoder.service.ts | 11 ++++++----- src/services/imageDataConverter.service.ts | 3 ++- src/store/fileManagerApi.service.ts | 2 +- src/toolbar/toolbar.component.ts | 1 + 8 files changed, 19 insertions(+), 14 deletions(-) diff --git a/demo/src/app/app.module.ts b/demo/src/app/app.module.ts index 10ec28c..37d4b87 100644 --- a/demo/src/app/app.module.ts +++ b/demo/src/app/app.module.ts @@ -18,7 +18,7 @@ import {FileManagerModule} from '../../../main'; HttpModule ], providers: [ - {provide: 'fileManagerUrls', useValue: {foldersUrl: '/api/folder', filesUrl: '/api/files', folderMoveUrl: '/api/folder/move'}} + {provide: 'fileManagerUrls', useValue: {foldersUrl: '/api/folder', filesUrl: null, folderMoveUrl: '/api/folder/move'}} ], bootstrap: [AppComponent] }) diff --git a/src/configuration/IUrlConfiguration.ts b/src/configuration/IUrlConfiguration.ts index f2460ff..2f8dac3 100644 --- a/src/configuration/IUrlConfiguration.ts +++ b/src/configuration/IUrlConfiguration.ts @@ -1,5 +1,5 @@ export class IUrlConfiguration { - filesUrl: string; + filesUrl: string | null; foldersUrl: string; folderMoveUrl: string; } diff --git a/src/filemanager.module.ts b/src/filemanager.module.ts index dfd0613..2159ea1 100644 --- a/src/filemanager.module.ts +++ b/src/filemanager.module.ts @@ -29,11 +29,13 @@ import {FileManagerDispatcherService} from './store/fileManagerDispatcher.servic 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 {ImageDataConverter} from './services/imageDataConverter.service'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; @NgModule({ imports: [ BrowserModule, + BrowserAnimationsModule, ConfirmModule, EffectsModule.run(FileManagerEffectsService), FormsModule, diff --git a/src/filesList/fileManagerUploader.service.ts b/src/filesList/fileManagerUploader.service.ts index 3aa58ce..30690e5 100644 --- a/src/filesList/fileManagerUploader.service.ts +++ b/src/filesList/fileManagerUploader.service.ts @@ -8,7 +8,7 @@ export class FileManagerUploader { public constructor(@Inject('fileManagerUrls') urls: IUrlConfiguration) { - this.uploader = new ExtendedFileUploader({url: null}); + this.uploader = new ExtendedFileUploader({url: urls.filesUrl}); } public clear() { @@ -25,14 +25,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/services/extendedFileUplaoder.service.ts b/src/services/extendedFileUplaoder.service.ts index 10d5eed..987dfec 100644 --- a/src/services/extendedFileUplaoder.service.ts +++ b/src/services/extendedFileUplaoder.service.ts @@ -1,5 +1,5 @@ -import {FileItem, FileUploader, FileUploaderOptions} from 'ng2-file-upload'; -import {IImageDataProperties, ImageDataConverter} from "./imageDataConverter.service"; +import {FileItem, FileUploader} from 'ng2-file-upload'; +import {IImageDataProperties, ImageDataConverter} from './imageDataConverter.service'; export class ExtendedFileUploader extends FileUploader { @@ -8,6 +8,7 @@ export class ExtendedFileUploader extends FileUploader { super.uploadItem(value); } else { let imageDataConverter = new ImageDataConverter(); + this._onProgressItem(value, 0); if (this.isUploading) { return; @@ -15,13 +16,13 @@ export class ExtendedFileUploader extends FileUploader { this.isUploading = true; + this._onProgressItem(value, 50); imageDataConverter.getProperties(value._file) .subscribe((file: IImageDataProperties) => { - console.log(file); this.isUploading = false; - value._onComplete('', 200, {}); - value._onSuccess('', 200, {}); + this._onProgressItem(value, 100); + this._onCompleteItem(value, JSON.stringify(file), 200, {}); }) } } diff --git a/src/services/imageDataConverter.service.ts b/src/services/imageDataConverter.service.ts index 9e4c02a..42b73a4 100644 --- a/src/services/imageDataConverter.service.ts +++ b/src/services/imageDataConverter.service.ts @@ -1,5 +1,6 @@ import {Observable} from 'rxjs/Observable'; import {Injectable} from "@angular/core"; +import {UUID} from 'angular2-uuid'; export interface IFileDataProperties { id: string; @@ -23,7 +24,7 @@ export interface IImageDimensions { export class ImageDataConverter { public getProperties(file: File): Observable { let properties: IImageDataProperties = { - id: '12', + id: UUID.UUID(), name: file.name, size: file.size, type: file.type, diff --git a/src/store/fileManagerApi.service.ts b/src/store/fileManagerApi.service.ts index 93889ff..19ab585 100644 --- a/src/store/fileManagerApi.service.ts +++ b/src/store/fileManagerApi.service.ts @@ -3,7 +3,7 @@ import {IOuterNode} from '@rign/angular2-tree'; import {Observable} from 'rxjs/Observable'; import {UUID} from 'angular2-uuid'; import {IFileManagerApi} from './IFileManagerApi'; -import {IOuterFile} from "../filesList/interface/IOuterFile"; +import {IOuterFile} from '../filesList/interface/IOuterFile'; @Injectable() export class FileManagerApiService implements IFileManagerApi { diff --git a/src/toolbar/toolbar.component.ts b/src/toolbar/toolbar.component.ts index a721e91..059cc63 100644 --- a/src/toolbar/toolbar.component.ts +++ b/src/toolbar/toolbar.component.ts @@ -54,6 +54,7 @@ export class Toolbar implements OnChanges { }; this.fileManagerUploader.uploader.onCompleteItem = (item: any, response: any, status: number, headers: any) => { + console.log(response) if (status === 200) { this.fileManagerDispatcher.uploadSuccess(JSON.parse(response)); } else { From 97be2545f0d13a0f882cd2f4b0e47cbbcd0b55a0 Mon Sep 17 00:00:00 2001 From: Rafal Ignaszewski Date: Tue, 11 Jul 2017 09:59:02 +0200 Subject: [PATCH 07/23] File upload to local storage --- src/filesList/interface/IOuterFile.ts | 2 + src/services/extendedFileUplaoder.service.ts | 8 ++- src/services/imageDataConverter.service.ts | 12 ++-- src/store/fileManagerActions.service.ts | 10 +++ src/store/fileManagerApi.service.ts | 72 +++++++++++++++++--- src/store/fileManagerDispatcher.service.ts | 4 ++ src/store/fileManagerEffects.service.ts | 16 ++++- src/toolbar/toolbar.component.ts | 3 +- 8 files changed, 105 insertions(+), 22 deletions(-) diff --git a/src/filesList/interface/IOuterFile.ts b/src/filesList/interface/IOuterFile.ts index 9931857..77e0871 100644 --- a/src/filesList/interface/IOuterFile.ts +++ b/src/filesList/interface/IOuterFile.ts @@ -7,4 +7,6 @@ export interface IOuterFile { width: number; height: number; mime: string; + data?: string; + size?: number; } diff --git a/src/services/extendedFileUplaoder.service.ts b/src/services/extendedFileUplaoder.service.ts index 987dfec..54e064b 100644 --- a/src/services/extendedFileUplaoder.service.ts +++ b/src/services/extendedFileUplaoder.service.ts @@ -1,5 +1,5 @@ import {FileItem, FileUploader} from 'ng2-file-upload'; -import {IImageDataProperties, ImageDataConverter} from './imageDataConverter.service'; +import {IFileDataProperties, ImageDataConverter} from './imageDataConverter.service'; export class ExtendedFileUploader extends FileUploader { @@ -16,9 +16,11 @@ export class ExtendedFileUploader extends FileUploader { this.isUploading = true; + const header = this.options.headers.find((object: any) => object.name === 'folderId'); + this._onProgressItem(value, 50); - imageDataConverter.getProperties(value._file) - .subscribe((file: IImageDataProperties) => { + imageDataConverter.getProperties(value._file, header.value) + .subscribe((file: IFileDataProperties) => { this.isUploading = false; this._onProgressItem(value, 100); diff --git a/src/services/imageDataConverter.service.ts b/src/services/imageDataConverter.service.ts index 42b73a4..553404d 100644 --- a/src/services/imageDataConverter.service.ts +++ b/src/services/imageDataConverter.service.ts @@ -1,16 +1,14 @@ import {Observable} from 'rxjs/Observable'; -import {Injectable} from "@angular/core"; import {UUID} from 'angular2-uuid'; +import {Injectable} from '@angular/core'; export interface IFileDataProperties { id: string; + folderId: string; name: string; size: number; data: string; type: string; -} - -export interface IImageDataProperties extends IFileDataProperties { width?: number; height?: number; } @@ -22,9 +20,10 @@ export interface IImageDimensions { @Injectable() export class ImageDataConverter { - public getProperties(file: File): Observable { - let properties: IImageDataProperties = { + public getProperties(file: File, folderId: string): Observable { + let properties: IFileDataProperties = { id: UUID.UUID(), + folderId: folderId, name: file.name, size: file.size, type: file.type, @@ -64,7 +63,6 @@ export class ImageDataConverter { return Observable.fromEvent(reader, 'load') .map(() => { - console.log(reader); return reader.result; }); }; diff --git a/src/store/fileManagerActions.service.ts b/src/store/fileManagerActions.service.ts index 0d6d78f..c5c5cc4 100644 --- a/src/store/fileManagerActions.service.ts +++ b/src/store/fileManagerActions.service.ts @@ -23,6 +23,7 @@ export class FileManagerActionsService { static FILEMANAGER_DELETE_FILE_SUCCESS = 'FILEMANAGER_DELETE_FILE_SUCCESS'; static FILEMANAGER_LOAD_FILES = 'FILEMANAGER_LOAD_FILES'; static FILEMANAGER_LOAD_FILES_SUCCESS = 'FILEMANAGER_LOAD_FILES_SUCCESS'; + static FILEMANAGER_UPLOAD_FILE = 'FILEMANAGER_UPLOAD_FILE'; static FILEMANAGER_UPLOAD_FILE_ERROR = 'FILEMANAGER_UPLOAD_FILE_ERROR'; static FILEMANAGER_UPLOAD_FILE_SUCCESS = 'FILEMANAGER_UPLOAD_FILE_SUCCESS'; @@ -82,6 +83,15 @@ export class FileManagerActionsService { } } + public upload(file: IOuterFile): IFileManagerAction { + return { + type: FileManagerActionsService.FILEMANAGER_UPLOAD_FILE, + payload: { + files: [file] + } + } + } + public uploadSuccess(file: IOuterFile): IFileManagerAction { return { type: FileManagerActionsService.FILEMANAGER_UPLOAD_FILE_SUCCESS, diff --git a/src/store/fileManagerApi.service.ts b/src/store/fileManagerApi.service.ts index 19ab585..1c48255 100644 --- a/src/store/fileManagerApi.service.ts +++ b/src/store/fileManagerApi.service.ts @@ -4,15 +4,16 @@ import {Observable} from 'rxjs/Observable'; import {UUID} from 'angular2-uuid'; import {IFileManagerApi} from './IFileManagerApi'; import {IOuterFile} from '../filesList/interface/IOuterFile'; +import {IFileDataProperties} from '../services/imageDataConverter.service'; @Injectable() export class FileManagerApiService implements IFileManagerApi { protected treeName = 'fileManagerTree'; - protected fileManagerName= 'fileManagerFiles'; + protected fileManagerName = 'fileManagerFiles'; protected nodes: IOuterNode[]; - protected files: IOuterFile[]; + protected files: IFileDataProperties[]; public load(nodeId = ''): Observable { if (!this.nodes) { @@ -71,19 +72,29 @@ export class FileManagerApiService implements IFileManagerApi { } else { return Observable.throw('Node is not empty'); } - } public loadFiles(nodeId = ''): Observable { - if (!this.files) { this.files = this.getAllFileDataFromLocalStorage(); } const files = this.getFilesFromFolder(nodeId); - return Observable.of(files); + const newFiles: IOuterFile[] = files.map((file: IFileDataProperties) => { + return this.convertLocalData2IOuterFile(file); + }); + + return Observable.of(newFiles); + } + + public uploadFile(file: IOuterFile): Observable { + const fileData = this.convertIOuterFile2LocalData(file); + this.files.push(fileData); + this.saveFiles(); + + return Observable.of(this.convertLocalData2IOuterFile(fileData)); } @@ -97,8 +108,8 @@ export class FileManagerApiService implements IFileManagerApi { return this.nodes.filter((node: IOuterNode) => node.parentId === nodeId); } - private getFilesFromFolder(nodeId: string): IOuterFile[] { - return this.files.filter((file: IOuterFile) => file.folderId === nodeId); + private getFilesFromFolder(nodeId: string): IFileDataProperties[] { + return this.files.filter((file: IFileDataProperties) => file.folderId === nodeId); } protected getAllDataFromLocalStorage(): IOuterNode[] { @@ -116,9 +127,9 @@ export class FileManagerApiService implements IFileManagerApi { } } - protected getAllFileDataFromLocalStorage(): IOuterFile[] { + protected getAllFileDataFromLocalStorage(): IFileDataProperties[] { try { - const data = localStorage.getItem(this.fileManagerName) + const data = localStorage.getItem(this.fileManagerName); if (data) { return JSON.parse(data); @@ -138,4 +149,47 @@ export class FileManagerApiService implements IFileManagerApi { console.warn('State not save'); } } + + private saveFiles() { + try { + localStorage.setItem(this.fileManagerName, JSON.stringify(this.files)); + } catch (e) { + console.warn('State not save'); + } + } + + /** + * + * @param file + * @returns {{id: string, folderId: string, name: string, thumbnailUrl: string, url: string, width: number, height: number, mime: string}} + */ + private convertLocalData2IOuterFile(file: IFileDataProperties): IOuterFile { + return { + id: file.id, + folderId: file.folderId, + name: file.name, + thumbnailUrl: file.data, + url: file.data, + width: 0, + height: 0, + mime: file.type, + size: file.size + } + } + + /** + * + * @param file + * @returns {{id: (any|string), folderId: string, name: string, type: string, data: string, size: number}} + */ + private convertIOuterFile2LocalData(file: IOuterFile): IFileDataProperties { + return { + id: file.id.toString(), + folderId: file.folderId, + name: file.name, + type: file.mime, + data: file.data, + size: file.size + } + } } diff --git a/src/store/fileManagerDispatcher.service.ts b/src/store/fileManagerDispatcher.service.ts index 457fe25..68838be 100644 --- a/src/store/fileManagerDispatcher.service.ts +++ b/src/store/fileManagerDispatcher.service.ts @@ -28,6 +28,10 @@ export class FileManagerDispatcherService { this.store.dispatch(this.fileManagerActions.uploadError(file)); } + public upload(file: IOuterFile) { + this.store.dispatch(this.fileManagerActions.upload(file)); + } + public uploadSuccess(file: IOuterFile) { this.store.dispatch(this.fileManagerActions.uploadSuccess(file)); } diff --git a/src/store/fileManagerEffects.service.ts b/src/store/fileManagerEffects.service.ts index 4f80857..2c3790c 100644 --- a/src/store/fileManagerEffects.service.ts +++ b/src/store/fileManagerEffects.service.ts @@ -7,7 +7,7 @@ import {Observable} from 'rxjs/Observable'; import {IFileModel} from '../filesList/interface/IFileModel'; import {NotificationsService} from 'angular2-notifications'; import {ICropBounds} from '../crop/ICropBounds'; -import {FileManagerApiService} from "./fileManagerApi.service"; +import {FileManagerApiService} from './fileManagerApi.service'; @Injectable() export class FileManagerEffectsService { @@ -50,6 +50,16 @@ export class FileManagerEffectsService { .catch(() => Observable.of(this.onDeleteFileError(action.payload.file))) ); + + @Effect() + public uploadFile$ = this.actions$ + .ofType(FileManagerActionsService.FILEMANAGER_UPLOAD_FILE) + .switchMap((action: IFileManagerAction) => this.uploadFile(action.payload.files[0]) + .map((result: IOuterFile): IFileManagerAction => { + return this.fileManagerActions.uploadSuccess(result); + }) + ); + public uploadError$ = this.actions$ .ofType(FileManagerActionsService.FILEMANAGER_UPLOAD_FILE_ERROR) .map((action: IFileManagerAction) => { @@ -72,6 +82,10 @@ export class FileManagerEffectsService { return this.fileManagerApiService.loadFiles(folderId); } + protected uploadFile(file: IOuterFile): Observable { + return this.fileManagerApiService.uploadFile(file); + } + protected onCropFileError(file: IFileModel): void { console.warn('[FILEMANAGER] Can not crop file' + file.name); this.notificationService.error('Crop Image', 'Image has not been cropped'); diff --git a/src/toolbar/toolbar.component.ts b/src/toolbar/toolbar.component.ts index 059cc63..bab249c 100644 --- a/src/toolbar/toolbar.component.ts +++ b/src/toolbar/toolbar.component.ts @@ -54,9 +54,8 @@ export class Toolbar implements OnChanges { }; this.fileManagerUploader.uploader.onCompleteItem = (item: any, response: any, status: number, headers: any) => { - console.log(response) if (status === 200) { - this.fileManagerDispatcher.uploadSuccess(JSON.parse(response)); + this.fileManagerDispatcher.upload(JSON.parse(response)); } else { this.fileManagerDispatcher.uploadError(JSON.parse(response)); } From 890bbb110dccef5f58de46857f53975d979805f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ignaszewski?= Date: Mon, 31 Jul 2017 20:28:17 +0200 Subject: [PATCH 08/23] Remove file from list --- src/filemanager.component.ts | 4 ++-- src/filesList/file.model.ts | 2 +- src/filesList/filesList.component.ts | 8 +++---- src/filesList/interface/IOuterFile.ts | 2 +- src/store/fileManagerApi.service.ts | 29 ++++++++++++++++++++----- src/store/fileManagerEffects.service.ts | 6 ++++- 6 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/filemanager.component.ts b/src/filemanager.component.ts index 6a1e618..e5a1a5e 100644 --- a/src/filemanager.component.ts +++ b/src/filemanager.component.ts @@ -89,11 +89,11 @@ export class FileManagerComponent implements OnInit, OnChanges { public isCropMode: boolean = false; public notificationOptions = { - position: ["bottom", "right"], + position: ['bottom', 'right'], timeOut: 3000, lastOnBottom: false, preventDuplicates: true, - rtl: true, + rtl: false, showProgressBar: true, pauseOnHover: true }; diff --git a/src/filesList/file.model.ts b/src/filesList/file.model.ts index 74da4f5..d689229 100644 --- a/src/filesList/file.model.ts +++ b/src/filesList/file.model.ts @@ -56,7 +56,7 @@ export class FileModel implements IFileModel { } public getMime() { - return this._orgData.mime; + return this._orgData.type; } public getWidth(): number { diff --git a/src/filesList/filesList.component.ts b/src/filesList/filesList.component.ts index 98179b3..2b267c7 100644 --- a/src/filesList/filesList.component.ts +++ b/src/filesList/filesList.component.ts @@ -5,10 +5,10 @@ import {IFileModel} from './interface/IFileModel'; import {ConfirmOptions, Position} from 'angular2-bootstrap-confirm'; import {Positioning} from 'angular2-bootstrap-confirm/position'; import {FileManagerConfiguration} from '../configuration/fileManagerConfiguration.service'; -import {FileManagerActionsService, IFileManagerAction} from '../store/fileManagerActions.service'; +import {IFileManagerAction} from '../store/fileManagerActions.service'; import {FileManagerDispatcherService} from '../store/fileManagerDispatcher.service'; import {NotificationsService} from 'angular2-notifications'; -import {Actions} from '@ngrx/effects'; +import {FileManagerEffectsService} from '../store/fileManagerEffects.service'; @Component({ selector: 'ri-files-list', @@ -29,9 +29,9 @@ export class FilesListComponent { public constructor(public configuration: FileManagerConfiguration, private fileManagerDispatcher: FileManagerDispatcherService, notifications: NotificationsService, - actions$: Actions) { + fileManagerEffects: FileManagerEffectsService) { - actions$.ofType(FileManagerActionsService.FILEMANAGER_DELETE_FILE_SUCCESS) + fileManagerEffects.deleteFileSuccess$ .subscribe((action: IFileManagerAction) => { notifications.success('File delete', `${action.payload.file.name} has been deleted`); }); diff --git a/src/filesList/interface/IOuterFile.ts b/src/filesList/interface/IOuterFile.ts index 77e0871..1e698e3 100644 --- a/src/filesList/interface/IOuterFile.ts +++ b/src/filesList/interface/IOuterFile.ts @@ -6,7 +6,7 @@ export interface IOuterFile { url: string; width: number; height: number; - mime: string; + type: string; data?: string; size?: number; } diff --git a/src/store/fileManagerApi.service.ts b/src/store/fileManagerApi.service.ts index 1c48255..ee55aef 100644 --- a/src/store/fileManagerApi.service.ts +++ b/src/store/fileManagerApi.service.ts @@ -89,6 +89,19 @@ export class FileManagerApiService implements IFileManagerApi { return Observable.of(newFiles); } + public removeFile(file: IOuterFile): Observable { + const index = this.findIndexByFileId(file.id.toString()); + + if (index === -1) { + return Observable.of(false); + } + + this.files.splice(index, 1); + this.saveFiles(); + + return Observable.of(true); + } + public uploadFile(file: IOuterFile): Observable { const fileData = this.convertIOuterFile2LocalData(file); this.files.push(fileData); @@ -104,6 +117,10 @@ export class FileManagerApiService implements IFileManagerApi { }); } + private findIndexByFileId(fileId: string): number { + return this.files.findIndex((file) => file.id === fileId); + } + private getChildren(nodeId: string): IOuterNode[] { return this.nodes.filter((node: IOuterNode) => node.parentId === nodeId); } @@ -170,9 +187,9 @@ export class FileManagerApiService implements IFileManagerApi { name: file.name, thumbnailUrl: file.data, url: file.data, - width: 0, - height: 0, - mime: file.type, + width: file.width, + height: file.height, + type: file.type, size: file.size } } @@ -187,9 +204,11 @@ export class FileManagerApiService implements IFileManagerApi { id: file.id.toString(), folderId: file.folderId, name: file.name, - type: file.mime, + type: file.type, data: file.data, - size: file.size + size: file.size, + width: file.width, + height: file.height } } } diff --git a/src/store/fileManagerEffects.service.ts b/src/store/fileManagerEffects.service.ts index 2c3790c..6b08511 100644 --- a/src/store/fileManagerEffects.service.ts +++ b/src/store/fileManagerEffects.service.ts @@ -70,12 +70,16 @@ export class FileManagerEffectsService { .ofType(FileManagerActionsService.FILEMANAGER_CROP_FILE_SUCCESS); + public deleteFileSuccess$ = this.actions$ + .ofType(FileManagerActionsService.FILEMANAGER_DELETE_FILE_SUCCESS); + + protected cropFile(file: IFileModel, bounds: ICropBounds): Observable { return this.filesService.crop(file, bounds); } protected deleteFile(file: IFileModel): Observable { - return this.filesService.remove(file); + return this.fileManagerApiService.removeFile(file.toJSON()); } protected loadFiles(folderId: string | null): Observable { From ad7d5e7a468c392fedc7eab1abb2a5fb1a770ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ignaszewski?= Date: Tue, 1 Aug 2017 18:10:02 +0200 Subject: [PATCH 09/23] Crop image --- src/crop/crop.component.ts | 43 +++++++++++++------------ src/filemanager.component.ts | 12 +++---- src/store/fileManagerActions.service.ts | 10 ++++++ src/store/fileManagerApi.service.ts | 17 ++++++++++ src/store/fileManagerEffects.service.ts | 12 +++++-- 5 files changed, 65 insertions(+), 29 deletions(-) diff --git a/src/crop/crop.component.ts b/src/crop/crop.component.ts index d24c898..7d9f07c 100644 --- a/src/crop/crop.component.ts +++ b/src/crop/crop.component.ts @@ -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; } @@ -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); } diff --git a/src/filemanager.component.ts b/src/filemanager.component.ts index e5a1a5e..5f9af28 100644 --- a/src/filemanager.component.ts +++ b/src/filemanager.component.ts @@ -90,7 +90,7 @@ export class FileManagerComponent implements OnInit, OnChanges { public notificationOptions = { position: ['bottom', 'right'], - timeOut: 3000, + timeOut: 0, lastOnBottom: false, preventDuplicates: true, rtl: false, @@ -222,11 +222,11 @@ export class FileManagerComponent implements OnInit, OnChanges { this.isCropMode = true; this.currentSelectedFile = fileEventData.file; } - - @log - public onCropFile(event: any) { - this.fileManagerDispatcher.cropFile(event.file, event.bounds); - } + // + // @log + // public onCropFile(event: any) { + // this.fileManagerDispatcher.cropFile(event.file, event.bounds); + // } @log public onSelectFile(event: FileModel) { diff --git a/src/store/fileManagerActions.service.ts b/src/store/fileManagerActions.service.ts index c5c5cc4..1d40207 100644 --- a/src/store/fileManagerActions.service.ts +++ b/src/store/fileManagerActions.service.ts @@ -19,6 +19,7 @@ export interface IFileManagerAction extends Action { export class FileManagerActionsService { static FILEMANAGER_CROP_FILE = 'FILEMANAGER_CROP_FILE'; static FILEMANAGER_CROP_FILE_SUCCESS = 'FILEMANAGER_CROP_FILE_SUCCESS'; + static FILEMANAGER_CROP_FILE_ERROR = 'FILEMANAGER_CROP_FILE_ERROR'; static FILEMANAGER_DELETE_FILE = 'FILEMANAGER_DELETE_FILE'; static FILEMANAGER_DELETE_FILE_SUCCESS = 'FILEMANAGER_DELETE_FILE_SUCCESS'; static FILEMANAGER_LOAD_FILES = 'FILEMANAGER_LOAD_FILES'; @@ -46,6 +47,15 @@ export class FileManagerActionsService { } } + public cropFileError(file: IFileModel): IFileManagerAction { + return { + type: FileManagerActionsService.FILEMANAGER_CROP_FILE_ERROR, + payload: { + file: file + } + } + } + public deleteFile(file: IFileModel): IFileManagerAction { return { type: FileManagerActionsService.FILEMANAGER_DELETE_FILE, diff --git a/src/store/fileManagerApi.service.ts b/src/store/fileManagerApi.service.ts index ee55aef..0101f25 100644 --- a/src/store/fileManagerApi.service.ts +++ b/src/store/fileManagerApi.service.ts @@ -5,6 +5,7 @@ import {UUID} from 'angular2-uuid'; import {IFileManagerApi} from './IFileManagerApi'; import {IOuterFile} from '../filesList/interface/IOuterFile'; import {IFileDataProperties} from '../services/imageDataConverter.service'; +import {ICropBounds} from "../crop/ICropBounds"; @Injectable() export class FileManagerApiService implements IFileManagerApi { @@ -74,7 +75,23 @@ export class FileManagerApiService implements IFileManagerApi { } } + /** + * Crop file + * + * @param {IOuterFile} file + * @param {ICropBounds} bounds + * @returns {Observable} + */ + public cropFile(file: IOuterFile, bounds: ICropBounds): Observable { + return Observable.throw('This functionality is not available with LocalStorage'); + } + /** + * Load files from directory + * + * @param {string} nodeId + * @returns {Observable} + */ public loadFiles(nodeId = ''): Observable { if (!this.files) { this.files = this.getAllFileDataFromLocalStorage(); diff --git a/src/store/fileManagerEffects.service.ts b/src/store/fileManagerEffects.service.ts index 6b08511..c8738d8 100644 --- a/src/store/fileManagerEffects.service.ts +++ b/src/store/fileManagerEffects.service.ts @@ -37,7 +37,7 @@ export class FileManagerEffectsService { this.notificationService.success('Crop Image', 'Image has been cropped'); return this.fileManagerActions.cropFileSuccess(action.payload.file); }) - .catch(() => Observable.of(this.onCropFileError(action.payload.file))) + .catch(() => Observable.of(this.fileManagerActions.cropFileError(action.payload.file))) ); @Effect() @@ -70,12 +70,18 @@ export class FileManagerEffectsService { .ofType(FileManagerActionsService.FILEMANAGER_CROP_FILE_SUCCESS); + public cropFileError$ = this.actions$ + .ofType(FileManagerActionsService.FILEMANAGER_CROP_FILE_ERROR) + .subscribe((action: IFileManagerAction) => { + this.onCropFileError(action.payload.file); + }); + public deleteFileSuccess$ = this.actions$ .ofType(FileManagerActionsService.FILEMANAGER_DELETE_FILE_SUCCESS); protected cropFile(file: IFileModel, bounds: ICropBounds): Observable { - return this.filesService.crop(file, bounds); + return this.fileManagerApiService.cropFile(file.toJSON(), bounds); } protected deleteFile(file: IFileModel): Observable { @@ -92,7 +98,7 @@ export class FileManagerEffectsService { protected onCropFileError(file: IFileModel): void { console.warn('[FILEMANAGER] Can not crop file' + file.name); - this.notificationService.error('Crop Image', 'Image has not been cropped'); + this.notificationService.alert('Crop Image', 'Image has not been cropped'); } protected onDeleteFileError(file: IFileModel): void { From b7038dfe9b4f107f2f101b26f100d677d3d34755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ignaszewski?= Date: Mon, 7 Aug 2017 18:33:49 +0200 Subject: [PATCH 10/23] Limit to local storage size - display warning if local storage overloaded --- src/store/fileManagerApi.service.ts | 53 ++++++++++++++++++++----- src/store/fileManagerEffects.service.ts | 3 ++ 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/store/fileManagerApi.service.ts b/src/store/fileManagerApi.service.ts index 0101f25..ea6a935 100644 --- a/src/store/fileManagerApi.service.ts +++ b/src/store/fileManagerApi.service.ts @@ -32,9 +32,12 @@ export class FileManagerApiService implements IFileManagerApi { this.nodes.push(node); - this.saveNodes(); + if (this.saveNodes()) { + return Observable.of(node); + } else { + return Observable.empty(); + } - return Observable.of(node); } public move(srcNode: IOuterNode, targetNode: IOuterNode | null): Observable { @@ -44,18 +47,25 @@ export class FileManagerApiService implements IFileManagerApi { const index = this.findIndexByNodeId(srcId); this.nodes[index].parentId = targetId; - this.saveNodes(); - return Observable.of(this.nodes[index]); + if (this.saveNodes()) { + return Observable.of(this.nodes[index]); + } else { + return Observable.empty(); + } + } public update(node: IOuterNode): Observable { const index = this.findIndexByNodeId(node.id); this.nodes[index] = node; - this.saveNodes(); - return Observable.of(node); + if (this.saveNodes()) { + return Observable.of(node); + } else { + return Observable.empty(); + } } public remove(nodeId: string): Observable { @@ -122,9 +132,12 @@ export class FileManagerApiService implements IFileManagerApi { public uploadFile(file: IOuterFile): Observable { const fileData = this.convertIOuterFile2LocalData(file); this.files.push(fileData); - this.saveFiles(); - return Observable.of(this.convertLocalData2IOuterFile(fileData)); + if (this.saveFiles()) { + return Observable.of(this.convertLocalData2IOuterFile(fileData)); + } else { + return Observable.throw('Upload error'); + } } @@ -179,16 +192,34 @@ export class FileManagerApiService implements IFileManagerApi { private saveNodes() { try { localStorage.setItem(this.treeName, JSON.stringify(this.nodes)); + + return true; } catch (e) { - console.warn('State not save'); + console.warn('State not save. Reload previous state.'); + + this.files = null; + this.nodes = null; + + this.load(); + + return false; } } - private saveFiles() { + private saveFiles(): boolean { try { localStorage.setItem(this.fileManagerName, JSON.stringify(this.files)); + + return true; } catch (e) { - console.warn('State not save'); + console.warn('State not save. Reload previous data.'); + const nodeId = this.files[(this.files.length - 1)].folderId || null; + + this.files = null; + + this.load(nodeId); + + return false; } } diff --git a/src/store/fileManagerEffects.service.ts b/src/store/fileManagerEffects.service.ts index c8738d8..1a67b14 100644 --- a/src/store/fileManagerEffects.service.ts +++ b/src/store/fileManagerEffects.service.ts @@ -58,6 +58,9 @@ export class FileManagerEffectsService { .map((result: IOuterFile): IFileManagerAction => { return this.fileManagerActions.uploadSuccess(result); }) + .catch(() => { + return Observable.empty() + }) ); public uploadError$ = this.actions$ From fc50da1620c8e7d36e90332b1f118b5355bf580a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ignaszewski?= Date: Mon, 7 Aug 2017 19:01:01 +0200 Subject: [PATCH 11/23] Simple notification service --- src/filemanager.component.ts | 22 +++++++++------- src/filemanager.module.ts | 2 ++ src/services/FilemanagerNotifcations.ts | 19 ++++++++++++++ src/store/fileManagerApi.service.ts | 22 +++++++++++++--- src/store/fileManagerEffects.service.ts | 35 +++++++++++++++++++------ 5 files changed, 80 insertions(+), 20 deletions(-) create mode 100644 src/services/FilemanagerNotifcations.ts diff --git a/src/filemanager.component.ts b/src/filemanager.component.ts index 5f9af28..c15c569 100644 --- a/src/filemanager.component.ts +++ b/src/filemanager.component.ts @@ -33,7 +33,8 @@ 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 {FileManagerApiService} from './store/fileManagerApi.service'; +import {FilemanagerNotifcations, INotification} from './services/FilemanagerNotifcations'; @Component({ selector: 'ri-filemanager', @@ -42,7 +43,7 @@ import {FileManagerApiService} from "./store/fileManagerApi.service"; templateUrl: './filemanager.html' }) export class FileManagerComponent implements OnInit, OnChanges { - @Input() multiSelection: boolean = false; + @Input() multiSelection = false; @Output() onSingleFileSelect = new EventEmitter(); @ViewChild(TreeComponent) @@ -90,7 +91,7 @@ export class FileManagerComponent implements OnInit, OnChanges { public notificationOptions = { position: ['bottom', 'right'], - timeOut: 0, + timeOut: 3000, lastOnBottom: false, preventDuplicates: true, rtl: false, @@ -114,9 +115,17 @@ export class FileManagerComponent implements OnInit, OnChanges { private fileManagerDispatcher: FileManagerDispatcherService, private fileTypeFilter: FileTypeFilterService, private searchFilterService: SearchFilterService, - private fileManagerEffects: FileManagerEffectsService) { + 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() { @@ -222,11 +231,6 @@ export class FileManagerComponent implements OnInit, OnChanges { this.isCropMode = true; this.currentSelectedFile = fileEventData.file; } - // - // @log - // public onCropFile(event: any) { - // this.fileManagerDispatcher.cropFile(event.file, event.bounds); - // } @log public onSelectFile(event: FileModel) { diff --git a/src/filemanager.module.ts b/src/filemanager.module.ts index 2159ea1..cc27690 100644 --- a/src/filemanager.module.ts +++ b/src/filemanager.module.ts @@ -31,6 +31,7 @@ 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'; @NgModule({ imports: [ @@ -65,6 +66,7 @@ import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; FileManagerConfiguration, FileManagerDispatcherService, FileManagerEffectsService, + FilemanagerNotifcations, FileManagerUploader, FilesService, FileTypeFilterService, diff --git a/src/services/FilemanagerNotifcations.ts b/src/services/FilemanagerNotifcations.ts new file mode 100644 index 0000000..5ef4d64 --- /dev/null +++ b/src/services/FilemanagerNotifcations.ts @@ -0,0 +1,19 @@ +import {Subject} from 'rxjs/Subject'; + +export interface INotification { + type: 'alert' | 'error' | 'success'; + title: string; + message?: string; +} + +export class FilemanagerNotifcations { + private notifciation$ = new Subject(); + + public sendNotification(notification: INotification): void { + this.notifciation$.next(notification); + } + + public getNotificationStream(): Subject { + return this.notifciation$; + } +} diff --git a/src/store/fileManagerApi.service.ts b/src/store/fileManagerApi.service.ts index ea6a935..00a4fb6 100644 --- a/src/store/fileManagerApi.service.ts +++ b/src/store/fileManagerApi.service.ts @@ -5,7 +5,8 @@ import {UUID} from 'angular2-uuid'; import {IFileManagerApi} from './IFileManagerApi'; import {IOuterFile} from '../filesList/interface/IOuterFile'; import {IFileDataProperties} from '../services/imageDataConverter.service'; -import {ICropBounds} from "../crop/ICropBounds"; +import {ICropBounds} from '../crop/ICropBounds'; +import {FilemanagerNotifcations} from '../services/FilemanagerNotifcations'; @Injectable() export class FileManagerApiService implements IFileManagerApi { @@ -16,6 +17,12 @@ export class FileManagerApiService implements IFileManagerApi { protected nodes: IOuterNode[]; protected files: IFileDataProperties[]; + + public constructor(private filemanagerNotfication: FilemanagerNotifcations) { + + } + + public load(nodeId = ''): Observable { if (!this.nodes) { this.nodes = this.getAllDataFromLocalStorage(); @@ -195,7 +202,11 @@ export class FileManagerApiService implements IFileManagerApi { return true; } catch (e) { - console.warn('State not save. Reload previous state.'); + this.filemanagerNotfication.sendNotification({ + type: 'error', + title: 'State is not saved.', + message: 'Reload previous state.' + }); this.files = null; this.nodes = null; @@ -212,7 +223,12 @@ export class FileManagerApiService implements IFileManagerApi { return true; } catch (e) { - console.warn('State not save. Reload previous data.'); + this.filemanagerNotfication.sendNotification({ + type: 'error', + title: 'State is not saved.', + message: 'Reload previous state.' + }); + const nodeId = this.files[(this.files.length - 1)].folderId || null; this.files = null; diff --git a/src/store/fileManagerEffects.service.ts b/src/store/fileManagerEffects.service.ts index 1a67b14..5e2e04a 100644 --- a/src/store/fileManagerEffects.service.ts +++ b/src/store/fileManagerEffects.service.ts @@ -5,9 +5,9 @@ import {FileManagerActionsService, IFileManagerAction} from './fileManagerAction import {IOuterFile} from '../filesList/interface/IOuterFile'; import {Observable} from 'rxjs/Observable'; import {IFileModel} from '../filesList/interface/IFileModel'; -import {NotificationsService} from 'angular2-notifications'; import {ICropBounds} from '../crop/ICropBounds'; import {FileManagerApiService} from './fileManagerApi.service'; +import {FilemanagerNotifcations} from '../services/FilemanagerNotifcations'; @Injectable() export class FileManagerEffectsService { @@ -15,7 +15,7 @@ export class FileManagerEffectsService { constructor(private actions$: Actions, private filesService: FilesService, private fileManagerActions: FileManagerActionsService, - private notificationService: NotificationsService, + private filemanagerNotfication: FilemanagerNotifcations, private fileManagerApiService: FileManagerApiService) { } @@ -34,7 +34,11 @@ export class FileManagerEffectsService { .ofType(FileManagerActionsService.FILEMANAGER_CROP_FILE) .switchMap((action: IFileManagerAction) => this.cropFile(action.payload.file, action.payload.bounds) .map((result: IOuterFile): IFileManagerAction => { - this.notificationService.success('Crop Image', 'Image has been cropped'); + this.filemanagerNotfication.sendNotification({ + type: 'success', + title: 'Crop Image.', + message: 'Image has been cropped.' + }); return this.fileManagerActions.cropFileSuccess(action.payload.file); }) .catch(() => Observable.of(this.fileManagerActions.cropFileError(action.payload.file))) @@ -66,7 +70,11 @@ export class FileManagerEffectsService { public uploadError$ = this.actions$ .ofType(FileManagerActionsService.FILEMANAGER_UPLOAD_FILE_ERROR) .map((action: IFileManagerAction) => { - this.notificationService.alert('File upload', `${action.payload.file.name} exists on the server in this directory`); + this.filemanagerNotfication.sendNotification({ + type: 'alert', + title: 'File upload', + message: `${action.payload.file.name} exists on the server in this directory` + }); }); public cropFileSuccess$ = this.actions$ @@ -100,15 +108,26 @@ export class FileManagerEffectsService { } protected onCropFileError(file: IFileModel): void { - console.warn('[FILEMANAGER] Can not crop file' + file.name); - this.notificationService.alert('Crop Image', 'Image has not been cropped'); + this.filemanagerNotfication.sendNotification({ + type: 'alert', + title: 'Crop Image', + message: '[FILEMANAGER] Can not crop file' + }); } protected onDeleteFileError(file: IFileModel): void { - console.warn('[FILEMANAGER] Can not delete file' + file.name); + this.filemanagerNotfication.sendNotification({ + type: 'error', + title: 'Delete file', + message: '[FILEMANAGER] Can not delete file' + file.name + }); } protected onLoadFilesError(folderId: string): void { - console.warn('[FILEMANAGER] Can not load files for folder ' + folderId); + this.filemanagerNotfication.sendNotification({ + type: 'error', + title: 'Load files', + message: '[FILEMANAGER] Can not load files for folder ' + folderId + }); } } From a414fad6be401cee858df7788641277dfd032261 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ignaszewski?= Date: Mon, 7 Aug 2017 19:38:46 +0200 Subject: [PATCH 12/23] Filemanager configuration --- demo/src/app/app.component.html | 4 +- demo/src/app/app.component.ts | 17 ++++---- demo/src/app/app.module.ts | 13 +++++- .../IFileManagerConfiguration.ts | 8 ++++ .../fileManagerConfiguration.service.ts | 40 +++++++++++-------- src/configuration/tree.service.ts | 14 +++---- src/filemanager.component.ts | 1 + src/filemanager.module.ts | 9 ++++- src/filesList/fileManagerUploader.service.ts | 6 +-- 9 files changed, 74 insertions(+), 38 deletions(-) create mode 100644 src/configuration/IFileManagerConfiguration.ts diff --git a/demo/src/app/app.component.html b/demo/src/app/app.component.html index 84e174b..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.ts b/demo/src/app/app.component.ts index 1323479..1dd94f9 100644 --- a/demo/src/app/app.component.ts +++ b/demo/src/app/app.component.ts @@ -1,17 +1,18 @@ import {Component} from '@angular/core'; import {ISelectFile} from "../../../main"; +import {FileManagerConfiguration} from '../../../src/configuration/fileManagerConfiguration.service'; @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 constructor(public fileManagerConfiguration: FileManagerConfiguration) { - public isMultiSelection = false; + } - public toggleMultiSelection() { - this.isMultiSelection = !this.isMultiSelection; - } + public toggleMultiSelection() { + this.fileManagerConfiguration.isMultiSelection = !this.fileManagerConfiguration.isMultiSelection; + } } diff --git a/demo/src/app/app.module.ts b/demo/src/app/app.module.ts index 37d4b87..1df827d 100644 --- a/demo/src/app/app.module.ts +++ b/demo/src/app/app.module.ts @@ -5,7 +5,18 @@ import {HttpModule} from '@angular/http'; import {AppComponent} from './app.component'; import {FileManagerModule} from '../../../main'; +import {IFileManagerConfiguration} from '../../../src/configuration/IFileManagerConfiguration'; +const fileManagerConfiguration: IFileManagerConfiguration = { + urls: { + foldersUrl: '/api/folder', + filesUrl: null, + folderMoveUrl: '/api/folder/move' + }, + // isMultiSelection: true, + mimeTypes: ['images/jpg', 'images/jpeg', 'images/png'], + maxFileSize: 50 * 1024 * 1024 +} @NgModule({ declarations: [ @@ -18,7 +29,7 @@ import {FileManagerModule} from '../../../main'; HttpModule ], providers: [ - {provide: 'fileManagerUrls', useValue: {foldersUrl: '/api/folder', filesUrl: null, folderMoveUrl: '/api/folder/move'}} + {provide: 'fileManagerConfiguration', useValue: fileManagerConfiguration} ], bootstrap: [AppComponent] }) 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/fileManagerConfiguration.service.ts b/src/configuration/fileManagerConfiguration.service.ts index 47de2bf..3f3ec3a 100644 --- a/src/configuration/fileManagerConfiguration.service.ts +++ b/src/configuration/fileManagerConfiguration.service.ts @@ -3,12 +3,25 @@ import {Injectable, Inject} from '@angular/core'; import {IFileTypeFilter} from '../toolbar/interface/IFileTypeFilter'; import {ICropSize} from '../crop/ICropSize'; import {IUrlConfiguration} from './IUrlConfiguration'; +import {IFileManagerConfiguration} from './IFileManagerConfiguration'; @Injectable() export class FileManagerConfiguration { - public contextMenuItems: IContextMenu[] = []; - public isMultiSelection = false; + public allowedCropSize: ICropSize[] = [ + { + name: 'Landscape', + width: 300, + height: 100 + }, + { + name: 'Portrait', + width: 200, + height: 300 + } + ]; + + public contextMenuItems: IContextMenu[] = []; public fileTypesFilter: IFileTypeFilter[] = [ { @@ -46,21 +59,16 @@ export class FileManagerConfiguration { 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; + public mimeTypes: string[] | null; - constructor(@Inject('fileManagerUrls') urls: IUrlConfiguration) { - this.fileUrl = urls.filesUrl; + constructor(@Inject('fileManagerConfiguration') configuration: IFileManagerConfiguration) { + 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 a232cbb..5b3fc0a 100644 --- a/src/configuration/tree.service.ts +++ b/src/configuration/tree.service.ts @@ -1,19 +1,19 @@ import {Injectable, Inject} from '@angular/core'; import {NodeService} from '@rign/angular2-tree'; import {Http} from '@angular/http'; -import {IUrlConfiguration} from './IUrlConfiguration'; +import {IFileManagerConfiguration} from './IFileManagerConfiguration'; @Injectable() export class TreeService extends NodeService { - public constructor(protected 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, - moveUrl: urls.folderMoveUrl + addUrl: configuration.urls.foldersUrl, + getUrl: configuration.urls.foldersUrl, + updateUrl: configuration.urls.foldersUrl, + removeUrl: configuration.urls.foldersUrl, + moveUrl: configuration.urls.folderMoveUrl }; } } diff --git a/src/filemanager.component.ts b/src/filemanager.component.ts index c15c569..48af6e3 100644 --- a/src/filemanager.component.ts +++ b/src/filemanager.component.ts @@ -35,6 +35,7 @@ import {FileManagerDispatcherService} from './store/fileManagerDispatcher.servic import {FileManagerEffectsService} from './store/fileManagerEffects.service'; import {FileManagerApiService} from './store/fileManagerApi.service'; import {FilemanagerNotifcations, INotification} from './services/FilemanagerNotifcations'; +import {IFileManagerConfiguration} from './configuration/IFileManagerConfiguration'; @Component({ selector: 'ri-filemanager', diff --git a/src/filemanager.module.ts b/src/filemanager.module.ts index cc27690..72887f7 100644 --- a/src/filemanager.module.ts +++ b/src/filemanager.module.ts @@ -1,4 +1,4 @@ -import {NgModule, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +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'; @@ -32,6 +32,7 @@ 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 {IFileManagerConfiguration} from './configuration/IFileManagerConfiguration'; @NgModule({ imports: [ @@ -79,5 +80,11 @@ import {FilemanagerNotifcations} from './services/FilemanagerNotifcations'; schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class FileManagerModule { + constructor(@Inject('fileManagerConfiguration') private configuration: IFileManagerConfiguration) { + console.log(configuration); + } + private initDefaultConfiguration () { + this.configuration.isMultiSelection = this.configuration.isMultiSelection || false; + } } diff --git a/src/filesList/fileManagerUploader.service.ts b/src/filesList/fileManagerUploader.service.ts index 30690e5..f0f0d48 100644 --- a/src/filesList/fileManagerUploader.service.ts +++ b/src/filesList/fileManagerUploader.service.ts @@ -1,14 +1,14 @@ import {Injectable, Inject} from '@angular/core'; -import {IUrlConfiguration} from '../configuration/IUrlConfiguration'; import {ExtendedFileUploader} from '../services/extendedFileUplaoder.service'; +import {IFileManagerConfiguration} from '../configuration/IFileManagerConfiguration'; @Injectable() export class FileManagerUploader { public uploader: ExtendedFileUploader; - public constructor(@Inject('fileManagerUrls') urls: IUrlConfiguration) { - this.uploader = new ExtendedFileUploader({url: urls.filesUrl}); + public constructor(@Inject('fileManagerConfiguration') configuration: IFileManagerConfiguration) { + this.uploader = new ExtendedFileUploader({url: configuration.urls.filesUrl}); } public clear() { From 1fe9e7cdc4ba3c9917d6d35a5ca57500386120fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ignaszewski?= Date: Wed, 9 Aug 2017 21:12:24 +0200 Subject: [PATCH 13/23] Configuration options - filter mime types and single file size --- README.md | 16 ++++++++++++-- demo/src/app/app.module.ts | 6 ++--- .../fileManagerConfiguration.service.ts | 1 - src/filemanager.module.ts | 7 ------ src/filesList/fileManagerUploader.service.ts | 11 ++++++++-- src/services/extendedFileUplaoder.service.ts | 22 ++++++++++++++++++- src/toolbar/toolbar.component.ts | 1 + 7 files changed, 48 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index c07eed6..47378ab 100644 --- a/README.md +++ b/README.md @@ -65,13 +65,25 @@ In your project put this line To override endpoints to manage files and directories provide special provider in you module + + const fileManagerConfiguration: IFileManagerConfiguration = { + urls: { + foldersUrl: '/api/folder', + filesUrl: null, + folderMoveUrl: '/api/folder/move' + }, + isMultiSelection: true, //allow multiselect (default: false) + mimeTypes: ['image/jpg', 'image/jpeg', 'image/png'], // allow upload only selected mime types (default: [] - all types) + maxFileSize: 50 * 1024 // max size (KB) of the file (default: 0 - no limits) + } + @NgModule({ ... providers: [ ... { - provide: 'fileManagerUrls', - useValue: {foldersUrl: '/api/filemanager/folder', filesUrl: '/api/filemanager/file'} + provide: 'fileManagerConfiguration', + useValue: fileManagerConfiguration } ] ... diff --git a/demo/src/app/app.module.ts b/demo/src/app/app.module.ts index 1df827d..629f0cf 100644 --- a/demo/src/app/app.module.ts +++ b/demo/src/app/app.module.ts @@ -13,9 +13,9 @@ const fileManagerConfiguration: IFileManagerConfiguration = { filesUrl: null, folderMoveUrl: '/api/folder/move' }, - // isMultiSelection: true, - mimeTypes: ['images/jpg', 'images/jpeg', 'images/png'], - maxFileSize: 50 * 1024 * 1024 + isMultiSelection: true, + mimeTypes: ['image/jpg', 'image/jpeg', 'image/png'], + maxFileSize: 50 * 1024 } @NgModule({ diff --git a/src/configuration/fileManagerConfiguration.service.ts b/src/configuration/fileManagerConfiguration.service.ts index 3f3ec3a..9903d56 100644 --- a/src/configuration/fileManagerConfiguration.service.ts +++ b/src/configuration/fileManagerConfiguration.service.ts @@ -2,7 +2,6 @@ import {IContextMenu} from '@rign/angular2-tree'; import {Injectable, Inject} from '@angular/core'; import {IFileTypeFilter} from '../toolbar/interface/IFileTypeFilter'; import {ICropSize} from '../crop/ICropSize'; -import {IUrlConfiguration} from './IUrlConfiguration'; import {IFileManagerConfiguration} from './IFileManagerConfiguration'; @Injectable() diff --git a/src/filemanager.module.ts b/src/filemanager.module.ts index 72887f7..2d2491d 100644 --- a/src/filemanager.module.ts +++ b/src/filemanager.module.ts @@ -80,11 +80,4 @@ import {IFileManagerConfiguration} from './configuration/IFileManagerConfigurati schemas: [CUSTOM_ELEMENTS_SCHEMA] }) export class FileManagerModule { - constructor(@Inject('fileManagerConfiguration') private configuration: IFileManagerConfiguration) { - console.log(configuration); - } - - private initDefaultConfiguration () { - this.configuration.isMultiSelection = this.configuration.isMultiSelection || false; - } } diff --git a/src/filesList/fileManagerUploader.service.ts b/src/filesList/fileManagerUploader.service.ts index f0f0d48..2273258 100644 --- a/src/filesList/fileManagerUploader.service.ts +++ b/src/filesList/fileManagerUploader.service.ts @@ -1,14 +1,21 @@ 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: 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('fileManagerConfiguration') configuration: IFileManagerConfiguration) { - this.uploader = new ExtendedFileUploader({url: configuration.urls.filesUrl}); + this.uploader = new ExtendedFileUploader(options, filemanagerNotification); } public clear() { diff --git a/src/services/extendedFileUplaoder.service.ts b/src/services/extendedFileUplaoder.service.ts index 54e064b..3baa9e9 100644 --- a/src/services/extendedFileUplaoder.service.ts +++ b/src/services/extendedFileUplaoder.service.ts @@ -1,8 +1,28 @@ -import {FileItem, FileUploader} from 'ng2-file-upload'; +import {FileItem, FileLikeObject, FileUploader, FileUploaderOptions} from 'ng2-file-upload'; import {IFileDataProperties, ImageDataConverter} from './imageDataConverter.service'; +import {FilemanagerNotifcations, INotification} from './FilemanagerNotifcations'; export class ExtendedFileUploader extends FileUploader { + public constructor(options: FileUploaderOptions, private filemanagerNotification: FilemanagerNotifcations) { + super(options); + } + + public onWhenAddingFileFailed(item: FileLikeObject, filter: any, options: FileUploaderOptions) { + const notification: INotification = { + type: 'alert', + title: 'Add file to queue', + message: `File not add to queue` + }; + + if (filter.name === 'fileSize') { + notification.message = `File size is too large - max size is ${options.maxFileSize / 1024} KB`; + } else { + notification.message = `File mime type "${item.type}" is not allowed`; + } + this.filemanagerNotification.sendNotification(notification) + } + public uploadItem(value: FileItem): void { if (this.options.url) { super.uploadItem(value); diff --git a/src/toolbar/toolbar.component.ts b/src/toolbar/toolbar.component.ts index bab249c..3d9907f 100644 --- a/src/toolbar/toolbar.component.ts +++ b/src/toolbar/toolbar.component.ts @@ -8,6 +8,7 @@ import {Positioning} from "angular2-bootstrap-confirm/position"; import {FileManagerConfiguration} from "../configuration/fileManagerConfiguration.service"; import {FileManagerUploader} from "../filesList/fileManagerUploader.service"; import {FileManagerDispatcherService} from '../store/fileManagerDispatcher.service'; +import {FileItem} from 'ng2-file-upload'; @Component({ selector: 'toolbar', From ac562de77b225c24ad5741121eab8619a08b1532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ignaszewski?= Date: Thu, 17 Aug 2017 17:32:44 +0200 Subject: [PATCH 14/23] Multiselection --- src/filemanager.component.ts | 6 +- src/filesList/file.model.ts | 1 + src/filesList/files-list.less | 10 +-- src/filesList/files.html | 7 +- src/filesList/filesList.component.ts | 12 ++-- src/filesList/interface/IFileModel.ts | 2 +- src/filesList/interface/IOuterFile.ts | 1 + src/services/imageDataConverter.service.ts | 1 + src/store/fileManagerActions.service.ts | 60 ++++++++++++++++ src/store/fileManagerApi.service.ts | 3 +- src/store/fileManagerDispatcher.service.ts | 24 +++++++ src/store/fileManagerReducer.ts | 82 +++++++++++++++++++++- src/toolbar/toolbar.component.ts | 1 + 13 files changed, 193 insertions(+), 17 deletions(-) diff --git a/src/filemanager.component.ts b/src/filemanager.component.ts index 48af6e3..40aa33a 100644 --- a/src/filemanager.component.ts +++ b/src/filemanager.component.ts @@ -253,13 +253,13 @@ export class FileManagerComponent implements OnInit, OnChanges { // }); 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(); diff --git a/src/filesList/file.model.ts b/src/filesList/file.model.ts index d689229..32fcc86 100644 --- a/src/filesList/file.model.ts +++ b/src/filesList/file.model.ts @@ -37,6 +37,7 @@ export class FileModel implements IFileModel { this._orgData = data; this.name = data.name; + this.selected = data.selected || false; } public toJSON() { 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..ade5c82 100644 --- a/src/filesList/files.html +++ b/src/filesList/files.html @@ -1,6 +1,9 @@
-
- +
+ +
+ +
{{file.name}} diff --git a/src/filesList/filesList.component.ts b/src/filesList/filesList.component.ts index 2b267c7..19857f8 100644 --- a/src/filesList/filesList.component.ts +++ b/src/filesList/filesList.component.ts @@ -66,7 +66,7 @@ export class FilesListComponent { this.files.map((file) => file.selected = !file.selected); } - public openPreview(file: FileModel) { + public openPreview(file: FileModel): void { let fileEvent: IFileEvent = { eventName: 'onPreviewFile', file: file @@ -74,7 +74,7 @@ export class FilesListComponent { this.onPreviewFile.emit(fileEvent); } - public openCrop(file: FileModel) { + public openCrop(file: FileModel): void { let fileEvent: IFileEvent = { eventName: 'onCropFile', file: file @@ -82,7 +82,11 @@ export class FilesListComponent { this.onCropFile.emit(fileEvent); } - public clickImage(file: FileModel) { - this.onSelectFile.next(file); + public toggleSelection(file: IFileModel): void { + if (file.selected) { + this.fileManagerDispatcher.unSelectFile(file); + } else { + this.fileManagerDispatcher.selectFile(file); + } } } diff --git a/src/filesList/interface/IFileModel.ts b/src/filesList/interface/IFileModel.ts index 809b107..cb62f65 100644 --- a/src/filesList/interface/IFileModel.ts +++ b/src/filesList/interface/IFileModel.ts @@ -1,4 +1,4 @@ -import {IOuterFile} from "./IOuterFile"; +import {IOuterFile} from './IOuterFile'; export interface IFileModel { name: string; thumbnailUrl: string; diff --git a/src/filesList/interface/IOuterFile.ts b/src/filesList/interface/IOuterFile.ts index 1e698e3..609a4f1 100644 --- a/src/filesList/interface/IOuterFile.ts +++ b/src/filesList/interface/IOuterFile.ts @@ -9,4 +9,5 @@ export interface IOuterFile { type: string; data?: string; size?: number; + selected?: boolean; } diff --git a/src/services/imageDataConverter.service.ts b/src/services/imageDataConverter.service.ts index 553404d..2116e5b 100644 --- a/src/services/imageDataConverter.service.ts +++ b/src/services/imageDataConverter.service.ts @@ -11,6 +11,7 @@ export interface IFileDataProperties { type: string; width?: number; height?: number; + selected?: boolean; } export interface IImageDimensions { diff --git a/src/store/fileManagerActions.service.ts b/src/store/fileManagerActions.service.ts index 1d40207..b5ad7f0 100644 --- a/src/store/fileManagerActions.service.ts +++ b/src/store/fileManagerActions.service.ts @@ -22,8 +22,15 @@ export class FileManagerActionsService { static FILEMANAGER_CROP_FILE_ERROR = 'FILEMANAGER_CROP_FILE_ERROR'; static FILEMANAGER_DELETE_FILE = 'FILEMANAGER_DELETE_FILE'; static FILEMANAGER_DELETE_FILE_SUCCESS = 'FILEMANAGER_DELETE_FILE_SUCCESS'; + static FILEMANAGER_DELETE_FILE_SELECTION = 'FILEMANAGER_DELETE_FILE_SELECTION'; + static FILEMANAGER_DELETE_FILE_SELECTION_SUCCESS = 'FILEMANAGER_DELETE_FILE_SELECTION_SUCCESS'; + static FILEMANAGER_INVERSE_FILE_SELECTION = 'FILEMANAGER_INVERSE_FILE_SELECTION'; static FILEMANAGER_LOAD_FILES = 'FILEMANAGER_LOAD_FILES'; static FILEMANAGER_LOAD_FILES_SUCCESS = 'FILEMANAGER_LOAD_FILES_SUCCESS'; + static FILEMANAGER_SELECT_ALL = 'FILEMANAGER_SELECT_ALL'; + static FILEMANAGER_SELECT_FILE = 'FILEMANAGER_SELECT_FILE'; + static FILEMANAGER_UNSELECT_FILE = 'FILEMANAGER_UNSELECT_FILE'; + static FILEMANAGER_UNSELECT_ALL = 'FILEMANAGER_UNSELECT_ALL'; static FILEMANAGER_UPLOAD_FILE = 'FILEMANAGER_UPLOAD_FILE'; static FILEMANAGER_UPLOAD_FILE_ERROR = 'FILEMANAGER_UPLOAD_FILE_ERROR'; static FILEMANAGER_UPLOAD_FILE_SUCCESS = 'FILEMANAGER_UPLOAD_FILE_SUCCESS'; @@ -74,6 +81,20 @@ export class FileManagerActionsService { } } + public deleteSelectedFiles(): IFileManagerAction { + return { + type: FileManagerActionsService.FILEMANAGER_DELETE_FILE_SELECTION, + payload: {} + } + } + + public deleteSelectedFilesSuccess(files: IOuterFile[]): IFileManagerAction { + return { + type: FileManagerActionsService.FILEMANAGER_DELETE_FILE_SELECTION, + payload: {files} + } + } + public loadFiles(folderId: string): IFileManagerAction { return { type: FileManagerActionsService.FILEMANAGER_LOAD_FILES, @@ -83,6 +104,13 @@ export class FileManagerActionsService { } } + public inverseFileSelection(): IFileManagerAction { + return { + type: FileManagerActionsService.FILEMANAGER_INVERSE_FILE_SELECTION, + payload: {} + } + } + public loadFilesSuccess(folderId: string, files: IOuterFile[]): IFileManagerAction { return { type: FileManagerActionsService.FILEMANAGER_LOAD_FILES_SUCCESS, @@ -93,6 +121,38 @@ export class FileManagerActionsService { } } + public selectAllFiles(): IFileManagerAction { + return { + type: FileManagerActionsService.FILEMANAGER_SELECT_ALL, + payload: {} + } + } + + public selectFile(file: IFileModel): IFileManagerAction { + return { + type: FileManagerActionsService.FILEMANAGER_SELECT_FILE, + payload: { + file: file + } + } + } + + public unSelectAll(): IFileManagerAction { + return { + type: FileManagerActionsService.FILEMANAGER_UNSELECT_ALL, + payload: {} + } + } + + public unSelectFile(file: IFileModel): IFileManagerAction { + return { + type: FileManagerActionsService.FILEMANAGER_UNSELECT_FILE, + payload: { + file: file + } + } + } + public upload(file: IOuterFile): IFileManagerAction { return { type: FileManagerActionsService.FILEMANAGER_UPLOAD_FILE, diff --git a/src/store/fileManagerApi.service.ts b/src/store/fileManagerApi.service.ts index 00a4fb6..8e82ff1 100644 --- a/src/store/fileManagerApi.service.ts +++ b/src/store/fileManagerApi.service.ts @@ -10,6 +10,7 @@ import {FilemanagerNotifcations} from '../services/FilemanagerNotifcations'; @Injectable() export class FileManagerApiService implements IFileManagerApi { + protected treeName = 'fileManagerTree'; protected fileManagerName = 'fileManagerFiles'; @@ -17,9 +18,7 @@ export class FileManagerApiService implements IFileManagerApi { protected nodes: IOuterNode[]; protected files: IFileDataProperties[]; - public constructor(private filemanagerNotfication: FilemanagerNotifcations) { - } diff --git a/src/store/fileManagerDispatcher.service.ts b/src/store/fileManagerDispatcher.service.ts index 68838be..76d4026 100644 --- a/src/store/fileManagerDispatcher.service.ts +++ b/src/store/fileManagerDispatcher.service.ts @@ -20,10 +20,34 @@ export class FileManagerDispatcherService { this.store.dispatch(this.fileManagerActions.deleteFile(file)); } + public deleteSelectedFiles(): void { + this.store.dispatch(this.fileManagerActions.deleteSelectedFiles()); + } + + public inverseSelection(): void { + this.store.dispatch(this.fileManagerActions.inverseFileSelection()); + } + public loadFiles(folderId: string | null): void { this.store.dispatch(this.fileManagerActions.loadFiles(folderId)); } + public selectAllFiles(): void { + this.store.dispatch(this.fileManagerActions.selectAllFiles()); + } + + public selectFile(file: IFileModel): void { + this.store.dispatch(this.fileManagerActions.selectFile(file)); + } + + public unSelectAllFiles(): void { + this.store.dispatch(this.fileManagerActions.unSelectAll()); + } + + public unSelectFile(file: IFileModel): void { + this.store.dispatch(this.fileManagerActions.unSelectFile(file)); + } + public uploadError(file: IOuterFile) { this.store.dispatch(this.fileManagerActions.uploadError(file)); } diff --git a/src/store/fileManagerReducer.ts b/src/store/fileManagerReducer.ts index d496574..db5c471 100644 --- a/src/store/fileManagerReducer.ts +++ b/src/store/fileManagerReducer.ts @@ -1,5 +1,8 @@ import {IOuterFile} from '../filesList/interface/IOuterFile'; import {FileManagerActionsService, IFileManagerAction} from './fileManagerActions.service'; +import {State} from '@ngrx/store'; +import {IFileModel} from '../filesList/interface/IFileModel'; +import {FileModel} from '../filesList/file.model'; export interface IFileManagerState extends Array { } @@ -16,6 +19,14 @@ function cropFile(state: IFileManagerState, action: IFileManagerAction): IFileMa return newState; } +function inverseFilesSelection(state: IFileManagerState): IFileManagerState { + return state.map((file: IOuterFile) => { + file.selected = !file.selected; + + return file; + }); +} + function loadFiles(state: IFileManagerState, action: IFileManagerAction): IFileManagerState { return [...action.payload.files]; } @@ -26,20 +37,89 @@ function removeFile(state: IFileManagerState, action: IFileManagerAction): IFile return [...state.filter((file: IOuterFile) => file.id !== id)] } +function removeSelectedFiles(state: IFileManagerState): IFileManagerState { + return [...state.filter((file: IOuterFile) => !file.selected)] +} + +function selectFile(state: IFileManagerState, action: IFileManagerAction): IFileManagerState { + return state.map((file: IOuterFile) => { + if (file.id === action.payload.file.getId()) { + file.selected = true; + + return copyOuterFile(file); + } + + return file; + }); +} + +function selectAllFiles(state: IFileManagerState): IFileManagerState { + let newState = [...state]; + return newState.map((file: IOuterFile) => { + file.selected = true; + + return file; + }); +} + function uploadFiles(state: IFileManagerState, action: IFileManagerAction): IFileManagerState { return [...state, ...action.payload.files]; } +function unSelectAllFiles(state: IFileManagerState): IFileManagerState { + return state.map((file: IOuterFile) => { + file.selected = false; + + return file; + }); +} + +function unSelectFile(state: IFileManagerState, action: IFileManagerAction): IFileManagerState { + const file = getFileById(state, action.payload.file.getId()); + + file.selected = false; + + return [...state]; +} + + +function getFileById(state: IFileManagerState, id: string | number): IOuterFile { + const files = state.filter((file: IOuterFile) => file.id === id); + + return files.length > 0 ? files[0] : null; +} + + +function copyOuterFile(inputFile: IOuterFile): IOuterFile { + let {id, folderId, name, thumbnailUrl, url, width, height, type, data, size, selected} = inputFile; + + return {id, folderId, name, thumbnailUrl, url, width, height, type, data, size, selected}; +} + + export function fileManagerReducer(state: IFileManagerState = [], action: IFileManagerAction): IFileManagerState { switch (action.type) { case FileManagerActionsService.FILEMANAGER_CROP_FILE_SUCCESS: return cropFile(state, action); + case FileManagerActionsService.FILEMANAGER_INVERSE_FILE_SELECTION: + return inverseFilesSelection(state); + case FileManagerActionsService.FILEMANAGER_DELETE_FILE_SELECTION: + return removeSelectedFiles(state); case FileManagerActionsService.FILEMANAGER_DELETE_FILE_SUCCESS: return removeFile(state, action); case FileManagerActionsService.FILEMANAGER_LOAD_FILES_SUCCESS: return loadFiles(state, action); + case FileManagerActionsService.FILEMANAGER_SELECT_ALL: + return selectAllFiles(state); + case FileManagerActionsService.FILEMANAGER_SELECT_FILE: + return selectFile(state, action); + case FileManagerActionsService.FILEMANAGER_UNSELECT_ALL: + return unSelectAllFiles(state); + case FileManagerActionsService.FILEMANAGER_UNSELECT_FILE: + return unSelectFile(state, action); case FileManagerActionsService.FILEMANAGER_UPLOAD_FILE_SUCCESS: return uploadFiles(state, action); + case FileManagerActionsService.FILEMANAGER_DELETE_FILE_SELECTION_SUCCESS: case FileManagerActionsService.FILEMANAGER_CROP_FILE: case FileManagerActionsService.FILEMANAGER_DELETE_FILE: case FileManagerActionsService.FILEMANAGER_LOAD_FILES: @@ -47,5 +127,5 @@ export function fileManagerReducer(state: IFileManagerState = [], action: IFileM default: return state; } - } + diff --git a/src/toolbar/toolbar.component.ts b/src/toolbar/toolbar.component.ts index 3d9907f..a144c7c 100644 --- a/src/toolbar/toolbar.component.ts +++ b/src/toolbar/toolbar.component.ts @@ -54,6 +54,7 @@ export class Toolbar implements OnChanges { this.onUpload.emit(this.currentFolderId || ''); }; + this.fileManagerUploader.uploader.onCompleteItem = (item: any, response: any, status: number, headers: any) => { if (status === 200) { this.fileManagerDispatcher.upload(JSON.parse(response)); From 2629b31471685b0a2f52d524193ac606cf0de172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ignaszewski?= Date: Sun, 3 Sep 2017 10:32:25 +0200 Subject: [PATCH 15/23] Example with real backend --- angular-cli.json | 29 ++ demo/backend/index.js | 2 +- demo/src/app/app.component.ts | 1 - .../appWithBackend.component.html | 10 + .../appWithBackend.component.less | 3 + .../appWithBackend.component.ts | 17 ++ .../appWithBackend/appWithBackend.module.ts | 40 +++ .../fileManagerBackendApi.service.ts | 272 ++++++++++++++++++ demo/src/mainWithBackend.ts | 11 + main.ts | 14 +- package.json | 4 +- .../fileManagerConfiguration.service.ts | 3 + src/filemanager.component.ts | 6 +- src/filemanager.module.ts | 3 - src/filesList/fileManagerUploader.service.ts | 3 +- src/filesList/files.service.ts | 49 ---- src/services/imageDataConverter.service.ts | 2 +- src/store/fileManagerEffects.service.ts | 2 - 18 files changed, 406 insertions(+), 65 deletions(-) create mode 100644 demo/src/appWithBackend/appWithBackend.component.html create mode 100644 demo/src/appWithBackend/appWithBackend.component.less create mode 100644 demo/src/appWithBackend/appWithBackend.component.ts create mode 100644 demo/src/appWithBackend/appWithBackend.module.ts create mode 100644 demo/src/appWithBackend/fileManagerBackendApi.service.ts create mode 100644 demo/src/mainWithBackend.ts delete mode 100644 src/filesList/files.service.ts diff --git a/angular-cli.json b/angular-cli.json index 605383c..f9cf3ba 100644 --- a/angular-cli.json +++ b/angular-cli.json @@ -6,6 +6,7 @@ }, "apps": [ { + "name": "withoutBackend", "root": "demo/src", "outDir": "dist", "assets": [ @@ -31,6 +32,34 @@ "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": [ + "../../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" + } } ], "lint": [ diff --git a/demo/backend/index.js b/demo/backend/index.js index 0c764c6..0947435 100644 --- a/demo/backend/index.js +++ b/demo/backend/index.js @@ -182,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 }; diff --git a/demo/src/app/app.component.ts b/demo/src/app/app.component.ts index 1dd94f9..ff00618 100644 --- a/demo/src/app/app.component.ts +++ b/demo/src/app/app.component.ts @@ -1,5 +1,4 @@ import {Component} from '@angular/core'; -import {ISelectFile} from "../../../main"; import {FileManagerConfiguration} from '../../../src/configuration/fileManagerConfiguration.service'; @Component({ 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..d9f17c8 --- /dev/null +++ b/demo/src/appWithBackend/appWithBackend.component.ts @@ -0,0 +1,17 @@ +import {Component} from '@angular/core'; +import {FileManagerConfiguration} from '../../../src/configuration/fileManagerConfiguration.service'; + +@Component({ + selector: 'app-root', + templateUrl: './appWithBackend.component.html', + styleUrls: ['./appWithBackend.component.less'] +}) +export class AppWithBackendComponent { + public constructor(public fileManagerConfiguration: FileManagerConfiguration) { + + } + + public toggleMultiSelection() { + this.fileManagerConfiguration.isMultiSelection = !this.fileManagerConfiguration.isMultiSelection; + } +} diff --git a/demo/src/appWithBackend/appWithBackend.module.ts b/demo/src/appWithBackend/appWithBackend.module.ts new file mode 100644 index 0000000..e844aa2 --- /dev/null +++ b/demo/src/appWithBackend/appWithBackend.module.ts @@ -0,0 +1,40 @@ +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} from '../../../main'; +import {IFileManagerConfiguration} from '../../../src/configuration/IFileManagerConfiguration'; +import {FileManagerBackendApiService} from './fileManagerBackendApi.service'; + +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: [ + FileManagerBackendApiService, + {provide: 'fileManagerConfiguration', useValue: fileManagerConfiguration}, + {provide: FileManagerApiService, useClass: FileManagerBackendApiService} + ], + bootstrap: [AppWithBackendComponent] +}) +export class AppWithBackendModule { +} diff --git a/demo/src/appWithBackend/fileManagerBackendApi.service.ts b/demo/src/appWithBackend/fileManagerBackendApi.service.ts new file mode 100644 index 0000000..97911ac --- /dev/null +++ b/demo/src/appWithBackend/fileManagerBackendApi.service.ts @@ -0,0 +1,272 @@ +import {Injectable} from '@angular/core'; +import {IOuterNode} from '@rign/angular2-tree'; +import {Observable} from 'rxjs/Observable'; +import {ICropBounds, IFileManagerApi, IOuterFile, IFileDataProperties} from '../../../main'; +import {Http, Response, URLSearchParams} from '@angular/http'; +import {FileManagerConfiguration} from '../../../src/configuration/fileManagerConfiguration.service'; + +@Injectable() +export class FileManagerBackendApiService implements IFileManagerApi { + + protected nodes: IOuterNode[] = []; + protected files: IFileDataProperties[] = []; + + public constructor(private $http: Http, + private configuration: FileManagerConfiguration) { + } + + /** + * Load folder chidls for given folder id + * + * @param {string} nodeId + * @returns {Observable} + */ + public load(nodeId = ''): Observable { + const nodeIds = this.nodes.map((node: IOuterNode) => node.id); + + const params = new URLSearchParams(); + params.append('nodeId', nodeId); + + return this.$http.get(this.configuration.folderUrls.foldersUrl, {search: params}) + .map((response: Response): IOuterNode[] => { + return response.json(); + }) + .map((nodes: IOuterNode[]) => { + nodes.forEach((node: IOuterNode) => { + if (nodeIds.indexOf(node.id) === -1) { + this.nodes.push(node); + } else { + const index = this.nodes.findIndex((item: IOuterNode) => node.id === item.id); + this.nodes[index] = node; + } + }); + + return nodes; + }); + } + + /** + * Create new folder + * + * @param {IOuterNode} node + * @param {string} parentNodeId + * @returns {Observable} + */ + public add(node: IOuterNode, parentNodeId: string = null): Observable { + const data = { + node: node, + parentNodeId: parentNodeId + }; + + return this.$http.post(this.configuration.folderUrls.foldersUrl, data) + .map((res: Response): IOuterNode => { + return res.json(); + }) + .map((newNode: IOuterNode) => { + this.nodes.push(newNode); + + return newNode; + }) + + } + + /** + * Move folder from source parent to target parent + * + * @param {IOuterNode} srcNode + * @param {IOuterNode} targetNode + * @returns {Observable} + */ + public move(srcNode: IOuterNode, targetNode: IOuterNode | null): Observable { + const srcId = srcNode.id; + const targetId = targetNode ? targetNode.id : null; + + + return this.$http.put(this.configuration.folderUrls.folderMoveUrl, {source: srcId, target: targetId}) + .map((res: Response) => { + return res.json(); + }) + .map((movedNode: IOuterNode) => { + const index = this.findIndexByNodeId(srcId); + this.nodes[index].parentId = targetId; + + return movedNode; + }); + } + + /** + * Update folder name + * + * @param {IOuterNode} node + * @returns {Observable} + */ + public update(node: IOuterNode): Observable { + return this.$http.put(this.configuration.folderUrls.foldersUrl, node) + .map((res: Response) => { + const newNode = res.json(); + const index = this.findIndexByNodeId(node.id); + + this.nodes[index] = newNode.name; + + return newNode; + }); + } + + /** + * Remove node by given id + * + * @param {string} nodeId + * @returns {Observable} + */ + public remove(nodeId: string): Observable { + const index = this.findIndexByNodeId(nodeId); + + const hasChildren = this.getChildren(nodeId).length > 0; + + if (!hasChildren) { + return this.$http.delete(this.configuration.folderUrls.foldersUrl, {body: {nodeId: nodeId}}) + .map((res: Response) => { + const removedNode = res.json(); + this.nodes.splice(index, 1); + + return removedNode; + }); + } else { + return Observable.throw('Node is not empty'); + } + } + + /** + * Crop file + * + * @param {IOuterFile} file + * @param {ICropBounds} bounds + * @returns {Observable} + */ + public cropFile(file: IOuterFile, bounds: ICropBounds): Observable { + return this.$http.put(this.configuration.fileUrl, {id: file.id, bounds: bounds}) + .map((res: Response) => { + return res.json(); + }); + } + + /** + * Load files from directory + * + * @param {string} nodeId + * @returns {Observable} + */ + public loadFiles(nodeId = ''): Observable { + const params = new URLSearchParams(); + params.append('dirId', nodeId); + + // return Observable.of([]); + + return this.$http.get(this.configuration.fileUrl, {search: params}) + .map((response: Response): IOuterFile[] => { + return response.json(); + }) + .map((files: IOuterFile[]) => { + this.files = files.map((file: IOuterFile) => file); + + return files; + }) + } + + /** + * Remove file from folder + * + * @param {IOuterFile} file + * @returns {Observable} + */ + public removeFile(file: IOuterFile): Observable { + const index = this.findIndexByFileId(file.id.toString()); + + if (index === -1) { + return Observable.of(false); + } + + const params = new URLSearchParams(); + params.set('id', file.id.toString()); + + return this.$http.delete(this.configuration.fileUrl, {search: params}) + .map((res: Response) => { + this.files.splice(index, 1); + + return true; + }); + } + + /** + * This method is success method, real upload is done in ExtendedFileUploader + * @param {IOuterFile} file + * @returns {Observable} + */ + public uploadFile(file: IOuterFile): Observable { + const fileData = this.convertIOuterFile2LocalData(file); + this.files.push(fileData); + + return Observable.of(this.convertLocalData2IOuterFile(fileData)); + } + + /** + * @param {string} nodeId + * @returns {number} + */ + private findIndexByNodeId(nodeId: string): number { + return this.nodes.findIndex((node) => { + return node.id === nodeId; + }); + } + + /** + * @param {string} fileId + * @returns {number} + */ + private findIndexByFileId(fileId: string): number { + return this.files.findIndex((file) => file.id === fileId); + } + + /** + * @param {string} nodeId + * @returns {IOuterNode[]} + */ + private getChildren(nodeId: string): IOuterNode[] { + return this.nodes.filter((node: IOuterNode) => node.parentId === nodeId); + } + + /** + * @param file + * @returns {{id: string, folderId: string, name: string, thumbnailUrl: string, url: string, width: number, height: number, mime: string}} + */ + private convertLocalData2IOuterFile(file: IFileDataProperties): IOuterFile { + return { + id: file.id, + folderId: file.folderId, + name: file.name, + thumbnailUrl: file.data, + url: file.data, + width: file.width, + height: file.height, + type: file.type, + size: file.size + } + } + + /** + * @param file + * @returns {{id: (any|string), folderId: string, name: string, type: string, data: string, size: number}} + */ + private convertIOuterFile2LocalData(file: IOuterFile): IFileDataProperties { + return { + id: file.id.toString(), + folderId: file.folderId, + name: file.name, + type: file.type, + data: file.data, + size: file.size, + width: file.width, + height: file.height + } + } +} 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/main.ts b/main.ts index a3ebb64..1345236 100644 --- a/main.ts +++ b/main.ts @@ -1,13 +1,25 @@ -import {FileManagerModule} from './src/filemanager.module'; +import {FileManagerApiService} from './src/store/fileManagerApi.service'; import {FileManagerComponent} from './src/filemanager.component'; import {FileManagerConfiguration} from './src/configuration/fileManagerConfiguration.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 {ICropBounds} from './src/crop/ICropBounds'; export { FileManagerModule, FileManagerComponent, FileManagerConfiguration, FileManagerUploader, + FilemanagerNotifcations, + FileManagerApiService, + ICropBounds, + IFileDataProperties, + IFileManagerApi, + IOuterFile, ISelectFile } diff --git a/package.json b/package.json index f46db1c..7767484 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,9 @@ "angular-cli": {}, "scripts": { "backend": "node demo/backend/index.js", - "start": "ng serve --proxy-config demo/proxy.config.example.json" + "start": "ng serve --app withoutBackend", + "startWithBackend": "ng serve --proxy-config demo/proxy.config.example.json --app withBackend " + }, "repository": { "type": "git", diff --git a/src/configuration/fileManagerConfiguration.service.ts b/src/configuration/fileManagerConfiguration.service.ts index 9903d56..79f0397 100644 --- a/src/configuration/fileManagerConfiguration.service.ts +++ b/src/configuration/fileManagerConfiguration.service.ts @@ -56,6 +56,7 @@ export class FileManagerConfiguration { } ]; + public folderUrls: {foldersUrl: string, folderMoveUrl: string}; public fileUrl = '/api/files'; public isMultiSelection: boolean; @@ -65,6 +66,8 @@ export class FileManagerConfiguration { public mimeTypes: string[] | null; 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; diff --git a/src/filemanager.component.ts b/src/filemanager.component.ts index 40aa33a..17ab3d8 100644 --- a/src/filemanager.component.ts +++ b/src/filemanager.component.ts @@ -13,7 +13,6 @@ import { TreeActionsService, NodeDispatcherService } from '@rign/angular2-tree'; -import {FilesService} from './filesList/files.service'; import {IOuterFile} from './filesList/interface/IOuterFile'; import {FileModel} from './filesList/file.model'; import {log} from './decorators/logFunction.decorator'; @@ -25,7 +24,6 @@ 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 {TreeService} from './configuration/tree.service'; import {Observable} from 'rxjs/Observable'; import {Store} from '@ngrx/store'; import {IFileManagerState} from './store/fileManagerReducer'; @@ -35,11 +33,10 @@ import {FileManagerDispatcherService} from './store/fileManagerDispatcher.servic import {FileManagerEffectsService} from './store/fileManagerEffects.service'; import {FileManagerApiService} from './store/fileManagerApi.service'; import {FilemanagerNotifcations, INotification} from './services/FilemanagerNotifcations'; -import {IFileManagerConfiguration} from './configuration/IFileManagerConfiguration'; @Component({ selector: 'ri-filemanager', - providers: [NodeService, FilesService, NotificationsService], + providers: [NodeService, NotificationsService], styleUrls: ['./main.less'], templateUrl: './filemanager.html' }) @@ -110,7 +107,6 @@ export class FileManagerComponent implements OnInit, OnChanges { private treeActions: TreeActionsService, private nodeDispatcherService: NodeDispatcherService, private treeService: FileManagerApiService, - private filesService: FilesService, private notifications: NotificationsService, private configuration: FileManagerConfiguration, private fileManagerDispatcher: FileManagerDispatcherService, diff --git a/src/filemanager.module.ts b/src/filemanager.module.ts index 2d2491d..d48de08 100644 --- a/src/filemanager.module.ts +++ b/src/filemanager.module.ts @@ -20,7 +20,6 @@ import {EffectsModule} from '@ngrx/effects'; import {FileManagerEffectsService} from './store/fileManagerEffects.service'; import {StoreModule} from '@ngrx/store'; import {fileManagerReducer} from './store/fileManagerReducer'; -import {FilesService} from './filesList/files.service'; import {FileManagerActionsService} from './store/fileManagerActions.service'; import { StoreDevtoolsModule } from '@ngrx/store-devtools'; import {FileTypeFilterService} from './services/fileTypeFilter.service'; @@ -32,7 +31,6 @@ 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 {IFileManagerConfiguration} from './configuration/IFileManagerConfiguration'; @NgModule({ imports: [ @@ -69,7 +67,6 @@ import {IFileManagerConfiguration} from './configuration/IFileManagerConfigurati FileManagerEffectsService, FilemanagerNotifcations, FileManagerUploader, - FilesService, FileTypeFilterService, ImageDataConverter, NotificationsService, diff --git a/src/filesList/fileManagerUploader.service.ts b/src/filesList/fileManagerUploader.service.ts index 2273258..db48bac 100644 --- a/src/filesList/fileManagerUploader.service.ts +++ b/src/filesList/fileManagerUploader.service.ts @@ -8,7 +8,8 @@ import {FileUploaderOptions} from 'ng2-file-upload'; export class FileManagerUploader { public uploader: ExtendedFileUploader; - public constructor(@Inject('fileManagerConfiguration') configuration: IFileManagerConfiguration, filemanagerNotification: FilemanagerNotifcations) { + public constructor(@Inject('fileManagerConfiguration') configuration: IFileManagerConfiguration, + filemanagerNotification: FilemanagerNotifcations) { const options: FileUploaderOptions = { allowedMimeType: configuration.mimeTypes, url: configuration.urls.filesUrl, diff --git a/src/filesList/files.service.ts b/src/filesList/files.service.ts deleted file mode 100644 index 7faa8fc..0000000 --- a/src/filesList/files.service.ts +++ /dev/null @@ -1,49 +0,0 @@ -import {Http, Response, URLSearchParams} from "@angular/http"; -import {Injectable} from "@angular/core"; -import {Observable} from "rxjs/Rx"; -import {IOuterFile} from "./interface/IOuterFile"; -import {IFileModel} from "./interface/IFileModel"; -import {FileManagerConfiguration} from "../configuration/fileManagerConfiguration.service"; -import {ICropBounds} from "../crop/ICropBounds"; - -@Injectable() -export class FilesService { - private url: string; - - public constructor(private http: Http, private configuration: FileManagerConfiguration) { - this.url = configuration.fileUrl; - } - - public crop(file: IFileModel, bounds: ICropBounds): Observable { - return this.http.put(this.url, {id: file.getId(), bounds: bounds}) - .map((res: Response) => { - let body = res.json(); - - return body || []; - }); - } - - public load(folderId: string): Observable { - let params = new URLSearchParams(); - params.set('dirId', folderId); - - return this.http.get(this.url, {search: params}) - .map((res: Response) => { - let body = res.json(); - - return body || []; - }); - } - - public remove(file: IFileModel) { - let params = new URLSearchParams(); - params.set('id', file.getId().toString()); - - return this.http.delete(this.url, {search: params}) - .map((res: Response) => { - let body = res.json(); - - return true; - }) - } -} diff --git a/src/services/imageDataConverter.service.ts b/src/services/imageDataConverter.service.ts index 2116e5b..ee3ee6c 100644 --- a/src/services/imageDataConverter.service.ts +++ b/src/services/imageDataConverter.service.ts @@ -3,7 +3,7 @@ import {UUID} from 'angular2-uuid'; import {Injectable} from '@angular/core'; export interface IFileDataProperties { - id: string; + id: string | number; folderId: string; name: string; size: number; diff --git a/src/store/fileManagerEffects.service.ts b/src/store/fileManagerEffects.service.ts index 5e2e04a..1bdd68b 100644 --- a/src/store/fileManagerEffects.service.ts +++ b/src/store/fileManagerEffects.service.ts @@ -1,6 +1,5 @@ import {Injectable} from '@angular/core'; import {Actions, Effect} from '@ngrx/effects'; -import {FilesService} from '../filesList/files.service'; import {FileManagerActionsService, IFileManagerAction} from './fileManagerActions.service'; import {IOuterFile} from '../filesList/interface/IOuterFile'; import {Observable} from 'rxjs/Observable'; @@ -13,7 +12,6 @@ import {FilemanagerNotifcations} from '../services/FilemanagerNotifcations'; export class FileManagerEffectsService { constructor(private actions$: Actions, - private filesService: FilesService, private fileManagerActions: FileManagerActionsService, private filemanagerNotfication: FilemanagerNotifcations, private fileManagerApiService: FileManagerApiService) { From 5a1b085be5fabec66292c739c2d331ec90523d0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ignaszewski?= Date: Tue, 5 Sep 2017 17:36:41 +0200 Subject: [PATCH 16/23] Unit tests --- karma.conf.js | 30 +++++--------- package.json | 39 ++++++++++--------- src/filemanager.module.ts | 10 ++--- src/filesList/files.html | 2 +- src/filesList/filesList.component.ts | 3 -- src/services/FilemanagerNotifications.spec.ts | 38 ++++++++++++++++++ src/services/extendedFileUplaoder.service.ts | 3 +- src/services/fileTypeFilter.service.spec.ts | 39 +++++++++++++++++++ src/services/imageDataConverter.service.ts | 5 +++ src/services/searchFilter.service.spec.ts | 30 ++++++++++++++ src/test.ts | 34 ++++++++++++++++ src/toolbar/toolbar.component.ts | 20 ++++------ src/tsconfig.spec.json | 20 ++++++++++ tsconfig.json | 5 +-- 14 files changed, 213 insertions(+), 65 deletions(-) create mode 100644 src/services/FilemanagerNotifications.spec.ts create mode 100644 src/services/fileTypeFilter.service.spec.ts create mode 100644 src/services/searchFilter.service.spec.ts create mode 100644 src/test.ts create mode 100644 src/tsconfig.spec.json diff --git a/karma.conf.js b/karma.conf.js index 1f2613a..34cc2aa 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -4,35 +4,25 @@ 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, diff --git a/package.json b/package.json index 7767484..d385fca 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "scripts": { "backend": "node demo/backend/index.js", "start": "ng serve --app withoutBackend", - "startWithBackend": "ng serve --proxy-config demo/proxy.config.example.json --app withBackend " - + "startWithBackend": "ng serve --proxy-config demo/proxy.config.example.json --app withBackend ", + "test": "./node_modules/.bin/karma --config karma.conf.js start" }, "repository": { "type": "git", @@ -30,23 +30,24 @@ "@angular/platform-browser": "^4.0.0", "@angular/platform-browser-dynamic": "^4.0.0", "@angular/router": "^4.0.0", - "@rign/angular2-tree": "^2.0.0", - "@ngrx/core": "^1.2.0", - "@ngrx/effects": "^2.0.3", - "core-js": "^2.4.1", - "@ngrx/store": "^2.2.2", - "angular2-bootstrap-confirm": "^1.0.4", - "angular2-contextmenu": "^0.7.7", - "angular2-notifications": "^0.4.53", - "angular2-uuid": "^1.1.1", - "bootstrap": "^3.3.7", - "font-awesome": "^4.6.3", - "ng2-dnd": "^2.2.2", - "ng2-file-upload": "^1.2.0", - "ng2-img-cropper": "^0.7.7", - "rxjs": "^5.1.0", - "ts-helpers": "^1.1.1", - "zone.js": "^0.8.4" + "@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.2.0", diff --git a/src/filemanager.module.ts b/src/filemanager.module.ts index d48de08..5b35d61 100644 --- a/src/filemanager.module.ts +++ b/src/filemanager.module.ts @@ -2,11 +2,10 @@ 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, TreeEffectsService} from '@rign/angular2-tree'; +import {TreeModule, treeReducer} from '@rign/angular2-tree'; import {NotificationsService, SimpleNotificationsModule} from 'angular2-notifications'; -import {ConfirmModule} from 'angular2-bootstrap-confirm'; import {FileManagerComponent} from './filemanager.component'; -import {Toolbar} from './toolbar/toolbar.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'; @@ -31,12 +30,13 @@ 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'; @NgModule({ imports: [ BrowserModule, BrowserAnimationsModule, - ConfirmModule, + ConfirmationPopoverModule.forRoot(), EffectsModule.run(FileManagerEffectsService), FormsModule, FileUploadModule, @@ -50,7 +50,7 @@ import {FilemanagerNotifcations} from './services/FilemanagerNotifcations'; declarations: [ FileManagerComponent, FileTypeFilterComponent, - Toolbar, + ToolbarComponent, FilesListComponent, DropdownComponent, PreviewComponent, diff --git a/src/filesList/files.html b/src/filesList/files.html index ade5c82..4c5c87a 100644 --- a/src/filesList/files.html +++ b/src/filesList/files.html @@ -8,7 +8,7 @@ [style.background-image]="'url(' + file.thumbnailUrl + ')'">
{{file.name}}
-
- +
diff --git a/src/filesList/files.html b/src/filesList/files.html index 4c5c87a..d419441 100644 --- a/src/filesList/files.html +++ b/src/filesList/files.html @@ -1,7 +1,6 @@
- -
+
Date: Thu, 7 Sep 2017 11:11:59 +0200 Subject: [PATCH 21/23] Update README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 120bb74..cdcbb3a 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ In your project put this line Loading... -## Provide configuration +### Provide configuration In your module add following lines with configuration import {FileManagerModule, IFileManagerConfiguration} from '../../../main'; @@ -112,7 +112,7 @@ Then you have to provide this constant as a configuration service export class AppModule { } -## Create API service +### 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 @@ -134,7 +134,7 @@ 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 +### 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): From 780cab731fdb7f4755ced0fed52db7da877ab89b Mon Sep 17 00:00:00 2001 From: Rafal Ignaszewski Date: Thu, 7 Sep 2017 16:45:53 +0200 Subject: [PATCH 22/23] Update Unit Test --- src/_unitTestMocks/fileDataMock.ts | 13 + src/_unitTestMocks/folderDataMock.ts | 10 + src/store/fileManagerActions.service.spec.ts | 258 ++++++++++++++++++ src/store/fileManagerApi.service.spec.ts | 49 ++++ .../fileManagerBackendApi.service.spec.ts | 44 +++ 5 files changed, 374 insertions(+) create mode 100644 src/_unitTestMocks/fileDataMock.ts create mode 100644 src/_unitTestMocks/folderDataMock.ts create mode 100644 src/store/fileManagerActions.service.spec.ts create mode 100644 src/store/fileManagerApi.service.spec.ts create mode 100644 src/store/fileManagerBackendApi.service.spec.ts 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/store/fileManagerActions.service.spec.ts b/src/store/fileManagerActions.service.spec.ts new file mode 100644 index 0000000..27aef11 --- /dev/null +++ b/src/store/fileManagerActions.service.spec.ts @@ -0,0 +1,258 @@ +import {FileManagerActionsService, IFileManagerAction} from './fileManagerActions.service'; +import {IFileModel} from '../filesList/interface/IFileModel'; +import {IOuterFile} from '../filesList/interface/IOuterFile'; +import {FileModel} from '../filesList/file.model'; +import {ICropBounds} from '../crop/ICropBounds'; + +describe('fileManagerActions.service', () => { + let service: FileManagerActionsService; + let file: IFileModel; + let files: IOuterFile[]; + let fileData: IOuterFile; + + beforeEach(() => { + fileData = { + 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 + }; + + file = new FileModel(fileData); + + files = [fileData]; + + service = new FileManagerActionsService(); + }); + + describe('should contains static values', () => { + it('FILEMANAGER_CROP_FILE', () => { + expect(FileManagerActionsService.FILEMANAGER_CROP_FILE).toBe('FILEMANAGER_CROP_FILE'); + }); + + it('FILEMANAGER_CROP_FILE_SUCCESS', () => { + expect(FileManagerActionsService.FILEMANAGER_CROP_FILE_SUCCESS).toBe('FILEMANAGER_CROP_FILE_SUCCESS'); + }); + + it('FILEMANAGER_CROP_FILE_ERROR', () => { + expect(FileManagerActionsService.FILEMANAGER_CROP_FILE_ERROR).toBe('FILEMANAGER_CROP_FILE_ERROR'); + }); + + it('FILEMANAGER_DELETE_FILE', () => { + expect(FileManagerActionsService.FILEMANAGER_DELETE_FILE).toBe('FILEMANAGER_DELETE_FILE'); + }); + + it('FILEMANAGER_DELETE_FILE_SUCCESS', () => { + expect(FileManagerActionsService.FILEMANAGER_DELETE_FILE_SUCCESS).toBe('FILEMANAGER_DELETE_FILE_SUCCESS'); + }); + + it('FILEMANAGER_DELETE_FILE_SELECTION', () => { + expect(FileManagerActionsService.FILEMANAGER_DELETE_FILE_SELECTION).toBe('FILEMANAGER_DELETE_FILE_SELECTION'); + }); + + it('FILEMANAGER_DELETE_FILE_SELECTION_SUCCESS', () => { + expect(FileManagerActionsService.FILEMANAGER_DELETE_FILE_SELECTION_SUCCESS).toBe('FILEMANAGER_DELETE_FILE_SELECTION_SUCCESS'); + }); + + it('FILEMANAGER_INVERSE_FILE_SELECTION', () => { + expect(FileManagerActionsService.FILEMANAGER_INVERSE_FILE_SELECTION).toBe('FILEMANAGER_INVERSE_FILE_SELECTION'); + }); + + it('FILEMANAGER_LOAD_FILES', () => { + expect(FileManagerActionsService.FILEMANAGER_LOAD_FILES).toBe('FILEMANAGER_LOAD_FILES'); + }); + + it('FILEMANAGER_LOAD_FILES_SUCCESS', () => { + expect(FileManagerActionsService.FILEMANAGER_LOAD_FILES_SUCCESS).toBe('FILEMANAGER_LOAD_FILES_SUCCESS'); + }); + + it('FILEMANAGER_SELECT_ALL', () => { + expect(FileManagerActionsService.FILEMANAGER_SELECT_ALL).toBe('FILEMANAGER_SELECT_ALL'); + }); + + it('FILEMANAGER_SELECT_FILE', () => { + expect(FileManagerActionsService.FILEMANAGER_SELECT_FILE).toBe('FILEMANAGER_SELECT_FILE'); + }); + + it('FILEMANAGER_UNSELECT_FILE', () => { + expect(FileManagerActionsService.FILEMANAGER_UNSELECT_FILE).toBe('FILEMANAGER_UNSELECT_FILE'); + }); + + it('FILEMANAGER_UNSELECT_ALL', () => { + expect(FileManagerActionsService.FILEMANAGER_UNSELECT_ALL).toBe('FILEMANAGER_UNSELECT_ALL'); + }); + + it('FILEMANAGER_UPLOAD_FILE', () => { + expect(FileManagerActionsService.FILEMANAGER_UPLOAD_FILE).toBe('FILEMANAGER_UPLOAD_FILE'); + }); + + it('FILEMANAGER_UPLOAD_FILE_ERROR', () => { + expect(FileManagerActionsService.FILEMANAGER_UPLOAD_FILE_ERROR).toBe('FILEMANAGER_UPLOAD_FILE_ERROR'); + }); + + it('FILEMANAGER_UPLOAD_FILE_SUCCESS', () => { + expect(FileManagerActionsService.FILEMANAGER_UPLOAD_FILE_SUCCESS).toBe('FILEMANAGER_UPLOAD_FILE_SUCCESS'); + }); + }); + + describe('cropFile', () => { + it('should return proper object', () => { + const bounds: ICropBounds = { + x: 0, + y: 0, + width: 100, + height: 200 + }; + + const expected: IFileManagerAction = { + type: FileManagerActionsService.FILEMANAGER_CROP_FILE, + payload: {file, bounds} + }; + + expect(service.cropFile(file, bounds)).toEqual(expected); + }); + }); + + describe('cropFileSuccess', () => { + it('should return proper object', () => { + const expected: IFileManagerAction = { + type: FileManagerActionsService.FILEMANAGER_CROP_FILE_SUCCESS, + payload: {file} + }; + + expect(service.cropFileSuccess(file)).toEqual(expected); + }); + }); + + describe('deleteFile', () => { + it('should return proper object', () => { + const expected: IFileManagerAction = { + type: FileManagerActionsService.FILEMANAGER_DELETE_FILE, + payload: {file} + }; + + expect(service.deleteFile(file)).toEqual(expected); + }); + }); + + describe('deleteFileSuccess', () => { + it('should return proper object', () => { + const expected: IFileManagerAction = { + type: FileManagerActionsService.FILEMANAGER_DELETE_FILE_SUCCESS, + payload: {file} + }; + + expect(service.deleteFileSuccess(file)).toEqual(expected); + }); + }); + + describe('loadFiles', () => { + it('should return proper object', () => { + const folderId = '124'; + const expected: IFileManagerAction = { + type: FileManagerActionsService.FILEMANAGER_LOAD_FILES, + payload: {folderId} + }; + + expect(service.loadFiles(folderId)).toEqual(expected); + }); + }); + + describe('inverseFileSelection', () => { + it('should return proper object', () => { + const expected: IFileManagerAction = { + type: FileManagerActionsService.FILEMANAGER_INVERSE_FILE_SELECTION, + payload: {} + }; + + expect(service.inverseFileSelection()).toEqual(expected); + }); + }); + + describe('loadFilesSuccess', () => { + it('should return proper object', () => { + const folderId = '124'; + const expected: IFileManagerAction = { + type: FileManagerActionsService.FILEMANAGER_LOAD_FILES_SUCCESS, + payload: {folderId, files} + }; + + expect(service.loadFilesSuccess(folderId, files)).toEqual(expected); + }); + }); + + describe('selectAllFiles', () => { + it('should return proper object', () => { + const expected: IFileManagerAction = { + type: FileManagerActionsService.FILEMANAGER_SELECT_ALL, + payload: {} + }; + + expect(service.selectAllFiles()).toEqual(expected); + }); + }); + + describe('selectFile', () => { + it('should return proper object', () => { + const expected: IFileManagerAction = { + type: FileManagerActionsService.FILEMANAGER_SELECT_FILE, + payload: {file} + }; + + expect(service.selectFile(file)).toEqual(expected); + }); + }); + + describe('unSelectAll', () => { + it('should return proper object', () => { + const expected: IFileManagerAction = { + type: FileManagerActionsService.FILEMANAGER_UNSELECT_ALL, + payload: {} + }; + + expect(service.unSelectAll()).toEqual(expected); + }); + }); + + describe('unSelectFile', () => { + it('should return proper object', () => { + const expected: IFileManagerAction = { + type: FileManagerActionsService.FILEMANAGER_UNSELECT_FILE, + payload: {file} + }; + + expect(service.unSelectFile(file)).toEqual(expected); + }); + }); + + describe('uploadSuccess', () => { + it('should return proper object', () => { + const expected: IFileManagerAction = { + type: FileManagerActionsService.FILEMANAGER_UPLOAD_FILE_SUCCESS, + payload: { + files: [fileData] + } + }; + + expect(service.uploadSuccess(fileData)).toEqual(expected); + }); + }); + + describe('uploadError', () => { + it('should return proper object', () => { + const expected: IFileManagerAction = { + type: FileManagerActionsService.FILEMANAGER_UPLOAD_FILE_ERROR, + payload: { + files: [fileData] + } + }; + + expect(service.uploadError(fileData)).toEqual(expected); + }); + }); +}); diff --git a/src/store/fileManagerApi.service.spec.ts b/src/store/fileManagerApi.service.spec.ts new file mode 100644 index 0000000..9c8a7ae --- /dev/null +++ b/src/store/fileManagerApi.service.spec.ts @@ -0,0 +1,49 @@ +import {FilemanagerNotifcations} from '../services/FilemanagerNotifcations'; +import {FileManagerApiService} from './fileManagerApi.service'; +import {IOuterNode} from '@rign/angular2-tree'; + +describe('fileManagerApi.service', () => { + let service: FileManagerApiService; + let filemanagerNotifications: FilemanagerNotifcations; + let rootNode: IOuterNode = { + 'id': 'dd9b20d8-260b-54c1-7eca-c22eae257edc', + 'treeId': 'tree', + 'name': 'Nowy folder', + 'parentId': '', + 'children': [], + 'parents': [] + }; + let nodesData: IOuterNode[] = [rootNode]; + let handler: any; + + beforeEach(() => { + localStorage.setItem('fileManagerTree', JSON.stringify(nodesData)); + + handler = jasmine.createSpy('handler'); + filemanagerNotifications = jasmine.createSpyObj('FilemanagerNotifcations', ['sendNotification']); + + service = new FileManagerApiService(filemanagerNotifications); + }); + + afterEach(() => { + localStorage.removeItem('fileManagerTree'); + }); + + describe('load', () => { + it('should load all nodes from local storage', function () { + service.load('') + .subscribe(handler); + + expect(handler).toHaveBeenCalledWith([rootNode]); + }); + + it('should use previously load data', function () { + service.load(''); + + service.load('') + .subscribe(handler); + + expect(handler).toHaveBeenCalledWith([rootNode]); + }); + }); +}); diff --git a/src/store/fileManagerBackendApi.service.spec.ts b/src/store/fileManagerBackendApi.service.spec.ts new file mode 100644 index 0000000..e26e356 --- /dev/null +++ b/src/store/fileManagerBackendApi.service.spec.ts @@ -0,0 +1,44 @@ +import {IOuterNode} from '@rign/angular2-tree'; +import {Http, Response, ResponseOptions} from '@angular/http'; +import {FileManagerConfiguration} from '../configuration/fileManagerConfiguration.service'; +import {FileManagerBackendApiService} from './fileManagerBackendApi.service'; +import {rootNode} from '../_unitTestMocks/folderDataMock'; +import {Observable} from 'rxjs/Observable'; + +fdescribe('fileManagerApi.service', () => { + let service: FileManagerBackendApiService; + let configuration: FileManagerConfiguration; + let nodesData: IOuterNode[] = [rootNode]; + let handler: any; + let http: Http | any; + + beforeEach(() => { + localStorage.setItem('fileManagerTree', JSON.stringify(nodesData)); + + handler = jasmine.createSpy('handler'); + + configuration = { + folderUrls: { + foldersUrl: '/files' + } + }; + http = jasmine.createSpyObj('Http', ['get']); + + service = new FileManagerBackendApiService(http, configuration); + }); + + afterEach(() => { + localStorage.removeItem('fileManagerTree'); + }); + + describe('load', () => { + it('should load all nodes from given node', function () { + http.get.and.returnValue(Observable.of(new Response(new ResponseOptions({body: JSON.stringify([rootNode])})))); + + service.load('') + .subscribe(handler); + + expect(handler).toHaveBeenCalledWith([rootNode]); + }); + }); +}); From e641814b69dd4a9aaece1e11ee8975ddc1378a4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ignaszewski?= Date: Thu, 7 Sep 2017 18:53:38 +0200 Subject: [PATCH 23/23] Fix Unit tests --- src/store/fileManagerBackendApi.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/fileManagerBackendApi.service.spec.ts b/src/store/fileManagerBackendApi.service.spec.ts index e26e356..cf86a90 100644 --- a/src/store/fileManagerBackendApi.service.spec.ts +++ b/src/store/fileManagerBackendApi.service.spec.ts @@ -5,7 +5,7 @@ import {FileManagerBackendApiService} from './fileManagerBackendApi.service'; import {rootNode} from '../_unitTestMocks/folderDataMock'; import {Observable} from 'rxjs/Observable'; -fdescribe('fileManagerApi.service', () => { +describe('fileManagerApi.service', () => { let service: FileManagerBackendApiService; let configuration: FileManagerConfiguration; let nodesData: IOuterNode[] = [rootNode];