(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
.onGet('api/apps/file-manager')
.reply(() => {
.reply(({request}) => {
// 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
const folders = items.filter(item => item.type === 'folder');
@ -46,11 +54,38 @@ export class FileManagerMockApi
folders.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 [
200,
{
folders,
files
files,
path
}
];
});

View File

@ -2,6 +2,7 @@
export const items = [
{
id : 'cd6897cb-acfd-4016-8b53-3f66a5b5fc68',
folderId : null,
name : 'Personal',
createdBy : 'Brian Hughes',
createdAt : 'April 24, 2018',
@ -13,6 +14,7 @@ export const items = [
},
{
id : '6da8747f-b474-4c9a-9eba-5ef212285500',
folderId : null,
name : 'Photos',
createdBy : 'Brian Hughes',
createdAt : 'November 01, 2021',
@ -24,6 +26,7 @@ export const items = [
},
{
id : 'ed58add1-45a7-41db-887d-3ca7ee7f2719',
folderId : null,
name : 'Work',
createdBy : 'Brian Hughes',
createdAt : 'May 8, 2020',
@ -35,6 +38,7 @@ export const items = [
},
{
id : '5cb66e32-d1ac-4b9a-8c34-5991ce25add2',
folderId : null,
name : 'Contract #123',
createdBy : 'Brian Hughes',
createdAt : 'January 14, 2021',
@ -46,6 +50,7 @@ export const items = [
},
{
id : '3ffc3d84-8f2d-4929-903a-ef6fc21657a7',
folderId : null,
name : 'Estimated budget',
createdBy : 'Brian Hughes',
createdAt : 'December 14, 2020',
@ -57,6 +62,7 @@ export const items = [
},
{
id : '157adb9a-14f8-4559-ac93-8be893c9f80a',
folderId : null,
name : 'DMCA notice #42',
createdBy : 'Brian Hughes',
createdAt : 'May 8, 2021',
@ -68,6 +74,7 @@ export const items = [
},
{
id : '4f64597a-df7e-461c-ad60-f33e5f7e0747',
folderId : null,
name : 'Invoices',
createdBy : 'Brian Hughes',
createdAt : 'January 12, 2020',
@ -79,6 +86,7 @@ export const items = [
},
{
id : 'e445c445-57b2-4476-8c62-b068e3774b8e',
folderId : null,
name : 'Crash logs',
createdBy : 'Brian Hughes',
createdAt : 'June 8, 2020',
@ -90,6 +98,7 @@ export const items = [
},
{
id : 'b482f93e-7847-4614-ad48-b78b78309f81',
folderId : null,
name : 'System logs',
createdBy : 'Brian Hughes',
createdAt : 'June 8, 2020',
@ -101,6 +110,7 @@ export const items = [
},
{
id : 'ec07a98d-2e5b-422c-a9b2-b5d1c0e263f5',
folderId : null,
name : 'Personal projects',
createdBy : 'Brian Hughes',
createdAt : 'March 18, 2020',
@ -112,6 +122,7 @@ export const items = [
},
{
id : 'ae908d59-07da-4dd8-aba0-124e50289295',
folderId : null,
name : 'Biometric portrait',
createdBy : 'Brian Hughes',
createdAt : 'August 29, 2020',
@ -123,6 +134,7 @@ export const items = [
},
{
id : '4038a5b6-5b1a-432d-907c-e037aeb817a8',
folderId : null,
name : 'Scanned image 20201012-1',
createdBy : 'Brian Hughes',
createdAt : 'September 13, 2020',
@ -134,6 +146,7 @@ export const items = [
},
{
id : '630d2e9a-d110-47a0-ac03-256073a0f56d',
folderId : null,
name : 'Scanned image 20201012-2',
createdBy : 'Brian Hughes',
createdAt : 'September 14, 2020',
@ -145,6 +158,7 @@ export const items = [
},
{
id : '1417d5ed-b616-4cff-bfab-286677b69d79',
folderId : null,
name : 'Prices',
createdBy : 'Brian Hughes',
createdAt : 'April 07, 2020',
@ -156,6 +170,7 @@ export const items = [
},
{
id : 'bd2817c7-6751-40dc-b252-b6b5634c0689',
folderId : null,
name : 'Shopping list',
createdBy : 'Brian Hughes',
createdAt : 'March 26, 2021',
@ -167,6 +182,7 @@ export const items = [
},
{
id : '14fb47c9-6eeb-4070-919c-07c8133285d1',
folderId : null,
name : 'Summer budget',
createdBy : 'Brian Hughes',
createdAt : 'June 02, 2020',
@ -175,5 +191,67 @@ export const items = [
type : 'XLS',
contents : 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">
<button
mat-icon-button
[routerLink]="['../']">
[routerLink]="['../../']">
<mat-icon [svgIcon]="'heroicons_outline:x'"></mat-icon>
</button>
</div>

View File

@ -22,17 +22,17 @@ export class CanDeactivateFileManagerDetails implements CanDeactivate<FileManage
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
// tasks app
// file manager app
if ( !nextState.url.includes('/file-manager') )
{
// Let it navigate
return true;
}
// If we are navigating to another task...
if ( nextRoute.paramMap.get('id') )
// If we are navigating to another item...
if ( nextState.url.includes('/details') )
{
// Just navigate
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({
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 { FileManagerListComponent } from 'app/modules/admin/apps/file-manager/list/list.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[] = [
{
path : '',
component: FileManagerComponent,
children : [
{
path : 'folders/:folderId',
component: FileManagerListComponent,
resolve : {
item: FileManagerFolderResolver
},
children: [
{
path : 'details/:id',
component : FileManagerDetailsComponent,
resolve : {
item: FileManagerItemResolver
},
canDeactivate: [CanDeactivateFileManagerDetails]
}
]
},
{
path : '',
component: FileManagerListComponent,
@ -18,7 +35,7 @@ export const fileManagerRoutes: Route[] = [
},
children : [
{
path : ':id',
path : 'details/:id',
component : FileManagerDetailsComponent,
resolve : {
item: FileManagerItemResolver

View File

@ -47,9 +47,9 @@ export class FileManagerService
/**
* 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) => {
this._items.next(response);
})

View File

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

View File

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