(apps/file-manager) Added support for nested folder views

This commit is contained in:
sercan 2021-06-05 13:57:39 +03:00
parent fc1e7b02b0
commit 11ad2c89df
9 changed files with 303 additions and 78 deletions

View File

@ -33,10 +33,18 @@ export class FileManagerMockApi
// ----------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------
this._fuseMockApiService this._fuseMockApiService
.onGet('api/apps/file-manager') .onGet('api/apps/file-manager')
.reply(() => { .reply(({request}) => {
// Clone the items // Clone the items
const items = cloneDeep(this._items); let items = cloneDeep(this._items);
// See if a folder id exist
const folderId = request.params.get('folderId') ?? null;
// Filter the items by folder id. If folder id is null,
// that means we want to root items which have folder id
// of null
items = items.filter(item => item.folderId === folderId);
// Separate the items by folders and files // Separate the items by folders and files
const folders = items.filter(item => item.type === 'folder'); const folders = items.filter(item => item.type === 'folder');
@ -46,11 +54,38 @@ export class FileManagerMockApi
folders.sort((a, b) => a.name.localeCompare(b.name)); folders.sort((a, b) => a.name.localeCompare(b.name));
files.sort((a, b) => a.name.localeCompare(b.name)); files.sort((a, b) => a.name.localeCompare(b.name));
// Figure out the path and attach it to the response
// Prepare the empty paths array
const pathItems = cloneDeep(this._items);
const path = [];
// Prepare the current folder
let currentFolder = null;
// Get the current folder and add it as the first entry
if ( folderId )
{
currentFolder = pathItems.find(item => item.id === folderId);
path.push(currentFolder);
}
// Start traversing and storing the folders as a path array
// until we hit null on the folder id
while ( currentFolder?.folderId )
{
currentFolder = pathItems.find(item => item.id === currentFolder.folderId);
if ( currentFolder )
{
path.unshift(currentFolder);
}
}
return [ return [
200, 200,
{ {
folders, folders,
files files,
path
} }
]; ];
}); });

View File

