[45520] Refactored LocationPicker to accept new backend state

pull/11920/head
Andreas Pfohl 2 years ago
parent 328a5354aa
commit 83fc0e7269
No known key found for this signature in database
GPG Key ID: FF58F3B771328EB4
  1. 6
      frontend/src/app/core/state/storage-files/storage-file.model.ts
  2. 3
      frontend/src/app/core/state/storage-files/storage-files.service.ts
  3. 6
      frontend/src/app/shared/components/storages/file-link-list-item/file-link-list-item.component.ts
  4. 67
      frontend/src/app/shared/components/storages/file-picker-base-modal.component.ts/file-picker-base-modal.component.ts
  5. 13
      frontend/src/app/shared/components/storages/file-picker-modal/file-picker-modal.component.ts
  6. 9
      frontend/src/app/shared/components/storages/functions/storages.functions.ts
  7. 27
      frontend/src/app/shared/components/storages/location-picker-modal/location-picker-modal.component.ts
  8. 9
      frontend/src/app/shared/components/storages/pipes/sort-files.pipe.ts
  9. 1
      frontend/src/app/shared/components/storages/services/upload-storage-files.service.ts
  10. 12
      frontend/src/app/shared/components/storages/storage-file-list-item/storage-file-list-item.ts
  11. 4
      modules/storages/app/common/storages/peripherals/storage_interaction/nextcloud/files_query.rb

@ -32,4 +32,8 @@ interface IStorageFileLocation {
location:string;
}
export interface IStorageFile extends IFileLinkOriginData, IStorageFileLocation {}
interface IStorageFilePermissions {
permissions:Array<'readable'|'writeable'>
}
export interface IStorageFile extends IFileLinkOriginData, IStorageFileLocation, IStorageFilePermissions {}

@ -28,7 +28,7 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { map, take, tap } from 'rxjs/operators';
import {
CollectionStore,
@ -60,6 +60,7 @@ export class StorageFilesResourceService extends ResourceCollectionService<IStor
insertCollectionIntoState(this.store, collection, link.href);
}),
map((collection) => collection._embedded.elements),
take(1),
);
}

