From 0342964e2afb5209d3a10680c1b2dc04fb900c41 Mon Sep 17 00:00:00 2001 From: Kyrylo Petrov Date: Sun, 13 Jul 2025 22:18:06 +0300 Subject: [PATCH 1/3] feat(registry-files): files refactor, registry-files --- .../nav-menu/nav-menu.component.scss | 4 + src/app/core/constants/nav-items.constant.ts | 1 + .../my-projects/my-projects.component.html | 2 +- .../my-projects/my-projects.component.ts | 5 + .../file-step/file-step.component.html | 5 +- .../stepper/file-step/file-step.component.ts | 11 +- .../features/preprints/preprints.routes.ts | 3 + .../preprint-stepper.actions.ts | 6 + .../preprint-stepper.model.ts | 1 + .../preprint-stepper.selectors.ts | 5 + .../preprint-stepper.state.ts | 9 +- .../move-file-dialog.component.ts | 26 ++- .../project-files-state-defaults.const.ts | 10 + .../models/files-tree-actions.interface.ts | 5 +- .../project-files.component.html | 185 ++++++++++-------- .../project-files.component.scss | 6 +- .../project-files/project-files.component.ts | 152 +++++++++++--- .../files/store/project-files.actions.ts | 17 +- .../files/store/project-files.model.ts | 3 + .../files/store/project-files.selectors.ts | 21 ++ .../files/store/project-files.state.ts | 103 +++++----- .../mappers/project-overview.mapper.ts | 4 + .../models/project-overview.models.ts | 14 ++ src/app/features/project/project.routes.ts | 3 + .../mappers/registry-overview.mapper.ts | 3 + .../get-registry-overview-json-api.model.ts | 14 ++ .../models/registry-overview.models.ts | 3 + src/app/features/registry/pages/index.ts | 3 +- .../registry-files.component.html | 47 +++++ .../registry-files.component.scss | 0 .../registry-files.component.spec.ts | 22 +++ .../registry-files.component.ts | 167 ++++++++++++++++ .../registry-overview.component.scss | 1 + src/app/features/registry/registry.routes.ts | 12 +- .../registry/store/registry-files/index.ts | 4 + .../registry-files/registry-files.actions.ts | 25 +++ .../registry-files/registry-files.model.ts | 8 + .../registry-files.selectors.ts | 23 +++ .../registry-files/registry-files.state.ts | 70 +++++++ .../registry-overview.model.ts | 8 +- .../registry-overview.selectors.ts | 7 +- .../files-tree/files-tree.component.html | 35 ++-- .../files-tree/files-tree.component.scss | 49 +++-- .../files-tree/files-tree.component.ts | 117 ++++++++--- .../factories/files-tree-selectors.factory.ts | 35 ++++ src/app/shared/factories/index.ts | 1 + src/app/shared/mappers/files/files.mapper.ts | 12 +- .../addons/configured-storage-addon.model.ts | 4 + src/app/shared/models/addons/index.ts | 1 + src/app/shared/models/files/file.model.ts | 15 +- .../get-configured-storage-addons.model.ts | 14 ++ .../models/files/get-files-response.model.ts | 1 + src/app/shared/models/index.ts | 1 + src/app/shared/services/files.service.ts | 117 ++++++----- src/app/shared/services/toast.service.ts | 4 +- .../tokens/files-tree-selectors.token.ts | 11 ++ src/app/shared/tokens/index.ts | 1 + src/assets/i18n/en.json | 6 +- src/environments/environment.development.ts | 1 + src/environments/environment.ts | 1 + 60 files changed, 1138 insertions(+), 306 deletions(-) create mode 100644 src/app/features/registry/pages/registry-files/registry-files.component.html create mode 100644 src/app/features/registry/pages/registry-files/registry-files.component.scss create mode 100644 src/app/features/registry/pages/registry-files/registry-files.component.spec.ts create mode 100644 src/app/features/registry/pages/registry-files/registry-files.component.ts create mode 100644 src/app/features/registry/store/registry-files/index.ts create mode 100644 src/app/features/registry/store/registry-files/registry-files.actions.ts create mode 100644 src/app/features/registry/store/registry-files/registry-files.model.ts create mode 100644 src/app/features/registry/store/registry-files/registry-files.selectors.ts create mode 100644 src/app/features/registry/store/registry-files/registry-files.state.ts create mode 100644 src/app/shared/factories/files-tree-selectors.factory.ts create mode 100644 src/app/shared/factories/index.ts create mode 100644 src/app/shared/models/addons/configured-storage-addon.model.ts create mode 100644 src/app/shared/models/files/get-configured-storage-addons.model.ts create mode 100644 src/app/shared/tokens/files-tree-selectors.token.ts diff --git a/src/app/core/components/nav-menu/nav-menu.component.scss b/src/app/core/components/nav-menu/nav-menu.component.scss index c16ce5464..97b64216b 100644 --- a/src/app/core/components/nav-menu/nav-menu.component.scss +++ b/src/app/core/components/nav-menu/nav-menu.component.scss @@ -7,4 +7,8 @@ border-radius: 0.5rem; font-weight: 700; } + + ::ng-deep li[aria-label="navigation.registriesSubRoutes.registryDetails"] { + border-left-color: transparent !important; + } } diff --git a/src/app/core/constants/nav-items.constant.ts b/src/app/core/constants/nav-items.constant.ts index 5c247a216..4913a43f2 100644 --- a/src/app/core/constants/nav-items.constant.ts +++ b/src/app/core/constants/nav-items.constant.ts @@ -25,6 +25,7 @@ export const MENU_ITEMS: MenuItem[] = [ label: 'navigation.registries', icon: 'osf-icon-registries', routerLinkActiveOptions: { exact: true }, + items: [ { routerLink: '/registries/overview', diff --git a/src/app/features/my-projects/my-projects.component.html b/src/app/features/my-projects/my-projects.component.html index b7499b273..703de0354 100644 --- a/src/app/features/my-projects/my-projects.component.html +++ b/src/app/features/my-projects/my-projects.component.html @@ -62,7 +62,7 @@ [isLoading]="isLoading()" (pageChange)="onPageChange($event)" (sort)="onSort($event)" - (itemClick)="navigateToProject($event)" + (itemClick)="navigateToRegistry($event)" /> diff --git a/src/app/features/my-projects/my-projects.component.ts b/src/app/features/my-projects/my-projects.component.ts index 6830e85ac..58c55bd1c 100644 --- a/src/app/features/my-projects/my-projects.component.ts +++ b/src/app/features/my-projects/my-projects.component.ts @@ -350,4 +350,9 @@ export class MyProjectsComponent implements OnInit { this.activeProject.set(project); this.#router.navigate(['/my-projects', project.id]); } + + protected navigateToRegistry(registry: MyProjectsItem): void { + this.activeProject.set(registry); + this.#router.navigate(['/registries', registry.id]); + } } diff --git a/src/app/features/preprints/components/stepper/file-step/file-step.component.html b/src/app/features/preprints/components/stepper/file-step/file-step.component.html index c3ab6ee12..719d6b9ba 100644 --- a/src/app/features/preprints/components/stepper/file-step/file-step.component.html +++ b/src/app/features/preprints/components/stepper/file-step/file-step.component.html @@ -87,10 +87,7 @@

{{ 'preprints.preprintStepper.file.title' | translate }}

@if (selectedProjectId()) { diff --git a/src/app/features/preprints/components/stepper/file-step/file-step.component.ts b/src/app/features/preprints/components/stepper/file-step/file-step.component.ts index e423e6d01..e6c498d2c 100644 --- a/src/app/features/preprints/components/stepper/file-step/file-step.component.ts +++ b/src/app/features/preprints/components/stepper/file-step/file-step.component.ts @@ -9,7 +9,7 @@ import { Select, SelectChangeEvent } from 'primeng/select'; import { Skeleton } from 'primeng/skeleton'; import { Tooltip } from 'primeng/tooltip'; -import { debounceTime, distinctUntilChanged, EMPTY, Observable } from 'rxjs'; +import { debounceTime, distinctUntilChanged, Observable } from 'rxjs'; import { NgClass, TitleCasePipe } from '@angular/common'; import { @@ -37,6 +37,7 @@ import { FetchProjectFilesByLink, PreprintStepperSelectors, ReuploadFile, + SetCurrentFolder, SetSelectedPreprintFileSource, UploadFile, } from '@osf/features/preprints/store/preprint-stepper'; @@ -77,6 +78,7 @@ export class FileStepComponent implements OnInit { getFilesForSelectedProject: FetchProjectFiles, getProjectFilesByLink: FetchProjectFilesByLink, copyFileFromProject: CopyFileFromProject, + setCurrentFolder: SetCurrentFolder, }); private destroyRef = inject(DestroyRef); @@ -94,7 +96,6 @@ export class FileStepComponent implements OnInit { projectFiles = select(PreprintStepperSelectors.getProjectFiles); areProjectFilesLoading = select(PreprintStepperSelectors.areProjectFilesLoading); selectedProjectId = signal(null); - currentFolder = signal(null); versionFileMode = signal(false); @@ -102,15 +103,11 @@ export class FileStepComponent implements OnInit { filesTreeActions: FilesTreeActions = { setCurrentFolder: (folder: OsfFile | null): Observable => { - this.currentFolder.set(folder); - return EMPTY; + return this.actions.setCurrentFolder(folder); }, getFiles: (filesLink: string): Observable => { return this.actions.getProjectFilesByLink(filesLink); }, - getRootFolderFiles: (projectId: string): Observable => { - return this.actions.getFilesForSelectedProject(projectId); - }, }; nextClicked = output(); diff --git a/src/app/features/preprints/preprints.routes.ts b/src/app/features/preprints/preprints.routes.ts index 00889a639..05768d5e6 100644 --- a/src/app/features/preprints/preprints.routes.ts +++ b/src/app/features/preprints/preprints.routes.ts @@ -69,6 +69,9 @@ export const preprintsRoutes: Routes = [ import('@osf/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component').then( (c) => c.SubmitPreprintStepperComponent ), + data: { + context: 'submitPreprints', + }, canDeactivate: [ConfirmLeavingGuard], }, { diff --git a/src/app/features/preprints/store/preprint-stepper/preprint-stepper.actions.ts b/src/app/features/preprints/store/preprint-stepper/preprint-stepper.actions.ts index 1f8a2f92a..4f23efb8f 100644 --- a/src/app/features/preprints/store/preprint-stepper/preprint-stepper.actions.ts +++ b/src/app/features/preprints/store/preprint-stepper/preprint-stepper.actions.ts @@ -134,3 +134,9 @@ export class ResetState { export class DeletePreprint { static readonly type = '[Submit Preprint] Delete Preprint'; } + +export class SetCurrentFolder { + static readonly type = '[Submit Preprint] Set Current Folder'; + + constructor(public folder: OsfFile | null) {} +} diff --git a/src/app/features/preprints/store/preprint-stepper/preprint-stepper.model.ts b/src/app/features/preprints/store/preprint-stepper/preprint-stepper.model.ts index dd1cf8b2a..05f7c2ed2 100644 --- a/src/app/features/preprints/store/preprint-stepper/preprint-stepper.model.ts +++ b/src/app/features/preprints/store/preprint-stepper/preprint-stepper.model.ts @@ -13,6 +13,7 @@ export interface PreprintStepperStateModel { availableProjects: AsyncStateModel; projectFiles: AsyncStateModel; licenses: AsyncStateModel; + currentFolder: OsfFile | null; preprintProject: AsyncStateModel; hasBeenSubmitted: boolean; } diff --git a/src/app/features/preprints/store/preprint-stepper/preprint-stepper.selectors.ts b/src/app/features/preprints/store/preprint-stepper/preprint-stepper.selectors.ts index 8369c36a1..98e3487ca 100644 --- a/src/app/features/preprints/store/preprint-stepper/preprint-stepper.selectors.ts +++ b/src/app/features/preprints/store/preprint-stepper/preprint-stepper.selectors.ts @@ -82,4 +82,9 @@ export class PreprintStepperSelectors { static hasBeenSubmitted(state: PreprintStepperStateModel) { return state.hasBeenSubmitted; } + + @Selector([PreprintStepperState]) + static getCurrentFolder(state: PreprintStepperStateModel) { + return state.currentFolder; + } } diff --git a/src/app/features/preprints/store/preprint-stepper/preprint-stepper.state.ts b/src/app/features/preprints/store/preprint-stepper/preprint-stepper.state.ts index ba7e256cd..9557f413d 100644 --- a/src/app/features/preprints/store/preprint-stepper/preprint-stepper.state.ts +++ b/src/app/features/preprints/store/preprint-stepper/preprint-stepper.state.ts @@ -38,6 +38,7 @@ import { ResetState, ReuploadFile, SaveLicense, + SetCurrentFolder, SetSelectedPreprintFileSource, SetSelectedPreprintProviderId, SubmitPreprint, @@ -85,6 +86,7 @@ const DefaultState: PreprintStepperStateModel = { error: null, }, hasBeenSubmitted: false, + currentFolder: null, }; @State({ @@ -166,7 +168,7 @@ export class PreprintStepperState { ctx.setState(patch({ preprintFiles: patch({ isLoading: true }) })); - return this.fileService.uploadFileByLink(action.file, state.preprintFilesLinks.data.uploadFileLink).pipe( + return this.fileService.uploadFile(action.file, state.preprintFilesLinks.data.uploadFileLink).pipe( filter((event) => event.type === HttpEventType.Response), switchMap((event) => { const file = event.body!.data; @@ -501,6 +503,11 @@ export class PreprintStepperState { return EMPTY; } + @Action(SetCurrentFolder) + setCurrentFolder(ctx: StateContext, action: SetCurrentFolder) { + ctx.patchState({ currentFolder: action.folder }); + } + private handleError( ctx: StateContext, section: keyof PreprintStepperStateModel, diff --git a/src/app/features/project/files/components/move-file-dialog/move-file-dialog.component.ts b/src/app/features/project/files/components/move-file-dialog/move-file-dialog.component.ts index 4201f1609..0dacb028e 100644 --- a/src/app/features/project/files/components/move-file-dialog/move-file-dialog.component.ts +++ b/src/app/features/project/files/components/move-file-dialog/move-file-dialog.component.ts @@ -6,7 +6,8 @@ import { Button } from 'primeng/button'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; import { Tooltip } from 'primeng/tooltip'; -import { finalize, take } from 'rxjs'; +import { finalize, take, throwError } from 'rxjs'; +import { catchError } from 'rxjs/operators'; import { NgOptimizedImage } from '@angular/common'; import { ChangeDetectionStrategy, Component, computed, DestroyRef, inject, signal } from '@angular/core'; @@ -22,7 +23,7 @@ import { } from '@osf/features/project/files/store'; import { IconComponent, LoadingSpinnerComponent } from '@shared/components'; import { OsfFile } from '@shared/models'; -import { FilesService } from '@shared/services'; +import { FilesService, ToastService } from '@shared/services'; @Component({ selector: 'osf-move-file-dialog', @@ -38,10 +39,12 @@ export class MoveFileDialogComponent { private readonly filesService = inject(FilesService); private readonly destroyRef = inject(DestroyRef); private readonly translateService = inject(TranslateService); + private readonly toastService = inject(ToastService); protected readonly files = select(ProjectFilesSelectors.getMoveFileFiles); protected readonly isLoading = select(ProjectFilesSelectors.isMoveFileFilesLoading); protected readonly currentFolder = select(ProjectFilesSelectors.getMoveFileCurrentFolder); + private readonly rootFolders = select(ProjectFilesSelectors.getRootFolders); protected readonly isFilesUpdating = signal(false); protected readonly isFolderSame = computed(() => { return this.currentFolder()?.id === this.config.data.file.relationships.parentFolderId; @@ -58,8 +61,11 @@ export class MoveFileDialogComponent { constructor() { const filesLink = this.currentFolder()?.relationships.filesLink; + const rootFolders = this.rootFolders(); if (filesLink) { this.dispatch.getMoveFileFiles(filesLink); + } else if (rootFolders) { + this.dispatch.getMoveFileFiles(rootFolders[0].relationships.filesLink); } } @@ -83,6 +89,10 @@ export class MoveFileDialogComponent { takeUntilDestroyed(this.destroyRef), finalize(() => { this.isFilesUpdating.set(false); + }), + catchError((error) => { + this.toastService.showError(error.error.message, 5000); + return throwError(error); }) ) .subscribe((folder) => { @@ -107,7 +117,7 @@ export class MoveFileDialogComponent { .moveFile( this.config.data.file.links.move, path, - this.config.data.projectId, + this.config.data.resourceId, this.provider(), this.config.data.action ) @@ -117,6 +127,11 @@ export class MoveFileDialogComponent { this.dispatch.setCurrentFolder(this.currentFolder()); this.dispatch.setMoveFileCurrentFolder(null); this.isFilesUpdating.set(false); + this.dialogRef.close(); + }), + catchError((error) => { + this.toastService.showError(error.error.message, 5000); + return throwError(error); }) ) .subscribe((file) => { @@ -124,10 +139,11 @@ export class MoveFileDialogComponent { if (file.id) { const filesLink = this.currentFolder()?.relationships.filesLink; + const rootFolders = this.rootFolders(); if (filesLink) { this.dispatch.getFiles(filesLink); - } else { - this.dispatch.getRootFolderFiles(this.config.data.projectId); + } else if (rootFolders) { + this.dispatch.getMoveFileFiles(rootFolders[0].relationships.filesLink); } } }); diff --git a/src/app/features/project/files/models/data/project-files-state-defaults.const.ts b/src/app/features/project/files/models/data/project-files-state-defaults.const.ts index eebbe8690..0b959d759 100644 --- a/src/app/features/project/files/models/data/project-files-state-defaults.const.ts +++ b/src/app/features/project/files/models/data/project-files-state-defaults.const.ts @@ -46,4 +46,14 @@ export const projectFilesStateDefaults = { isLoading: false, error: null, }, + rootFolders: { + data: [], + isLoading: true, + error: null, + }, + configuredStorageAddons: { + data: [], + isLoading: true, + error: null, + }, }; diff --git a/src/app/features/project/files/models/files-tree-actions.interface.ts b/src/app/features/project/files/models/files-tree-actions.interface.ts index 983a00038..404b9bbaf 100644 --- a/src/app/features/project/files/models/files-tree-actions.interface.ts +++ b/src/app/features/project/files/models/files-tree-actions.interface.ts @@ -4,11 +4,8 @@ import { OsfFile } from '@shared/models'; export interface FilesTreeActions { setCurrentFolder: (folder: OsfFile | null) => Observable; - setSearch?: (search: string) => Observable; - setSort?: (sort: string) => Observable; - setFilesIsLoading?: (isLoading: boolean) => Observable; + setFilesIsLoading?: (isLoading: boolean) => void; getFiles: (filesLink: string) => Observable; - getRootFolderFiles: (projectId: string) => Observable; deleteEntry?: (projectId: string, link: string) => Observable; renameEntry?: (projectId: string, link: string, newName: string) => Observable; setMoveFileCurrentFolder?: (folder: OsfFile | null) => Observable; diff --git a/src/app/features/project/files/pages/project-files/project-files.component.html b/src/app/features/project/files/pages/project-files/project-files.component.html index 92046a31b..afd79db36 100644 --- a/src/app/features/project/files/pages/project-files/project-files.component.html +++ b/src/app/features/project/files/pages/project-files/project-files.component.html @@ -1,100 +1,111 @@ -
-
- - - - -
+@if (!dataLoaded()) { + +} @else { +
+
+ + + +

{{ selectedOption.label }}

+
+ +

{{ option.label }}

+
+
+ +
+
-
-
- +
+
+ -
- +
+ +
-
-
- - +
+ + - - + + - - + + + + +
+
- +
+ +
+ {{ fileName() }} +
+ +
+

{{ progress() }} %

+
+
-
-
- -
- {{ fileName() }} -
- -
-

{{ progress() }} %

-
-
+
- - - -
+} diff --git a/src/app/features/project/files/pages/project-files/project-files.component.scss b/src/app/features/project/files/pages/project-files/project-files.component.scss index 61085d375..d513bbd19 100644 --- a/src/app/features/project/files/pages/project-files/project-files.component.scss +++ b/src/app/features/project/files/pages/project-files/project-files.component.scss @@ -16,5 +16,9 @@ } .upload-dialog { - width: mix.rem(48px); + width: mix.rem(128px); +} + +.provider-name { + text-transform: capitalize; } diff --git a/src/app/features/project/files/pages/project-files/project-files.component.ts b/src/app/features/project/files/pages/project-files/project-files.component.ts index 09f6f92cd..2ef472170 100644 --- a/src/app/features/project/files/pages/project-files/project-files.component.ts +++ b/src/app/features/project/files/pages/project-files/project-files.component.ts @@ -10,10 +10,20 @@ import { FloatLabel } from 'primeng/floatlabel'; import { Select } from 'primeng/select'; import { TableModule } from 'primeng/table'; -import { debounceTime, filter, finalize, Observable, skip, take } from 'rxjs'; +import { debounceTime, EMPTY, filter, finalize, Observable, skip, take } from 'rxjs'; import { HttpEventType } from '@angular/common/http'; -import { ChangeDetectionStrategy, Component, DestroyRef, HostBinding, inject, signal } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + computed, + DestroyRef, + effect, + HostBinding, + inject, + model, + signal, +} from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; @@ -23,8 +33,9 @@ import { FilesTreeActions } from '@osf/features/project/files/models'; import { CreateFolder, DeleteEntry, + GetConfiguredStorageAddons, GetFiles, - GetRootFolderFiles, + GetRootFolders, ProjectFilesSelectors, RenameEntry, SetCurrentFolder, @@ -34,6 +45,7 @@ import { SetSort, } from '@osf/features/project/files/store'; import { approveFile } from '@osf/features/project/files/utils'; +import { GetProjectById, ProjectOverviewSelectors } from '@osf/features/project/overview/store'; import { ALL_SORT_OPTIONS } from '@osf/shared/constants'; import { FilesTreeComponent, @@ -42,7 +54,7 @@ import { SearchInputComponent, SubHeaderComponent, } from '@shared/components'; -import { OsfFile } from '@shared/models'; +import { ConfiguredStorageAddon, OsfFile } from '@shared/models'; import { FilesService } from '@shared/services'; @Component({ @@ -80,13 +92,15 @@ export class ProjectFilesComponent { createFolder: CreateFolder, deleteEntry: DeleteEntry, getFiles: GetFiles, - getRootFolderFiles: GetRootFolderFiles, renameEntry: RenameEntry, setCurrentFolder: SetCurrentFolder, setFilesIsLoading: SetFilesIsLoading, setMoveFileCurrentFolder: SetMoveFileCurrentFolder, setSearch: SetSearch, setSort: SetSort, + getProject: GetProjectById, + getRootFolders: GetRootFolders, + getConfiguredStorageAddons: GetConfiguredStorageAddons, }); protected readonly files = select(ProjectFilesSelectors.getFiles); @@ -94,12 +108,32 @@ export class ProjectFilesComponent { protected readonly currentFolder = select(ProjectFilesSelectors.getCurrentFolder); protected readonly provider = select(ProjectFilesSelectors.getProvider); + protected readonly project = select(ProjectOverviewSelectors.getProject); protected readonly projectId = signal(''); + private readonly rootFolders = select(ProjectFilesSelectors.getRootFolders); + protected isRootFoldersLoading = select(ProjectFilesSelectors.isRootFoldersLoading); + private readonly configuredStorageAddons = select(ProjectFilesSelectors.getConfiguredStorageAddons); + protected isConfiguredStorageAddonsLoading = select(ProjectFilesSelectors.isConfiguredStorageAddonsLoading); + protected currentRootFolder = model<{ label: string; folder: OsfFile } | null>(null); protected readonly progress = signal(0); protected readonly fileName = signal(''); + protected readonly dataLoaded = signal(false); protected readonly searchControl = new FormControl(''); protected readonly sortControl = new FormControl(ALL_SORT_OPTIONS[0].value); + protected readonly rootFoldersOptions = computed(() => { + const rootFolders = this.rootFolders(); + const addons = this.configuredStorageAddons(); + + if (rootFolders && addons) { + return rootFolders.map((folder) => ({ + label: this.getAddonName(addons, folder.provider), + folder: folder, + })); + } + return []; + }); + fileIsUploading = signal(false); isFolderOpening = signal(false); @@ -107,11 +141,8 @@ export class ProjectFilesComponent { protected readonly filesTreeActions: FilesTreeActions = { setCurrentFolder: (folder) => this.actions.setCurrentFolder(folder), - setSearch: (search) => this.actions.setSearch(search), - setSort: (sort) => this.actions.setSort(sort), setFilesIsLoading: (isLoading) => this.actions.setFilesIsLoading(isLoading), getFiles: (filesLink) => this.actions.getFiles(filesLink), - getRootFolderFiles: (projectId) => this.actions.getRootFolderFiles(projectId), deleteEntry: (projectId, link) => this.actions.deleteEntry(projectId, link), renameEntry: (projectId, link, newName) => this.actions.renameEntry(projectId, link, newName), setMoveFileCurrentFolder: (folder) => this.actions.setMoveFileCurrentFolder(folder), @@ -121,7 +152,55 @@ export class ProjectFilesComponent { this.activeRoute.parent?.parent?.parent?.params.subscribe((params) => { if (params['id']) { this.projectId.set(params['id']); - this.actions.getRootFolderFiles(params['id']); + if (!this.project()) { + this.filesTreeActions.setFilesIsLoading?.(true); + this.actions.getProject(params['id']); + } + } + }); + + effect(() => { + const project = this.project(); + + if (project) { + this.actions.getRootFolders(project.links.rootFolder); + this.actions.getConfiguredStorageAddons(project.links.iri); + } + }); + + effect(() => { + const rootFolders = this.rootFolders(); + + if (rootFolders) { + const osfRootFolder = rootFolders.find((folder) => folder.provider === 'osfstorage'); + if (osfRootFolder) { + this.currentRootFolder.set({ + label: 'Osf Storage', + folder: osfRootFolder, + }); + } + } + }); + + effect(() => { + const currentRootFolder = this.currentRootFolder(); + + if (currentRootFolder) { + this.actions.setCurrentFolder(currentRootFolder.folder); + } + }); + + effect(() => { + const currentFolder = this.currentFolder(); + + if (currentFolder) { + this.actions.getFiles(currentFolder.relationships.filesLink); + } + }); + + effect(() => { + if (!this.isFilesLoading() && !this.isConfiguredStorageAddonsLoading() && !this.isRootFoldersLoading()) { + this.dataLoaded.set(true); } }); @@ -142,21 +221,23 @@ export class ProjectFilesComponent { }); } - onFileSelected(event: Event): void { - const input = event.target as HTMLInputElement; - const file = input.files?.[0]; - if (!file) return; + uploadFile(file: File): void { + const currentFolder = this.currentFolder(); + const uploadLink = currentFolder?.links.upload; + + if (!uploadLink) return; this.fileName.set(file.name); this.fileIsUploading.set(true); + this.filesService - .uploadFile(file, this.projectId(), this.provider(), this.currentFolder()) + .uploadFile(file, uploadLink) .pipe( takeUntilDestroyed(this.destroyRef), finalize(() => { + this.fileIsUploading.set(false); this.fileName.set(''); - input.value = ''; - this.updateFilesList().subscribe(() => this.fileIsUploading.set(false)); + this.updateFilesList(); }) ) .subscribe((event) => { @@ -173,7 +254,20 @@ export class ProjectFilesComponent { }); } + onFileSelected(event: Event): void { + const input = event.target as HTMLInputElement; + const file = input.files?.[0]; + if (!file) return; + + this.uploadFile(file); + } + createFolder(): void { + const currentFolder = this.currentFolder(); + const newFolderLink = currentFolder?.links.newFolder; + + if (!newFolderLink) return; + this.dialogService .open(CreateFolderDialogComponent, { width: '448px', @@ -186,11 +280,7 @@ export class ProjectFilesComponent { .onClose.pipe(filter((folderName: string) => !!folderName)) .subscribe((folderName) => { this.actions - .createFolder( - this.projectId(), - folderName, - this.currentFolder()?.relationships?.parentFolderId ? this.currentFolder()!.id : '' - ) + .createFolder(newFolderLink, folderName) .pipe( take(1), finalize(() => { @@ -205,13 +295,14 @@ export class ProjectFilesComponent { const projectId = this.projectId(); const folderId = this.currentFolder()?.id ?? ''; const isRootFolder = !this.currentFolder()?.relationships?.parentFolderLink; + const provider = this.currentRootFolder()?.folder?.provider ?? 'osfstorage'; if (projectId && folderId) { if (isRootFolder) { - const link = this.filesService.getFolderDownloadLink(projectId, this.provider(), '', true); + const link = this.filesService.getFolderDownloadLink(projectId, provider, '', true); window.open(link, '_blank')?.focus(); } else { - const link = this.filesService.getFolderDownloadLink(projectId, this.provider(), folderId, false); + const link = this.filesService.getFolderDownloadLink(projectId, provider, folderId, false); window.open(link, '_blank')?.focus(); } } @@ -220,10 +311,11 @@ export class ProjectFilesComponent { updateFilesList(): Observable { const currentFolder = this.currentFolder(); if (currentFolder?.relationships.filesLink) { - return this.actions.getFiles(currentFolder?.relationships.filesLink).pipe(takeUntilDestroyed(this.destroyRef)); - } else { - return this.actions.getRootFolderFiles(this.projectId()); + this.filesTreeActions.setFilesIsLoading?.(true); + return this.actions.getFiles(currentFolder?.relationships.filesLink).pipe(take(1)); } + + return EMPTY; } folderIsOpening(value: boolean): void { @@ -237,4 +329,12 @@ export class ProjectFilesComponent { navigateToFile(file: OsfFile) { this.router.navigate([file.guid], { relativeTo: this.activeRoute }); } + + getAddonName(addons: ConfiguredStorageAddon[], provider: string): string { + if (provider === 'osfstorage') { + return 'Osf Storage'; + } else { + return addons.find((addon) => addon.externalServiceName === provider)?.displayName ?? ''; + } + } } diff --git a/src/app/features/project/files/store/project-files.actions.ts b/src/app/features/project/files/store/project-files.actions.ts index db3cdb0de..10a1f5a0b 100644 --- a/src/app/features/project/files/store/project-files.actions.ts +++ b/src/app/features/project/files/store/project-files.actions.ts @@ -65,9 +65,8 @@ export class CreateFolder { static readonly type = '[Project Files] Create folder'; constructor( - public projectId: string, - public folderName: string, - public folderId: string + public newFolderLink: string, + public folderName: string ) {} } @@ -133,3 +132,15 @@ export class UpdateTags { public fileGuid: string ) {} } + +export class GetRootFolders { + static readonly type = '[Project Files] Get Folders'; + + constructor(public folderLink: string) {} +} + +export class GetConfiguredStorageAddons { + static readonly type = '[Project Files] Get ConfiguredStorageAddons'; + + constructor(public resourceUri: string) {} +} diff --git a/src/app/features/project/files/store/project-files.model.ts b/src/app/features/project/files/store/project-files.model.ts index 7071f7751..b97c74559 100644 --- a/src/app/features/project/files/store/project-files.model.ts +++ b/src/app/features/project/files/store/project-files.model.ts @@ -6,6 +6,7 @@ import { OsfProjectMetadata, } from '@osf/features/project/files/models'; import { OsfFile } from '@shared/models'; +import { ConfiguredStorageAddon } from '@shared/models/addons'; import { AsyncStateModel } from '@shared/models/store'; export interface ProjectFilesStateModel { @@ -22,4 +23,6 @@ export interface ProjectFilesStateModel { contributors: AsyncStateModel; fileRevisions: AsyncStateModel; tags: AsyncStateModel; + rootFolders: AsyncStateModel; + configuredStorageAddons: AsyncStateModel; } diff --git a/src/app/features/project/files/store/project-files.selectors.ts b/src/app/features/project/files/store/project-files.selectors.ts index fb8fa6028..24c6fda03 100644 --- a/src/app/features/project/files/store/project-files.selectors.ts +++ b/src/app/features/project/files/store/project-files.selectors.ts @@ -7,6 +7,7 @@ import { OsfProjectMetadata, } from '@osf/features/project/files/models'; import { OsfFile } from '@shared/models'; +import { ConfiguredStorageAddon } from '@shared/models/addons'; import { ProjectFilesStateModel } from './project-files.model'; import { ProjectFilesState } from './project-files.state'; @@ -106,4 +107,24 @@ export class ProjectFilesSelectors { static isFileTagsLoading(state: ProjectFilesStateModel): boolean { return state.tags.isLoading; } + + @Selector([ProjectFilesState]) + static getRootFolders(state: ProjectFilesStateModel): OsfFile[] | null { + return state.rootFolders.data; + } + + @Selector([ProjectFilesState]) + static isRootFoldersLoading(state: ProjectFilesStateModel): boolean { + return state.rootFolders.isLoading; + } + + @Selector([ProjectFilesState]) + static getConfiguredStorageAddons(state: ProjectFilesStateModel): ConfiguredStorageAddon[] | null { + return state.configuredStorageAddons.data; + } + + @Selector([ProjectFilesState]) + static isConfiguredStorageAddonsLoading(state: ProjectFilesStateModel): boolean { + return state.configuredStorageAddons.isLoading; + } } diff --git a/src/app/features/project/files/store/project-files.state.ts b/src/app/features/project/files/store/project-files.state.ts index 8cfc6108c..a4587b1df 100644 --- a/src/app/features/project/files/store/project-files.state.ts +++ b/src/app/features/project/files/store/project-files.state.ts @@ -1,6 +1,6 @@ import { Action, State, StateContext } from '@ngxs/store'; -import { catchError, finalize, forkJoin, switchMap, tap, throwError } from 'rxjs'; +import { catchError, finalize, forkJoin, tap, throwError } from 'rxjs'; import { inject, Injectable } from '@angular/core'; @@ -8,6 +8,7 @@ import { MapProjectMetadata } from '@osf/features/project/files/mappers'; import { CreateFolder, DeleteEntry, + GetConfiguredStorageAddons, GetFile, GetFileMetadata, GetFileProjectContributors, @@ -15,8 +16,8 @@ import { GetFileRevisions, GetFiles, GetMoveFileFiles, - GetMoveFileRootFiles, GetRootFolderFiles, + GetRootFolders, RenameEntry, SetCurrentFolder, SetFileMetadata, @@ -27,6 +28,7 @@ import { UpdateTags, } from '@osf/features/project/files/store/project-files.actions'; import { ProjectFilesStateModel } from '@osf/features/project/files/store/project-files.model'; +import { ToastService } from '@shared/services'; import { FilesService } from '@shared/services/files.service'; import { projectFilesStateDefaults } from '../models'; @@ -38,55 +40,7 @@ import { projectFilesStateDefaults } from '../models'; }) export class ProjectFilesState { filesService = inject(FilesService); - - @Action(GetRootFolderFiles) - getRootFolderFiles(ctx: StateContext, action: GetRootFolderFiles) { - const state = ctx.getState(); - ctx.patchState({ files: { ...state.files, isLoading: true, error: null } }); - - return this.filesService.getRootFolderFiles(action.projectId, state.provider, state.search, state.sort).pipe( - switchMap((files) => { - return this.filesService.getFolder(files[0].relationships.parentFolderLink).pipe( - tap({ - next: (parentFolder) => { - ctx.patchState({ - files: { - data: [...files], - isLoading: false, - error: null, - }, - currentFolder: parentFolder, - }); - }, - }) - ); - }), - catchError((error) => this.handleError(ctx, 'files', error)) - ); - } - - @Action(GetMoveFileRootFiles) - getMoveFileRootFiles(ctx: StateContext, action: GetMoveFileRootFiles) { - const state = ctx.getState(); - ctx.patchState({ - moveFileFiles: { ...state.moveFileFiles, isLoading: true, error: null }, - }); - - return this.filesService.getRootFolderFiles(action.projectId, state.provider, '', '').pipe( - tap({ - next: (files) => { - ctx.patchState({ - moveFileFiles: { - data: files, - isLoading: false, - error: null, - }, - }); - }, - }), - catchError((error) => this.handleError(ctx, 'moveFileFiles', error)) - ); - } + toastService = inject(ToastService); @Action(GetMoveFileFiles) getMoveFileFiles(ctx: StateContext, action: GetMoveFileFiles) { @@ -154,7 +108,7 @@ export class ProjectFilesState { ctx.patchState({ files: { ...state.files, isLoading: true, error: null } }); return this.filesService - .createFolder(action.projectId, state.provider, action.folderName, action.folderId) + .createFolder(action.newFolderLink, action.folderName) .pipe(finalize(() => ctx.patchState({ files: { ...state.files, isLoading: false, error: null } }))); } @@ -319,6 +273,46 @@ export class ProjectFilesState { ); } + @Action(GetRootFolders) + getRootFolders(ctx: StateContext, action: GetRootFolders) { + const state = ctx.getState(); + ctx.patchState({ rootFolders: { ...state.rootFolders, isLoading: true, error: null } }); + + return this.filesService.getFolders(action.folderLink).pipe( + tap({ + next: (folders) => + ctx.patchState({ + rootFolders: { + data: folders, + isLoading: false, + error: null, + }, + }), + }), + catchError((error) => this.handleError(ctx, 'rootFolders', error)) + ); + } + + @Action(GetConfiguredStorageAddons) + getConfiguredStorageAddons(ctx: StateContext, action: GetConfiguredStorageAddons) { + const state = ctx.getState(); + ctx.patchState({ configuredStorageAddons: { ...state.configuredStorageAddons, isLoading: true, error: null } }); + + return this.filesService.getConfiguredStorageAddons(action.resourceUri).pipe( + tap({ + next: (addons) => + ctx.patchState({ + configuredStorageAddons: { + data: addons, + isLoading: false, + error: null, + }, + }), + }), + catchError((error) => this.handleError(ctx, 'configuredStorageAddons', error)) + ); + } + private handleError( ctx: StateContext, section: @@ -329,7 +323,9 @@ export class ProjectFilesState { | 'projectMetadata' | 'contributors' | 'fileRevisions' - | 'tags', + | 'tags' + | 'rootFolders' + | 'configuredStorageAddons', error: Error ) { ctx.patchState({ @@ -339,6 +335,7 @@ export class ProjectFilesState { error: error.message, }, }); + this.toastService.showError(error.message); return throwError(() => error); } } diff --git a/src/app/features/project/overview/mappers/project-overview.mapper.ts b/src/app/features/project/overview/mappers/project-overview.mapper.ts index 5653e9fc0..b840f42b1 100644 --- a/src/app/features/project/overview/mappers/project-overview.mapper.ts +++ b/src/app/features/project/overview/mappers/project-overview.mapper.ts @@ -78,6 +78,10 @@ export class ProjectOverviewMapper { region: response.relationships.region?.data, forksCount: response.relationships.forks.links.related.meta.count, viewOnlyLinksCount: response.relationships.view_only_links.links.related.meta.count, + links: { + rootFolder: response.relationships?.files?.links?.related?.href, + iri: response.links?.iri, + }, }; } diff --git a/src/app/features/project/overview/models/project-overview.models.ts b/src/app/features/project/overview/models/project-overview.models.ts index 44a8edcf7..ce507c7ac 100644 --- a/src/app/features/project/overview/models/project-overview.models.ts +++ b/src/app/features/project/overview/models/project-overview.models.ts @@ -114,6 +114,10 @@ export interface ProjectOverview { }[]; forksCount: number; viewOnlyLinksCount: number; + links: { + rootFolder: string; + iri: string; + }; } export interface ProjectOverviewSubject { @@ -256,6 +260,16 @@ export interface ProjectOverviewGetResponseJsoApi { }; }; }; + files: { + links: { + related: { + href: string; + }; + }; + }; + }; + links: { + iri: string; }; } diff --git a/src/app/features/project/project.routes.ts b/src/app/features/project/project.routes.ts index 4c5f037ba..e382a7bde 100644 --- a/src/app/features/project/project.routes.ts +++ b/src/app/features/project/project.routes.ts @@ -34,6 +34,9 @@ export const projectRoutes: Routes = [ path: 'files', loadChildren: () => import('../project/files/project-files.routes').then((mod) => mod.projectFilesRoutes), providers: [provideStates([ProjectFilesState])], + data: { + context: 'project', + }, }, { path: 'registrations', diff --git a/src/app/features/registry/mappers/registry-overview.mapper.ts b/src/app/features/registry/mappers/registry-overview.mapper.ts index 8c841fd64..484556521 100644 --- a/src/app/features/registry/mappers/registry-overview.mapper.ts +++ b/src/app/features/registry/mappers/registry-overview.mapper.ts @@ -62,5 +62,8 @@ export function MapRegistryOverview(data: RegistryOverviewJsonApiData): Registry })), status: MapRegistryStatus(data.attributes), revisionStatus: data.attributes.revision_state, + links: { + files: data?.embeds?.files?.data?.[0]?.relationships?.files?.links?.related?.href, + }, } as RegistryOverview; } diff --git a/src/app/features/registry/models/get-registry-overview-json-api.model.ts b/src/app/features/registry/models/get-registry-overview-json-api.model.ts index 99d9f1952..f7c55242c 100644 --- a/src/app/features/registry/models/get-registry-overview-json-api.model.ts +++ b/src/app/features/registry/models/get-registry-overview-json-api.model.ts @@ -100,6 +100,20 @@ export interface RegistryOverviewJsonApiEmbed { }; }[]; }; + files: { + data: { + id: string; + relationships: { + files: { + links: { + related: { + href: string; + }; + }; + }; + }; + }[]; + }; } export interface RegistryOverviewJsonApiRelationships { diff --git a/src/app/features/registry/models/registry-overview.models.ts b/src/app/features/registry/models/registry-overview.models.ts index 6ff2f5349..91cdc6d53 100644 --- a/src/app/features/registry/models/registry-overview.models.ts +++ b/src/app/features/registry/models/registry-overview.models.ts @@ -61,4 +61,7 @@ export interface RegistryOverview { }[]; status: RegistryStatus; revisionStatus: RevisionReviewStates; + links: { + files: string; + }; } diff --git a/src/app/features/registry/pages/index.ts b/src/app/features/registry/pages/index.ts index e2880e23c..de079877f 100644 --- a/src/app/features/registry/pages/index.ts +++ b/src/app/features/registry/pages/index.ts @@ -1 +1,2 @@ -export * from './registry-overview/registry-overview.component'; +export * from '@osf/features/registry/pages/registry-files/registry-files.component'; +export * from '@osf/features/registry/pages/registry-overview/registry-overview.component'; diff --git a/src/app/features/registry/pages/registry-files/registry-files.component.html b/src/app/features/registry/pages/registry-files/registry-files.component.html new file mode 100644 index 000000000..d30f028b5 --- /dev/null +++ b/src/app/features/registry/pages/registry-files/registry-files.component.html @@ -0,0 +1,47 @@ + + +@if (!dataLoaded()) { + +} @else { +
+
+
+ + +
+ +
+
+ + + +
+ + + +
+} diff --git a/src/app/features/registry/pages/registry-files/registry-files.component.scss b/src/app/features/registry/pages/registry-files/registry-files.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/registry/pages/registry-files/registry-files.component.spec.ts b/src/app/features/registry/pages/registry-files/registry-files.component.spec.ts new file mode 100644 index 000000000..2dd8158f1 --- /dev/null +++ b/src/app/features/registry/pages/registry-files/registry-files.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RegistryFilesComponent } from './registry-files.component'; + +describe('RegistryFilesComponent', () => { + let component: RegistryFilesComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RegistryFilesComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(RegistryFilesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/registry/pages/registry-files/registry-files.component.ts b/src/app/features/registry/pages/registry-files/registry-files.component.ts new file mode 100644 index 000000000..1a63fe9f9 --- /dev/null +++ b/src/app/features/registry/pages/registry-files/registry-files.component.ts @@ -0,0 +1,167 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; + +import { TreeDragDropService } from 'primeng/api'; +import { Button } from 'primeng/button'; +import { DialogService } from 'primeng/dynamicdialog'; + +import { debounceTime, EMPTY, Observable, skip } from 'rxjs'; + +import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormControl } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { FilesTreeActions } from '@osf/features/project/files/models'; +import { + FilesTreeComponent, + FormSelectComponent, + LoadingSpinnerComponent, + SearchInputComponent, + SubHeaderComponent, +} from '@shared/components'; +import { ALL_SORT_OPTIONS } from '@shared/constants'; +import { OsfFile } from '@shared/models'; +import { FilesService } from '@shared/services'; + +import { + GetRegistryFiles, + SetCurrentFolder, + SetSearch, + SetSort, +} from '../../store/registry-files/registry-files.actions'; +import { RegistryFilesSelectors } from '../../store/registry-files/registry-files.selectors'; +import { GetRegistryById } from '../../store/registry-overview/registry-overview.actions'; +import { RegistryOverviewSelectors } from '../../store/registry-overview/registry-overview.selectors'; + +@Component({ + selector: 'osf-registry-files', + imports: [ + SubHeaderComponent, + TranslatePipe, + Button, + FilesTreeComponent, + FormSelectComponent, + SearchInputComponent, + LoadingSpinnerComponent, + ], + templateUrl: './registry-files.component.html', + styleUrl: './registry-files.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [DialogService, TreeDragDropService], +}) +export class RegistryFilesComponent { + protected readonly registry = select(RegistryOverviewSelectors.getRegistry); + protected readonly isRegistryLoading = select(RegistryOverviewSelectors.isRegistryLoading); + protected readonly files = select(RegistryFilesSelectors.getFiles); + protected readonly isFilesLoading = select(RegistryFilesSelectors.isFilesLoading); + protected readonly currentFolder = select(RegistryFilesSelectors.getCurrentFolder); + private readonly router = inject(Router); + private readonly route = inject(ActivatedRoute); + private readonly filesService = inject(FilesService); + private readonly destroyRef = inject(DestroyRef); + + private readonly actions = createDispatchMap({ + setCurrentFolder: SetCurrentFolder, + getFiles: GetRegistryFiles, + getRootFolderFiles: GetRegistryFiles, + getRegistryById: GetRegistryById, + setSearch: SetSearch, + setSort: SetSort, + }); + + protected readonly filesTreeActions: FilesTreeActions = { + setCurrentFolder: (folder) => this.actions.setCurrentFolder(folder), + getFiles: (filesLink) => this.actions.getFiles(filesLink), + }; + + protected readonly searchControl = new FormControl(''); + protected readonly sortControl = new FormControl(ALL_SORT_OPTIONS[0].value); + + protected isFolderOpening = signal(false); + protected registryId = signal(''); + protected dataLoaded = signal(false); + + protected readonly sortOptions = ALL_SORT_OPTIONS; + protected readonly provider = 'osfstorage'; + + constructor() { + this.route.parent?.params.subscribe((params) => { + const id = params['registrationId']; + if (id) { + this.registryId.set(id); + if (!this.registry()) { + this.actions.getRegistryById(id); + } + } + }); + + effect(() => { + const registry = this.registry(); + + if (registry) { + this.actions.getFiles(registry.links.files).subscribe(() => this.dataLoaded.set(true)); + } + }); + + this.searchControl.valueChanges + .pipe(skip(1), takeUntilDestroyed(this.destroyRef), debounceTime(500)) + .subscribe((searchText) => { + this.actions.setSearch(searchText ?? ''); + if (!this.isFolderOpening()) { + this.updateFilesList(); + } + }); + + this.sortControl.valueChanges.pipe(skip(1), takeUntilDestroyed(this.destroyRef)).subscribe((sort) => { + this.actions.setSort(sort ?? ''); + if (!this.isFolderOpening()) { + this.updateFilesList(); + } + }); + } + + folderIsOpening(value: boolean): void { + this.isFolderOpening.set(value); + if (value) { + this.searchControl.setValue(''); + this.sortControl.setValue(ALL_SORT_OPTIONS[0].value); + } + } + + updateFilesList(): Observable { + const currentFolder = this.currentFolder(); + if (currentFolder?.relationships.filesLink) { + return this.actions.getFiles(currentFolder?.relationships.filesLink).pipe(takeUntilDestroyed(this.destroyRef)); + } + + const registry = this.registry(); + + if (registry) { + return this.actions.getFiles(registry.links.files); + } + + return EMPTY; + } + + navigateToFile(file: OsfFile) { + this.router.navigate([file.guid], { relativeTo: this.route }); + } + + downloadFolder(): void { + const registryId = this.registry()?.id; + const folderId = this.currentFolder()?.id ?? ''; + const isRootFolder = !this.currentFolder()?.relationships?.parentFolderLink; + + if (registryId) { + if (isRootFolder || !folderId) { + const link = this.filesService.getFolderDownloadLink(registryId, this.provider, '', true); + window.open(link, '_blank')?.focus(); + } else { + const link = this.filesService.getFolderDownloadLink(registryId, this.provider, folderId, false); + window.open(link, '_blank')?.focus(); + } + } + } +} diff --git a/src/app/features/registry/pages/registry-overview/registry-overview.component.scss b/src/app/features/registry/pages/registry-overview/registry-overview.component.scss index b90d683af..3fd87ac8b 100644 --- a/src/app/features/registry/pages/registry-overview/registry-overview.component.scss +++ b/src/app/features/registry/pages/registry-overview/registry-overview.component.scss @@ -24,6 +24,7 @@ flex: 1; border: 1px solid var.$grey-2; border-radius: mix.rem(12px); + height: max-content; } .no-padding { diff --git a/src/app/features/registry/registry.routes.ts b/src/app/features/registry/registry.routes.ts index 574fa893d..eb5f17053 100644 --- a/src/app/features/registry/registry.routes.ts +++ b/src/app/features/registry/registry.routes.ts @@ -2,12 +2,13 @@ import { provideStates } from '@ngxs/store'; import { Routes } from '@angular/router'; +import { RegistryFilesState } from '@osf/features/registry/store/registry-files'; +import { RegistryOverviewState } from '@osf/features/registry/store/registry-overview'; import { ResourceType } from '@osf/shared/enums'; import { ContributorsState, ViewOnlyLinkState } from '@osf/shared/stores'; import { AnalyticsState } from '../project/analytics/store'; -import { RegistryOverviewState } from './store/registry-overview'; import { RegistryComponent } from './registry.component'; export const registryRoutes: Routes = [ @@ -39,6 +40,15 @@ export const registryRoutes: Routes = [ data: { resourceType: ResourceType.Registration }, providers: [provideStates([AnalyticsState])], }, + { + path: 'files', + loadComponent: () => + import('./pages/registry-files/registry-files.component').then((c) => c.RegistryFilesComponent), + providers: [provideStates([RegistryFilesState])], + data: { + context: 'registry', + }, + }, ], }, ]; diff --git a/src/app/features/registry/store/registry-files/index.ts b/src/app/features/registry/store/registry-files/index.ts new file mode 100644 index 000000000..6c68e2520 --- /dev/null +++ b/src/app/features/registry/store/registry-files/index.ts @@ -0,0 +1,4 @@ +export * from './registry-files.actions'; +export * from './registry-files.model'; +export * from './registry-files.selectors'; +export * from './registry-files.state'; diff --git a/src/app/features/registry/store/registry-files/registry-files.actions.ts b/src/app/features/registry/store/registry-files/registry-files.actions.ts new file mode 100644 index 000000000..60015342f --- /dev/null +++ b/src/app/features/registry/store/registry-files/registry-files.actions.ts @@ -0,0 +1,25 @@ +import { OsfFile } from '@shared/models'; + +export class GetRegistryFiles { + static readonly type = '[Registry Files] Get Registry Files'; + + constructor(public filesLink: string) {} +} + +export class SetCurrentFolder { + static readonly type = '[Registry Files] Set Current Folder'; + + constructor(public folder: OsfFile | null) {} +} + +export class SetSearch { + static readonly type = '[Registry Files] Set Search'; + + constructor(public search: string) {} +} + +export class SetSort { + static readonly type = '[Registry Files] Set Sort'; + + constructor(public sort: string) {} +} diff --git a/src/app/features/registry/store/registry-files/registry-files.model.ts b/src/app/features/registry/store/registry-files/registry-files.model.ts new file mode 100644 index 000000000..8ee42bb5c --- /dev/null +++ b/src/app/features/registry/store/registry-files/registry-files.model.ts @@ -0,0 +1,8 @@ +import { AsyncStateModel, OsfFile } from '@shared/models'; + +export interface RegistryFilesStateModel { + files: AsyncStateModel; + search: string; + sort: string; + currentFolder: OsfFile | null; +} diff --git a/src/app/features/registry/store/registry-files/registry-files.selectors.ts b/src/app/features/registry/store/registry-files/registry-files.selectors.ts new file mode 100644 index 000000000..a68bc23e4 --- /dev/null +++ b/src/app/features/registry/store/registry-files/registry-files.selectors.ts @@ -0,0 +1,23 @@ +import { Selector } from '@ngxs/store'; + +import { OsfFile } from '@shared/models'; + +import { RegistryFilesStateModel } from './registry-files.model'; +import { RegistryFilesState } from './registry-files.state'; + +export class RegistryFilesSelectors { + @Selector([RegistryFilesState]) + static getFiles(state: RegistryFilesStateModel): OsfFile[] { + return state.files.data; + } + + @Selector([RegistryFilesState]) + static isFilesLoading(state: RegistryFilesStateModel): boolean { + return state.files.isLoading; + } + + @Selector([RegistryFilesState]) + static getCurrentFolder(state: RegistryFilesStateModel): OsfFile | null { + return state.currentFolder; + } +} diff --git a/src/app/features/registry/store/registry-files/registry-files.state.ts b/src/app/features/registry/store/registry-files/registry-files.state.ts new file mode 100644 index 000000000..13fd5cf16 --- /dev/null +++ b/src/app/features/registry/store/registry-files/registry-files.state.ts @@ -0,0 +1,70 @@ +import { Action, State, StateContext } from '@ngxs/store'; + +import { tap } from 'rxjs'; +import { catchError } from 'rxjs/operators'; + +import { inject, Injectable } from '@angular/core'; + +import { handleSectionError } from '@core/handlers'; +import { FilesService, ToastService } from '@shared/services'; + +import { GetRegistryFiles, SetCurrentFolder, SetSearch, SetSort } from './registry-files.actions'; +import { RegistryFilesStateModel } from './registry-files.model'; + +@Injectable() +@State({ + name: 'registryFiles', + defaults: { + files: { + data: [], + isLoading: false, + error: null, + }, + search: '', + sort: '', + currentFolder: null, + }, +}) +export class RegistryFilesState { + private readonly filesService = inject(FilesService); + private readonly toastService = inject(ToastService); + + @Action(GetRegistryFiles) + getRegistryFiles(ctx: StateContext, action: GetRegistryFiles) { + const state = ctx.getState(); + ctx.patchState({ files: { ...state.files, isLoading: true, error: null } }); + + return this.filesService.getFiles(action.filesLink, state.search, state.sort).pipe( + tap({ + next: (files) => { + ctx.patchState({ + files: { + data: files, + isLoading: false, + error: null, + }, + }); + }, + }), + catchError((error) => { + this.toastService.showError(error, 5000); + return handleSectionError(ctx, 'files', error); + }) + ); + } + + @Action(SetCurrentFolder) + setSelectedFolder(ctx: StateContext, action: SetCurrentFolder) { + ctx.patchState({ currentFolder: action.folder }); + } + + @Action(SetSearch) + setSearch(ctx: StateContext, action: SetSearch) { + ctx.patchState({ search: action.search }); + } + + @Action(SetSort) + setSort(ctx: StateContext, action: SetSort) { + ctx.patchState({ sort: action.sort }); + } +} diff --git a/src/app/features/registry/store/registry-overview/registry-overview.model.ts b/src/app/features/registry/store/registry-overview/registry-overview.model.ts index c60c1306e..ff735cd99 100644 --- a/src/app/features/registry/store/registry-overview/registry-overview.model.ts +++ b/src/app/features/registry/store/registry-overview/registry-overview.model.ts @@ -1,5 +1,9 @@ -import { RegistryInstitution, RegistryOverview, RegistrySubject } from '@osf/features/registry/models'; -import { RegistrySchemaBlock } from '@osf/features/registry/models/registry-schema-block.model'; +import { + RegistryInstitution, + RegistryOverview, + RegistrySchemaBlock, + RegistrySubject, +} from '@osf/features/registry/models'; import { AsyncStateModel } from '@shared/models'; export interface RegistryOverviewStateModel { diff --git a/src/app/features/registry/store/registry-overview/registry-overview.selectors.ts b/src/app/features/registry/store/registry-overview/registry-overview.selectors.ts index bfac7b37c..395b5c292 100644 --- a/src/app/features/registry/store/registry-overview/registry-overview.selectors.ts +++ b/src/app/features/registry/store/registry-overview/registry-overview.selectors.ts @@ -1,6 +1,11 @@ import { Selector } from '@ngxs/store'; -import { RegistryInstitution, RegistryOverview, RegistrySchemaBlock, RegistrySubject } from '../../models'; +import { + RegistryInstitution, + RegistryOverview, + RegistrySchemaBlock, + RegistrySubject, +} from '@osf/features/registry/models'; import { RegistryOverviewStateModel } from './registry-overview.model'; import { RegistryOverviewState } from './registry-overview.state'; diff --git a/src/app/shared/components/files-tree/files-tree.component.html b/src/app/shared/components/files-tree/files-tree.component.html index 87293b098..208da859d 100644 --- a/src/app/shared/components/files-tree/files-tree.component.html +++ b/src/app/shared/components/files-tree/files-tree.component.html @@ -1,3 +1,12 @@ +
+ @if (isDragOver()) { +
+ +

Drop a file to upload

+
+ } +
+ @if (isLoading()) {
@@ -26,18 +35,8 @@
} @else { -
-
+
+
@if (file.kind !== 'folder') {
@@ -62,10 +61,20 @@
{{ file.dateModified | date: 'MMM d, y hh:mm a' }}
- @if (!viewOnly()) { + + @if (!viewOnly() && !viewOnlyDownloadable()) {
+ } @else if (viewOnly() && viewOnlyDownloadable()) { +
+ +
}
} diff --git a/src/app/shared/components/files-tree/files-tree.component.scss b/src/app/shared/components/files-tree/files-tree.component.scss index 2397abaf8..d301a4ea3 100644 --- a/src/app/shared/components/files-tree/files-tree.component.scss +++ b/src/app/shared/components/files-tree/files-tree.component.scss @@ -13,6 +13,9 @@ color: var.$dark-blue-1; display: grid; align-items: center; + grid-template-columns: + minmax(mix.rem(200px), 32rem) minmax(mix.rem(150px), 0.7fr) minmax(mix.rem(100px), 100px) + minmax(mix.rem(150px), 1fr) minmax(mix.rem(50px), 50px); grid-template-rows: mix.rem(44px); border-bottom: 1px solid var.$grey-2; padding: 0 mix.rem(12px); @@ -39,18 +42,6 @@ max-width: 95%; } } - - &-readonly { - grid-template-columns: - minmax(mix.rem(200px), 35rem) minmax(mix.rem(150px), 0.7fr) minmax(mix.rem(100px), 100px) - minmax(mix.rem(150px), 0.5fr); - } - - &-editable { - grid-template-columns: - minmax(mix.rem(200px), 32rem) minmax(mix.rem(150px), 0.7fr) minmax(mix.rem(100px), 100px) - minmax(mix.rem(150px), 1fr) minmax(mix.rem(50px), 50px); - } } .entry-title { @@ -64,6 +55,10 @@ .blue-text { color: var.$pr-blue-1; + + &:hover { + color: var.$pr-blue-3; + } } .tree-table { @@ -71,3 +66,33 @@ padding: 0; } } + +.drop-zone { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + color: white; + transition: + background 0.3s ease, + backdrop-filter 0.3s ease; + pointer-events: none; + background: transparent; + + &.active { + backdrop-filter: blur(0.3rem); + background: rgba(132, 174, 210, 0.5); + pointer-events: all; + } + + .drop-text { + text-transform: none; + color: white; + pointer-events: none; + } +} diff --git a/src/app/shared/components/files-tree/files-tree.component.ts b/src/app/shared/components/files-tree/files-tree.component.ts index 99b7f1715..d554a7d98 100644 --- a/src/app/shared/components/files-tree/files-tree.component.ts +++ b/src/app/shared/components/files-tree/files-tree.component.ts @@ -4,19 +4,34 @@ import { PrimeTemplate } from 'primeng/api'; import { DialogService } from 'primeng/dynamicdialog'; import { Tree, TreeNodeDropEvent } from 'primeng/tree'; -import { finalize, firstValueFrom, Observable, take } from 'rxjs'; +import { EMPTY, finalize, firstValueFrom, Observable, take, throwError } from 'rxjs'; +import { catchError } from 'rxjs/operators'; import { DatePipe } from '@angular/common'; -import { ChangeDetectionStrategy, Component, computed, effect, inject, input, output } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + computed, + effect, + HostBinding, + inject, + input, + OnDestroy, + OnInit, + output, + signal, +} from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { MoveFileDialogComponent, RenameFileDialogComponent } from '@osf/features/project/files/components'; import { embedDynamicJs, embedStaticHtml, FilesTreeActions } from '@osf/features/project/files/models'; import { FileMenuType } from '@osf/shared/enums'; import { FileMenuComponent, LoadingSpinnerComponent } from '@shared/components'; +import { filesTreeSelectorsFactory } from '@shared/factories'; import { FileMenuAction, OsfFile } from '@shared/models'; import { FileSizePipe } from '@shared/pipes'; import { CustomConfirmationService, FilesService, ToastService } from '@shared/services'; +import { FILES_TREE_SELECTORS } from '@shared/tokens'; @Component({ selector: 'osf-files-tree', @@ -24,8 +39,16 @@ import { CustomConfirmationService, FilesService, ToastService } from '@shared/s templateUrl: './files-tree.component.html', styleUrl: './files-tree.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: FILES_TREE_SELECTORS, + useFactory: filesTreeSelectorsFactory, + deps: [ActivatedRoute], + }, + ], }) -export class FilesTreeComponent { +export class FilesTreeComponent implements OnInit, OnDestroy { + @HostBinding('class') classes = 'relative'; readonly filesService = inject(FilesService); readonly router = inject(Router); readonly toastService = inject(ToastService); @@ -34,16 +57,23 @@ export class FilesTreeComponent { readonly dialogService = inject(DialogService); readonly translateService = inject(TranslateService); - files = input.required(); - projectId = input.required(); + private readonly selectors = inject(FILES_TREE_SELECTORS); + protected readonly files = this.selectors.getFiles(); + protected readonly isLoading = this.selectors.isFilesLoading(); + protected readonly currentFolder = this.selectors.getCurrentFolder(); + + resourceId = input.required(); actions = input.required(); viewOnly = input(true); + viewOnlyDownloadable = input(false); provider = input(); - isLoading = input(false); - currentFolder = input.required(); - entryFileClicked = output(); + isDragOver = signal(false); + entryFileClicked = output(); folderIsOpening = output(); + uploadFileConfirmed = output(); + + protected readonly FileMenuType = FileMenuType; protected readonly nodes = computed(() => { if (this.currentFolder()?.relationships?.parentFolderLink) { @@ -59,9 +89,46 @@ export class FilesTreeComponent { } }); + ngOnInit(): void { + window.addEventListener('dragenter', this.onGlobalDragEnter); + } + + ngOnDestroy(): void { + window.removeEventListener('dragenter', this.onGlobalDragEnter); + } + + onGlobalDragEnter = (event: DragEvent) => { + if (event.dataTransfer?.types?.includes('Files')) { + this.isDragOver.set(true); + } + }; + + onDragOver(event: DragEvent) { + event.preventDefault(); + event.dataTransfer!.dropEffect = 'copy'; + this.isDragOver.set(true); + } + + onDrop(event: DragEvent) { + event.preventDefault(); + this.isDragOver.set(false); + const files = event.dataTransfer?.files; + + if (files && files.length > 0) { + this.customConfirmationService.confirmAccept({ + headerKey: 'project.files.dialogs.uploadFile.title', + messageParams: { name: files[0].name }, + messageKey: 'project.files.dialogs.uploadFile.message', + acceptLabelKey: 'common.buttons.upload', + onConfirm: () => this.uploadFileConfirmed.emit(files[0]), + }); + } + } + constructor() { effect(() => { const currentFolder = this.currentFolder(); + if (currentFolder) { this.updateFilesList().subscribe(() => this.folderIsOpening.emit(false)); } @@ -89,7 +156,13 @@ export class FilesTreeComponent { this.filesService .getFolder(currentFolder.relationships.parentFolderLink) - .pipe(take(1)) + .pipe( + take(1), + catchError((error) => { + this.toastService.showError(error.error.message, 5000); + return throwError(error); + }) + ) .subscribe({ next: (folder) => { this.actions().setCurrentFolder(folder); @@ -172,7 +245,7 @@ export class FilesTreeComponent { deleteEntry(link: string): void { this.actions().setFilesIsLoading?.(true); - this.actions().deleteEntry?.(this.projectId(), link); + this.actions().deleteEntry?.(this.resourceId(), link); } confirmRename(file: OsfFile): void { @@ -198,7 +271,7 @@ export class FilesTreeComponent { renameEntry(newName: string, file: OsfFile): void { if (newName.trim() && file.links.upload) { this.actions().setFilesIsLoading?.(true); - this.actions().renameEntry?.(this.projectId(), file.links.upload, newName).pipe(take(1)).subscribe(); + this.actions().renameEntry?.(this.resourceId(), file.links.upload, newName); } } @@ -215,13 +288,13 @@ export class FilesTreeComponent { } downloadFolder(folderId: string, rootFolder: boolean): void { - const projectId = this.projectId(); - if (projectId && folderId) { + const resourceId = this.resourceId(); + if (resourceId && folderId) { if (rootFolder) { - const link = this.filesService.getFolderDownloadLink(projectId, this.provider()!, '', true); + const link = this.filesService.getFolderDownloadLink(resourceId, this.provider()!, '', true); window.open(link, '_blank')?.focus(); } else { - const link = this.filesService.getFolderDownloadLink(projectId, this.provider()!, folderId, false); + const link = this.filesService.getFolderDownloadLink(resourceId, this.provider()!, folderId, false); window.open(link, '_blank')?.focus(); } } @@ -246,7 +319,7 @@ export class FilesTreeComponent { closable: true, data: { file: file, - projectId: this.projectId(), + resourceId: this.resourceId(), action: action, }, }); @@ -255,11 +328,11 @@ export class FilesTreeComponent { updateFilesList(): Observable { const currentFolder = this.currentFolder(); + if (currentFolder?.relationships.filesLink) { - return this.actions().getFiles(currentFolder?.relationships.filesLink).pipe(take(1)); - } else { - return this.actions().getRootFolderFiles(this.projectId()); + return this.actions().getFiles(currentFolder?.relationships.filesLink); } + return EMPTY; } copyToClipboard(embedHtml: string): void { @@ -291,8 +364,6 @@ export class FilesTreeComponent { const filesLink = this.currentFolder()?.relationships.filesLink; if (filesLink) { this.actions().getFiles(filesLink); - } else { - this.actions().getRootFolderFiles(this.projectId()); } }, }); @@ -321,7 +392,7 @@ export class FilesTreeComponent { } this.filesService - .moveFile(moveLink, path, this.projectId(), this.provider()!, 'move') + .moveFile(moveLink, path, this.resourceId(), this.provider()!, 'move') .pipe( take(1), finalize(() => { @@ -335,8 +406,6 @@ export class FilesTreeComponent { if (filesLink) { this.actions().getFiles(filesLink); - } else { - this.actions().getRootFolderFiles(this.projectId()); } } else { const filesLink = dropNode?.relationships.filesLink; diff --git a/src/app/shared/factories/files-tree-selectors.factory.ts b/src/app/shared/factories/files-tree-selectors.factory.ts new file mode 100644 index 000000000..7dd2b6762 --- /dev/null +++ b/src/app/shared/factories/files-tree-selectors.factory.ts @@ -0,0 +1,35 @@ +import { select } from '@ngxs/store'; + +import { ActivatedRoute } from '@angular/router'; + +import { PreprintStepperSelectors } from '@osf/features/preprints/store/preprint-stepper'; +import { ProjectFilesSelectors } from '@osf/features/project/files/store'; +import { RegistryFilesSelectors } from '@osf/features/registry/store/registry-files'; +import { FilesTreeSelectors } from '@shared/tokens/files-tree-selectors.token'; + +export function filesTreeSelectorsFactory(route: ActivatedRoute): FilesTreeSelectors { + const context = route.snapshot.data['context'] as 'project' | 'registry' | 'submitPreprints'; + + switch (context) { + case 'project': + return { + isFilesLoading: () => select(ProjectFilesSelectors.isFilesLoading), + getFiles: () => select(ProjectFilesSelectors.getFiles), + getCurrentFolder: () => select(ProjectFilesSelectors.getCurrentFolder), + }; + case 'registry': + return { + isFilesLoading: () => select(RegistryFilesSelectors.isFilesLoading), + getFiles: () => select(RegistryFilesSelectors.getFiles), + getCurrentFolder: () => select(RegistryFilesSelectors.getCurrentFolder), + }; + case 'submitPreprints': + return { + isFilesLoading: () => select(PreprintStepperSelectors.areProjectFilesLoading), + getFiles: () => select(PreprintStepperSelectors.getProjectFiles), + getCurrentFolder: () => select(PreprintStepperSelectors.getCurrentFolder), + }; + default: + throw new Error(`Unknown context for FilesTreeSelectors: ${context}`); + } +} diff --git a/src/app/shared/factories/index.ts b/src/app/shared/factories/index.ts new file mode 100644 index 000000000..e4ce3fcb6 --- /dev/null +++ b/src/app/shared/factories/index.ts @@ -0,0 +1 @@ +export * from './files-tree-selectors.factory'; diff --git a/src/app/shared/mappers/files/files.mapper.ts b/src/app/shared/mappers/files/files.mapper.ts index 42d68c685..7a4bde085 100644 --- a/src/app/shared/mappers/files/files.mapper.ts +++ b/src/app/shared/mappers/files/files.mapper.ts @@ -21,7 +21,17 @@ export function MapFile( dateModified: file.attributes.date_modified, dateCreated: file.attributes.date_created, extra: file.attributes.extra, - links: file.links, + links: { + info: file.links?.info, + move: file.links?.move, + upload: file.links?.upload, + delete: file.links?.delete, + download: file.links?.download, + self: file.links?.self, + html: file.links?.html, + render: file.links?.render, + newFolder: file.links?.new_folder, + }, path: file.attributes.path, materializedPath: file.attributes.materialized_path, tags: file.attributes.tags, diff --git a/src/app/shared/models/addons/configured-storage-addon.model.ts b/src/app/shared/models/addons/configured-storage-addon.model.ts new file mode 100644 index 000000000..6f3ba6e09 --- /dev/null +++ b/src/app/shared/models/addons/configured-storage-addon.model.ts @@ -0,0 +1,4 @@ +export interface ConfiguredStorageAddon { + externalServiceName: string; + displayName: string; +} diff --git a/src/app/shared/models/addons/index.ts b/src/app/shared/models/addons/index.ts index e2f3ceaa8..e2edd3d77 100644 --- a/src/app/shared/models/addons/index.ts +++ b/src/app/shared/models/addons/index.ts @@ -1,6 +1,7 @@ export * from './addon-form.model'; export * from './addon-terms.model'; export * from './addons.models'; +export * from './configured-storage-addon.model'; export * from './operation-invocation.models'; export * from './operation-invoke-data.model'; export * from './term.model'; diff --git a/src/app/shared/models/files/file.model.ts b/src/app/shared/models/files/file.model.ts index 399d1b321..5f3ed04da 100644 --- a/src/app/shared/models/files/file.model.ts +++ b/src/app/shared/models/files/file.model.ts @@ -1,5 +1,4 @@ import { OsfFileTarget } from '@osf/features/project/files/models'; -import { FileLinks } from '@shared/models'; export interface OsfFile { id: string; @@ -24,11 +23,23 @@ export interface OsfFile { currentUserCanComment: boolean; currentVersion: number; showAsUnviewed: boolean; - links: FileLinks; + links: { + info: string; + move: string; + upload: string; + delete: string; + download: string; + self: string; + html: string; + render: string; + newFolder: string; + }; relationships: { parentFolderLink: string; parentFolderId: string; filesLink: string; + uploadLink: string; + newFolderLink: string; }; target: OsfFileTarget; previousFolder: boolean; diff --git a/src/app/shared/models/files/get-configured-storage-addons.model.ts b/src/app/shared/models/files/get-configured-storage-addons.model.ts new file mode 100644 index 000000000..9c03703cf --- /dev/null +++ b/src/app/shared/models/files/get-configured-storage-addons.model.ts @@ -0,0 +1,14 @@ +import { ApiData, JsonApiResponse } from '@core/models'; + +export type GetConfiguredStorageAddonsJsonApi = JsonApiResponse< + ApiData< + { + display_name: string; + external_service_name: string; + }, + null, + null, + null + >[], + null +>; diff --git a/src/app/shared/models/files/get-files-response.model.ts b/src/app/shared/models/files/get-files-response.model.ts index eb7d2ddcd..22b774ced 100644 --- a/src/app/shared/models/files/get-files-response.model.ts +++ b/src/app/shared/models/files/get-files-response.model.ts @@ -73,4 +73,5 @@ export interface FileLinks { self: string; html: string; render: string; + new_folder: string; } diff --git a/src/app/shared/models/index.ts b/src/app/shared/models/index.ts index 809165bd6..732163f6a 100644 --- a/src/app/shared/models/index.ts +++ b/src/app/shared/models/index.ts @@ -7,6 +7,7 @@ export * from './contributors'; export * from './create-component-form.model'; export * from './file-menu-action.model'; export * from './files/file.model'; +export * from './files/get-configured-storage-addons.model'; export * from './files/get-files-response.model'; export * from './filter-labels.model'; export * from './filters'; diff --git a/src/app/shared/services/files.service.ts b/src/app/shared/services/files.service.ts index 570418965..79995f155 100644 --- a/src/app/shared/services/files.service.ts +++ b/src/app/shared/services/files.service.ts @@ -1,5 +1,5 @@ -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { EMPTY, Observable, switchMap, throwError } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; import { HttpEvent } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; @@ -31,6 +31,9 @@ import { GetFilesResponse, OsfFile, } from '@shared/models'; +import { ConfiguredStorageAddon } from '@shared/models/addons/configured-storage-addon.model'; +import { GetConfiguredStorageAddonsJsonApi } from '@shared/models/files/get-configured-storage-addons.model'; +import { ToastService } from '@shared/services/toast.service'; import { environment } from 'src/environments/environment'; @@ -40,18 +43,7 @@ import { environment } from 'src/environments/environment'; export class FilesService { #jsonApiService = inject(JsonApiService); filesFields = 'name,guid,kind,extra,size,path,materialized_path,date_modified,parent_folder,files'; - - getRootFolderFiles(resourceId: string, provider: string, search: string, sort: string): Observable { - const params: Record = { - sort: sort, - 'fields[files]': this.filesFields, - 'filter[name]': search, - }; - - return this.#jsonApiService - .get(`${environment.apiUrl}/nodes/${resourceId}/files/${provider}/`, params) - .pipe(map((response) => MapFiles(response.data))); - } + readonly toastService = inject(ToastService); getFiles(filesLink: string, search: string, sort: string): Observable { const params: Record = { @@ -65,39 +57,26 @@ export class FilesService { .pipe(map((response) => MapFiles(response.data))); } - getFilesWithoutFiltering(filesLink: string): Observable { - return this.#jsonApiService.get(filesLink).pipe(map((response) => MapFiles(response.data))); + getFolders(folderLink: string): Observable { + return this.#jsonApiService.get(`${folderLink}`).pipe(map((response) => MapFiles(response.data))); } - uploadFile( - file: File, - resourceId: string, - provider: string, - parentFolder: OsfFile | null - ): Observable>> { - const params = { - kind: 'file', - name: file.name, - }; - - let link = ''; - - if (parentFolder?.relationships.parentFolderLink) { - link = `${environment.fileApiUrl}/resources/${resourceId}/providers/${provider}/${parentFolder?.id ? parentFolder?.id + '/' : ''}`; - } else { - link = `${environment.fileApiUrl}/resources/${resourceId}/providers/${provider}/`; - } - - return this.#jsonApiService.putFile(link, file, params); + getFilesWithoutFiltering(filesLink: string): Observable { + return this.#jsonApiService.get(filesLink).pipe(map((response) => MapFiles(response.data))); } - uploadFileByLink(file: File, uploadLink: string): Observable>> { + uploadFile(file: File, uploadLink: string): Observable>> { const params = { kind: 'file', name: file.name, }; - return this.#jsonApiService.putFile(uploadLink, file, params); + return this.#jsonApiService.putFile(uploadLink, file, params).pipe( + catchError((error) => { + this.toastService.showError(error.error.message, 5000); + return throwError(error); + }) + ); } updateFileContent(file: File, link: string) { @@ -108,23 +87,20 @@ export class FilesService { return this.#jsonApiService.put(link, file, params); } - createFolder(resourceId: string, provider: string, folderName: string, folderId?: string): Observable { - const params: Record = { - kind: 'folder', - name: folderName, - }; - - const link = `${environment.fileApiUrl}/resources/${resourceId}/providers/${provider}/${folderId ? folderId + '/' : ''}`; - + createFolder(link: string, folderName: string): Observable { return this.#jsonApiService - .put(link, null, params) + .put(link, null, { name: folderName }) .pipe(map((response) => MapFile(response))); } getFolder(link: string): Observable { - return this.#jsonApiService - .get>(link) - .pipe(map((response) => MapFile(response.data))); + return this.#jsonApiService.get>(link).pipe( + map((response) => MapFile(response.data)), + catchError((error) => { + this.toastService.showError(error.error.message, 5000); + return throwError(error); + }) + ); } deleteEntry(link: string) { @@ -148,9 +124,13 @@ export class FilesService { resource: resourceId, }; - return this.#jsonApiService - .post>(link, body) - .pipe(map((response) => MapFile(response.data))); + return this.#jsonApiService.post>(link, body).pipe( + map((response) => MapFile(response.data)), + catchError((error) => { + this.toastService.showError(error.error.message, 5000); + return throwError(error); + }) + ); } getFolderDownloadLink(resourceId: string, provider: string, folderId: string, isRootFolder: boolean): string { @@ -265,4 +245,35 @@ export class FilesService { >(moveLink, body) .pipe(map((response) => MapFile(response.data))); } + + getResourceReferences(resourceUri: string): Observable { + const params = { + 'filter[resource_uri]': resourceUri, + }; + + return this.#jsonApiService + .get< + JsonApiResponse[], null> + >(`${environment.addonsV1Url}/resource-references`, params) + .pipe(map((response) => response.data?.[0]?.links?.self ?? '')); + } + + getConfiguredStorageAddons(resourceUri: string): Observable { + return this.getResourceReferences(resourceUri).pipe( + switchMap((referenceUrl: string) => { + if (!referenceUrl) return EMPTY; + + return this.#jsonApiService + .get(`${referenceUrl}/configured_storage_addons`) + .pipe( + map((response) => + response.data.map((addon) => ({ + externalServiceName: addon.attributes.external_service_name, + displayName: addon.attributes.display_name, + })) + ) + ); + }) + ); + } } diff --git a/src/app/shared/services/toast.service.ts b/src/app/shared/services/toast.service.ts index 3ce635052..007e2574a 100644 --- a/src/app/shared/services/toast.service.ts +++ b/src/app/shared/services/toast.service.ts @@ -16,7 +16,7 @@ export class ToastService { this.messageService.add({ severity: 'warn', summary }); } - showError(summary: string) { - this.messageService.add({ severity: 'error', summary }); + showError(summary: string, life = 3000) { + this.messageService.add({ severity: 'error', summary, life: life }); } } diff --git a/src/app/shared/tokens/files-tree-selectors.token.ts b/src/app/shared/tokens/files-tree-selectors.token.ts new file mode 100644 index 000000000..ae1112950 --- /dev/null +++ b/src/app/shared/tokens/files-tree-selectors.token.ts @@ -0,0 +1,11 @@ +import { InjectionToken, Signal } from '@angular/core'; + +import { OsfFile } from '@shared/models'; + +export interface FilesTreeSelectors { + isFilesLoading: () => Signal; + getFiles: () => Signal; + getCurrentFolder: () => Signal; +} + +export const FILES_TREE_SELECTORS = new InjectionToken('FILES_TREE_SELECTORS'); diff --git a/src/app/shared/tokens/index.ts b/src/app/shared/tokens/index.ts index 82d99a590..ed5bbff66 100644 --- a/src/app/shared/tokens/index.ts +++ b/src/app/shared/tokens/index.ts @@ -1 +1,2 @@ +export * from './files-tree-selectors.token'; export * from './subjects.token'; diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 029cfefcc..7d996dd0d 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -31,7 +31,8 @@ "update": "Update", "continue update": "Continue update", "withdraw": "Withdraw", - "submit": "Submit" + "submit": "Submit", + "upload": "Upload" }, "search": { "title": "Search", @@ -741,7 +742,8 @@ }, "dialogs": { "uploadFile": { - "title": "Upload file" + "title": "Upload file", + "message": "Are you sure you want to update {{name}}?" }, "createFolder": { "title": "Create folder", diff --git a/src/environments/environment.development.ts b/src/environments/environment.development.ts index bd43d69df..418d0467a 100644 --- a/src/environments/environment.development.ts +++ b/src/environments/environment.development.ts @@ -10,4 +10,5 @@ export const environment = { fileApiUrl: 'https://files.us.staging4.osf.io/v1', baseResourceUri: 'https://staging4.osf.io/', funderApiUrl: 'https://api.crossref.org/', + addonsV1Url: 'https://addons.staging4.osf.io/v1', }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index bd43d69df..418d0467a 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -10,4 +10,5 @@ export const environment = { fileApiUrl: 'https://files.us.staging4.osf.io/v1', baseResourceUri: 'https://staging4.osf.io/', funderApiUrl: 'https://api.crossref.org/', + addonsV1Url: 'https://addons.staging4.osf.io/v1', }; From 8f5b4644f07ffeaa91827ca7bba64bf31ae27491 Mon Sep 17 00:00:00 2001 From: Kyrylo Petrov Date: Mon, 14 Jul 2025 15:31:54 +0300 Subject: [PATCH 2/3] fix(registry-files): minor fixes --- .../components/nav-menu/nav-menu.component.ts | 22 +++++++++++++++++-- src/app/core/constants/nav-items.constant.ts | 2 +- .../features/preprints/preprints.routes.ts | 3 ++- .../move-file-dialog.component.ts | 8 +++---- src/app/features/project/project.routes.ts | 2 +- .../registry-files.component.ts | 2 +- src/app/features/registry/registry.routes.ts | 4 ++-- .../registry-files/registry-files.state.ts | 2 +- .../files-tree/files-tree.component.html | 2 +- .../files-tree/files-tree.component.scss | 6 ++--- .../files-tree/files-tree.component.ts | 4 ++-- .../factories/files-tree-selectors.factory.ts | 9 ++++---- src/app/shared/services/files.service.ts | 12 +++++----- src/app/shared/services/toast.service.ts | 4 ++-- src/assets/i18n/en.json | 1 + 15 files changed, 52 insertions(+), 31 deletions(-) diff --git a/src/app/core/components/nav-menu/nav-menu.component.ts b/src/app/core/components/nav-menu/nav-menu.component.ts index 71f1b6f54..65bc62176 100644 --- a/src/app/core/components/nav-menu/nav-menu.component.ts +++ b/src/app/core/components/nav-menu/nav-menu.component.ts @@ -5,7 +5,7 @@ import { PanelMenuModule } from 'primeng/panelmenu'; import { filter, map } from 'rxjs'; -import { Component, computed, inject, output } from '@angular/core'; +import { Component, computed, effect, inject, output } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { ActivatedRoute, NavigationEnd, Router, RouterLink, RouterLinkActive } from '@angular/router'; @@ -24,7 +24,7 @@ export class NavMenuComponent { private readonly router = inject(Router); private readonly route = inject(ActivatedRoute); - protected readonly menuItems = MENU_ITEMS; + protected menuItems = MENU_ITEMS; protected readonly myProjectMenuItems = PROJECT_MENU_ITEMS; protected readonly registrationMenuItems = REGISTRATION_MENU_ITEMS; @@ -46,6 +46,22 @@ export class NavMenuComponent { protected readonly isProjectRoute = computed(() => !!this.currentResourceId()); protected readonly isCollectionsRoute = computed(() => this.currentRoute().isCollectionsWithId); protected readonly isRegistryRoute = computed(() => this.currentRoute().isRegistryRoute); + protected readonly isRegistryRouteDetails = computed(() => this.currentRoute().isRegistryRouteDetails); + + constructor() { + effect(() => { + const isRouteDetails = this.isRegistryRouteDetails(); + if (isRouteDetails) { + this.menuItems = this.menuItems.map((menuItem) => { + if (menuItem.id === 'registries') { + menuItem.expanded = true; + return menuItem; + } + return menuItem; + }); + } + }); + } private getRouteInfo() { const urlSegments = this.router.url.split('/').filter((segment) => segment); @@ -55,12 +71,14 @@ export class NavMenuComponent { const isCollectionsWithId = urlSegments[0] === 'collections' && urlSegments[1] && urlSegments[1] !== ''; const isRegistryRoute = urlSegments[0] === 'registries' && !!urlSegments[2]; + const isRegistryRouteDetails = urlSegments[0] === 'registries' && urlSegments[2]; return { resourceId, section, isCollectionsWithId, isRegistryRoute, + isRegistryRouteDetails, }; } diff --git a/src/app/core/constants/nav-items.constant.ts b/src/app/core/constants/nav-items.constant.ts index 4913a43f2..0dd95362a 100644 --- a/src/app/core/constants/nav-items.constant.ts +++ b/src/app/core/constants/nav-items.constant.ts @@ -22,10 +22,10 @@ export const MENU_ITEMS: MenuItem[] = [ styleClass: 'mt-5', }, { + id: 'registries', label: 'navigation.registries', icon: 'osf-icon-registries', routerLinkActiveOptions: { exact: true }, - items: [ { routerLink: '/registries/overview', diff --git a/src/app/features/preprints/preprints.routes.ts b/src/app/features/preprints/preprints.routes.ts index 05768d5e6..5e68a0c62 100644 --- a/src/app/features/preprints/preprints.routes.ts +++ b/src/app/features/preprints/preprints.routes.ts @@ -9,6 +9,7 @@ import { PreprintStepperState } from '@osf/features/preprints/store/preprint-ste import { PreprintsDiscoverState } from '@osf/features/preprints/store/preprints-discover'; import { PreprintsResourcesFiltersState } from '@osf/features/preprints/store/preprints-resources-filters'; import { PreprintsResourcesFiltersOptionsState } from '@osf/features/preprints/store/preprints-resources-filters-options'; +import { ResourceType } from '@shared/enums'; import { ContributorsState, SubjectsState } from '@shared/stores'; import { ModeratorsState } from '../moderation/store/moderation'; @@ -70,7 +71,7 @@ export const preprintsRoutes: Routes = [ (c) => c.SubmitPreprintStepperComponent ), data: { - context: 'submitPreprints', + context: ResourceType.Preprint, }, canDeactivate: [ConfirmLeavingGuard], }, diff --git a/src/app/features/project/files/components/move-file-dialog/move-file-dialog.component.ts b/src/app/features/project/files/components/move-file-dialog/move-file-dialog.component.ts index 0dacb028e..2cb2615be 100644 --- a/src/app/features/project/files/components/move-file-dialog/move-file-dialog.component.ts +++ b/src/app/features/project/files/components/move-file-dialog/move-file-dialog.component.ts @@ -91,8 +91,8 @@ export class MoveFileDialogComponent { this.isFilesUpdating.set(false); }), catchError((error) => { - this.toastService.showError(error.error.message, 5000); - return throwError(error); + this.toastService.showError(error.error.message); + return throwError(() => error); }) ) .subscribe((folder) => { @@ -130,8 +130,8 @@ export class MoveFileDialogComponent { this.dialogRef.close(); }), catchError((error) => { - this.toastService.showError(error.error.message, 5000); - return throwError(error); + this.toastService.showError(error.error.message); + return throwError(() => error); }) ) .subscribe((file) => { diff --git a/src/app/features/project/project.routes.ts b/src/app/features/project/project.routes.ts index e382a7bde..94cbef903 100644 --- a/src/app/features/project/project.routes.ts +++ b/src/app/features/project/project.routes.ts @@ -35,7 +35,7 @@ export const projectRoutes: Routes = [ loadChildren: () => import('../project/files/project-files.routes').then((mod) => mod.projectFilesRoutes), providers: [provideStates([ProjectFilesState])], data: { - context: 'project', + context: ResourceType.Project, }, }, { diff --git a/src/app/features/registry/pages/registry-files/registry-files.component.ts b/src/app/features/registry/pages/registry-files/registry-files.component.ts index 1a63fe9f9..a35ea3f08 100644 --- a/src/app/features/registry/pages/registry-files/registry-files.component.ts +++ b/src/app/features/registry/pages/registry-files/registry-files.component.ts @@ -88,7 +88,7 @@ export class RegistryFilesComponent { constructor() { this.route.parent?.params.subscribe((params) => { - const id = params['registrationId']; + const id = params['id']; if (id) { this.registryId.set(id); if (!this.registry()) { diff --git a/src/app/features/registry/registry.routes.ts b/src/app/features/registry/registry.routes.ts index eb5f17053..fef5eec8f 100644 --- a/src/app/features/registry/registry.routes.ts +++ b/src/app/features/registry/registry.routes.ts @@ -15,6 +15,7 @@ export const registryRoutes: Routes = [ { path: '', component: RegistryComponent, + providers: [provideStates([RegistryOverviewState])], children: [ { path: '', @@ -25,7 +26,6 @@ export const registryRoutes: Routes = [ path: 'overview', loadComponent: () => import('./pages/registry-overview/registry-overview.component').then((c) => c.RegistryOverviewComponent), - providers: [provideStates([RegistryOverviewState])], }, { path: 'contributors', @@ -46,7 +46,7 @@ export const registryRoutes: Routes = [ import('./pages/registry-files/registry-files.component').then((c) => c.RegistryFilesComponent), providers: [provideStates([RegistryFilesState])], data: { - context: 'registry', + context: ResourceType.Registration, }, }, ], diff --git a/src/app/features/registry/store/registry-files/registry-files.state.ts b/src/app/features/registry/store/registry-files/registry-files.state.ts index 13fd5cf16..45ccddbc7 100644 --- a/src/app/features/registry/store/registry-files/registry-files.state.ts +++ b/src/app/features/registry/store/registry-files/registry-files.state.ts @@ -47,7 +47,7 @@ export class RegistryFilesState { }, }), catchError((error) => { - this.toastService.showError(error, 5000); + this.toastService.showError(error); return handleSectionError(ctx, 'files', error); }) ); diff --git a/src/app/shared/components/files-tree/files-tree.component.html b/src/app/shared/components/files-tree/files-tree.component.html index 208da859d..5eab5f92d 100644 --- a/src/app/shared/components/files-tree/files-tree.component.html +++ b/src/app/shared/components/files-tree/files-tree.component.html @@ -2,7 +2,7 @@ @if (isDragOver()) {
-

Drop a file to upload

+

{{ 'project.files.dropText' | translate }}

}
diff --git a/src/app/shared/components/files-tree/files-tree.component.scss b/src/app/shared/components/files-tree/files-tree.component.scss index d301a4ea3..864001576 100644 --- a/src/app/shared/components/files-tree/files-tree.component.scss +++ b/src/app/shared/components/files-tree/files-tree.component.scss @@ -68,7 +68,7 @@ } .drop-zone { - position: absolute; + position: fixed; top: 0; left: 0; width: 100%; @@ -77,7 +77,7 @@ display: flex; align-items: center; justify-content: center; - color: white; + color: var.$white; transition: background 0.3s ease, backdrop-filter 0.3s ease; @@ -92,7 +92,7 @@ .drop-text { text-transform: none; - color: white; + color: var.$white; pointer-events: none; } } diff --git a/src/app/shared/components/files-tree/files-tree.component.ts b/src/app/shared/components/files-tree/files-tree.component.ts index d554a7d98..83c2d2f90 100644 --- a/src/app/shared/components/files-tree/files-tree.component.ts +++ b/src/app/shared/components/files-tree/files-tree.component.ts @@ -159,8 +159,8 @@ export class FilesTreeComponent implements OnInit, OnDestroy { .pipe( take(1), catchError((error) => { - this.toastService.showError(error.error.message, 5000); - return throwError(error); + this.toastService.showError(error.error.message); + return throwError(() => error); }) ) .subscribe({ diff --git a/src/app/shared/factories/files-tree-selectors.factory.ts b/src/app/shared/factories/files-tree-selectors.factory.ts index 7dd2b6762..e1585d9ce 100644 --- a/src/app/shared/factories/files-tree-selectors.factory.ts +++ b/src/app/shared/factories/files-tree-selectors.factory.ts @@ -5,25 +5,26 @@ import { ActivatedRoute } from '@angular/router'; import { PreprintStepperSelectors } from '@osf/features/preprints/store/preprint-stepper'; import { ProjectFilesSelectors } from '@osf/features/project/files/store'; import { RegistryFilesSelectors } from '@osf/features/registry/store/registry-files'; +import { ResourceType } from '@shared/enums'; import { FilesTreeSelectors } from '@shared/tokens/files-tree-selectors.token'; export function filesTreeSelectorsFactory(route: ActivatedRoute): FilesTreeSelectors { - const context = route.snapshot.data['context'] as 'project' | 'registry' | 'submitPreprints'; + const context = route.snapshot.data['context'] as ResourceType; switch (context) { - case 'project': + case ResourceType.Project: return { isFilesLoading: () => select(ProjectFilesSelectors.isFilesLoading), getFiles: () => select(ProjectFilesSelectors.getFiles), getCurrentFolder: () => select(ProjectFilesSelectors.getCurrentFolder), }; - case 'registry': + case ResourceType.Registration: return { isFilesLoading: () => select(RegistryFilesSelectors.isFilesLoading), getFiles: () => select(RegistryFilesSelectors.getFiles), getCurrentFolder: () => select(RegistryFilesSelectors.getCurrentFolder), }; - case 'submitPreprints': + case ResourceType.Preprint: return { isFilesLoading: () => select(PreprintStepperSelectors.areProjectFilesLoading), getFiles: () => select(PreprintStepperSelectors.getProjectFiles), diff --git a/src/app/shared/services/files.service.ts b/src/app/shared/services/files.service.ts index 79995f155..fd33304ab 100644 --- a/src/app/shared/services/files.service.ts +++ b/src/app/shared/services/files.service.ts @@ -73,8 +73,8 @@ export class FilesService { return this.#jsonApiService.putFile(uploadLink, file, params).pipe( catchError((error) => { - this.toastService.showError(error.error.message, 5000); - return throwError(error); + this.toastService.showError(error.error.message); + return throwError(() => error); }) ); } @@ -97,8 +97,8 @@ export class FilesService { return this.#jsonApiService.get>(link).pipe( map((response) => MapFile(response.data)), catchError((error) => { - this.toastService.showError(error.error.message, 5000); - return throwError(error); + this.toastService.showError(error.error.message); + return throwError(() => error); }) ); } @@ -127,8 +127,8 @@ export class FilesService { return this.#jsonApiService.post>(link, body).pipe( map((response) => MapFile(response.data)), catchError((error) => { - this.toastService.showError(error.error.message, 5000); - return throwError(error); + this.toastService.showError(error.error.message); + return throwError(() => error); }) ); } diff --git a/src/app/shared/services/toast.service.ts b/src/app/shared/services/toast.service.ts index 007e2574a..56a9bb255 100644 --- a/src/app/shared/services/toast.service.ts +++ b/src/app/shared/services/toast.service.ts @@ -16,7 +16,7 @@ export class ToastService { this.messageService.add({ severity: 'warn', summary }); } - showError(summary: string, life = 3000) { - this.messageService.add({ severity: 'error', summary, life: life }); + showError(summary: string) { + this.messageService.add({ severity: 'error', summary, life: 5000 }); } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 7d996dd0d..ae6a04ab8 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -771,6 +771,7 @@ "message": "Are you sure you want to delete {{name}}?" } }, + "dropText": "Drop a file to upload", "emptyState": "This folder is empty", "detail": { "backToList": "Back to list of files", From a3eba68ad1c2ac32a411f742d536261b4d23006b Mon Sep 17 00:00:00 2001 From: Kyrylo Petrov Date: Mon, 14 Jul 2025 15:55:41 +0300 Subject: [PATCH 3/3] fix(registry-files): remove unnecesary code --- .../components/stepper/file-step/file-step.component.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/features/preprints/components/stepper/file-step/file-step.component.ts b/src/app/features/preprints/components/stepper/file-step/file-step.component.ts index e6c498d2c..4a4bdc44c 100644 --- a/src/app/features/preprints/components/stepper/file-step/file-step.component.ts +++ b/src/app/features/preprints/components/stepper/file-step/file-step.component.ts @@ -93,8 +93,6 @@ export class FileStepComponent implements OnInit { arePreprintFilesLoading = select(PreprintStepperSelectors.arePreprintFilesLoading); availableProjects = select(PreprintStepperSelectors.getAvailableProjects); areAvailableProjectsLoading = select(PreprintStepperSelectors.areAvailableProjectsLoading); - projectFiles = select(PreprintStepperSelectors.getProjectFiles); - areProjectFilesLoading = select(PreprintStepperSelectors.areProjectFilesLoading); selectedProjectId = signal(null); versionFileMode = signal(false);