@ -2,6 +2,7 @@
export const items = [ export const items = [
{ {
id : 'cd6897cb-acfd-4016-8b53-3f66a5b5fc68', id : 'cd6897cb-acfd-4016-8b53-3f66a5b5fc68',
folderId : null,
name : 'Personal', name : 'Personal',
createdBy : 'Brian Hughes', createdBy : 'Brian Hughes',
createdAt : 'April 24, 2018', createdAt : 'April 24, 2018',
@ -13,6 +14,7 @@ export const items = [
}, },
{ {
id : '6da8747f-b474-4c9a-9eba-5ef212285500', id : '6da8747f-b474-4c9a-9eba-5ef212285500',
folderId : null,
name : 'Photos', name : 'Photos',
createdBy : 'Brian Hughes', createdBy : 'Brian Hughes',
createdAt : 'November 01, 2021', createdAt : 'November 01, 2021',
@ -24,6 +26,7 @@ export const items = [
}, },
{ {
id : 'ed58add1-45a7-41db-887d-3ca7ee7f2719', id : 'ed58add1-45a7-41db-887d-3ca7ee7f2719',
folderId : null,
name : 'Work', name : 'Work',
createdBy : 'Brian Hughes', createdBy : 'Brian Hughes',
createdAt : 'May 8, 2020', createdAt : 'May 8, 2020',
@ -35,6 +38,7 @@ export const items = [
}, },
{ {
id : '5cb66e32-d1ac-4b9a-8c34-5991ce25add2', id : '5cb66e32-d1ac-4b9a-8c34-5991ce25add2',
folderId : null,
name : 'Contract #123', name : 'Contract #123',
createdBy : 'Brian Hughes', createdBy : 'Brian Hughes',
createdAt : 'January 14, 2021', createdAt : 'January 14, 2021',
@ -46,6 +50,7 @@ export const items = [
}, },
{ {
id : '3ffc3d84-8f2d-4929-903a-ef6fc21657a7', id : '3ffc3d84-8f2d-4929-903a-ef6fc21657a7',
folderId : null,
name : 'Estimated budget', name : 'Estimated budget',
createdBy : 'Brian Hughes', createdBy : 'Brian Hughes',
createdAt : 'December 14, 2020', createdAt : 'December 14, 2020',
@ -57,6 +62,7 @@ export const items = [
}, },
{ {
id : '157adb9a-14f8-4559-ac93-8be893c9f80a', id : '157adb9a-14f8-4559-ac93-8be893c9f80a',
folderId : null,
name : 'DMCA notice #42', name : 'DMCA notice #42',
createdBy : 'Brian Hughes', createdBy : 'Brian Hughes',
createdAt : 'May 8, 2021', createdAt : 'May 8, 2021',
@ -68,6 +74,7 @@ export const items = [
}, },
{ {
id : '4f64597a-df7e-461c-ad60-f33e5f7e0747', id : '4f64597a-df7e-461c-ad60-f33e5f7e0747',
folderId : null,
name : 'Invoices', name : 'Invoices',
createdBy : 'Brian Hughes', createdBy : 'Brian Hughes',
createdAt : 'January 12, 2020', createdAt : 'January 12, 2020',
@ -79,6 +86,7 @@ export const items = [
}, },
{ {
id : 'e445c445-57b2-4476-8c62-b068e3774b8e', id : 'e445c445-57b2-4476-8c62-b068e3774b8e',
folderId : null,
name : 'Crash logs', name : 'Crash logs',
createdBy : 'Brian Hughes', createdBy : 'Brian Hughes',
createdAt : 'June 8, 2020', createdAt : 'June 8, 2020',
@ -90,6 +98,7 @@ export const items = [
}, },
{ {
id : 'b482f93e-7847-4614-ad48-b78b78309f81', id : 'b482f93e-7847-4614-ad48-b78b78309f81',
folderId : null,
name : 'System logs', name : 'System logs',
createdBy : 'Brian Hughes', createdBy : 'Brian Hughes',
createdAt : 'June 8, 2020', createdAt : 'June 8, 2020',
@ -101,6 +110,7 @@ export const items = [
}, },
{ {
id : 'ec07a98d-2e5b-422c-a9b2-b5d1c0e263f5', id : 'ec07a98d-2e5b-422c-a9b2-b5d1c0e263f5',
folderId : null,
name : 'Personal projects', name : 'Personal projects',
createdBy : 'Brian Hughes', createdBy : 'Brian Hughes',
createdAt : 'March 18, 2020', createdAt : 'March 18, 2020',
@ -112,6 +122,7 @@ export const items = [
}, },
{ {
id : 'ae908d59-07da-4dd8-aba0-124e50289295', id : 'ae908d59-07da-4dd8-aba0-124e50289295',
folderId : null,
name : 'Biometric portrait', name : 'Biometric portrait',
createdBy : 'Brian Hughes', createdBy : 'Brian Hughes',
createdAt : 'August 29, 2020', createdAt : 'August 29, 2020',
@ -123,6 +134,7 @@ export const items = [
}, },
{ {
id : '4038a5b6-5b1a-432d-907c-e037aeb817a8', id : '4038a5b6-5b1a-432d-907c-e037aeb817a8',
folderId : null,
name : 'Scanned image 20201012-1', name : 'Scanned image 20201012-1',
createdBy : 'Brian Hughes', createdBy : 'Brian Hughes',
createdAt : 'September 13, 2020', createdAt : 'September 13, 2020',
@ -134,6 +146,7 @@ export const items = [
}, },
{ {
id : '630d2e9a-d110-47a0-ac03-256073a0f56d', id : '630d2e9a-d110-47a0-ac03-256073a0f56d',
folderId : null,
name : 'Scanned image 20201012-2', name : 'Scanned image 20201012-2',
createdBy : 'Brian Hughes', createdBy : 'Brian Hughes',
createdAt : 'September 14, 2020', createdAt : 'September 14, 2020',
@ -145,6 +158,7 @@ export const items = [
}, },
{ {
id : '1417d5ed-b616-4cff-bfab-286677b69d79', id : '1417d5ed-b616-4cff-bfab-286677b69d79',
folderId : null,
name : 'Prices', name : 'Prices',
createdBy : 'Brian Hughes', createdBy : 'Brian Hughes',
createdAt : 'April 07, 2020', createdAt : 'April 07, 2020',
@ -156,6 +170,7 @@ export const items = [
}, },
{ {
id : 'bd2817c7-6751-40dc-b252-b6b5634c0689', id : 'bd2817c7-6751-40dc-b252-b6b5634c0689',
folderId : null,
name : 'Shopping list', name : 'Shopping list',
createdBy : 'Brian Hughes', createdBy : 'Brian Hughes',
createdAt : 'March 26, 2021', createdAt : 'March 26, 2021',
@ -167,6 +182,7 @@ export const items = [
}, },
{ {
id : '14fb47c9-6eeb-4070-919c-07c8133285d1', id : '14fb47c9-6eeb-4070-919c-07c8133285d1',
folderId : null,
name : 'Summer budget', name : 'Summer budget',
createdBy : 'Brian Hughes', createdBy : 'Brian Hughes',
createdAt : 'June 02, 2020', createdAt : 'June 02, 2020',
@ -175,5 +191,67 @@ export const items = [
type : 'XLS', type : 'XLS',
contents : null, contents : null,
description: null description: null
},
{
id : '894e8514-03d3-4f5e-bb28-f6c092501fae',
folderId : 'cd6897cb-acfd-4016-8b53-3f66a5b5fc68',
name : 'A personal file',
createdBy : 'Brian Hughes',
createdAt : 'June 02, 2020',
modifiedAt : 'June 02, 2020',
size : '943 KB',
type : 'XLS',
contents : null,
description: null
},
{
id : '74010810-16cf-441d-a1aa-c9fb620fceea',
folderId : 'cd6897cb-acfd-4016-8b53-3f66a5b5fc68',
name : 'A personal folder',
createdBy : 'Brian Hughes',
createdAt : 'November 01, 2021',
modifiedAt : 'November 01, 2021',
size : '3015 MB',
type : 'folder',
contents : '907 files',
description: 'Personal photos; selfies, family, vacation and etc.'
},
{
id : 'a8c73e5a-8114-436d-ab54-d900b50b3762',
folderId : '74010810-16cf-441d-a1aa-c9fb620fceea',
name : 'A personal file within the personal folder',
createdBy : 'Brian Hughes',
createdAt : 'June 02, 2020',
modifiedAt : 'June 02, 2020',
size : '943 KB',
type : 'XLS',
contents : null,
description: null
},
{
id : '12d851a8-4f60-473e-8a59-abe4b422ea99',
folderId : '6da8747f-b474-4c9a-9eba-5ef212285500',
name : 'Photos file',
createdBy : 'Brian Hughes',
createdAt : 'June 02, 2020',
modifiedAt : 'June 02, 2020',
size : '943 KB',
type : 'XLS',
contents : null,
description: null
},
{
id : '2836766d-27e1-4f40-a31a-5a8419105e7e',
folderId : 'ed58add1-45a7-41db-887d-3ca7ee7f2719',
name : 'Work file',
createdBy : 'Brian Hughes',
createdAt : 'June 02, 2020',
modifiedAt : 'June 02, 2020',
size : '943 KB',
type : 'XLS',
contents : null,
description: null
} }
]; ];

View File

@ -4,7 +4,7 @@
<div class="flex items-center justify-end"> <div class="flex items-center justify-end">
<button <button
mat-icon-button mat-icon-button
[routerLink]="['../']"> [routerLink]="['../../']">
<mat-icon [svgIcon]="'heroicons_outline:x'"></mat-icon> <mat-icon [svgIcon]="'heroicons_outline:x'"></mat-icon>
</button> </button>
</div> </div>

View File

@ -22,17 +22,17 @@ export class CanDeactivateFileManagerDetails implements CanDeactivate<FileManage
nextRoute = nextRoute.firstChild; nextRoute = nextRoute.firstChild;
} }
// If the next state doesn't contain '/files' // If the next state doesn't contain '/file-manager'
// it means we are navigating away from the // it means we are navigating away from the
// tasks app // file manager app
if ( !nextState.url.includes('/file-manager') ) if ( !nextState.url.includes('/file-manager') )
{ {
// Let it navigate // Let it navigate
return true; return true;
} }
// If we are navigating to another task... // If we are navigating to another item...
if ( nextRoute.paramMap.get('id') ) if ( nextState.url.includes('/details') )
{ {
// Just navigate // Just navigate
return true; return true;

View File

@ -33,6 +33,54 @@ export class FileManagerItemsResolver implements Resolve<any>
} }
} }
@Injectable({
providedIn: 'root'
})
export class FileManagerFolderResolver implements Resolve<any>
{
/**
* Constructor
*/
constructor(
private _router: Router,
private _fileManagerService: FileManagerService
)
{
}
// -----------------------------------------------------------------------------------------------------
// @ Public methods
// -----------------------------------------------------------------------------------------------------
/**
* Resolver
*
* @param route
* @param state
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Item[]>
{
return this._fileManagerService.getItems(route.paramMap.get('folderId'))
.pipe(
// Error here means the requested task is not available
catchError((error) => {
// Log the error
console.error(error);
// Get the parent url
const parentUrl = state.url.split('/').slice(0, -1).join('/');
// Navigate to there
this._router.navigateByUrl(parentUrl);
// Throw an error
return throwError(error);
})
);
}
}
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })

View File

@ -3,13 +3,30 @@ import { CanDeactivateFileManagerDetails } from 'app/modules/admin/apps/file-man
import { FileManagerComponent } from 'app/modules/admin/apps/file-manager/file-manager.component'; import { FileManagerComponent } from 'app/modules/admin/apps/file-manager/file-manager.component';
import { FileManagerListComponent } from 'app/modules/admin/apps/file-manager/list/list.component'; import { FileManagerListComponent } from 'app/modules/admin/apps/file-manager/list/list.component';
import { FileManagerDetailsComponent } from 'app/modules/admin/apps/file-manager/details/details.component'; import { FileManagerDetailsComponent } from 'app/modules/admin/apps/file-manager/details/details.component';
import { FileManagerItemResolver, FileManagerItemsResolver } from 'app/modules/admin/apps/file-manager/file-manager.resolvers'; import { FileManagerFolderResolver, FileManagerItemResolver, FileManagerItemsResolver } from 'app/modules/admin/apps/file-manager/file-manager.resolvers';
export const fileManagerRoutes: Route[] = [ export const fileManagerRoutes: Route[] = [
{ {
path : '', path : '',
component: FileManagerComponent, component: FileManagerComponent,
children : [ children : [
{
path : 'folders/:folderId',
component: FileManagerListComponent,
resolve : {
item: FileManagerFolderResolver
},
children: [
{
path : 'details/:id',
component : FileManagerDetailsComponent,
resolve : {
item: FileManagerItemResolver
},
canDeactivate: [CanDeactivateFileManagerDetails]
}
]
},
{ {
path : '', path : '',
component: FileManagerListComponent, component: FileManagerListComponent,
@ -18,7 +35,7 @@ export const fileManagerRoutes: Route[] = [
}, },
children : [ children : [
{ {
path : ':id', path : 'details/:id',
component : FileManagerDetailsComponent, component : FileManagerDetailsComponent,
resolve : { resolve : {
item: FileManagerItemResolver item: FileManagerItemResolver

View File

@ -47,9 +47,9 @@ export class FileManagerService
/** /**
* Get items * Get items
*/ */
getItems(): Observable<Item[]> getItems(folderId: string | null = null): Observable<Item[]>
{ {
return this._httpClient.get<Items>('api/apps/file-manager').pipe( return this._httpClient.get<Items>('api/apps/file-manager', {params: {folderId}}).pipe(
tap((response: any) => { tap((response: any) => {
this._items.next(response); this._items.next(response);
}) })

View File

@ -2,11 +2,13 @@ export interface Items
{ {
folders: Item[]; folders: Item[];
files: Item[]; files: Item[];
path: any[];
} }
export interface Item export interface Item
{ {
id?: string; id?: string;
folderId?: string;
name?: string; name?: string;
createdBy?: string; createdBy?: string;
createdAt?: string; createdAt?: string;

View File

@ -26,7 +26,32 @@
<div> <div>
<div class="text-4xl font-extrabold tracking-tight leading-none">File Manager</div> <div class="text-4xl font-extrabold tracking-tight leading-none">File Manager</div>
<div class="flex items-center mt-0.5 font-medium text-secondary"> <div class="flex items-center mt-0.5 font-medium text-secondary">
<ng-container *ngIf="!items.path.length">
{{items.folders.length}} folders, {{items.files.length}} files {{items.folders.length}} folders, {{items.files.length}} files
</ng-container>
<!-- Breadcrumbs -->
<ng-container *ngIf="items.path.length">
<div class="flex items-center space-x-2">
<a
class="text-primary cursor-pointer"
[routerLink]="['/apps/file-manager']">Home
</a>
<div class="">/</div>
<ng-container *ngFor="let path of items.path; let last = last; trackBy: trackByFn">
<ng-container *ngIf="!last">
<a
class="text-primary cursor-pointer"
[routerLink]="['/apps/file-manager/folders/', path.id]">{{path.name}}</a>
</ng-container>
<ng-container *ngIf="last">
<div>{{path.name}}</div>
</ng-container>
<ng-container *ngIf="!last">
<div class="">/</div>
</ng-container>
</ng-container>
</div>
</ng-container>
</div> </div>
</div> </div>
<!-- Actions --> <!-- Actions -->
@ -42,75 +67,95 @@
</div> </div>
<!-- Items list --> <!-- Items list -->
<ng-container *ngIf="items && items.folders.length && items.files.length > 0; else noItems"> <ng-container *ngIf="items && (items.folders.length > 0 || items.files.length > 0); else noItems">
<div class="p-6 md:p-8"> <div class="p-6 md:p-8 space-y-8">
<!-- Folders --> <!-- Folders -->
<ng-container *ngIf="items.folders.length > 0">
<div>
<div class="font-medium">Folders</div> <div class="font-medium">Folders</div>
<div <div
class="flex flex-wrap -m-2 mt-2"> class="flex flex-wrap -m-2 mt-2">
<ng-container *ngFor="let folder of items.folders; trackBy:trackByFn"> <ng-container *ngFor="let folder of items.folders; trackBy:trackByFn">
<ng-container *ngTemplateOutlet="item, context: {$implicit: folder}"></ng-container> <div class="relative w-40 h-40 m-2 p-4 shadow rounded-2xl bg-card">
</ng-container>
</div>
<!-- Files -->
<div class="font-medium mt-8">Files</div>
<div
class="flex flex-wrap -m-2 mt-2">
<ng-container *ngFor="let file of items.files; trackBy:trackByFn">
<ng-container *ngTemplateOutlet="item, context: {$implicit: file}"></ng-container>
</ng-container>
</div>
</div>
</ng-container>
<!-- Item template -->
<ng-template
#item
let-item>
<a <a
class="flex flex-col w-40 h-40 m-2 p-4 shadow rounded-2xl cursor-pointer bg-card" class="absolute z-20 top-1.5 right-1.5 w-8 h-8 min-h-8"
[routerLink]="['./', item.id]"> (click)="$event.preventDefault()"
[routerLink]="['./details/', folder.id]"
mat-icon-button>
<mat-icon
class="icon-size-5"
[svgIcon]="'heroicons_solid:information-circle'"></mat-icon>
</a>
<a
class="z-10 absolute inset-0 flex flex-col p-4 cursor-pointer"
[routerLink]="['/apps/file-manager/folders/', folder.id]">
<div class="aspect-w-9 aspect-h-6"> <div class="aspect-w-9 aspect-h-6">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<!-- Icons --> <!-- Icon -->
<ng-container [ngSwitch]="item.type">
<!-- Folder -->
<ng-container *ngSwitchCase="'folder'">
<mat-icon <mat-icon
class="icon-size-14 text-hint" class="icon-size-14 text-hint"
[svgIcon]="'iconsmind:folder'"></mat-icon> [svgIcon]="'iconsmind:folder'"></mat-icon>
</div>
</div>
<div class="flex flex-col flex-auto justify-center text-center text-sm font-medium">
<div
class="truncate"
[matTooltip]="folder.name">{{folder.name}}</div>
<ng-container *ngIf="folder.contents">
<div class="text-secondary truncate">{{folder.contents}}</div>
</ng-container> </ng-container>
<!-- File --> </div>
<ng-container *ngSwitchDefault> </a>
</div>
</ng-container>
</div>
</div>
</ng-container>
<!-- Files -->
<ng-container *ngIf="items.files.length > 0">
<div>
<div class="font-medium">Files</div>
<div
class="flex flex-wrap -m-2 mt-2">
<ng-container *ngFor="let file of items.files; trackBy:trackByFn">
<a
class="flex flex-col w-40 h-40 m-2 p-4 shadow rounded-2xl cursor-pointer bg-card"
[routerLink]="['./details/', file.id]">
<div class="aspect-w-9 aspect-h-6">
<div class="flex items-center justify-center">
<!-- Icons -->
<div class="relative"> <div class="relative">
<mat-icon <mat-icon
class="icon-size-14 text-hint" class="icon-size-14 text-hint"
[svgIcon]="'iconsmind:file'"></mat-icon> [svgIcon]="'iconsmind:file'"></mat-icon>
<div <div
class="absolute left-0 bottom-0 px-1.5 rounded text-sm font-semibold leading-5 text-white" class="absolute left-0 bottom-0 px-1.5 rounded text-sm font-semibold leading-5 text-white"
[class.bg-red-600]="item.type === 'PDF'" [class.bg-red-600]="file.type === 'PDF'"
[class.bg-blue-600]="item.type === 'DOC'" [class.bg-blue-600]="file.type === 'DOC'"
[class.bg-green-600]="item.type === 'XLS'" [class.bg-green-600]="file.type === 'XLS'"
[class.bg-gray-600]="item.type === 'TXT'" [class.bg-gray-600]="file.type === 'TXT'"
[class.bg-amber-600]="item.type === 'JPG'"> [class.bg-amber-600]="file.type === 'JPG'">
{{item.type.toUpperCase()}} {{file.type.toUpperCase()}}
</div> </div>
</div> </div>
</ng-container>
</ng-container>
</div> </div>
</div> </div>
<div class="flex flex-col flex-auto justify-center text-center text-sm font-medium"> <div class="flex flex-col flex-auto justify-center text-center text-sm font-medium">
<div <div
class="truncate" class="truncate"
[matTooltip]="item.name">{{item.name}}</div> [matTooltip]="file.name">{{file.name}}</div>
<ng-container *ngIf="item.contents"> <ng-container *ngIf="file.contents">
<div class="text-secondary truncate">{{item.contents}}</div> <div class="text-secondary truncate">{{file.contents}}</div>
</ng-container> </ng-container>
</div> </div>
</a> </a>
</ng-template> </ng-container>
</div>
</div>
</ng-container>
</div>
</ng-container>
<!-- No items template --> <!-- No items template -->
<ng-template #noItems> <ng-template #noItems>