@ -110,7 +110,7 @@ export class FileLinkListItemComponent implements OnInit, AfterViewInit {
this.fileLinkIcon = getIconForMimeType(this.originData.mimeType);
this.downloadAllowed = !isDirectory(this.originData.mimeType);
this.downloadAllowed = !isDirectory(this.originData);
this.text.title.downloadFileLink = this.i18n.t(
'js.storages.file_links.download',
@ -123,14 +123,14 @@ export class FileLinkListItemComponent implements OnInit, AfterViewInit {
ngAfterViewInit():void {
if (this.originData.lastModifiedByName) {
this.principalRendererService.render(
this.avatar.nativeElement,
this.avatar.nativeElement as HTMLElement,
{ name: this.originData.lastModifiedByName, href: '/external_users/1' },
{ hide: true, link: false },
{ hide: false, size: 'mini' },
);
} else {
this.principalRendererService.render(
this.avatar.nativeElement,
this.avatar.nativeElement as HTMLElement,
{ name: 'Not Available', href: '/placeholder_users/1' },
{ hide: true, link: false },
{ hide: false, size: 'mini' },

@ -35,7 +35,7 @@ import {
OnInit,
} from '@angular/core';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { map } from 'rxjs/operators';
import { IHalResourceLink } from 'core-app/core/state/hal-resource';
import { IStorageFile } from 'core-app/core/state/storage-files/storage-file.model';
@ -56,15 +56,17 @@ import {
@Directive()
export abstract class FilePickerBaseModalComponent extends OpModalComponent implements OnInit, OnDestroy {
private loadingSubscription:Subscription;
protected readonly storageFiles$ = new BehaviorSubject<IStorageFile[]>([]);
public breadcrumbs:BreadcrumbsContent = new BreadcrumbsContent(
[{
text: this.locals.storageName as string,
icon: getIconForStorageType(this.locals.storageType as string),
navigate: () => this.changeLevel(null, this.breadcrumbs.crumbs.slice(0, 1)),
}],
);
protected currentDirectory:IStorageFile;
protected get storageLink():IHalResourceLink {
return this.locals.storageLink as IHalResourceLink;
}
public breadcrumbs:BreadcrumbsContent = new BreadcrumbsContent([]);
public listItems$:Observable<StorageFileListItem[]> = this.storageFiles$
.pipe(
@ -75,12 +77,10 @@ export abstract class FilePickerBaseModalComponent extends OpModalComponent impl
public readonly loading$ = new BehaviorSubject<boolean>(true);
protected get storageLink():IHalResourceLink {
return this.locals.storageLink as IHalResourceLink;
public get location():string {
return this.currentDirectory.location;
}
private loadingSubscription:Subscription;
protected constructor(
@Inject(OpModalLocalsToken) public locals:OpModalLocalsMap,
readonly elementRef:ElementRef,
@ -94,10 +94,25 @@ export abstract class FilePickerBaseModalComponent extends OpModalComponent impl
ngOnInit():void {
super.ngOnInit();
this.storageFilesResourceService.files(makeFilesCollectionLink(this.storageLink, null))
.pipe(take(1))
this.storageFilesResourceService
.files(makeFilesCollectionLink(this.storageLink, undefined))
.subscribe((files) => {
this.storageFiles$.next(files);
const root = files.find((file) => file.name === '/');
if (root === undefined) {
throw new Error('Collection does not contain a root directory!');
}
this.currentDirectory = root;
this.breadcrumbs = new BreadcrumbsContent(
[{
text: this.locals.storageName as string,
icon: getIconForStorageType(this.locals.storageType as string),
navigate: () => this.changeLevel(root, this.breadcrumbs.crumbs.slice(0, 1)),
}],
);
this.storageFiles$.next(files.filter((file) => file.name !== '/'));
this.loading$.next(false);
});
}
@ -109,13 +124,13 @@ export abstract class FilePickerBaseModalComponent extends OpModalComponent impl
}
public openStorageLocation():void {
window.open(this.locals.storageLocation, '_blank');
window.open(this.locals.storageLocation as string, '_blank');
}
protected abstract storageFileToListItem(file:IStorageFile, index:number):StorageFileListItem;
protected enterDirectoryCallback(directory:IStorageFile):() => void {
if (!isDirectory(directory.mimeType)) {
if (!isDirectory(directory)) {
return () => {};
}
@ -126,19 +141,23 @@ export abstract class FilePickerBaseModalComponent extends OpModalComponent impl
text: directory.name,
// The navigate-callback needs to slice the future breadcrumb, which contains the new crumb itself.
// Therefore, we need the closure in here.
navigate: () => this.changeLevel(directory.location, this.breadcrumbs.crumbs.slice(0, end)),
navigate: () => this.changeLevel(directory, this.breadcrumbs.crumbs.slice(0, end)),
};
this.changeLevel(directory.location, crumbs.concat(newCrumb));
this.changeLevel(directory, crumbs.concat(newCrumb));
};
}
protected changeLevel(parent:string|null, crumbs:Breadcrumb[]):void {
protected changeLevel(directory:IStorageFile, crumbs:Breadcrumb[]):void {
this.currentDirectory = directory;
this.cancelCurrentLoading();
this.loading$.next(true);
this.breadcrumbs = new BreadcrumbsContent(crumbs);
this.loadingSubscription = this.storageFilesResourceService.files(makeFilesCollectionLink(this.storageLink, parent))
.pipe(take(1))
this.loadingSubscription = this.storageFilesResourceService
.files(makeFilesCollectionLink(this.storageLink, directory.location))
.pipe(map((files) => files.filter((file) => file.name !== this.currentDirectory.name)))
.subscribe((files) => {
this.storageFiles$.next(files);
this.loading$.next(false);
@ -146,8 +165,6 @@ export abstract class FilePickerBaseModalComponent extends OpModalComponent impl
}
private cancelCurrentLoading():void {
if (this.loadingSubscription) {
this.loadingSubscription.unsubscribe();
}
this.loadingSubscription?.unsubscribe();
}
}

@ -27,11 +27,7 @@
//++
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
Inject,
ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Inject,
} from '@angular/core';
import { take } from 'rxjs/operators';
@ -147,20 +143,17 @@ export class FilePickerModalComponent extends FilePickerBaseModalComponent {
}
protected storageFileToListItem(file:IStorageFile, index:number):StorageFileListItem {
const isFolder = isDirectory(file.mimeType);
const enterDirectoryCallback = isFolder ? this.enterDirectoryCallback(file) : undefined;
return new StorageFileListItem(
this.timezoneService,
file,
this.isAlreadyLinked(file),
index === 0,
isFolder ? this.text.tooltip.alreadyLinkedDirectory : this.text.tooltip.alreadyLinkedFile,
this.enterDirectoryCallback(file),
isDirectory(file) ? this.text.tooltip.alreadyLinkedDirectory : this.text.tooltip.alreadyLinkedFile,
{
selected: this.selection.has(file.id as string),
changeSelection: () => { this.changeSelection(file); },
},
enterDirectoryCallback,
);
}

@ -32,9 +32,10 @@ import {
storageIconMappings,
} from 'core-app/shared/components/storages/icons.mapping';
import { IHalResourceLink } from 'core-app/core/state/hal-resource';
import { IFileLinkOriginData } from 'core-app/core/state/file-links/file-link.model';
export function isDirectory(mimeType?:string):boolean {
return mimeType === 'application/x-op-directory';
export function isDirectory(originData:IFileLinkOriginData):boolean {
return originData.mimeType === 'application/x-op-directory';
}
export function getIconForMimeType(mimeType?:string):IFileIcon {
@ -53,8 +54,8 @@ export function getIconForStorageType(storageType?:string):string {
return storageIconMappings.default;
}
export function makeFilesCollectionLink(storageLink:IHalResourceLink, location:string|null):IHalResourceLink {
const query = location !== null ? `?parent=${location}` : '';
export function makeFilesCollectionLink(storageLink:IHalResourceLink, location:string|undefined):IHalResourceLink {
const query = location !== undefined ? `?parent=${location}` : '';
return {
href: `${storageLink.href}/files${query}`,

@ -27,11 +27,7 @@
//++
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
Inject,
ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Inject,
} from '@angular/core';
import { I18nService } from 'core-app/core/i18n/i18n.service';
@ -39,7 +35,6 @@ import { TimezoneService } from 'core-app/core/datetime/timezone.service';
import { IStorageFile } from 'core-app/core/state/storage-files/storage-file.model';
import { OpModalLocalsMap } from 'core-app/shared/components/modal/modal.types';
import { OpModalLocalsToken } from 'core-app/shared/components/modal/modal.service';
import { Breadcrumb } from 'core-app/spot/components/breadcrumbs/breadcrumbs-content';
import { SortFilesPipe } from 'core-app/shared/components/storages/pipes/sort-files.pipe';
import { isDirectory } from 'core-app/shared/components/storages/functions/storages.functions';
import { StorageFilesResourceService } from 'core-app/core/state/storage-files/storage-files.service';
@ -69,10 +64,12 @@ export class LocationPickerModalComponent extends FilePickerBaseModalComponent {
};
public get canChooseLocation():boolean {
return this.breadcrumbs.crumbs.length > 1;
}
if (!this.currentDirectory) {
return false;
}
public location = '/';
return this.currentDirectory.permissions.some((value) => value === 'writeable');
}
constructor(
@Inject(OpModalLocalsToken) public locals:OpModalLocalsMap,
@ -98,22 +95,14 @@ export class LocationPickerModalComponent extends FilePickerBaseModalComponent {
}
protected storageFileToListItem(file:IStorageFile, index:number):StorageFileListItem {
const isFolder = isDirectory(file.mimeType);
const enterDirectoryCallback = isFolder ? this.enterDirectoryCallback(file) : undefined;
return new StorageFileListItem(
this.timezoneService,
file,
!isFolder,
!isDirectory(file),
index === 0,
this.enterDirectoryCallback(file),
undefined,
undefined,
enterDirectoryCallback,
);
}
protected changeLevel(parent:string | null, crumbs:Breadcrumb[]):void {
this.location = parent === null ? '/' : parent;
super.changeLevel(parent, crumbs);
}
}

@ -1,22 +1,23 @@
import { Pipe, PipeTransform } from '@angular/core';
import { isDirectory } from 'core-app/shared/components/storages/functions/storages.functions';
import { IFileLinkOriginData } from 'core-app/core/state/file-links/file-link.model';
@Pipe({
name: 'sortFiles',
})
export class SortFilesPipe implements PipeTransform {
transform<T extends { mimeType?:string, name:string }>(array:T[]):T[] {
transform<T extends IFileLinkOriginData>(array:T[]):T[] {
return array.sort((a, b):number => {
if (isDirectory(a.mimeType) && isDirectory(b.mimeType)) {
if (isDirectory(a) && isDirectory(b)) {
return a.name.localeCompare(b.name);
}
if (isDirectory(a.mimeType)) {
if (isDirectory(a)) {
return -1;
}
if (isDirectory(b.mimeType)) {
if (isDirectory(b)) {
return 1;
}

@ -111,6 +111,7 @@ export class UploadStorageFilesService {
createdByName: creator,
lastModifiedAt,
lastModifiedByName: creator,
permissions: [],
};
}

@ -60,7 +60,15 @@ export class StorageFileListItem {
}
get isDirectory():boolean {
return isDirectory(this.storageFile.mimeType);
return isDirectory(this.storageFile);
}
get isWriteable():boolean {
return this.storageFile.permissions.some((permission) => permission === 'writeable');
}
get isReadable():boolean {
return this.storageFile.permissions.some((permission) => permission === 'readable');
}
constructor(
@ -68,8 +76,8 @@ export class StorageFileListItem {
private readonly storageFile:IStorageFile,
public readonly disabled:boolean,
public readonly isFirst:boolean,
public readonly enterDirectory:() => void,
public readonly tooltip?:string,
public readonly checkbox?:StorageFileListItemCheckbox,
public readonly enterDirectory?:() => void,
) {}
}

@ -114,7 +114,7 @@ module Storages::Peripherals::StorageInteraction::Nextcloud
def storage_file(file_element)
location = name(file_element)
name = location == @base_path ? '/' : CGI.unescape(location.split('/').last)
name = location == '/' ? location : CGI.unescape(location.split('/').last)
::Storages::StorageFile.new(
id(file_element),
@ -145,7 +145,7 @@ module Storages::Peripherals::StorageInteraction::Nextcloud
return nil if texts.empty?
element_name = texts.first
element_name = texts.first.delete_prefix(@base_path)
return element_name if element_name == '/'

Loading…
Cancel
Save