From af970b33104365aeeac8b7a22123bdbd1a664acd Mon Sep 17 00:00:00 2001 From: nsemets Date: Thu, 30 Apr 2026 18:31:17 +0300 Subject: [PATCH 01/13] fix(status-badge): update status badge --- .../status-badge/status-badge.component.html | 4 ++-- .../status-badge/status-badge.component.ts | 12 +++--------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/app/shared/components/status-badge/status-badge.component.html b/src/app/shared/components/status-badge/status-badge.component.html index 0f2b18722..261413749 100644 --- a/src/app/shared/components/status-badge/status-badge.component.html +++ b/src/app/shared/components/status-badge/status-badge.component.html @@ -1,3 +1,3 @@ -@if (label) { - +@if (label()) { + } diff --git a/src/app/shared/components/status-badge/status-badge.component.ts b/src/app/shared/components/status-badge/status-badge.component.ts index 5610e175b..1fa211862 100644 --- a/src/app/shared/components/status-badge/status-badge.component.ts +++ b/src/app/shared/components/status-badge/status-badge.component.ts @@ -2,11 +2,10 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Tag } from 'primeng/tag'; -import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; import { RegistryStatusMap } from '@osf/shared/constants/registration-statuses'; import { RegistryStatus } from '@osf/shared/enums/registry-status.enum'; -import { TagSeverityType } from '@osf/shared/models/severity.type'; @Component({ selector: 'osf-status-badge', @@ -18,11 +17,6 @@ import { TagSeverityType } from '@osf/shared/models/severity.type'; export class StatusBadgeComponent { status = input.required(); - get label(): string { - return RegistryStatusMap[this.status()]?.label ?? 'Unknown'; - } - - get severity(): TagSeverityType | null { - return RegistryStatusMap[this.status()]?.severity ?? null; - } + label = computed(() => RegistryStatusMap[this.status()]?.label ?? 'resourceCard.type.null'); + severity = computed(() => RegistryStatusMap[this.status()]?.severity ?? null); } From 6ad1024c63bd82acc6b9b516c9dde5454b9e3709 Mon Sep 17 00:00:00 2001 From: nsemets Date: Thu, 30 Apr 2026 19:07:27 +0300 Subject: [PATCH 02/13] fix(files-component): updated files component and fixed some requests --- ...confirm-move-file-dialog.component.spec.ts | 24 ++----------- .../confirm-move-file-dialog.component.ts | 9 +---- .../file-browser-info.component.ts | 6 ++-- .../file-keywords.component.spec.ts | 9 +++-- .../file-resource-metadata.component.html | 8 ++--- src/app/features/files/files.routes.ts | 7 ++-- src/app/features/files/store/files.actions.ts | 15 +++++--- src/app/features/files/store/files.state.ts | 10 +++--- .../stepper/file-step/file-step.component.ts | 3 +- .../files-widget/files-widget.component.ts | 13 +++---- .../files-control/files-control.component.ts | 3 +- .../file-menu/file-menu.component.html | 2 +- .../file-select-destination.component.ts | 11 ++---- .../components/subjects/subjects.component.ts | 16 ++++++--- .../models/files/file-page-link.model.ts | 4 +++ .../models/files/renamed-file-link.model.ts | 4 +++ src/app/shared/services/files.service.ts | 34 +++++++++++++------ 17 files changed, 86 insertions(+), 92 deletions(-) create mode 100644 src/app/shared/models/files/file-page-link.model.ts create mode 100644 src/app/shared/models/files/renamed-file-link.model.ts diff --git a/src/app/features/files/components/confirm-move-file-dialog/confirm-move-file-dialog.component.spec.ts b/src/app/features/files/components/confirm-move-file-dialog/confirm-move-file-dialog.component.spec.ts index 6e91d5349..0a6fa8dd5 100644 --- a/src/app/features/files/components/confirm-move-file-dialog/confirm-move-file-dialog.component.spec.ts +++ b/src/app/features/files/components/confirm-move-file-dialog/confirm-move-file-dialog.component.spec.ts @@ -1,12 +1,9 @@ -import { MockComponents, MockProvider } from 'ng-mocks'; +import { MockProvider } from 'ng-mocks'; import { DynamicDialogConfig } from 'primeng/dynamicdialog'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FileSelectDestinationComponent } from '@osf/shared/components/file-select-destination/file-select-destination.component'; -import { IconComponent } from '@osf/shared/components/icon/icon.component'; -import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; import { FilesService } from '@osf/shared/services/files.service'; import { ToastService } from '@osf/shared/services/toast.service'; @@ -14,11 +11,8 @@ import { ToastService } from '@osf/shared/services/toast.service'; import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomConfirmationServiceMock } from '@testing/providers/custom-confirmation-provider.mock'; import { provideDynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock'; -import { provideMockStore } from '@testing/providers/store-provider.mock'; import { ToastServiceMock } from '@testing/providers/toast-provider.mock'; -import { FilesSelectors } from '../../store'; - import { ConfirmMoveFileDialogComponent } from './confirm-move-file-dialog.component'; describe('ConfirmConfirmMoveFileDialogComponent', () => { @@ -31,10 +25,7 @@ describe('ConfirmConfirmMoveFileDialogComponent', () => { }; TestBed.configureTestingModule({ - imports: [ - ConfirmMoveFileDialogComponent, - ...MockComponents(IconComponent, LoadingSpinnerComponent, FileSelectDestinationComponent), - ], + imports: [ConfirmMoveFileDialogComponent], providers: [ provideOSFCore(), provideDynamicDialogRefMock(), @@ -42,12 +33,6 @@ describe('ConfirmConfirmMoveFileDialogComponent', () => { MockProvider(FilesService), MockProvider(ToastService, ToastServiceMock.simple()), MockProvider(CustomConfirmationService, CustomConfirmationServiceMock.simple()), - provideMockStore({ - signals: [ - { selector: FilesSelectors.getMoveDialogFiles, value: [] }, - { selector: FilesSelectors.getProvider, value: null }, - ], - }), ], }); @@ -63,10 +48,5 @@ describe('ConfirmConfirmMoveFileDialogComponent', () => { it('should initialize with correct properties', () => { expect(component.config).toBeDefined(); expect(component.dialogRef).toBeDefined(); - expect(component.files).toBeDefined(); - }); - - it('should get files from store', () => { - expect(component.files()).toEqual([]); }); }); diff --git a/src/app/features/files/components/confirm-move-file-dialog/confirm-move-file-dialog.component.ts b/src/app/features/files/components/confirm-move-file-dialog/confirm-move-file-dialog.component.ts index 038478525..2bb6771f8 100644 --- a/src/app/features/files/components/confirm-move-file-dialog/confirm-move-file-dialog.component.ts +++ b/src/app/features/files/components/confirm-move-file-dialog/confirm-move-file-dialog.component.ts @@ -1,5 +1,3 @@ -import { select } from '@ngxs/store'; - import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import { Button } from 'primeng/button'; @@ -11,7 +9,6 @@ import { catchError } from 'rxjs/operators'; import { ChangeDetectionStrategy, Component, DestroyRef, inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { FilesSelectors } from '@osf/features/files/store'; import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; import { FilesService } from '@osf/shared/services/files.service'; import { ToastService } from '@osf/shared/services/toast.service'; @@ -35,8 +32,6 @@ export class ConfirmMoveFileDialogComponent { private readonly toastService = inject(ToastService); private readonly customConfirmationService = inject(CustomConfirmationService); - readonly files = select(FilesSelectors.getMoveDialogFiles); - readonly provider = this.config.data.storageProvider; private fileProjectId = this.config.data.resourceId; @@ -112,9 +107,7 @@ export class ConfirmMoveFileDialogComponent { this.customConfirmationService.confirmDelete({ headerKey: conflictFiles.length > 1 ? 'files.dialogs.replaceFile.multiple' : 'files.dialogs.replaceFile.single', messageKey: 'files.dialogs.replaceFile.message', - messageParams: { - name: conflictFiles.map((c) => c.file.name).join(', '), - }, + messageParams: { name: conflictFiles.map((c) => c.file.name).join(', ') }, acceptLabelKey: 'common.buttons.replace', onConfirm: () => { const replaceRequests$ = conflictFiles.map(({ link }) => diff --git a/src/app/features/files/components/file-browser-info/file-browser-info.component.ts b/src/app/features/files/components/file-browser-info/file-browser-info.component.ts index 7110c2873..ea5e520d6 100644 --- a/src/app/features/files/components/file-browser-info/file-browser-info.component.ts +++ b/src/app/features/files/components/file-browser-info/file-browser-info.component.ts @@ -24,7 +24,7 @@ export class FileBrowserInfoComponent { readonly infoItems = FILE_BROWSER_INFO_ITEMS; - readonly filteredInfoItems = computed(() => { - return this.infoItems.filter((item) => item.showForResourceTypes.includes(this.resourceType())); - }); + readonly filteredInfoItems = computed(() => + this.infoItems.filter((item) => item.showForResourceTypes.includes(this.resourceType())) + ); } diff --git a/src/app/features/files/components/file-keywords/file-keywords.component.spec.ts b/src/app/features/files/components/file-keywords/file-keywords.component.spec.ts index 4daf81e0e..fa9da88d8 100644 --- a/src/app/features/files/components/file-keywords/file-keywords.component.spec.ts +++ b/src/app/features/files/components/file-keywords/file-keywords.component.spec.ts @@ -1,4 +1,3 @@ -import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { provideOSFCore } from '@testing/osf.testing.provider'; @@ -26,10 +25,10 @@ describe('FileKeywordsComponent', () => { provideOSFCore(), provideMockStore({ signals: [ - { selector: FilesSelectors.getFileTags, value: signal(mockTags) }, - { selector: FilesSelectors.isFileTagsLoading, value: signal(false) }, - { selector: FilesSelectors.getOpenedFile, value: signal(mockFile) }, - { selector: FilesSelectors.hasWriteAccess, value: signal(true) }, + { selector: FilesSelectors.getFileTags, value: mockTags }, + { selector: FilesSelectors.isFileTagsLoading, value: false }, + { selector: FilesSelectors.getOpenedFile, value: mockFile }, + { selector: FilesSelectors.hasWriteAccess, value: true }, ], }), ], diff --git a/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.html b/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.html index 06c3a5c5a..722daa7b4 100644 --- a/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.html +++ b/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.html @@ -9,7 +9,7 @@

} @else { - @for (funder of resourceMetadata()?.funders; track $index) { + @for (funder of resourceMetadata()?.funders; track funder.funderIdentifier) {
@if (funder.funderName) {
@@ -23,10 +23,10 @@

{{ 'files.detail.resourceMetadata.fields.awardTitle' | translate }}

{{ funder.awardTitle }}
} - @if (funder.awardTitle) { + @if (funder.awardNumber) {

{{ 'files.detail.resourceMetadata.fields.awardNumber' | translate }}

- {{ funder.awardTitle }} + {{ funder.awardNumber }}
} @if (funder.awardUri) { @@ -98,7 +98,7 @@

{{ 'files.detail.resourceMetadata.fields.dateModified' | translate }}

@if (isResourceContributorsLoading()) { } @else { - @if (hasViewOnly() || contributors().length) { + @if (contributors().length) {

{{ 'common.labels.contributors' | translate }}

diff --git a/src/app/features/files/files.routes.ts b/src/app/features/files/files.routes.ts index 8b4cb2b77..9f53d2c08 100644 --- a/src/app/features/files/files.routes.ts +++ b/src/app/features/files/files.routes.ts @@ -29,11 +29,8 @@ export const filesRoutes: Routes = [ { path: ':fileGuid', data: { canonicalPathTemplate: 'files/:fileGuid' }, - loadComponent: () => { - return import('@osf/features/files/pages/file-detail/file-detail.component').then( - (c) => c.FileDetailComponent - ); - }, + loadComponent: () => + import('@osf/features/files/pages/file-detail/file-detail.component').then((c) => c.FileDetailComponent), }, ], }, diff --git a/src/app/features/files/store/files.actions.ts b/src/app/features/files/store/files.actions.ts index 999863b71..f7988a318 100644 --- a/src/app/features/files/store/files.actions.ts +++ b/src/app/features/files/store/files.actions.ts @@ -1,4 +1,5 @@ import { FileFolderModel } from '@osf/shared/models/files/file-folder.model'; +import { ResourceType } from '@shared/enums/resource-type.enum'; import { PatchFileMetadata } from '../models'; @@ -131,25 +132,31 @@ export class DeleteEntry { export class GetRootFolders { static readonly type = '[Files] Get Folders'; - constructor(public folderLink: string) {} + constructor( + public resourceId: string, + public resourceType: ResourceType + ) {} } export class GetConfiguredStorageAddons { static readonly type = '[Files] Get ConfiguredStorageAddons'; - constructor(public resourceUri: string) {} + constructor(public resourceId: string) {} } export class GetMoveDialogRootFolders { static readonly type = '[Files] Get Move Dialog Folders'; - constructor(public folderLink: string) {} + constructor( + public resourceId: string, + public resourceType: ResourceType + ) {} } export class GetMoveDialogConfiguredStorageAddons { static readonly type = '[Files] Get Move Dialog ConfiguredStorageAddons'; - constructor(public resourceUri: string) {} + constructor(public resourceId: string) {} } export class GetStorageSupportedFeatures { diff --git a/src/app/features/files/store/files.state.ts b/src/app/features/files/store/files.state.ts index 52e6e31a8..ea2c68229 100644 --- a/src/app/features/files/store/files.state.ts +++ b/src/app/features/files/store/files.state.ts @@ -7,7 +7,6 @@ import { inject, Injectable } from '@angular/core'; import { SupportedFeature } from '@osf/shared/enums/addon-supported-features.enum'; import { handleSectionError } from '@osf/shared/helpers/state-error.handler'; import { FilesService } from '@osf/shared/services/files.service'; -import { ToastService } from '@osf/shared/services/toast.service'; import { MapResourceMetadata } from '../mappers'; @@ -45,7 +44,6 @@ import { FILES_STATE_DEFAULTS, FilesStateModel } from './files.model'; }) export class FilesState { filesService = inject(FilesService); - toastService = inject(ToastService); @Action(GetFiles) getFiles(ctx: StateContext, action: GetFiles) { @@ -262,7 +260,7 @@ export class FilesState { getRootFolders(ctx: StateContext, action: GetRootFolders) { const state = ctx.getState(); ctx.patchState({ rootFolders: { ...state.rootFolders, isLoading: true } }); - return this.filesService.getFolders(action.folderLink).pipe( + return this.filesService.getRootFolders(action.resourceId, action.resourceType).pipe( tap((response) => ctx.patchState({ rootFolders: { @@ -282,7 +280,7 @@ export class FilesState { const state = ctx.getState(); ctx.patchState({ moveDialogRootFolders: { ...state.moveDialogRootFolders, isLoading: true } }); - return this.filesService.getFolders(action.folderLink).pipe( + return this.filesService.getRootFolders(action.resourceId, action.resourceType).pipe( tap((response) => ctx.patchState({ moveDialogRootFolders: { @@ -302,7 +300,7 @@ export class FilesState { const state = ctx.getState(); ctx.patchState({ configuredStorageAddons: { ...state.configuredStorageAddons, isLoading: true } }); - return this.filesService.getConfiguredStorageAddons(action.resourceUri).pipe( + return this.filesService.getConfiguredStorageAddons(action.resourceId).pipe( tap((addons) => ctx.patchState({ configuredStorageAddons: { @@ -326,7 +324,7 @@ export class FilesState { moveDialogConfiguredStorageAddons: { ...state.moveDialogConfiguredStorageAddons, isLoading: true }, }); - return this.filesService.getConfiguredStorageAddons(action.resourceUri).pipe( + return this.filesService.getConfiguredStorageAddons(action.resourceId).pipe( tap((addons) => ctx.patchState({ moveDialogConfiguredStorageAddons: { 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 b8f771b61..f3b469834 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 @@ -46,6 +46,7 @@ import { ClearFileDirective } from '@osf/shared/directives/clear-file.directive' import { StringOrNull } from '@osf/shared/helpers/types.helper'; import { FileModel } from '@osf/shared/models/files/file.model'; import { FileFolderModel } from '@osf/shared/models/files/file-folder.model'; +import { FilePageLinkModel } from '@osf/shared/models/files/file-page-link.model'; import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; import { ToastService } from '@osf/shared/services/toast.service'; @@ -233,7 +234,7 @@ export class FileStepComponent implements OnInit { this.actions.getProjectFilesByLink(folder.links.filesLink, 1); } - onLoadFiles(event: { link: string; page: number }) { + onLoadFiles(event: FilePageLinkModel) { this.actions.getProjectFilesByLink(event.link, event.page); } } diff --git a/src/app/features/project/overview/components/files-widget/files-widget.component.ts b/src/app/features/project/overview/components/files-widget/files-widget.component.ts index 535b01a43..4de98cd51 100644 --- a/src/app/features/project/overview/components/files-widget/files-widget.component.ts +++ b/src/app/features/project/overview/components/files-widget/files-widget.component.ts @@ -20,7 +20,6 @@ import { } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { ENVIRONMENT } from '@core/provider/environment.provider'; import { FileProvider } from '@osf/features/files/constants'; import { FilesSelectors, @@ -32,11 +31,13 @@ import { } from '@osf/features/files/store'; import { FilesTreeComponent } from '@osf/shared/components/files-tree/files-tree.component'; import { SelectComponent } from '@osf/shared/components/select/select.component'; +import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { Primitive } from '@osf/shared/helpers/types.helper'; import { ConfiguredAddonModel } from '@osf/shared/models/addons/configured-addon.model'; import { FileModel } from '@osf/shared/models/files/file.model'; import { FileFolderModel } from '@osf/shared/models/files/file-folder.model'; import { FileLabelModel } from '@osf/shared/models/files/file-label.model'; +import { FilePageLinkModel } from '@osf/shared/models/files/file-page-link.model'; import { NodeShortInfoModel } from '@osf/shared/models/nodes/node-with-children.model'; import { ProjectModel } from '@osf/shared/models/projects/projects.model'; import { SelectOption } from '@osf/shared/models/select-option.model'; @@ -56,7 +57,6 @@ export class FilesWidgetComponent { router = inject(Router); activeRoute = inject(ActivatedRoute); - private readonly environment = inject(ENVIRONMENT); private readonly destroyRef = inject(DestroyRef); private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); private readonly platformId = inject(PLATFORM_ID); @@ -160,11 +160,8 @@ export class FilesWidgetComponent { } private getStorageAddons(projectId: string) { - const resourcePath = 'nodes'; - const folderLink = `${this.environment.apiDomainUrl}/v2/${resourcePath}/${projectId}/files/`; - const iriLink = `${this.environment.webUrl}/${projectId}`; - this.actions.getRootFolders(folderLink); - this.actions.getConfiguredStorageAddons(iriLink); + this.actions.getRootFolders(projectId, ResourceType.Project); + this.actions.getConfiguredStorageAddons(projectId); } private flatComponents( @@ -234,7 +231,7 @@ export class FilesWidgetComponent { window.open(url, '_blank'); } - onLoadFiles(event: { link: string; page: number }) { + onLoadFiles(event: FilePageLinkModel) { this.actions.getFiles(event.link, event.page); } diff --git a/src/app/features/registries/components/files-control/files-control.component.ts b/src/app/features/registries/components/files-control/files-control.component.ts index 423a65d45..58d1081a8 100644 --- a/src/app/features/registries/components/files-control/files-control.component.ts +++ b/src/app/features/registries/components/files-control/files-control.component.ts @@ -17,6 +17,7 @@ import { FilesTreeComponent } from '@osf/shared/components/files-tree/files-tree import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; import { FILE_SIZE_LIMIT } from '@osf/shared/constants/files-limits.const'; import { ClearFileDirective } from '@osf/shared/directives/clear-file.directive'; +import { FilePageLinkModel } from '@osf/shared/models/files/file-page-link.model'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { FilesService } from '@osf/shared/services/files.service'; import { ToastService } from '@osf/shared/services/toast.service'; @@ -163,7 +164,7 @@ export class FilesControlComponent { this.filesSelection = [...new Set(this.filesSelection)]; } - onLoadFiles(event: { link: string; page: number }) { + onLoadFiles(event: FilePageLinkModel) { this.actions.getFiles(event.link, event.page); } diff --git a/src/app/shared/components/file-menu/file-menu.component.html b/src/app/shared/components/file-menu/file-menu.component.html index cb17cb23f..f5b180995 100644 --- a/src/app/shared/components/file-menu/file-menu.component.html +++ b/src/app/shared/components/file-menu/file-menu.component.html @@ -6,7 +6,7 @@ variant="text" [raised]="true" icon="fas fa-ellipsis-v" - (click)="onMenuToggle($event)" + (onClick)="onMenuToggle($event)" > diff --git a/src/app/shared/components/file-select-destination/file-select-destination.component.ts b/src/app/shared/components/file-select-destination/file-select-destination.component.ts index b88134e4c..8ed92083d 100644 --- a/src/app/shared/components/file-select-destination/file-select-destination.component.ts +++ b/src/app/shared/components/file-select-destination/file-select-destination.component.ts @@ -23,7 +23,6 @@ import { } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { ENVIRONMENT } from '@core/provider/environment.provider'; import { FileProvider } from '@osf/features/files/constants'; import { FilesSelectors, @@ -34,6 +33,7 @@ import { SetMoveDialogCurrentFolder, } from '@osf/features/files/store'; import { SupportedFeature } from '@osf/shared/enums/addon-supported-features.enum'; +import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { UserPermissions } from '@osf/shared/enums/user-permissions.enum'; import { Primitive } from '@osf/shared/helpers/types.helper'; import { ConfiguredAddonModel } from '@osf/shared/models/addons/configured-addon.model'; @@ -58,7 +58,6 @@ export class FileSelectDestinationComponent implements OnInit { selectProject = output(); selectStorage = output(); - private readonly environment = inject(ENVIRONMENT); private readonly destroyRef = inject(DestroyRef); readonly rootFolders = select(FilesSelectors.getMoveDialogRootFolders); @@ -149,13 +148,9 @@ export class FileSelectDestinationComponent implements OnInit { } private getStorageAddons(projectId: string) { - const resourcePath = 'nodes'; - const folderLink = `${this.environment.apiDomainUrl}/v2/${resourcePath}/${projectId}/files/`; - const iriLink = `${this.environment.webUrl}/${projectId}`; - forkJoin({ - rootFolders: this.actions.getRootFolders(folderLink), - addons: this.actions.getConfiguredStorageAddons(iriLink), + rootFolders: this.actions.getRootFolders(projectId, ResourceType.Project), + addons: this.actions.getConfiguredStorageAddons(projectId), }) .pipe( takeUntilDestroyed(this.destroyRef), diff --git a/src/app/shared/components/subjects/subjects.component.ts b/src/app/shared/components/subjects/subjects.component.ts index 966742a8b..f201d0fbe 100644 --- a/src/app/shared/components/subjects/subjects.component.ts +++ b/src/app/shared/components/subjects/subjects.component.ts @@ -11,7 +11,8 @@ import { Tree, TreeModule } from 'primeng/tree'; import { debounceTime, distinctUntilChanged } from 'rxjs'; -import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, DestroyRef, inject, input, output } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormControl, FormsModule } from '@angular/forms'; import { SubjectModel } from '@osf/shared/models/subject/subject.model'; @@ -27,11 +28,14 @@ import { SearchInputComponent } from '../search-input/search-input.component'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class SubjectsComponent { + readonly destroyRef = inject(DestroyRef); + subjects = select(SubjectsSelectors.getSubjects); subjectsLoading = select(SubjectsSelectors.getSubjectsLoading); searchedSubjects = select(SubjectsSelectors.getSearchedSubjects); - areSubjectsUpdating = input(false); isSearching = select(SubjectsSelectors.getSearchedSubjectsLoading); + + areSubjectsUpdating = input(false); selected = input([]); readonly = input(false); searchChanged = output(); @@ -51,9 +55,11 @@ export class SubjectsComponent { searchControl = new FormControl(''); constructor() { - this.searchControl.valueChanges.pipe(debounceTime(300), distinctUntilChanged()).subscribe((value) => { - this.searchChanged.emit(value ?? ''); - }); + this.searchControl.valueChanges + .pipe(debounceTime(300), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)) + .subscribe((value) => { + this.searchChanged.emit(value ?? ''); + }); } loadNode(event: TreeNode) { diff --git a/src/app/shared/models/files/file-page-link.model.ts b/src/app/shared/models/files/file-page-link.model.ts new file mode 100644 index 000000000..a5b6ed0f5 --- /dev/null +++ b/src/app/shared/models/files/file-page-link.model.ts @@ -0,0 +1,4 @@ +export interface FilePageLinkModel { + link: string; + page: number; +} diff --git a/src/app/shared/models/files/renamed-file-link.model.ts b/src/app/shared/models/files/renamed-file-link.model.ts new file mode 100644 index 000000000..81876ff0e --- /dev/null +++ b/src/app/shared/models/files/renamed-file-link.model.ts @@ -0,0 +1,4 @@ +export interface RenamedFileLinkModel { + newName: string; + link: string; +} diff --git a/src/app/shared/services/files.service.ts b/src/app/shared/services/files.service.ts index ad78cb023..d08367a77 100644 --- a/src/app/shared/services/files.service.ts +++ b/src/app/shared/services/files.service.ts @@ -19,6 +19,7 @@ import { import { PaginatedData } from '@osf/shared/models/paginated-data.model'; import { FileKind } from '../enums/file-kind.enum'; +import { ResourceType } from '../enums/resource-type.enum'; import { AddonMapper } from '../mappers/addon.mapper'; import { ContributorsMapper } from '../mappers/contributors'; import { FilesMapper } from '../mappers/files/files.mapper'; @@ -60,7 +61,13 @@ export class FilesService { return this.environment.addonsApiUrl; } - filesFields = 'name,guid,kind,extra,size,path,materialized_path,date_modified,parent_folder,files'; + private readonly filesFields = 'name,guid,kind,extra,size,path,materialized_path,date_modified,parent_folder,files'; + + private readonly resourcePathMap: Record = { + [ResourceType.Project]: 'nodes', + [ResourceType.Registration]: 'registrations', + [ResourceType.Preprint]: 'preprints', + }; getFiles( filesLink: string, @@ -86,10 +93,17 @@ export class FilesService { .pipe(map((response) => ({ files: FilesMapper.getFileFolders(response.data), meta: response.meta }))); } + getRootFolders( + resourceId: string, + resourceType: ResourceType + ): Observable<{ files: FileFolderModel[]; meta?: MetaJsonApi }> { + const resourcePath = this.resourcePathMap[resourceType]; + return this.getFolders(`${this.apiUrl}/${resourcePath}/${resourceId}/files/`); + } + getFilesWithoutFiltering(filesLink: string, page = 1): Observable> { - const params: Record = { - page: page.toString(), - }; + const params: Record = { page: page.toString() }; + return this.jsonApiService.get(filesLink, params).pipe( map((response) => ({ data: FilesMapper.getFiles(response.data), @@ -169,9 +183,7 @@ export class FilesService { } getFileGuid(id: string): Observable { - const params = { - create_guid: 'true', - }; + const params = { create_guid: 'true' }; return this.jsonApiService .get(`${this.apiUrl}/files/${id}/`, params) @@ -278,9 +290,7 @@ export class FilesService { } getResourceReferences(resourceUri: string): Observable { - const params = { - 'filter[resource_uri]': resourceUri, - }; + const params = { 'filter[resource_uri]': resourceUri }; return this.jsonApiService .get< @@ -289,7 +299,9 @@ export class FilesService { .pipe(map((response) => response.data?.[0]?.links?.self ?? '')); } - getConfiguredStorageAddons(resourceUri: string): Observable { + getConfiguredStorageAddons(resourceId: string): Observable { + const resourceUri = `${this.environment.webUrl}/${resourceId}`; + return this.getResourceReferences(resourceUri).pipe( switchMap((referenceUrl: string) => { if (!referenceUrl) return of([]); From f2dd1e45794cf552a17b1055890282a74de48486 Mon Sep 17 00:00:00 2001 From: nsemets Date: Fri, 1 May 2026 16:32:42 +0300 Subject: [PATCH 03/13] refactor(file-share-embed): created service for share and embed --- src/app/features/files/constants/index.ts | 1 - .../file-detail/file-detail.component.ts | 2 +- .../constants/file-embed.constants.ts} | 0 .../models/files/file-share-link.model.ts | 4 ++ .../services/files-share-embed.service.ts | 65 +++++++++++++++++++ 5 files changed, 70 insertions(+), 2 deletions(-) rename src/app/{features/files/constants/embed-content.constants.ts => shared/constants/file-embed.constants.ts} (100%) create mode 100644 src/app/shared/models/files/file-share-link.model.ts create mode 100644 src/app/shared/services/files-share-embed.service.ts diff --git a/src/app/features/files/constants/index.ts b/src/app/features/files/constants/index.ts index af9827638..bed8ae58c 100644 --- a/src/app/features/files/constants/index.ts +++ b/src/app/features/files/constants/index.ts @@ -1,4 +1,3 @@ -export * from './embed-content.constants'; export * from './file-browser-info.constants'; export * from './file-metadata-fields.constants'; export * from './file-provider.constants'; diff --git a/src/app/features/files/pages/file-detail/file-detail.component.ts b/src/app/features/files/pages/file-detail/file-detail.component.ts index 37df9219e..2e03d601c 100644 --- a/src/app/features/files/pages/file-detail/file-detail.component.ts +++ b/src/app/features/files/pages/file-detail/file-detail.component.ts @@ -51,6 +51,7 @@ import { MetaTagsBuilderService } from '@osf/shared/services/meta-tags-builder.s import { SignpostingService } from '@osf/shared/services/signposting.service'; import { ToastService } from '@osf/shared/services/toast.service'; import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; +import { embedDynamicJs, embedStaticHtml } from '@shared/constants/file-embed.constants'; import { FileDetailsModel } from '@shared/models/files/file.model'; import { MetadataTabsModel } from '@shared/models/metadata-tabs.model'; @@ -60,7 +61,6 @@ import { FileResourceMetadataComponent, FileRevisionsComponent, } from '../../components'; -import { embedDynamicJs, embedStaticHtml } from '../../constants'; import { FileDetailTab } from '../../enums'; import { DeleteEntry, diff --git a/src/app/features/files/constants/embed-content.constants.ts b/src/app/shared/constants/file-embed.constants.ts similarity index 100% rename from src/app/features/files/constants/embed-content.constants.ts rename to src/app/shared/constants/file-embed.constants.ts diff --git a/src/app/shared/models/files/file-share-link.model.ts b/src/app/shared/models/files/file-share-link.model.ts new file mode 100644 index 000000000..60a018ea6 --- /dev/null +++ b/src/app/shared/models/files/file-share-link.model.ts @@ -0,0 +1,4 @@ +export interface FileShareLink { + link: string; + target: '_self' | '_blank'; +} diff --git a/src/app/shared/services/files-share-embed.service.ts b/src/app/shared/services/files-share-embed.service.ts new file mode 100644 index 000000000..9d1751d47 --- /dev/null +++ b/src/app/shared/services/files-share-embed.service.ts @@ -0,0 +1,65 @@ +import { Clipboard } from '@angular/cdk/clipboard'; +import { inject, Injectable } from '@angular/core'; + +import { ENVIRONMENT } from '@osf/core/provider/environment.provider'; +import { embedDynamicJs, embedStaticHtml } from '@shared/constants/file-embed.constants'; + +import { FileModel } from '../models/files/file.model'; +import { FileShareLink } from '../models/files/file-share-link.model'; + +@Injectable({ + providedIn: 'root', +}) +export class FilesShareEmbedService { + private readonly environment = inject(ENVIRONMENT); + private readonly clipboard = inject(Clipboard); + + private readonly EMBED_PLACEHOLDER = 'ENCODED_URL'; + + getShareLink(file: FileModel, shareType?: string): FileShareLink | null { + const url = file.links?.html; + const name = file.name; + + if (!url || !name) return null; + + const encodedUrl = encodeURIComponent(url); + const encodedName = encodeURIComponent(name); + + switch (shareType) { + case 'email': + return { + link: `mailto:?subject=${encodedName}&body=${encodedUrl}`, + target: '_self', + }; + case 'twitter': + return { + link: `https://x.com/intent/tweet?url=${encodedUrl}&text=${encodedName}&via=OSFramework`, + target: '_blank', + }; + case 'facebook': { + const appId = this.environment.facebookAppId; + return { + link: `https://www.facebook.com/dialog/share?app_id=${appId}&display=popup&href=${encodedUrl}&redirect_uri=${encodedUrl}`, + target: '_blank', + }; + } + default: + return null; + } + } + + getEmbedHtml(file: FileModel, embedType?: string): string { + switch (embedType) { + case 'dynamic': + return embedDynamicJs.replace(this.EMBED_PLACEHOLDER, file.links.render); + case 'static': + return embedStaticHtml.replace(this.EMBED_PLACEHOLDER, file.links.render); + default: + return ''; + } + } + + copyToClipboard(value: string): boolean { + return this.clipboard.copy(value); + } +} From b2b366d9b14c7bdb194770247a073f3f189a8aeb Mon Sep 17 00:00:00 2001 From: nsemets Date: Fri, 1 May 2026 16:35:42 +0300 Subject: [PATCH 04/13] refactor(files): updated interfaces and refactored code --- .../files/mappers/file-menu-actions.mapper.ts | 17 +++ .../files/pages/files/files.component.html | 9 +- .../files/pages/files/files.component.scss | 16 --- .../files/pages/files/files.component.ts | 112 ++++++------------ 4 files changed, 54 insertions(+), 100 deletions(-) create mode 100644 src/app/features/files/mappers/file-menu-actions.mapper.ts diff --git a/src/app/features/files/mappers/file-menu-actions.mapper.ts b/src/app/features/files/mappers/file-menu-actions.mapper.ts new file mode 100644 index 000000000..186a2b39d --- /dev/null +++ b/src/app/features/files/mappers/file-menu-actions.mapper.ts @@ -0,0 +1,17 @@ +import { SupportedFeature } from '@osf/shared/enums/addon-supported-features.enum'; +import { FileMenuType } from '@osf/shared/enums/file-menu-type.enum'; + +export function mapMenuActions(supportedFeatures: SupportedFeature[]): Record { + return { + [FileMenuType.Download]: supportedFeatures.includes(SupportedFeature.DownloadAsZip), + [FileMenuType.Rename]: supportedFeatures.includes(SupportedFeature.AddUpdateFiles), + [FileMenuType.Delete]: supportedFeatures.includes(SupportedFeature.DeleteFiles), + [FileMenuType.Move]: + supportedFeatures.includes(SupportedFeature.CopyInto) && + supportedFeatures.includes(SupportedFeature.DeleteFiles) && + supportedFeatures.includes(SupportedFeature.AddUpdateFiles), + [FileMenuType.Embed]: true, + [FileMenuType.Share]: true, + [FileMenuType.Copy]: true, + }; +} diff --git a/src/app/features/files/pages/files/files.component.html b/src/app/features/files/pages/files/files.component.html index 43d076f6b..0e37f5963 100644 --- a/src/app/features/files/pages/files/files.component.html +++ b/src/app/features/files/pages/files/files.component.html @@ -17,14 +17,7 @@ [(ngModel)]="currentRootFolder" (onChange)="handleRootFolderChange($event.value)" variant="filled" - > - -

{{ selectedOption.label }}

-
- -

{{ option.label }}

-
- + /> }
@if (filesSelection.length) { diff --git a/src/app/features/files/pages/files/files.component.scss b/src/app/features/files/pages/files/files.component.scss index c9be697b1..fd15c2680 100644 --- a/src/app/features/files/pages/files/files.component.scss +++ b/src/app/features/files/pages/files/files.component.scss @@ -5,19 +5,3 @@ flex: 1; overflow: hidden; } - -.blue-text { - color: var(--pr-blue-1); -} - -.filename { - overflow-wrap: anywhere; -} - -.upload-dialog { - width: mix.rem(128px); -} - -.provider-name { - text-transform: capitalize; -} diff --git a/src/app/features/files/pages/files/files.component.ts b/src/app/features/files/pages/files/files.component.ts index bd3f57497..59e4760a9 100644 --- a/src/app/features/files/pages/files/files.component.ts +++ b/src/app/features/files/pages/files/files.component.ts @@ -39,22 +39,6 @@ import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; -import { ENVIRONMENT } from '@core/provider/environment.provider'; -import { - CreateFolder, - DeleteEntry, - GetConfiguredStorageAddons, - GetFiles, - GetRootFolders, - GetStorageSupportedFeatures, - RenameEntry, - ResetFilesState, - SetCurrentProvider, - SetFilesCurrentFolder, - SetMoveDialogCurrentFolder, - SetSearch, - SetSort, -} from '@osf/features/files/store'; import { FileUploadDialogComponent } from '@osf/shared/components/file-upload-dialog/file-upload-dialog.component'; import { FilesTreeComponent } from '@osf/shared/components/files-tree/files-tree.component'; import { FormSelectComponent } from '@osf/shared/components/form-select/form-select.component'; @@ -68,7 +52,8 @@ import { ALL_SORT_OPTIONS } from '@osf/shared/constants/sort-options.const'; import { SupportedFeature } from '@osf/shared/enums/addon-supported-features.enum'; import { FileMenuType } from '@osf/shared/enums/file-menu-type.enum'; import { ResourceType } from '@osf/shared/enums/resource-type.enum'; -import { UserPermissions } from '@osf/shared/enums/user-permissions.enum'; +import { FilePageLinkModel } from '@osf/shared/models/files/file-page-link.model'; +import { RenamedFileLinkModel } from '@osf/shared/models/files/renamed-file-link.model'; import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { FilesService } from '@osf/shared/services/files.service'; @@ -82,14 +67,28 @@ import { FileFolderModel } from '@shared/models/files/file-folder.model'; import { FileLabelModel } from '@shared/models/files/file-label.model'; import { DataciteService } from '@shared/services/datacite/datacite.service'; -import { - CreateFolderDialogComponent, - FileBrowserInfoComponent, - FilesSelectionActionsComponent, - MoveFileDialogComponent, -} from '../../components'; +import { CreateFolderDialogComponent } from '../../components/create-folder-dialog/create-folder-dialog.component'; +import { FileBrowserInfoComponent } from '../../components/file-browser-info/file-browser-info.component'; +import { FilesSelectionActionsComponent } from '../../components/files-selection-actions/files-selection-actions.component'; +import { MoveFileDialogComponent } from '../../components/move-file-dialog/move-file-dialog.component'; import { FileProvider } from '../../constants'; -import { FilesSelectors } from '../../store'; +import { mapMenuActions } from '../../mappers/file-menu-actions.mapper'; +import { + CreateFolder, + DeleteEntry, + FilesSelectors, + GetConfiguredStorageAddons, + GetFiles, + GetRootFolders, + GetStorageSupportedFeatures, + RenameEntry, + ResetFilesState, + SetCurrentProvider, + SetFilesCurrentFolder, + SetMoveDialogCurrentFolder, + SetSearch, + SetSort, +} from '../../store'; @Component({ selector: 'osf-files', @@ -107,7 +106,6 @@ import { FilesSelectors } from '../../store'; SubHeaderComponent, FileUploadDialogComponent, ViewOnlyLinkMessageComponent, - GoogleFilePickerComponent, FilesSelectionActionsComponent, TranslatePipe, ], @@ -128,15 +126,10 @@ export class FilesComponent { private readonly translateService = inject(TranslateService); private readonly router = inject(Router); private readonly dataciteService = inject(DataciteService); - private readonly environment = inject(ENVIRONMENT); private readonly customConfirmationService = inject(CustomConfirmationService); private readonly toastService = inject(ToastService); private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); - private readonly platformId = inject(PLATFORM_ID); - private readonly isBrowser = isPlatformBrowser(this.platformId); - - private readonly webUrl = this.environment.webUrl; - private readonly apiDomainUrl = this.environment.apiDomainUrl; + private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); private readonly actions = createDispatchMap({ createFolder: CreateFolder, @@ -167,6 +160,8 @@ export class FilesComponent { readonly configuredStorageAddons = select(FilesSelectors.getConfiguredStorageAddons); readonly isConfiguredStorageAddonsLoading = select(FilesSelectors.isConfiguredStorageAddonsLoading); readonly supportedFeatures = select(FilesSelectors.getStorageSupportedFeatures); + readonly hasWriteAccess = select(CurrentResourceSelectors.hasResourceWriteAccess); + readonly hasAdminAccess = select(CurrentResourceSelectors.hasResourceAdminAccess); readonly isGoogleDrive = signal(false); readonly accountId = signal(''); @@ -193,17 +188,12 @@ export class FilesComponent { allowRevisions = false; filesSelection: FileModel[] = []; - private readonly urlMap = new Map([ - [ResourceType.Project, 'nodes'], - [ResourceType.Registration, 'registrations'], - ]); - readonly allowedMenuActions = computed(() => { const provider = this.provider(); const supportedFeatures = this.supportedFeatures()[provider] || []; const hasViewOnly = this.hasViewOnly(); const isRegistration = this.resourceType() === ResourceType.Registration; - const menuMap = this.mapMenuActions(supportedFeatures); + const menuMap = mapMenuActions(supportedFeatures); const result: Record = { ...menuMap }; @@ -235,16 +225,7 @@ export class FilesComponent { ); readonly hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router)); - - readonly canEdit = computed(() => { - const details = this.resourceDetails(); - const hasAdminOrWrite = details.currentUserPermissions?.some( - (permission) => permission === UserPermissions.Admin || permission === UserPermissions.Write - ); - - return hasAdminOrWrite; - }); - + readonly canEdit = computed(() => this.hasWriteAccess() || this.hasAdminAccess()); readonly isRegistration = computed(() => this.resourceType() === ResourceType.Registration); canUploadFiles = computed( @@ -260,9 +241,8 @@ export class FilesComponent { () => this.isButtonDisabled() || (this.googleFilePickerComponent()?.isGFPDisabled() ?? false) ); - private route = inject(ActivatedRoute); readonly providerName = toSignal( - this.route?.params?.pipe(map((params) => params['fileProvider'])) ?? of('osfstorage') + this.activeRoute?.params?.pipe(map((params) => params['fileProvider'])) ?? of('osfstorage') ); constructor() { @@ -274,14 +254,11 @@ export class FilesComponent { effect(() => { const resourceId = this.resourceId(); + if (!resourceId) return; - const resourcePath = this.urlMap.get(this.resourceType()!); - const folderLink = `${this.apiDomainUrl}/v2/${resourcePath}/${resourceId}/files/`; - const iriLink = `${this.webUrl}/${resourceId}`; - - this.actions.getResourceDetails(resourceId, this.resourceType()!); - this.actions.getRootFolders(folderLink); - this.actions.getConfiguredStorageAddons(iriLink); + this.actions.getResourceDetails(resourceId, this.resourceType()); + this.actions.getRootFolders(resourceId, this.resourceType()); + this.actions.getConfiguredStorageAddons(resourceId); }); effect(() => { @@ -358,7 +335,7 @@ export class FilesComponent { }); } - onLoadFiles(event: { link: string; page: number }) { + onLoadFiles(event: FilePageLinkModel) { this.actions.getFiles(event.link, event.page); } @@ -427,9 +404,7 @@ export class FilesComponent { this.customConfirmationService.confirmDelete({ headerKey: conflictFiles.length > 1 ? 'files.dialogs.replaceFile.multiple' : 'files.dialogs.replaceFile.single', messageKey: 'files.dialogs.replaceFile.message', - messageParams: { - name: conflictFiles.map((c) => c.file.name).join(', '), - }, + messageParams: { name: conflictFiles.map((c) => c.file.name).join(', ') }, acceptLabelKey: 'common.buttons.replace', onConfirm: () => { const replaceRequests$ = conflictFiles.map(({ file, link }) => @@ -626,7 +601,7 @@ export class FilesComponent { }); } - renameEntry(event: { newName: string; link: string }) { + renameEntry(event: RenamedFileLinkModel) { const { newName, link } = event; this.actions.renameEntry(link, newName).subscribe(() => { this.toastService.showSuccess('files.dialogs.renameFile.success'); @@ -663,21 +638,6 @@ export class FilesComponent { } } - private mapMenuActions(supportedFeatures: SupportedFeature[]): Record { - return { - [FileMenuType.Download]: supportedFeatures.includes(SupportedFeature.DownloadAsZip), - [FileMenuType.Rename]: supportedFeatures.includes(SupportedFeature.AddUpdateFiles), - [FileMenuType.Delete]: supportedFeatures.includes(SupportedFeature.DeleteFiles), - [FileMenuType.Move]: - supportedFeatures.includes(SupportedFeature.CopyInto) && - supportedFeatures.includes(SupportedFeature.DeleteFiles) && - supportedFeatures.includes(SupportedFeature.AddUpdateFiles), - [FileMenuType.Embed]: true, - [FileMenuType.Share]: true, - [FileMenuType.Copy]: true, - }; - } - openGoogleFilePicker(): void { this.googleFilePickerComponent()?.createPicker(); this.updateFilesList(); From 0f64c52b168fcf0cc980e099f1bd23a3b18578fd Mon Sep 17 00:00:00 2001 From: nsemets Date: Fri, 1 May 2026 16:52:56 +0300 Subject: [PATCH 05/13] refactor(files-tree): added shared service and drop zone --- .../files-drop-zone.component.html | 20 ++ .../files-drop-zone.component.scss | 27 +++ .../files-drop-zone.component.ts | 73 +++++++ .../files-tree/files-tree.component.html | 30 +-- .../files-tree/files-tree.component.scss | 24 -- .../files-tree/files-tree.component.ts | 205 +++++------------- src/assets/i18n/en.json | 2 +- 7 files changed, 188 insertions(+), 193 deletions(-) create mode 100644 src/app/shared/components/files-drop-zone/files-drop-zone.component.html create mode 100644 src/app/shared/components/files-drop-zone/files-drop-zone.component.scss create mode 100644 src/app/shared/components/files-drop-zone/files-drop-zone.component.ts diff --git a/src/app/shared/components/files-drop-zone/files-drop-zone.component.html b/src/app/shared/components/files-drop-zone/files-drop-zone.component.html new file mode 100644 index 000000000..a8bc8dc2c --- /dev/null +++ b/src/app/shared/components/files-drop-zone/files-drop-zone.component.html @@ -0,0 +1,20 @@ +
+ @if (enabled()) { +
+ @if (isDragOver()) { +
+ +

{{ 'files.dropText' | translate }}

+
+ } +
+ } + + +
diff --git a/src/app/shared/components/files-drop-zone/files-drop-zone.component.scss b/src/app/shared/components/files-drop-zone/files-drop-zone.component.scss new file mode 100644 index 000000000..8e5f40b7b --- /dev/null +++ b/src/app/shared/components/files-drop-zone/files-drop-zone.component.scss @@ -0,0 +1,27 @@ +.drop-zone-container { + position: relative; +} + +.drop-zone { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + color: var(--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; + } +} diff --git a/src/app/shared/components/files-drop-zone/files-drop-zone.component.ts b/src/app/shared/components/files-drop-zone/files-drop-zone.component.ts new file mode 100644 index 000000000..fb8f1e264 --- /dev/null +++ b/src/app/shared/components/files-drop-zone/files-drop-zone.component.ts @@ -0,0 +1,73 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { ChangeDetectionStrategy, Component, input, output, signal } from '@angular/core'; + +@Component({ + selector: 'osf-files-drop-zone', + imports: [TranslatePipe], + templateUrl: './files-drop-zone.component.html', + styleUrl: './files-drop-zone.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FilesDropZoneComponent { + enabled = input(true); + filesDropped = output(); + isDragOver = signal(false); + + private dragDepth = 0; + + onDragEnter(event: DragEvent): void { + if (!this.enabled()) { + return; + } + + if (event.dataTransfer?.types?.includes('Files')) { + this.dragDepth += 1; + this.isDragOver.set(true); + } + } + + onDragOver(event: DragEvent): void { + if (!this.enabled()) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'copy'; + } + this.isDragOver.set(true); + } + + onDragLeave(event: Event): void { + if (!this.enabled()) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + this.dragDepth = Math.max(0, this.dragDepth - 1); + if (this.dragDepth === 0) { + this.isDragOver.set(false); + } + } + + onDrop(event: DragEvent): void { + event.preventDefault(); + event.stopPropagation(); + this.dragDepth = 0; + this.isDragOver.set(false); + + if (!this.enabled()) { + return; + } + + const files = event.dataTransfer?.files; + if (!files || files.length === 0) { + return; + } + + this.filesDropped.emit(Array.from(files)); + } +} 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 fa2954b12..30b87b35f 100644 --- a/src/app/shared/components/files-tree/files-tree.component.html +++ b/src/app/shared/components/files-tree/files-tree.component.html @@ -1,21 +1,8 @@ -
- @if (!hasViewOnly() && supportUpload()) { -
- @if (isDragOver()) { -
- -

{{ 'files.dropText' | translate }}

-
- } -
- } - + @if (isLoading() && !isLoadingMore()) {
@@ -30,7 +17,6 @@ [lazy]="true" [loading]="isLoadingMore()" [selectionMode]="selectionMode()" - [selection]="selectedNodes()" [scrollHeight]="scrollHeight()" [value]="nodes()" [virtualScroll]="true" @@ -84,8 +70,8 @@ {{ file.dateModified | date: 'MMM d, y hh:mm a' }}
- @if (isSomeFileActionAllowed && !selectedFiles().length) { -
+ @if (canShowMenu()) { +
{{ 'files.emptyState' | 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 3984d465f..aa5a43a34 100644 --- a/src/app/shared/components/files-tree/files-tree.component.scss +++ b/src/app/shared/components/files-tree/files-tree.component.scss @@ -62,27 +62,3 @@ 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: var(--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; - } -} 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 f08df941e..e97cbccd3 100644 --- a/src/app/shared/components/files-tree/files-tree.component.ts +++ b/src/app/shared/components/files-tree/files-tree.component.ts @@ -5,51 +5,50 @@ import { TranslatePipe } from '@ngx-translate/core'; import { PrimeTemplate, TreeNode } from 'primeng/api'; import { Tree, TreeLazyLoadEvent, TreeNodeDropEvent, TreeNodeSelectEvent } from 'primeng/tree'; -import { Clipboard } from '@angular/cdk/clipboard'; +import { filter } from 'rxjs'; + import { DatePipe, isPlatformBrowser } from '@angular/common'; import { - AfterViewInit, ChangeDetectionStrategy, Component, computed, DestroyRef, effect, - ElementRef, HostBinding, inject, input, - OnDestroy, output, PLATFORM_ID, signal, - viewChild, } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { ActivatedRoute, Router } from '@angular/router'; +import { Router } from '@angular/router'; -import { ENVIRONMENT } from '@core/provider/environment.provider'; import { ConfirmMoveFileDialogComponent } from '@osf/features/files/components/confirm-move-file-dialog/confirm-move-file-dialog.component'; import { MoveFileDialogComponent } from '@osf/features/files/components/move-file-dialog/move-file-dialog.component'; import { RenameFileDialogComponent } from '@osf/features/files/components/rename-file-dialog/rename-file-dialog.component'; -import { embedDynamicJs, embedStaticHtml } from '@osf/features/files/constants'; import { StopPropagationDirective } from '@osf/shared/directives/stop-propagation.directive'; import { FileKind } from '@osf/shared/enums/file-kind.enum'; import { FileMenuType } from '@osf/shared/enums/file-menu-type.enum'; import { FilesMapper } from '@osf/shared/mappers/files/files.mapper'; +import { FilePageLinkModel } from '@osf/shared/models/files/file-page-link.model'; +import { RenamedFileLinkModel } from '@osf/shared/models/files/renamed-file-link.model'; import { FileSizePipe } from '@osf/shared/pipes/file-size.pipe'; import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { DataciteService } from '@osf/shared/services/datacite/datacite.service'; import { FilesService } from '@osf/shared/services/files.service'; +import { FilesShareEmbedService } from '@osf/shared/services/files-share-embed.service'; import { ToastService } from '@osf/shared/services/toast.service'; import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; +import { CurrentResourceSelectors } from '@osf/shared/stores/current-resource'; import { FileModel } from '@shared/models/files/file.model'; import { FileFolderModel } from '@shared/models/files/file-folder.model'; import { FileLabelModel } from '@shared/models/files/file-label.model'; import { FileMenuAction, FileMenuFlags } from '@shared/models/files/file-menu-action.model'; -import { CurrentResourceSelectors } from '@shared/stores/current-resource'; import { FileMenuComponent } from '../file-menu/file-menu.component'; +import { FilesDropZoneComponent } from '../files-drop-zone/files-drop-zone.component'; import { LoadingSpinnerComponent } from '../loading-spinner/loading-spinner.component'; // [NS] Temporary fix @@ -64,6 +63,7 @@ type FileTreeNode = FileModel & TreeNode; TranslatePipe, Tree, LoadingSpinnerComponent, + FilesDropZoneComponent, FileMenuComponent, StopPropagationDirective, ], @@ -71,22 +71,19 @@ type FileTreeNode = FileModel & TreeNode; styleUrl: './files-tree.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class FilesTreeComponent implements OnDestroy, AfterViewInit { +export class FilesTreeComponent { @HostBinding('class') classes = 'relative'; - private dropZoneContainerRef = viewChild('dropZoneContainer'); readonly filesService = inject(FilesService); readonly router = inject(Router); readonly toastService = inject(ToastService); - readonly route = inject(ActivatedRoute); readonly customConfirmationService = inject(CustomConfirmationService); readonly customDialogService = inject(CustomDialogService); readonly dataciteService = inject(DataciteService); + readonly filesShareEmbedService = inject(FilesShareEmbedService); private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); private readonly destroyRef = inject(DestroyRef); - private readonly environment = inject(ENVIRONMENT); - private readonly platformId = inject(PLATFORM_ID); - readonly clipboard = inject(Clipboard); + private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); files = input.required(); totalCount = input(0); @@ -107,8 +104,8 @@ export class FilesTreeComponent implements OnDestroy, AfterViewInit { setCurrentFolder = output(); setMoveDialogCurrentFolder = output(); deleteEntryAction = output(); - renameEntryAction = output<{ newName: string; link: string }>(); - loadFiles = output<{ link: string; page: number }>(); + renameEntryAction = output(); + loadFiles = output(); selectFile = output(); unselectFile = output(); clearSelection = output(); @@ -122,34 +119,20 @@ export class FilesTreeComponent implements OnDestroy, AfterViewInit { itemsPerPage = 10; virtualScrollItemSize = 46; - isDragOver = signal(false); isLoadingMore = signal(false); hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router) || this.viewOnly()); - visibleFilesCount = computed((): number => { - const height = parseInt(this.scrollHeight(), 10); - return Math.ceil(height / this.virtualScrollItemSize); - }); - - get isSomeFileActionAllowed(): boolean { - return Object.keys(this.allowedMenuActions()).length > 0; - } + canShowMenu = computed(() => Object.keys(this.allowedMenuActions()).length > 0 && !this.selectedFiles().length); readonly nodes = computed(() => { const currentFolder = this.currentFolder(); const files = this.files(); - const hasParent = this.foldersStack.length > 0; - if (hasParent) { - return [ - { - ...currentFolder, - previousFolder: hasParent, - }, - ...files, - ] as FileModel[]; - } else { - return [...files]; + + if (this.foldersStack.length === 0) { + return files; } + + return [{ ...currentFolder, previousFolder: true }, ...files] as FileModel[]; }); // [NS] Temporary fix @@ -171,66 +154,20 @@ export class FilesTreeComponent implements OnDestroy, AfterViewInit { }); } - ngAfterViewInit(): void { - if (!this.viewOnly()) { - this.dropZoneContainerRef()?.nativeElement?.addEventListener('dragenter', this.dragEnterHandler); - } - } - - ngOnDestroy(): void { - if (this.dropZoneContainerRef()?.nativeElement) { - this.dropZoneContainerRef()!.nativeElement.removeEventListener('dragenter', this.dragEnterHandler); - } - } - - private dragEnterHandler = (event: DragEvent) => { - if (event.dataTransfer?.types?.includes('Files') && !this.viewOnly()) { - this.isDragOver.set(true); - } - }; - - onDragOver(event: DragEvent) { - if (this.viewOnly()) { - return; - } - event.preventDefault(); - event.stopPropagation(); - event.dataTransfer!.dropEffect = 'copy'; - this.isDragOver.set(true); - } - - onDragLeave(event: Event) { - if (this.viewOnly()) { - return; - } - event.preventDefault(); - event.stopPropagation(); - this.isDragOver.set(false); - } - - onDrop(event: DragEvent) { - event.preventDefault(); - event.stopPropagation(); - this.isDragOver.set(false); - - if (this.viewOnly()) { + onDropFiles(fileArray: File[]): void { + if (!fileArray.length) { return; } - const files = event.dataTransfer?.files; - - if (files && files.length > 0) { - const fileArray = Array.from(files); - const isMultiple = files.length > 1; + const isMultiple = fileArray.length > 1; - this.customConfirmationService.confirmAccept({ - headerKey: isMultiple ? 'files.dialogs.uploadFiles.title' : 'files.dialogs.uploadFile.title', - messageParams: isMultiple ? { count: files.length } : { name: files[0].name }, - messageKey: isMultiple ? 'files.dialogs.uploadFiles.message' : 'files.dialogs.uploadFile.message', - acceptLabelKey: 'common.buttons.upload', - onConfirm: () => this.uploadFilesConfirmed.emit(fileArray), - }); - } + this.customConfirmationService.confirmAccept({ + headerKey: isMultiple ? 'files.dialogs.uploadFiles.title' : 'files.dialogs.uploadFile.title', + messageParams: isMultiple ? { count: fileArray.length } : { name: fileArray[0].name }, + messageKey: isMultiple ? 'files.dialogs.uploadFiles.message' : 'files.dialogs.uploadFile.message', + acceptLabelKey: 'common.buttons.upload', + onConfirm: () => this.uploadFilesConfirmed.emit(fileArray), + }); } openEntry(event: Event, file: FileModel | FileFolderModel) { @@ -294,46 +231,44 @@ export class FilesTreeComponent implements OnDestroy, AfterViewInit { downloadFileOrFolder(file: FileModel) { const resourceType = this.resourceMetadata()?.type ?? 'nodes'; + this.dataciteService .logFileDownload(this.resourceId(), resourceType) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(); + if (file.kind === FileKind.File) { this.downloadFile(file.links.download); } else { - const folder = FilesMapper.mapFileToFolder(file as FileModel); - this.downloadFolder(folder.links.download); + this.downloadFolder(file.links.upload); } } private handleShareAction(file: FileModel, shareType?: string): void { - const emailLink = `mailto:?subject=${file.name}&body=${file.links.html}`; - const twitterLink = `https://twitter.com/intent/tweet?url=${file.links.html}&text=${file.name}&via=OSFramework`; - const facebookLink = `https://www.facebook.com/dialog/share?app_id=${this.environment.facebookAppId}&display=popup&href=${file.links.html}&redirect_uri=${file.links.html}`; + const shareAction = this.filesShareEmbedService.getShareLink(file, shareType); + if (!shareAction || !this.isBrowser) { + return; + } - switch (shareType) { - case 'email': - this.openLink(emailLink); - break; - case 'twitter': - this.openLinkNewTab(twitterLink); - break; - case 'facebook': - this.openLinkNewTab(facebookLink); - break; + if (shareAction.target === '_self') { + window.location.href = shareAction.link; + return; } + + window.open(shareAction.link, shareAction.target, 'noopener,noreferrer'); } private handleEmbedAction(file: FileModel, embedType?: string): void { - let embedHtml = ''; - if (embedType === 'dynamic') { - embedHtml = embedDynamicJs.replace('ENCODED_URL', file.links.render); - } else if (embedType === 'static') { - embedHtml = embedStaticHtml.replace('ENCODED_URL', file.links.render); + const embedHtml = this.filesShareEmbedService.getEmbedHtml(file, embedType); + + if (!embedHtml) { + return; } - if (embedHtml) { - this.copyToClipboard(embedHtml); + const copied = this.filesShareEmbedService.copyToClipboard(embedHtml); + + if (copied) { + this.toastService.showSuccess('files.detail.toast.copiedToClipboard'); } } @@ -357,15 +292,13 @@ export class FilesTreeComponent implements OnDestroy, AfterViewInit { .open(RenameFileDialogComponent, { header: 'files.dialogs.renameFile.title', width: '448px', - data: { - currentName: file.name, - }, + data: { currentName: file.name }, }) - .onClose.subscribe((newName: string) => { - if (newName) { - this.renameEntry(newName, file); - } - }); + .onClose.pipe( + takeUntilDestroyed(this.destroyRef), + filter((newName: string) => !!newName) + ) + .subscribe((newName) => this.renameEntry(newName, file)); } renameEntry(newName: string, file: FileModel): void { @@ -376,25 +309,13 @@ export class FilesTreeComponent implements OnDestroy, AfterViewInit { } downloadFile(link: string): void { - if (isPlatformBrowser(this.platformId)) { + if (this.isBrowser) { window.open(link)?.focus(); } } - openLink(link: string): void { - if (isPlatformBrowser(this.platformId)) { - window.location.href = link; - } - } - - openLinkNewTab(link: string): void { - if (isPlatformBrowser(this.platformId)) { - window.open(link, '_blank', 'noopener,noreferrer'); - } - } - downloadFolder(downloadLink: string): void { - if (isPlatformBrowser(this.platformId) && downloadLink) { + if (downloadLink) { const link = this.filesService.getFolderDownloadLink(downloadLink); window.open(link, '_blank')?.focus(); } @@ -420,11 +341,6 @@ export class FilesTreeComponent implements OnDestroy, AfterViewInit { }); } - copyToClipboard(embedHtml: string): void { - this.clipboard.copy(embedHtml); - this.toastService.showSuccess('files.detail.toast.copiedToClipboard'); - } - private loadNextPage(): void { const total = this.totalCount(); const loaded = this.files().length; @@ -432,10 +348,7 @@ export class FilesTreeComponent implements OnDestroy, AfterViewInit { if (!this.isLoadingMore() && loaded < total) { this.isLoadingMore.set(true); - this.loadFiles.emit({ - link: this.currentFolder()?.links.filesLink ?? '', - page: nextPage, - }); + this.loadFiles.emit({ link: this.currentFolder()?.links.filesLink ?? '', page: nextPage }); } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index be31ffa9a..54433cd79 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -527,7 +527,7 @@ }, "descriptions": { "file_updated": { - "instant": "You'll be notified immediately when files are updated.", + "instantly": "You'll be notified immediately when files are updated.", "daily": "You'll receive a daily summary of file updates.", "none": "You won't receive file update notifications." } From 53a83ce20bee5b51baa297f126252d56d92df0ba Mon Sep 17 00:00:00 2001 From: nsemets Date: Mon, 4 May 2026 14:28:48 +0300 Subject: [PATCH 06/13] refactor(files-tree-row): added files tree row for clean structure --- .../files-tree-row.component.html | 54 ++++++++ .../files-tree-row.component.scss | 30 ++++ .../files-tree-row.component.spec.ts | 130 ++++++++++++++++++ .../files-tree-row.component.ts | 45 ++++++ src/styles/overrides/tree.scss | 15 +- 5 files changed, 272 insertions(+), 2 deletions(-) create mode 100644 src/app/shared/components/files-tree-row/files-tree-row.component.html create mode 100644 src/app/shared/components/files-tree-row/files-tree-row.component.scss create mode 100644 src/app/shared/components/files-tree-row/files-tree-row.component.spec.ts create mode 100644 src/app/shared/components/files-tree-row/files-tree-row.component.ts diff --git a/src/app/shared/components/files-tree-row/files-tree-row.component.html b/src/app/shared/components/files-tree-row/files-tree-row.component.html new file mode 100644 index 000000000..f08cab3ac --- /dev/null +++ b/src/app/shared/components/files-tree-row/files-tree-row.component.html @@ -0,0 +1,54 @@ +@if (file().previousFolder) { +
+ + + + {{ file().name }} + +
+} @else { +
+
+ +
+ +
+ @if (downloadsCount()) { + {{ downloadsCount() }} {{ 'common.labels.downloads' | translate }} + } +
+ +
+ {{ file().size | fileSize }} +
+ +
+ {{ file().dateModified | date: 'MMM d, y hh:mm a' }} +
+ + @if (showMenu()) { +
+ + +
+ } +
+} diff --git a/src/app/shared/components/files-tree-row/files-tree-row.component.scss b/src/app/shared/components/files-tree-row/files-tree-row.component.scss new file mode 100644 index 000000000..e24a1d7eb --- /dev/null +++ b/src/app/shared/components/files-tree-row/files-tree-row.component.scss @@ -0,0 +1,30 @@ +@use "styles/mixins" as mix; + +.files-table-row { + 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 0.75rem; + cursor: pointer; + + &.previous-folder { + grid-template-columns: auto; + } + + &:hover { + background: var(--bg-blue-3); + } + + &:active { + background: var(--bg-blue-2); + } + + .table-cell { + width: 100%; + height: 100%; + } +} diff --git a/src/app/shared/components/files-tree-row/files-tree-row.component.spec.ts b/src/app/shared/components/files-tree-row/files-tree-row.component.spec.ts new file mode 100644 index 000000000..9d2dc991f --- /dev/null +++ b/src/app/shared/components/files-tree-row/files-tree-row.component.spec.ts @@ -0,0 +1,130 @@ +import { MockComponent } from 'ng-mocks'; + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FileKind } from '@osf/shared/enums/file-kind.enum'; +import { FileMenuType } from '@osf/shared/enums/file-menu-type.enum'; +import { FileModel } from '@shared/models/files/file.model'; +import { FileMenuFlags } from '@shared/models/files/file-menu-action.model'; + +import { provideOSFCore } from '@testing/osf.testing.provider'; + +import { FileMenuComponent } from '../file-menu/file-menu.component'; + +import { FilesTreeRowComponent } from './files-tree-row.component'; + +describe('FilesTreeRowComponent', () => { + let component: FilesTreeRowComponent; + let fixture: ComponentFixture; + + const ALL_MENU_ACTIONS: FileMenuFlags = { + [FileMenuType.Download]: true, + [FileMenuType.Copy]: true, + [FileMenuType.Move]: true, + [FileMenuType.Delete]: true, + [FileMenuType.Rename]: true, + [FileMenuType.Share]: true, + [FileMenuType.Embed]: true, + }; + + function createTestFile(overrides: Partial = {}): FileModel { + return { + id: 'f1', + guid: 'g1', + name: 'test.pdf', + kind: FileKind.File, + path: '/test.pdf', + size: 2048, + materializedPath: '/test.pdf', + dateModified: '2024-06-01T12:00:00.000Z', + extra: { + hashes: { md5: 'm', sha256: 's' }, + downloads: 5, + }, + links: { + info: 'i', + move: 'm', + upload: 'u', + delete: 'd', + download: 'dl', + render: 'r', + html: 'h', + self: 's', + }, + filesLink: null, + previousFolder: false, + provider: 'osfstorage', + ...overrides, + }; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [FilesTreeRowComponent, MockComponent(FileMenuComponent)], + providers: [provideOSFCore()], + }); + + fixture = TestBed.createComponent(FilesTreeRowComponent); + component = fixture.componentInstance; + fixture.componentRef.setInput('file', createTestFile()); + fixture.componentRef.setInput('allowedMenuActions', ALL_MENU_ACTIONS); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should set isFolder when kind is folder', () => { + fixture.componentRef.setInput('file', createTestFile({ kind: FileKind.Folder })); + fixture.detectChanges(); + + expect(component.isFolder()).toBe(true); + }); + + it('should set isFolder false when kind is file', () => { + fixture.componentRef.setInput('file', createTestFile({ kind: FileKind.File })); + fixture.detectChanges(); + + expect(component.isFolder()).toBe(false); + }); + + it('should clear downloadsCount for folder', () => { + fixture.componentRef.setInput( + 'file', + createTestFile({ + kind: FileKind.Folder, + extra: { hashes: { md5: 'm', sha256: 's' }, downloads: 99 }, + }) + ); + fixture.detectChanges(); + + expect(component.downloadsCount()).toBe(''); + }); + + it('should expose downloadsCount for file with downloads', () => { + fixture.componentRef.setInput( + 'file', + createTestFile({ + kind: FileKind.File, + extra: { hashes: { md5: 'm', sha256: 's' }, downloads: 12 }, + }) + ); + fixture.detectChanges(); + + expect(component.downloadsCount()).toBe(12); + }); + + it('should clear downloadsCount when downloads is zero', () => { + fixture.componentRef.setInput( + 'file', + createTestFile({ + kind: FileKind.File, + extra: { hashes: { md5: 'm', sha256: 's' }, downloads: 0 }, + }) + ); + fixture.detectChanges(); + + expect(component.downloadsCount()).toBe(''); + }); +}); diff --git a/src/app/shared/components/files-tree-row/files-tree-row.component.ts b/src/app/shared/components/files-tree-row/files-tree-row.component.ts new file mode 100644 index 000000000..6839b3e99 --- /dev/null +++ b/src/app/shared/components/files-tree-row/files-tree-row.component.ts @@ -0,0 +1,45 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; + +import { DatePipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core'; + +import { StopPropagationDirective } from '@osf/shared/directives/stop-propagation.directive'; +import { FileKind } from '@osf/shared/enums/file-kind.enum'; +import { FileModel } from '@shared/models/files/file.model'; +import { FileMenuAction, FileMenuFlags } from '@shared/models/files/file-menu-action.model'; + +import { FileSizePipe } from '../../pipes/file-size.pipe'; +import { FileMenuComponent } from '../file-menu/file-menu.component'; + +@Component({ + selector: 'osf-files-tree-row', + imports: [Button, DatePipe, FileSizePipe, TranslatePipe, FileMenuComponent, StopPropagationDirective], + templateUrl: './files-tree-row.component.html', + styleUrl: './files-tree-row.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FilesTreeRowComponent { + file = input.required(); + hasFoldersStack = input(false); + showMenu = input(false); + allowedMenuActions = input.required(); + + readonly isFolder = computed(() => this.file().kind === FileKind.Folder); + + readonly downloadsCount = computed(() => { + if (!this.file().extra.downloads || this.isFolder()) { + return ''; + } + return this.file().extra.downloads; + }); + + openParentFolder = output(); + openEntry = output(); + menuAction = output(); + + onOpenEntry(): void { + this.openEntry.emit(this.file()); + } +} diff --git a/src/styles/overrides/tree.scss b/src/styles/overrides/tree.scss index e30a4f1e8..dc7dd38b3 100644 --- a/src/styles/overrides/tree.scss +++ b/src/styles/overrides/tree.scss @@ -2,6 +2,13 @@ .p-tree { padding: 0; + .files-table-row { + .p-button { + --p-button-label-font-weight: 400; + --p-button-link-color: var(--dark-blue-1); + } + } + .p-tree-node-toggle-button { display: none; } @@ -18,7 +25,7 @@ .p-tree-node-dragover { .files-table-row { - background: var(--bg-blue-3); + background-color: var(--bg-blue-3); } } @@ -53,7 +60,11 @@ .p-tree-node-selected { .files-table-row { color: var(--white); - background: var(--pr-blue-1); + background-color: var(--pr-blue-1); + + .p-button { + --p-button-link-color: var(--white); + } .blue-icon { color: var(--white); From 272f6c68b5145f280f62152efeea80bb209134e5 Mon Sep 17 00:00:00 2001 From: nsemets Date: Tue, 5 May 2026 17:53:42 +0300 Subject: [PATCH 07/13] refactor(files-selection): updated selected files input --- .../files-selection-actions.component.html | 4 ++-- .../files-selection-actions.component.ts | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/app/features/files/components/files-selection-actions/files-selection-actions.component.html b/src/app/features/files/components/files-selection-actions/files-selection-actions.component.html index a5c2a5ffc..c29b33428 100644 --- a/src/app/features/files/components/files-selection-actions/files-selection-actions.component.html +++ b/src/app/features/files/components/files-selection-actions/files-selection-actions.component.html @@ -1,8 +1,8 @@ -@if (selectedFiles().length > 0) { +@if (selectedFilesCount() > 0) {
- {{ selectedFiles().length }} {{ 'files.selectedFiles' | translate }} + {{ selectedFilesCount() }} {{ 'files.selectedFiles' | translate }} ([]); + selectedFilesCount = input(0); canUpdateFiles = input(true); hasViewOnly = input(false); copySelected = output(); From 7cde21025ac89c6d4cd49b0091ec089180ede5d0 Mon Sep 17 00:00:00 2001 From: nsemets Date: Tue, 5 May 2026 17:55:38 +0300 Subject: [PATCH 08/13] refactor(files): updated files and related components --- .../files/pages/files/files.component.html | 5 +- .../files/pages/files/files.component.ts | 39 +-- .../file-step/file-step.component.html | 1 + .../stepper/file-step/file-step.component.ts | 2 + .../files-widget/files-widget.component.html | 1 + .../files-widget/files-widget.component.ts | 22 +- .../files-control.component.html | 1 + .../files-control.component.spec.ts | 3 - .../files-control/files-control.component.ts | 4 +- .../files-tree/files-tree.component.html | 138 +++------- .../files-tree/files-tree.component.scss | 59 +---- .../files-tree/files-tree.component.spec.ts | 8 +- .../files-tree/files-tree.component.ts | 247 +++++++++--------- .../shared/mappers/files/file-tree.mapper.ts | 16 ++ .../models/files/file-tree-node.model.ts | 5 + 15 files changed, 238 insertions(+), 313 deletions(-) create mode 100644 src/app/shared/mappers/files/file-tree.mapper.ts create mode 100644 src/app/shared/models/files/file-tree-node.model.ts diff --git a/src/app/features/files/pages/files/files.component.html b/src/app/features/files/pages/files/files.component.html index 0e37f5963..012019d94 100644 --- a/src/app/features/files/pages/files/files.component.html +++ b/src/app/features/files/pages/files/files.component.html @@ -24,7 +24,7 @@ @if (!isMoveDialogOpened()) { diff --git a/src/app/features/files/pages/files/files.component.ts b/src/app/features/files/pages/files/files.component.ts index 59e4760a9..a7adcd271 100644 --- a/src/app/features/files/pages/files/files.component.ts +++ b/src/app/features/files/pages/files/files.component.ts @@ -2,7 +2,6 @@ import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { TreeDragDropService } from 'primeng/api'; import { Button } from 'primeng/button'; import { Select } from 'primeng/select'; import { TableModule } from 'primeng/table'; @@ -51,7 +50,7 @@ import { FILE_SIZE_LIMIT } from '@osf/shared/constants/files-limits.const'; import { ALL_SORT_OPTIONS } from '@osf/shared/constants/sort-options.const'; import { SupportedFeature } from '@osf/shared/enums/addon-supported-features.enum'; import { FileMenuType } from '@osf/shared/enums/file-menu-type.enum'; -import { ResourceType } from '@osf/shared/enums/resource-type.enum'; +import { CurrentResourceType, ResourceType } from '@osf/shared/enums/resource-type.enum'; import { FilePageLinkModel } from '@osf/shared/models/files/file-page-link.model'; import { RenamedFileLinkModel } from '@osf/shared/models/files/renamed-file-link.model'; import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; @@ -112,7 +111,6 @@ import { templateUrl: './files.component.html', styleUrl: './files.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, - providers: [TreeDragDropService], }) export class FilesComponent { googleFilePickerComponent = viewChild(GoogleFilePickerComponent); @@ -162,6 +160,9 @@ export class FilesComponent { readonly supportedFeatures = select(FilesSelectors.getStorageSupportedFeatures); readonly hasWriteAccess = select(CurrentResourceSelectors.hasResourceWriteAccess); readonly hasAdminAccess = select(CurrentResourceSelectors.hasResourceAdminAccess); + readonly currentResourceType = computed( + () => (this.resourceMetadata()?.type as CurrentResourceType) ?? CurrentResourceType.Projects + ); readonly isGoogleDrive = signal(false); readonly accountId = signal(''); @@ -428,8 +429,11 @@ export class FilesComponent { } onFileTreeSelected(file: FileModel): void { - this.filesSelection.push(file); - this.filesSelection = [...new Set(this.filesSelection)]; + if (this.filesSelection.some((selectedFile) => selectedFile.id === file.id)) { + return; + } + + this.filesSelection = [...this.filesSelection, file]; } onFileTreeUnselected(file: FileModel): void { @@ -610,13 +614,24 @@ export class FilesComponent { } navigateToFile(file: FileModel) { + if (file.guid) { + this.openFile(file.guid); + return; + } + + this.filesService.getFileGuid(file.id).subscribe((file) => { + if (file.guid) { + this.openFile(file.guid); + } + }); + } + + private openFile(guid: string): void { const extras = this.hasViewOnly() ? { queryParams: { view_only: this.viewOnlyService.getViewOnlyParamFromUrl(this.router.url) } } : undefined; - const url = this.router.serializeUrl(this.router.createUrlTree(['/', file.guid], extras)); - - window.open(url, '_blank'); + window.open(this.router.serializeUrl(this.router.createUrlTree(['/', guid], extras)), '_blank'); } getAddonName(addons: ConfiguredAddonModel[], provider: string): string { @@ -632,9 +647,7 @@ export class FilesComponent { const googleDrive = addons?.find((addon) => addon.externalServiceName === FileProvider.GoogleDrive); if (googleDrive) { this.accountId.set(googleDrive.baseAccountId); - this.selectedRootFolder.set({ - itemId: googleDrive.selectedStorageItemId, - }); + this.selectedRootFolder.set({ itemId: googleDrive.selectedStorageItemId }); } } @@ -643,10 +656,6 @@ export class FilesComponent { this.updateFilesList(); } - onUpdateFoldersStack(newStack: FileFolderModel[]): void { - this.foldersStack = [...newStack]; - } - handleRootFolderChange(selectedFolder: FileLabelModel) { const provider = selectedFolder.folder?.provider; const resourceId = this.resourceId(); 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 e3530c1ae..da47f68b4 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 @@ -95,6 +95,7 @@

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

[files]="projectFiles()" [totalCount]="filesTotalCount()" [storage]="null" + [resourceType]="resourceType" [selectionMode]="null" [isLoading]="areProjectFilesLoading() || isCurrentFolderLoading()" [resourceId]="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 f3b469834..f75b8b6b2 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 @@ -43,6 +43,7 @@ import { import { FilesTreeComponent } from '@osf/shared/components/files-tree/files-tree.component'; import { IconComponent } from '@osf/shared/components/icon/icon.component'; import { ClearFileDirective } from '@osf/shared/directives/clear-file.directive'; +import { CurrentResourceType } from '@osf/shared/enums/resource-type.enum'; import { StringOrNull } from '@osf/shared/helpers/types.helper'; import { FileModel } from '@osf/shared/models/files/file.model'; import { FileFolderModel } from '@osf/shared/models/files/file-folder.model'; @@ -89,6 +90,7 @@ export class FileStepComponent implements OnInit { }); readonly PreprintFileSource = PreprintFileSource; + readonly resourceType = CurrentResourceType.Preprints; provider = input.required(); preprint = select(PreprintStepperSelectors.getPreprint); diff --git a/src/app/features/project/overview/components/files-widget/files-widget.component.html b/src/app/features/project/overview/components/files-widget/files-widget.component.html index bc85b93cc..443aa125a 100644 --- a/src/app/features/project/overview/components/files-widget/files-widget.component.html +++ b/src/app/features/project/overview/components/files-widget/files-widget.component.html @@ -39,6 +39,7 @@

{{ 'project.overview.files.filesPreview' | translate }}

[storage]="currentRootFolder()!" [isLoading]="isFilesLoading() || isStorageLoading" [resourceId]="selectedRoot!" + [resourceType]="resourceType" [provider]="provider()" [selectionMode]="null" [scrollHeight]="'300px'" diff --git a/src/app/features/project/overview/components/files-widget/files-widget.component.ts b/src/app/features/project/overview/components/files-widget/files-widget.component.ts index 4de98cd51..dc8a6af22 100644 --- a/src/app/features/project/overview/components/files-widget/files-widget.component.ts +++ b/src/app/features/project/overview/components/files-widget/files-widget.component.ts @@ -31,7 +31,7 @@ import { } from '@osf/features/files/store'; import { FilesTreeComponent } from '@osf/shared/components/files-tree/files-tree.component'; import { SelectComponent } from '@osf/shared/components/select/select.component'; -import { ResourceType } from '@osf/shared/enums/resource-type.enum'; +import { CurrentResourceType, ResourceType } from '@osf/shared/enums/resource-type.enum'; import { Primitive } from '@osf/shared/helpers/types.helper'; import { ConfiguredAddonModel } from '@osf/shared/models/addons/configured-addon.model'; import { FileModel } from '@osf/shared/models/files/file.model'; @@ -41,6 +41,7 @@ import { FilePageLinkModel } from '@osf/shared/models/files/file-page-link.model import { NodeShortInfoModel } from '@osf/shared/models/nodes/node-with-children.model'; import { ProjectModel } from '@osf/shared/models/projects/projects.model'; import { SelectOption } from '@osf/shared/models/select-option.model'; +import { FilesService } from '@osf/shared/services/files.service'; import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; @Component({ @@ -59,6 +60,7 @@ export class FilesWidgetComponent { private readonly destroyRef = inject(DestroyRef); private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); + private readonly filesService = inject(FilesService); private readonly platformId = inject(PLATFORM_ID); private readonly isBrowser = isPlatformBrowser(this.platformId); @@ -76,6 +78,7 @@ export class FilesWidgetComponent { pageNumber = signal(1); readonly osfStorageLabel = 'OSF Storage'; + readonly resourceType = CurrentResourceType.Projects; readonly options = computed(() => { const components = this.components().filter((component) => this.rootOption().value !== component.id); @@ -222,13 +225,24 @@ export class FilesWidgetComponent { } navigateToFile(file: FileModel) { + if (file.guid) { + this.openFile(file.guid); + return; + } + + this.filesService.getFileGuid(file.id).subscribe((file) => { + if (file.guid) { + this.openFile(file.guid); + } + }); + } + + private openFile(guid: string): void { const extras = this.hasViewOnly() ? { queryParams: { view_only: this.viewOnlyService.getViewOnlyParamFromUrl(this.router.url) } } : undefined; - const url = this.router.serializeUrl(this.router.createUrlTree(['/', file.guid], extras)); - - window.open(url, '_blank'); + window.open(this.router.serializeUrl(this.router.createUrlTree(['/', guid], extras)), '_blank'); } onLoadFiles(event: FilePageLinkModel) { diff --git a/src/app/features/registries/components/files-control/files-control.component.html b/src/app/features/registries/components/files-control/files-control.component.html index 8d3350ae2..beda74069 100644 --- a/src/app/features/registries/components/files-control/files-control.component.html +++ b/src/app/features/registries/components/files-control/files-control.component.html @@ -49,6 +49,7 @@ [scrollHeight]="'500px'" [viewOnly]="filesViewOnly()" [resourceId]="projectId()" + [resourceType]="resourceType" [provider]="provider()" [selectedFiles]="filesSelection" (selectFile)="onFileTreeSelected($event)" diff --git a/src/app/features/registries/components/files-control/files-control.component.spec.ts b/src/app/features/registries/components/files-control/files-control.component.spec.ts index 874ea26d7..f62c2680e 100644 --- a/src/app/features/registries/components/files-control/files-control.component.spec.ts +++ b/src/app/features/registries/components/files-control/files-control.component.spec.ts @@ -2,8 +2,6 @@ import { Store } from '@ngxs/store'; import { MockComponents, MockProvider } from 'ng-mocks'; -import { TreeDragDropService } from 'primeng/api'; - import { of, Subject } from 'rxjs'; import { Mock } from 'vitest'; @@ -70,7 +68,6 @@ describe('FilesControlComponent', () => { MockProvider(CustomConfirmationService), MockProvider(FilesService, mockFilesService), MockProvider(CustomDialogService, mockDialogService), - MockProvider(TreeDragDropService), provideMockStore({ signals: [ { selector: RegistriesSelectors.getFiles, value: [] }, diff --git a/src/app/features/registries/components/files-control/files-control.component.ts b/src/app/features/registries/components/files-control/files-control.component.ts index 58d1081a8..683b41c28 100644 --- a/src/app/features/registries/components/files-control/files-control.component.ts +++ b/src/app/features/registries/components/files-control/files-control.component.ts @@ -2,7 +2,6 @@ import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; -import { TreeDragDropService } from 'primeng/api'; import { Button } from 'primeng/button'; import { filter, finalize, switchMap, take } from 'rxjs'; @@ -17,6 +16,7 @@ import { FilesTreeComponent } from '@osf/shared/components/files-tree/files-tree import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; import { FILE_SIZE_LIMIT } from '@osf/shared/constants/files-limits.const'; import { ClearFileDirective } from '@osf/shared/directives/clear-file.directive'; +import { CurrentResourceType } from '@osf/shared/enums/resource-type.enum'; import { FilePageLinkModel } from '@osf/shared/models/files/file-page-link.model'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { FilesService } from '@osf/shared/services/files.service'; @@ -46,7 +46,6 @@ import { templateUrl: './files-control.component.html', styleUrl: './files-control.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, - providers: [TreeDragDropService], }) export class FilesControlComponent { attachedFiles = input.required[]>(); @@ -69,6 +68,7 @@ export class FilesControlComponent { readonly progress = signal(0); readonly fileName = signal(''); readonly dataLoaded = signal(false); + readonly resourceType = CurrentResourceType.Registrations; fileIsUploading = signal(false); filesSelection: FileModel[] = []; 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 30b87b35f..2174c17d2 100644 --- a/src/app/shared/components/files-tree/files-tree.component.html +++ b/src/app/shared/components/files-tree/files-tree.component.html @@ -1,104 +1,52 @@ - + @if (isLoading() && !isLoadingMore()) {
} @else { -
-
- - - @if (file.previousFolder) { -
-
- - - {{ file.name ?? '' }} -
-
- } @else { -
-
-
- - {{ file?.name ?? '' }} -
-
+
+ + + + + -
- @if (file.extra.downloads) { - {{ - file.kind === 'file' ? file.extra.downloads + ' ' + ('common.labels.downloads' | translate) : '' - }} - } -
- -
- {{ file.size | fileSize }} -
- -
- {{ file.dateModified | date: 'MMM d, y hh:mm a' }} -
- - @if (canShowMenu()) { -
- - -
- } -
- } - - - - @if (!files().length) { -
- @if (hasViewOnly() || !supportUpload()) { -

{{ 'files.emptyState' | translate }}

- } @else { -
- -

{{ 'files.dropText' | translate }}

-
- } -
- } -
+ @if (!files().length) { +
+ @if (!canUpload()) { +

{{ 'files.emptyState' | translate }}

+ } @else { +
+ +

{{ '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 aa5a43a34..190e70847 100644 --- a/src/app/shared/components/files-tree/files-tree.component.scss +++ b/src/app/shared/components/files-tree/files-tree.component.scss @@ -1,64 +1,13 @@ -@use "styles/mixins" as mix; - :host { - min-height: 180px; display: flex; flex-direction: column; + min-height: 11.25rem; } .files-table { - display: flex; - flex-direction: column; border: 1px solid var(--grey-2); - border-radius: 8px; - overflow-x: auto; + border-radius: 0.5rem; min-width: 100%; - min-height: 180px; - - &-row { - 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); - cursor: pointer; - - &:hover { - background: var(--bg-blue-3); - } - - &:active { - background: var(--bg-blue-2); - } - - .table-cell { - width: 100%; - height: 100%; - display: flex; - align-items: center; - } - - > .table-cell:first-child { - max-width: 95%; - } - } -} - -.entry-title { - display: block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - min-width: 0; - max-width: 100%; -} - -.tree-table { - .p-tree { - padding: 0; - } + min-height: 11.25rem; + overflow-x: auto; } diff --git a/src/app/shared/components/files-tree/files-tree.component.spec.ts b/src/app/shared/components/files-tree/files-tree.component.spec.ts index 59a8bf61c..64d84dcbc 100644 --- a/src/app/shared/components/files-tree/files-tree.component.spec.ts +++ b/src/app/shared/components/files-tree/files-tree.component.spec.ts @@ -2,7 +2,6 @@ import { MockComponents, MockProvider } from 'ng-mocks'; import { TreeDragDropService } from 'primeng/api'; -import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { provideRouter } from '@angular/router'; @@ -12,14 +11,12 @@ import { CustomDialogService } from '@osf/shared/services/custom-dialog.service' import { DataciteService } from '@osf/shared/services/datacite/datacite.service'; import { FilesService } from '@osf/shared/services/files.service'; import { ToastService } from '@osf/shared/services/toast.service'; -import { CurrentResourceSelectors } from '@osf/shared/stores/current-resource'; import { FileFolderModel } from '@shared/models/files/file-folder.model'; import { FileLabelModel } from '@shared/models/files/file-label.model'; import { OSF_FILE_MOCK } from '@testing/mocks/osf-file.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; import { DataciteServiceMock, DataciteServiceMockType } from '@testing/providers/datacite.service.mock'; -import { provideMockStore } from '@testing/providers/store-provider.mock'; import { FileMenuComponent } from '../file-menu/file-menu.component'; import { LoadingSpinnerComponent } from '../loading-spinner/loading-spinner.component'; @@ -50,15 +47,12 @@ describe('FilesTreeComponent', () => { providers: [ provideOSFCore(), provideRouter([]), - provideMockStore({ - signals: [{ selector: CurrentResourceSelectors.getCurrentResource, value: signal(null) }], - }), MockProvider(DataciteService, dataciteMock), MockProvider(FilesService), MockProvider(ToastService), MockProvider(CustomConfirmationService), MockProvider(CustomDialogService), - TreeDragDropService, + MockProvider(TreeDragDropService), ], }); 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 e97cbccd3..42ac66de6 100644 --- a/src/app/shared/components/files-tree/files-tree.component.ts +++ b/src/app/shared/components/files-tree/files-tree.component.ts @@ -1,22 +1,20 @@ -import { select } from '@ngxs/store'; - import { TranslatePipe } from '@ngx-translate/core'; -import { PrimeTemplate, TreeNode } from 'primeng/api'; +import { PrimeTemplate, TreeDragDropService } from 'primeng/api'; import { Tree, TreeLazyLoadEvent, TreeNodeDropEvent, TreeNodeSelectEvent } from 'primeng/tree'; import { filter } from 'rxjs'; -import { DatePipe, isPlatformBrowser } from '@angular/common'; +import { isPlatformBrowser } from '@angular/common'; import { ChangeDetectionStrategy, Component, computed, DestroyRef, effect, - HostBinding, inject, input, + model, output, PLATFORM_ID, signal, @@ -27,13 +25,13 @@ import { Router } from '@angular/router'; import { ConfirmMoveFileDialogComponent } from '@osf/features/files/components/confirm-move-file-dialog/confirm-move-file-dialog.component'; import { MoveFileDialogComponent } from '@osf/features/files/components/move-file-dialog/move-file-dialog.component'; import { RenameFileDialogComponent } from '@osf/features/files/components/rename-file-dialog/rename-file-dialog.component'; -import { StopPropagationDirective } from '@osf/shared/directives/stop-propagation.directive'; import { FileKind } from '@osf/shared/enums/file-kind.enum'; import { FileMenuType } from '@osf/shared/enums/file-menu-type.enum'; +import { CurrentResourceType } from '@osf/shared/enums/resource-type.enum'; +import { FileTreeMapper } from '@osf/shared/mappers/files/file-tree.mapper'; import { FilesMapper } from '@osf/shared/mappers/files/files.mapper'; import { FilePageLinkModel } from '@osf/shared/models/files/file-page-link.model'; import { RenamedFileLinkModel } from '@osf/shared/models/files/renamed-file-link.model'; -import { FileSizePipe } from '@osf/shared/pipes/file-size.pipe'; import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { DataciteService } from '@osf/shared/services/datacite/datacite.service'; @@ -41,48 +39,33 @@ import { FilesService } from '@osf/shared/services/files.service'; import { FilesShareEmbedService } from '@osf/shared/services/files-share-embed.service'; import { ToastService } from '@osf/shared/services/toast.service'; import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; -import { CurrentResourceSelectors } from '@osf/shared/stores/current-resource'; import { FileModel } from '@shared/models/files/file.model'; import { FileFolderModel } from '@shared/models/files/file-folder.model'; import { FileLabelModel } from '@shared/models/files/file-label.model'; import { FileMenuAction, FileMenuFlags } from '@shared/models/files/file-menu-action.model'; -import { FileMenuComponent } from '../file-menu/file-menu.component'; import { FilesDropZoneComponent } from '../files-drop-zone/files-drop-zone.component'; +import { FilesTreeRowComponent } from '../files-tree-row/files-tree-row.component'; import { LoadingSpinnerComponent } from '../loading-spinner/loading-spinner.component'; -// [NS] Temporary fix -type FileTreeNode = FileModel & TreeNode; - @Component({ selector: 'osf-files-tree', - imports: [ - DatePipe, - FileSizePipe, - PrimeTemplate, - TranslatePipe, - Tree, - LoadingSpinnerComponent, - FilesDropZoneComponent, - FileMenuComponent, - StopPropagationDirective, - ], + imports: [PrimeTemplate, TranslatePipe, Tree, LoadingSpinnerComponent, FilesDropZoneComponent, FilesTreeRowComponent], + providers: [TreeDragDropService], templateUrl: './files-tree.component.html', styleUrl: './files-tree.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class FilesTreeComponent { - @HostBinding('class') classes = 'relative'; - readonly filesService = inject(FilesService); - readonly router = inject(Router); - readonly toastService = inject(ToastService); - readonly customConfirmationService = inject(CustomConfirmationService); - readonly customDialogService = inject(CustomDialogService); - readonly dataciteService = inject(DataciteService); - readonly filesShareEmbedService = inject(FilesShareEmbedService); - private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); - private readonly destroyRef = inject(DestroyRef); + private readonly router = inject(Router); + private readonly filesService = inject(FilesService); + private readonly toastService = inject(ToastService); + private readonly customConfirmationService = inject(CustomConfirmationService); + private readonly customDialogService = inject(CustomDialogService); + private readonly dataciteService = inject(DataciteService); + private readonly filesShareEmbedService = inject(FilesShareEmbedService); + private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); files = input.required(); @@ -91,6 +74,7 @@ export class FilesTreeComponent { currentFolder = input.required(); storage = input.required(); resourceId = input.required(); + resourceType = input(CurrentResourceType.Projects); viewOnly = input(true); provider = input(); allowedMenuActions = input({} as FileMenuFlags); @@ -109,41 +93,39 @@ export class FilesTreeComponent { selectFile = output(); unselectFile = output(); clearSelection = output(); - updateFoldersStack = output(); resetFilesProvider = output(); - readonly resourceMetadata = select(CurrentResourceSelectors.getCurrentResource); - - foldersStack: FileFolderModel[] = []; + foldersStack = model([]); lastSelectedFile: FileModel | null = null; itemsPerPage = 10; virtualScrollItemSize = 46; isLoadingMore = signal(false); - hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router) || this.viewOnly()); - canShowMenu = computed(() => Object.keys(this.allowedMenuActions()).length > 0 && !this.selectedFiles().length); + readonly hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router) || this.viewOnly()); + readonly canUpload = computed(() => !this.hasViewOnly() && this.supportUpload()); + readonly canShowMenu = computed( + () => Object.keys(this.allowedMenuActions()).length > 0 && !this.selectedFiles().length + ); readonly nodes = computed(() => { const currentFolder = this.currentFolder(); const files = this.files(); - if (this.foldersStack.length === 0) { - return files; - } + const values = this.foldersStack().length + ? ([{ ...currentFolder, previousFolder: true }, ...files] as FileModel[]) + : files; - return [{ ...currentFolder, previousFolder: true }, ...files] as FileModel[]; + return FileTreeMapper.toTreeNodes(values); }); - // [NS] Temporary fix - readonly selectedNodes = computed(() => this.selectedFiles() as FileTreeNode[]); + readonly selectedNodes = computed(() => FileTreeMapper.toTreeNodes(this.selectedFiles())); constructor() { effect(() => { const storageChanged = this.storage(); if (storageChanged) { - this.foldersStack = []; - this.updateFoldersStack.emit(this.foldersStack); + this.foldersStack.set([]); } }); @@ -170,21 +152,13 @@ export class FilesTreeComponent { }); } - openEntry(event: Event, file: FileModel | FileFolderModel) { - event.stopPropagation(); + openEntry(file: FileModel | FileFolderModel) { if (file.kind === FileKind.File) { - if (file.guid) { - this.entryFileClicked.emit(file); - } else { - this.filesService.getFileGuid(file.id).subscribe((file) => { - this.entryFileClicked.emit(file); - }); - } + this.entryFileClicked.emit(file); } else { const current = this.currentFolder(); if (current) { - this.foldersStack.push(current); - this.updateFoldersStack.emit(this.foldersStack); + this.foldersStack.update((stack) => [...stack, current]); } const folder = FilesMapper.mapFileToFolder(file as FileModel); this.setCurrentFolder.emit(folder); @@ -193,11 +167,14 @@ export class FilesTreeComponent { } openParentFolder() { - const previous = this.foldersStack.pop(); - this.updateFoldersStack.emit(this.foldersStack); + const stack = this.foldersStack(); + const previous = stack[stack.length - 1]; + this.foldersStack.set(stack.slice(0, -1)); + if (previous) { this.setCurrentFolder.emit(previous); } + this.clearSelection.emit(); } @@ -230,10 +207,8 @@ export class FilesTreeComponent { } downloadFileOrFolder(file: FileModel) { - const resourceType = this.resourceMetadata()?.type ?? 'nodes'; - this.dataciteService - .logFileDownload(this.resourceId(), resourceType) + .logFileDownload(this.resourceId(), this.resourceType()) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(); @@ -244,34 +219,6 @@ export class FilesTreeComponent { } } - private handleShareAction(file: FileModel, shareType?: string): void { - const shareAction = this.filesShareEmbedService.getShareLink(file, shareType); - if (!shareAction || !this.isBrowser) { - return; - } - - if (shareAction.target === '_self') { - window.location.href = shareAction.link; - return; - } - - window.open(shareAction.link, shareAction.target, 'noopener,noreferrer'); - } - - private handleEmbedAction(file: FileModel, embedType?: string): void { - const embedHtml = this.filesShareEmbedService.getEmbedHtml(file, embedType); - - if (!embedHtml) { - return; - } - - const copied = this.filesShareEmbedService.copyToClipboard(embedHtml); - - if (copied) { - this.toastService.showSuccess('files.detail.toast.copiedToClipboard'); - } - } - deleteEntry(file: FileModel): void { this.customConfirmationService.confirmDelete({ headerKey: file.kind === FileKind.Folder ? 'files.dialogs.deleteFolder.title' : 'files.dialogs.deleteFile.title', @@ -279,14 +226,10 @@ export class FilesTreeComponent { messageKey: file.kind === FileKind.Folder ? 'files.dialogs.deleteFolder.message' : 'files.dialogs.deleteFile.message', acceptLabelKey: 'common.buttons.remove', - onConfirm: () => this.confirmDeleteEntry(file.links.delete), + onConfirm: () => this.deleteEntryAction.emit(file.links.delete), }); } - confirmDeleteEntry(link: string): void { - this.deleteEntryAction.emit(link); - } - confirmRename(file: FileModel): void { this.customDialogService .open(RenameFileDialogComponent, { @@ -298,14 +241,12 @@ export class FilesTreeComponent { takeUntilDestroyed(this.destroyRef), filter((newName: string) => !!newName) ) - .subscribe((newName) => this.renameEntry(newName, file)); - } - - renameEntry(newName: string, file: FileModel): void { - if (newName.trim() && file.links.upload) { - const link = file.links.upload; - this.renameEntryAction.emit({ newName, link }); - } + .subscribe((newName) => { + if (newName.trim() && file.links.upload) { + const link = file.links.upload; + this.renameEntryAction.emit({ newName, link }); + } + }); } downloadFile(link: string): void { @@ -332,24 +273,11 @@ export class FilesTreeComponent { resourceId: this.resourceId(), action: action, storageProvider: this.storage()?.folder.provider, - foldersStack: structuredClone(this.foldersStack), + foldersStack: structuredClone(this.foldersStack()), initialFolder: structuredClone(this.currentFolder()), }, }) - .onClose.subscribe(() => { - this.resetFilesProvider.emit(); - }); - } - - private loadNextPage(): void { - const total = this.totalCount(); - const loaded = this.files().length; - const nextPage = Math.floor(loaded / this.itemsPerPage) + 1; - - if (!this.isLoadingMore() && loaded < total) { - this.isLoadingMore.set(true); - this.loadFiles.emit({ link: this.currentFolder()?.links.filesLink ?? '', page: nextPage }); - } + .onClose.subscribe(() => this.resetFilesProvider.emit()); } onLazyLoad(event: TreeLazyLoadEvent) { @@ -361,7 +289,12 @@ export class FilesTreeComponent { onNodeSelect(event: TreeNodeSelectEvent) { const files = this.files(); - const selectedNode = event.node as FileModel; + const selectedNode = event.node.data as FileModel; + + if (!selectedNode) { + return; + } + if ((event.originalEvent as PointerEvent).shiftKey && this.lastSelectedFile) { const lastIndex = files.indexOf(this.lastSelectedFile); const currentIndex = files.indexOf(selectedNode); @@ -376,25 +309,42 @@ export class FilesTreeComponent { this.selectFile.emit(file); } } + this.selectFile.emit(selectedNode); this.lastSelectedFile = selectedNode; } onNodeDrop(event: TreeNodeDropEvent) { - const dropFile = event.dropNode as FileModel; - if (dropFile.kind !== FileKind.Folder) { + const dropFile = event.dropNode?.data as FileModel; + + if (dropFile?.kind !== FileKind.Folder) { return; } - const files = this.selectedFiles(); - const dragFile = event.dragNode as FileModel; - if (!files.includes(dragFile)) { + + const selectedFiles = this.selectedFiles(); + const dragFile = event.dragNode?.data as FileModel; + + if (!dragFile) { + return; + } + + const filesToMove = selectedFiles.includes(dragFile) ? selectedFiles : [...selectedFiles, dragFile]; + + if (!selectedFiles.includes(dragFile)) { this.selectFile.emit(dragFile); } - this.moveFilesTo(files, dropFile); + + this.moveFilesTo(filesToMove, dropFile); } onNodeUnselect(event: TreeNodeSelectEvent) { - this.unselectFile.emit(event.node as FileModel); + const unselectedNode = event.node.data as FileModel; + + if (!unselectedNode) { + return; + } + + this.unselectFile.emit(unselectedNode); } private moveFilesTo(files: FileModel[], destination: FileModel) { @@ -410,8 +360,45 @@ export class FilesTreeComponent { storageProvider: this.storage()?.folder.provider, }, }) - .onClose.subscribe(() => { - this.resetFilesProvider.emit(); - }); + .onClose.subscribe(() => this.resetFilesProvider.emit()); + } + + private loadNextPage(): void { + const total = this.totalCount(); + const loaded = this.files().length; + const nextPage = Math.floor(loaded / this.itemsPerPage) + 1; + + if (!this.isLoadingMore() && loaded < total) { + this.isLoadingMore.set(true); + this.loadFiles.emit({ link: this.currentFolder()?.links.filesLink ?? '', page: nextPage }); + } + } + + private handleShareAction(file: FileModel, shareType?: string): void { + const shareAction = this.filesShareEmbedService.getShareLink(file, shareType); + if (!shareAction || !this.isBrowser) { + return; + } + + if (shareAction.target === '_self') { + window.location.href = shareAction.link; + return; + } + + window.open(shareAction.link, shareAction.target, 'noopener,noreferrer'); + } + + private handleEmbedAction(file: FileModel, embedType?: string): void { + const embedHtml = this.filesShareEmbedService.getEmbedHtml(file, embedType); + + if (!embedHtml) { + return; + } + + const copied = this.filesShareEmbedService.copyToClipboard(embedHtml); + + if (copied) { + this.toastService.showSuccess('files.detail.toast.copiedToClipboard'); + } } } diff --git a/src/app/shared/mappers/files/file-tree.mapper.ts b/src/app/shared/mappers/files/file-tree.mapper.ts new file mode 100644 index 000000000..7f84211fe --- /dev/null +++ b/src/app/shared/mappers/files/file-tree.mapper.ts @@ -0,0 +1,16 @@ +import { FileModel } from '@shared/models/files/file.model'; +import { FileTreeNode } from '@shared/models/files/file-tree-node.model'; + +export class FileTreeMapper { + static toTreeNode(file: FileModel): FileTreeNode { + return { + key: file.id, + label: file.name, + data: file, + }; + } + + static toTreeNodes(files: FileModel[]): FileTreeNode[] { + return files.map((file) => this.toTreeNode(file)); + } +} diff --git a/src/app/shared/models/files/file-tree-node.model.ts b/src/app/shared/models/files/file-tree-node.model.ts new file mode 100644 index 000000000..7f35896a8 --- /dev/null +++ b/src/app/shared/models/files/file-tree-node.model.ts @@ -0,0 +1,5 @@ +import { TreeNode } from 'primeng/api'; + +import { FileModel } from './file.model'; + +export type FileTreeNode = TreeNode; From 84f172a49d956eb84bcb2bcb22c6d67cbeeda5c4 Mon Sep 17 00:00:00 2001 From: nsemets Date: Wed, 6 May 2026 18:43:05 +0300 Subject: [PATCH 09/13] fix(social-links): updated social links and logic for it --- .../cedar-template-form.component.ts | 8 +-- .../files-tree/files-tree.component.ts | 18 ++----- .../socials-share-button.component.ts | 6 +-- .../shared/constants/social-share.config.ts | 3 +- .../socials/social-share-content.model.ts | 1 - .../services/files-share-embed.service.ts | 50 ++++++++++++------- .../shared/services/social-share.service.ts | 25 ++++++++-- 7 files changed, 64 insertions(+), 47 deletions(-) diff --git a/src/app/features/metadata/components/cedar-template-form/cedar-template-form.component.ts b/src/app/features/metadata/components/cedar-template-form/cedar-template-form.component.ts index 032e378f3..58b859259 100644 --- a/src/app/features/metadata/components/cedar-template-form/cedar-template-form.component.ts +++ b/src/app/features/metadata/components/cedar-template-form/cedar-template-form.component.ts @@ -23,6 +23,7 @@ import { toSignal } from '@angular/core/rxjs-interop'; import { ActivatedRoute } from '@angular/router'; import { ENVIRONMENT } from '@core/provider/environment.provider'; +import { SocialShareService } from '@osf/shared/services/social-share.service'; import { CEDAR_CONFIG, CEDAR_VIEWER_CONFIG } from '../../constants'; import { CedarMetadataHelper } from '../../helpers'; @@ -62,6 +63,7 @@ export class CedarTemplateFormComponent { private route = inject(ActivatedRoute); readonly environment = inject(ENVIRONMENT); + private readonly socialShareService = inject(SocialShareService); readonly recordId = signal(''); readonly downloadUrl = signal(''); @@ -184,18 +186,18 @@ export class CedarTemplateFormComponent { handleEmailShare(): void { const url = window.location.href; - window.location.href = `mailto:?subject=${this.schemaName()}&body=${url}`; + window.location.href = this.socialShareService.getEmailLink(this.schemaName(), url); } handleXShare(): void { const url = window.location.href; - const link = `https://x.com/intent/tweet?url=${url}&text=${this.schemaName()}&via=OSFramework`; + const link = this.socialShareService.getXLink(this.schemaName(), url); window.open(link, '_blank', 'noopener,noreferrer'); } handleFacebookShare(): void { const url = window.location.href; - const link = `https://www.facebook.com/sharer/sharer.php?u=${url}`; + const link = this.socialShareService.getFacebookLink(url); window.open(link, '_blank', 'noopener,noreferrer'); } } 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 eae44bfa6..a5e51c8f1 100644 --- a/src/app/shared/components/files-tree/files-tree.component.ts +++ b/src/app/shared/components/files-tree/files-tree.component.ts @@ -37,7 +37,6 @@ import { CustomDialogService } from '@osf/shared/services/custom-dialog.service' import { DataciteService } from '@osf/shared/services/datacite/datacite.service'; import { FilesService } from '@osf/shared/services/files.service'; import { FilesShareEmbedService } from '@osf/shared/services/files-share-embed.service'; -import { ToastService } from '@osf/shared/services/toast.service'; import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { FileModel } from '@shared/models/files/file.model'; import { FileFolderModel } from '@shared/models/files/file-folder.model'; @@ -60,7 +59,6 @@ export class FilesTreeComponent { private readonly destroyRef = inject(DestroyRef); private readonly router = inject(Router); private readonly filesService = inject(FilesService); - private readonly toastService = inject(ToastService); private readonly customConfirmationService = inject(CustomConfirmationService); private readonly customDialogService = inject(CustomDialogService); private readonly dataciteService = inject(DataciteService); @@ -88,7 +86,7 @@ export class FilesTreeComponent { uploadFilesConfirmed = output(); setCurrentFolder = output(); setMoveDialogCurrentFolder = output(); - deleteEntryAction = output(); + deleteEntryAction = output(); renameEntryAction = output(); loadFiles = output(); selectFile = output(); @@ -227,7 +225,7 @@ export class FilesTreeComponent { messageKey: file.kind === FileKind.Folder ? 'files.dialogs.deleteFolder.message' : 'files.dialogs.deleteFile.message', acceptLabelKey: 'common.buttons.remove', - onConfirm: () => this.deleteEntryAction.emit(file.links.delete), + onConfirm: () => this.deleteEntryAction.emit(file), }); } @@ -390,16 +388,6 @@ export class FilesTreeComponent { } private handleEmbedAction(file: FileModel, embedType?: string): void { - const embedHtml = this.filesShareEmbedService.getEmbedHtml(file, embedType); - - if (!embedHtml) { - return; - } - - const copied = this.filesShareEmbedService.copyToClipboard(embedHtml); - - if (copied) { - this.toastService.showSuccess('files.detail.toast.copiedToClipboard'); - } + this.filesShareEmbedService.copyEmbedToClipboard(file.links.render, embedType); } } diff --git a/src/app/shared/components/socials-share-button/socials-share-button.component.ts b/src/app/shared/components/socials-share-button/socials-share-button.component.ts index 23ed332eb..f51128d45 100644 --- a/src/app/shared/components/socials-share-button/socials-share-button.component.ts +++ b/src/app/shared/components/socials-share-button/socials-share-button.component.ts @@ -33,11 +33,7 @@ export class SocialsShareButtonComponent { ? this.socialShareService.createPreprintUrl(this.resourceId(), this.resourceProvider()) : this.socialShareService.createGuidUrl(this.resourceId()); - const shareableContent: SocialShareContentModel = { - id: this.resourceId(), - title: this.resourceTitle(), - url: resourceUrl, - }; + const shareableContent: SocialShareContentModel = { title: this.resourceTitle(), url: resourceUrl }; return this.socialShareService.generateSocialActionItems(shareableContent); }); diff --git a/src/app/shared/constants/social-share.config.ts b/src/app/shared/constants/social-share.config.ts index 88f7d17e2..7a0e2bfaf 100644 --- a/src/app/shared/constants/social-share.config.ts +++ b/src/app/shared/constants/social-share.config.ts @@ -1,7 +1,8 @@ export const SOCIAL_SHARE_URLS = { email: 'mailto:', - twitter: { preview_url: 'https://twitter.com/intent/tweet', viaHandle: 'OsfFramework' }, + x: { preview_url: 'https://x.com/intent/tweet', viaHandle: 'OsfFramework' }, facebook: 'https://www.facebook.com/sharer/sharer.php', + facebookShare: 'https://www.facebook.com/dialog/share', linkedIn: 'https://www.linkedin.com/sharing/share-offsite', mastodon: 'https://mastodonshare.com', bluesky: 'https://bsky.app/intent/compose', diff --git a/src/app/shared/models/socials/social-share-content.model.ts b/src/app/shared/models/socials/social-share-content.model.ts index fc5b94031..ec393ac08 100644 --- a/src/app/shared/models/socials/social-share-content.model.ts +++ b/src/app/shared/models/socials/social-share-content.model.ts @@ -1,5 +1,4 @@ export interface SocialShareContentModel { - id: string; title: string; url: string; } diff --git a/src/app/shared/services/files-share-embed.service.ts b/src/app/shared/services/files-share-embed.service.ts index 9d1751d47..399f81658 100644 --- a/src/app/shared/services/files-share-embed.service.ts +++ b/src/app/shared/services/files-share-embed.service.ts @@ -1,65 +1,77 @@ import { Clipboard } from '@angular/cdk/clipboard'; import { inject, Injectable } from '@angular/core'; -import { ENVIRONMENT } from '@osf/core/provider/environment.provider'; import { embedDynamicJs, embedStaticHtml } from '@shared/constants/file-embed.constants'; import { FileModel } from '../models/files/file.model'; import { FileShareLink } from '../models/files/file-share-link.model'; +import { SocialShareService } from './social-share.service'; +import { ToastService } from './toast.service'; + @Injectable({ providedIn: 'root', }) export class FilesShareEmbedService { - private readonly environment = inject(ENVIRONMENT); private readonly clipboard = inject(Clipboard); + private readonly socialShareService = inject(SocialShareService); + private readonly toastService = inject(ToastService); private readonly EMBED_PLACEHOLDER = 'ENCODED_URL'; getShareLink(file: FileModel, shareType?: string): FileShareLink | null { - const url = file.links?.html; - const name = file.name; - - if (!url || !name) return null; + const name = file.name ?? ''; + const url = file.links?.html ?? ''; - const encodedUrl = encodeURIComponent(url); - const encodedName = encodeURIComponent(name); + if (!url) { + return null; + } switch (shareType) { case 'email': return { - link: `mailto:?subject=${encodedName}&body=${encodedUrl}`, + link: this.socialShareService.getEmailLink(name, url), target: '_self', }; case 'twitter': return { - link: `https://x.com/intent/tweet?url=${encodedUrl}&text=${encodedName}&via=OSFramework`, + link: this.socialShareService.getXLink(name, url), target: '_blank', }; - case 'facebook': { - const appId = this.environment.facebookAppId; + case 'facebook': return { - link: `https://www.facebook.com/dialog/share?app_id=${appId}&display=popup&href=${encodedUrl}&redirect_uri=${encodedUrl}`, + link: this.socialShareService.getFacebookLink(url), target: '_blank', }; - } default: return null; } } - getEmbedHtml(file: FileModel, embedType?: string): string { + getEmbedHtml(url: string, embedType?: string): string { switch (embedType) { case 'dynamic': - return embedDynamicJs.replace(this.EMBED_PLACEHOLDER, file.links.render); + return embedDynamicJs.replace(this.EMBED_PLACEHOLDER, url); case 'static': - return embedStaticHtml.replace(this.EMBED_PLACEHOLDER, file.links.render); + return embedStaticHtml.replace(this.EMBED_PLACEHOLDER, url); default: return ''; } } - copyToClipboard(value: string): boolean { - return this.clipboard.copy(value); + copyEmbedToClipboard(url: string, embedType?: string): boolean { + const embedHtml = this.getEmbedHtml(url, embedType); + + if (!embedHtml) { + return false; + } + + const copied = this.clipboard.copy(embedHtml); + + if (copied) { + this.toastService.showSuccess('files.detail.toast.copiedToClipboard'); + } + + return copied; } } diff --git a/src/app/shared/services/social-share.service.ts b/src/app/shared/services/social-share.service.ts index 7b821619a..d8c9832f2 100644 --- a/src/app/shared/services/social-share.service.ts +++ b/src/app/shared/services/social-share.service.ts @@ -18,10 +18,29 @@ export class SocialShareService { return this.environment.webUrl; } + getEmailLink(title: string, url: string): string { + return this.generateEmailLink({ title, url }); + } + + getXLink(title: string, url: string): string { + return this.generateXLink({ title, url }); + } + + getFacebookLink(url: string): string { + return this.generateFacebookLink({ title: '', url }); + } + + getFacebookCustomLink(url: string): string { + const encodedUrl = encodeURIComponent(url); + const appId = this.environment.facebookAppId; + + return `${SOCIAL_SHARE_URLS.facebookShare}?app_id=${appId}&display=popup&href=${encodedUrl}&redirect_uri=${encodedUrl}`; + } + generateAllSharingLinks(content: SocialShareContentModel): SocialShareLinksModel { return { email: this.generateEmailLink(content), - twitter: this.generateTwitterLink(content), + twitter: this.generateXLink(content), facebook: this.generateFacebookLink(content), linkedIn: this.generateLinkedInLink(content), mastodon: this.generateMastodonLink(content), @@ -58,11 +77,11 @@ export class SocialShareService { return `${SOCIAL_SHARE_URLS.email}?subject=${subject}&body=${body}`; } - private generateTwitterLink(content: SocialShareContentModel): string { + private generateXLink(content: SocialShareContentModel): string { const url = encodeURIComponent(content.url); const text = encodeURIComponent(content.title); - return `${SOCIAL_SHARE_URLS.twitter.preview_url}?url=${url}&text=${text}&via=${SOCIAL_SHARE_URLS.twitter.viaHandle}`; + return `${SOCIAL_SHARE_URLS.x.preview_url}?url=${url}&text=${text}&via=${SOCIAL_SHARE_URLS.x.viaHandle}`; } private generateFacebookLink(content: SocialShareContentModel): string { From 52a96350ee2086e34a735026eb4ce4b54249388f Mon Sep 17 00:00:00 2001 From: nsemets Date: Wed, 6 May 2026 18:43:43 +0300 Subject: [PATCH 10/13] fix(files-tree): updated empty state --- .../files-drop-zone.component.html | 2 +- .../files-tree-row.component.ts | 8 +++--- .../files-tree/files-tree.component.html | 26 +++++++++---------- src/styles/overrides/tree.scss | 16 +----------- 4 files changed, 19 insertions(+), 33 deletions(-) diff --git a/src/app/shared/components/files-drop-zone/files-drop-zone.component.html b/src/app/shared/components/files-drop-zone/files-drop-zone.component.html index a8bc8dc2c..16f925a91 100644 --- a/src/app/shared/components/files-drop-zone/files-drop-zone.component.html +++ b/src/app/shared/components/files-drop-zone/files-drop-zone.component.html @@ -10,7 +10,7 @@ @if (isDragOver()) {
-

{{ 'files.dropText' | translate }}

+

{{ 'files.dropText' | translate }}

}
diff --git a/src/app/shared/components/files-tree-row/files-tree-row.component.ts b/src/app/shared/components/files-tree-row/files-tree-row.component.ts index 6839b3e99..4463ec213 100644 --- a/src/app/shared/components/files-tree-row/files-tree-row.component.ts +++ b/src/app/shared/components/files-tree-row/files-tree-row.component.ts @@ -26,6 +26,10 @@ export class FilesTreeRowComponent { showMenu = input(false); allowedMenuActions = input.required(); + openParentFolder = output(); + openEntry = output(); + menuAction = output(); + readonly isFolder = computed(() => this.file().kind === FileKind.Folder); readonly downloadsCount = computed(() => { @@ -35,10 +39,6 @@ export class FilesTreeRowComponent { return this.file().extra.downloads; }); - openParentFolder = output(); - openEntry = output(); - menuAction = output(); - onOpenEntry(): void { this.openEntry.emit(this.file()); } 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 2174c17d2..d038cb3fe 100644 --- a/src/app/shared/components/files-tree/files-tree.component.html +++ b/src/app/shared/components/files-tree/files-tree.component.html @@ -33,20 +33,20 @@ (menuAction)="onFileMenuAction($event, file.data)" > - - @if (!files().length) { -
- @if (!canUpload()) { -

{{ 'files.emptyState' | translate }}

- } @else { -
- -

{{ 'files.dropText' | translate }}

-
- } -
- } + +
+ @if (!canUpload()) { +

{{ 'files.emptyState' | translate }}

+ } @else { +
+ +

{{ 'files.dropText' | translate }}

+
+ } +
+
+
} diff --git a/src/styles/overrides/tree.scss b/src/styles/overrides/tree.scss index dc7dd38b3..86f6f2f96 100644 --- a/src/styles/overrides/tree.scss +++ b/src/styles/overrides/tree.scss @@ -54,7 +54,7 @@ } .p-tree-empty-message { - display: none; + height: 100%; } .p-tree-node-selected { @@ -72,18 +72,4 @@ } } } - - .empty-state-container { - position: absolute; - inset: 0; - top: 2.75rem; - display: flex; - justify-content: center; - align-items: center; - - .drop-text { - text-align: center; - margin-bottom: 2.75rem; - } - } } From ea8bc54a28429b7cb1d947e3f8547903779c42ad Mon Sep 17 00:00:00 2001 From: nsemets Date: Wed, 6 May 2026 18:46:34 +0300 Subject: [PATCH 11/13] fix(files): moved some logic into helpers --- .../file-preview/file-preview.component.ts | 26 +++------------- .../files-widget/files-widget.component.ts | 27 ++++------------ .../file-select-destination.component.ts | 26 +++------------- src/app/shared/helpers/mfr-url.helper.ts | 19 ++++++++++++ .../helpers/storage-addon-options.helper.ts | 31 +++++++++++++++++++ .../shared/models/files/file-link.model.ts | 4 +++ 6 files changed, 69 insertions(+), 64 deletions(-) create mode 100644 src/app/shared/helpers/mfr-url.helper.ts create mode 100644 src/app/shared/helpers/storage-addon-options.helper.ts create mode 100644 src/app/shared/models/files/file-link.model.ts diff --git a/src/app/features/files/pages/file-preview/file-preview.component.ts b/src/app/features/files/pages/file-preview/file-preview.component.ts index ab9bbd904..e4ead92cc 100644 --- a/src/app/features/files/pages/file-preview/file-preview.component.ts +++ b/src/app/features/files/pages/file-preview/file-preview.component.ts @@ -12,6 +12,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { FilesSelectors, GetFile } from '@osf/features/files/store'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; +import { getMfrUrlWithVersion } from '@osf/shared/helpers/mfr-url.helper'; import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; @Component({ @@ -50,30 +51,11 @@ export class FilePreviewComponent { } getIframeLink(version: string) { - const url = this.getMfrUrlWithVersion(version); + const viewOnlyParam = this.hasViewOnly() ? this.viewOnlyService.getViewOnlyParam() : null; + const url = getMfrUrlWithVersion(this.file()?.links.render, version, viewOnlyParam); + if (url) { this.safeLink = this.sanitizer.bypassSecurityTrustResourceUrl(url); } } - - getMfrUrlWithVersion(version?: string): string | null { - const mfrUrl = this.file()?.links.render; - if (!mfrUrl) return null; - const mfrUrlObj = new URL(mfrUrl); - const encodedDownloadUrl = mfrUrlObj.searchParams.get('url'); - if (!encodedDownloadUrl) return mfrUrl; - - const downloadUrlObj = new URL(decodeURIComponent(encodedDownloadUrl)); - - if (version) downloadUrlObj.searchParams.set('version', version); - - if (this.hasViewOnly()) { - const viewOnlyParam = this.viewOnlyService.getViewOnlyParam(); - if (viewOnlyParam) downloadUrlObj.searchParams.set('view_only', viewOnlyParam); - } - - mfrUrlObj.searchParams.set('url', downloadUrlObj.toString()); - - return mfrUrlObj.toString(); - } } diff --git a/src/app/features/project/overview/components/files-widget/files-widget.component.ts b/src/app/features/project/overview/components/files-widget/files-widget.component.ts index dc8a6af22..3f90d2cb8 100644 --- a/src/app/features/project/overview/components/files-widget/files-widget.component.ts +++ b/src/app/features/project/overview/components/files-widget/files-widget.component.ts @@ -1,6 +1,6 @@ import { createDispatchMap, select } from '@ngxs/store'; -import { TranslatePipe } from '@ngx-translate/core'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import { Skeleton } from 'primeng/skeleton'; import { TabsModule } from 'primeng/tabs'; @@ -32,8 +32,8 @@ import { import { FilesTreeComponent } from '@osf/shared/components/files-tree/files-tree.component'; import { SelectComponent } from '@osf/shared/components/select/select.component'; import { CurrentResourceType, ResourceType } from '@osf/shared/enums/resource-type.enum'; +import { mapRootFoldersToStorageLabels } from '@osf/shared/helpers/storage-addon-options.helper'; import { Primitive } from '@osf/shared/helpers/types.helper'; -import { ConfiguredAddonModel } from '@osf/shared/models/addons/configured-addon.model'; import { FileModel } from '@osf/shared/models/files/file.model'; import { FileFolderModel } from '@osf/shared/models/files/file-folder.model'; import { FileLabelModel } from '@osf/shared/models/files/file-label.model'; @@ -61,6 +61,7 @@ export class FilesWidgetComponent { private readonly destroyRef = inject(DestroyRef); private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); private readonly filesService = inject(FilesService); + private readonly translateService = inject(TranslateService); private readonly platformId = inject(PLATFORM_ID); private readonly isBrowser = isPlatformBrowser(this.platformId); @@ -77,7 +78,6 @@ export class FilesWidgetComponent { currentRootFolder = model(null); pageNumber = signal(1); - readonly osfStorageLabel = 'OSF Storage'; readonly resourceType = CurrentResourceType.Projects; readonly options = computed(() => { @@ -86,15 +86,8 @@ export class FilesWidgetComponent { }); readonly storageAddons = 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 []; + const osfLabel = this.translateService.instant('files.storageLocation'); + return mapRootFoldersToStorageLabels(this.rootFolders(), this.configuredStorageAddons(), osfLabel); }); readonly hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router)); @@ -132,7 +125,7 @@ export class FilesWidgetComponent { const osfRootFolder = rootFolders.find((folder) => folder.provider === FileProvider.OsfStorage); if (osfRootFolder) { this.currentRootFolder.set({ - label: this.osfStorageLabel, + label: this.translateService.instant('files.storageLocation'), folder: osfRootFolder, }); } @@ -205,14 +198,6 @@ export class FilesWidgetComponent { }, []); } - private getAddonName(addons: ConfiguredAddonModel[], provider: string): string { - if (provider === FileProvider.OsfStorage) { - return this.osfStorageLabel; - } else { - return addons.find((addon) => addon.externalServiceName === provider)?.displayName ?? ''; - } - } - onChangeProject(value: Primitive) { this.getStorageAddons(value as string); } diff --git a/src/app/shared/components/file-select-destination/file-select-destination.component.ts b/src/app/shared/components/file-select-destination/file-select-destination.component.ts index 8ed92083d..ab27aaf53 100644 --- a/src/app/shared/components/file-select-destination/file-select-destination.component.ts +++ b/src/app/shared/components/file-select-destination/file-select-destination.component.ts @@ -1,6 +1,6 @@ import { createDispatchMap, select } from '@ngxs/store'; -import { TranslatePipe } from '@ngx-translate/core'; +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Skeleton } from 'primeng/skeleton'; @@ -23,7 +23,6 @@ import { } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { FileProvider } from '@osf/features/files/constants'; import { FilesSelectors, GetMoveDialogConfiguredStorageAddons, @@ -35,8 +34,8 @@ import { import { SupportedFeature } from '@osf/shared/enums/addon-supported-features.enum'; import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { UserPermissions } from '@osf/shared/enums/user-permissions.enum'; +import { mapRootFoldersToStorageLabels } from '@osf/shared/helpers/storage-addon-options.helper'; import { Primitive } from '@osf/shared/helpers/types.helper'; -import { ConfiguredAddonModel } from '@osf/shared/models/addons/configured-addon.model'; import { FileLabelModel } from '@osf/shared/models/files/file-label.model'; import { NodeShortInfoModel } from '@osf/shared/models/nodes/node-with-children.model'; import { SelectOption } from '@osf/shared/models/select-option.model'; @@ -59,6 +58,7 @@ export class FileSelectDestinationComponent implements OnInit { selectStorage = output(); private readonly destroyRef = inject(DestroyRef); + private readonly translateService = inject(TranslateService); readonly rootFolders = select(FilesSelectors.getMoveDialogRootFolders); readonly isRootFoldersLoading = select(FilesSelectors.isMoveDialogRootFoldersLoading); @@ -76,7 +76,6 @@ export class FileSelectDestinationComponent implements OnInit { setCurrentProvider: SetCurrentProvider, }); - readonly osfStorageLabel = 'OSF Storage'; initialSetup = true; currentRootFolder = model(null); selectedProject = computed(() => this.options().find((c) => c.value === this.projectId()) || null); @@ -95,15 +94,8 @@ export class FileSelectDestinationComponent implements OnInit { }); readonly storageAddons = 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 []; + const osfLabel = this.translateService.instant('files.storageLocation'); + return mapRootFoldersToStorageLabels(this.rootFolders(), this.configuredStorageAddons(), osfLabel); }); private getHasWriteAccess = (project: NodeShortInfoModel): boolean => @@ -180,14 +172,6 @@ export class FileSelectDestinationComponent implements OnInit { }); } - private getAddonName(addons: ConfiguredAddonModel[], provider: string): string { - if (provider === FileProvider.OsfStorage) { - return this.osfStorageLabel; - } else { - return addons.find((addon) => addon.externalServiceName === provider)?.displayName ?? ''; - } - } - private buildOptions(nodes: NodeShortInfoModel[] = [], parentPath = '..'): SelectOption[] { return nodes.reduce((acc, node) => { const pathParts: string[] = []; diff --git a/src/app/shared/helpers/mfr-url.helper.ts b/src/app/shared/helpers/mfr-url.helper.ts new file mode 100644 index 000000000..a3fa1093b --- /dev/null +++ b/src/app/shared/helpers/mfr-url.helper.ts @@ -0,0 +1,19 @@ +export function getMfrUrlWithVersion( + mfrUrl: string | undefined, + version?: string, + viewOnlyParam?: string | null +): string | null { + if (!mfrUrl) return null; + const mfrUrlObj = new URL(mfrUrl); + const encodedDownloadUrl = mfrUrlObj.searchParams.get('url'); + if (!encodedDownloadUrl) return mfrUrl; + + const downloadUrlObj = new URL(decodeURIComponent(encodedDownloadUrl)); + + if (version) downloadUrlObj.searchParams.set('version', version); + if (viewOnlyParam) downloadUrlObj.searchParams.set('view_only', viewOnlyParam); + + mfrUrlObj.searchParams.set('url', downloadUrlObj.toString()); + + return mfrUrlObj.toString(); +} diff --git a/src/app/shared/helpers/storage-addon-options.helper.ts b/src/app/shared/helpers/storage-addon-options.helper.ts new file mode 100644 index 000000000..18ec482b5 --- /dev/null +++ b/src/app/shared/helpers/storage-addon-options.helper.ts @@ -0,0 +1,31 @@ +import { FileProvider } from '@osf/features/files/constants'; +import { ConfiguredAddonModel } from '@shared/models/addons/configured-addon.model'; +import { FileFolderModel } from '@shared/models/files/file-folder.model'; +import { FileLabelModel } from '@shared/models/files/file-label.model'; + +export function getConfiguredStorageAddonDisplayName( + addons: ConfiguredAddonModel[], + provider: string, + osfStorageLabel: string +): string { + if (provider === FileProvider.OsfStorage) { + return osfStorageLabel; + } + + return addons.find((addon) => addon.externalServiceName === provider)?.displayName ?? ''; +} + +export function mapRootFoldersToStorageLabels( + rootFolders: FileFolderModel[] | null | undefined, + addons: ConfiguredAddonModel[] | null | undefined, + osfStorageLabel: string +): FileLabelModel[] { + if (!rootFolders || !addons) { + return []; + } + + return rootFolders.map((folder) => ({ + label: getConfiguredStorageAddonDisplayName(addons, folder.provider, osfStorageLabel), + folder, + })); +} diff --git a/src/app/shared/models/files/file-link.model.ts b/src/app/shared/models/files/file-link.model.ts new file mode 100644 index 000000000..abf1f8d49 --- /dev/null +++ b/src/app/shared/models/files/file-link.model.ts @@ -0,0 +1,4 @@ +export interface FileLinkModel { + file: File; + link: string; +} From 2f5f5ce6b5abc6062d200605ce2e6eb2783a1090 Mon Sep 17 00:00:00 2001 From: nsemets Date: Thu, 7 May 2026 14:17:02 +0300 Subject: [PATCH 12/13] fix(files): updated files and file details --- .../edit-file-metadata-dialog.component.ts | 3 +- .../file-metadata/file-metadata.component.ts | 2 +- .../file-revisions.component.ts | 2 +- .../constants/file-browser-info.constants.ts | 2 +- .../file-metadata-fields.constants.ts | 2 +- .../mappers/file-custom-metadata.mapper.ts | 3 +- .../files/mappers/file-revision.mapper.ts | 3 +- .../models/files-actions-options.model.ts | 24 ++ .../models/files-upload-options.model.ts | 16 ++ src/app/features/files/models/index.ts | 9 - .../file-detail/file-detail.component.html | 29 +- .../file-detail/file-detail.component.ts | 261 +++++++++--------- .../files/pages/files/files.component.spec.ts | 5 +- .../files/pages/files/files.component.ts | 261 ++++++------------ .../files/services/files-actions.service.ts | 73 +++++ .../files/services/files-upload.service.ts | 103 +++++++ src/app/features/files/store/files.actions.ts | 2 +- src/app/features/files/store/files.model.ts | 3 +- .../features/files/store/files.selectors.ts | 3 +- src/app/shared/services/files.service.ts | 14 +- .../services/meta-tags-builder.service.ts | 2 +- 21 files changed, 469 insertions(+), 353 deletions(-) create mode 100644 src/app/features/files/models/files-actions-options.model.ts create mode 100644 src/app/features/files/models/files-upload-options.model.ts delete mode 100644 src/app/features/files/models/index.ts create mode 100644 src/app/features/files/services/files-actions.service.ts create mode 100644 src/app/features/files/services/files-upload.service.ts diff --git a/src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.ts b/src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.ts index a47be2ff6..35e9bb2ef 100644 --- a/src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.ts +++ b/src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.ts @@ -11,7 +11,8 @@ import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { languageCodes } from '@osf/shared/constants/language.const'; import { resourceTypes } from '@osf/shared/constants/resource-types.const'; -import { OsfFileCustomMetadata, PatchFileMetadata } from '../../models'; +import { OsfFileCustomMetadata } from '../../models/file-custom-metadata.model'; +import { PatchFileMetadata } from '../../models/patch-file-metadata.model'; @Component({ selector: 'osf-edit-file-metadata-dialog', diff --git a/src/app/features/files/components/file-metadata/file-metadata.component.ts b/src/app/features/files/components/file-metadata/file-metadata.component.ts index 2b9f22052..01e88fabb 100644 --- a/src/app/features/files/components/file-metadata/file-metadata.component.ts +++ b/src/app/features/files/components/file-metadata/file-metadata.component.ts @@ -18,7 +18,7 @@ import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-h import { LanguageCodeModel } from '@shared/models/language-code.model'; import { FileMetadataFields } from '../../constants'; -import { PatchFileMetadata } from '../../models'; +import { PatchFileMetadata } from '../../models/patch-file-metadata.model'; import { FilesSelectors, SetFileMetadata } from '../../store'; import { EditFileMetadataDialogComponent } from '../edit-file-metadata-dialog/edit-file-metadata-dialog.component'; diff --git a/src/app/features/files/components/file-revisions/file-revisions.component.ts b/src/app/features/files/components/file-revisions/file-revisions.component.ts index a360dd71e..fcb0a8d9b 100644 --- a/src/app/features/files/components/file-revisions/file-revisions.component.ts +++ b/src/app/features/files/components/file-revisions/file-revisions.component.ts @@ -11,7 +11,7 @@ import { CopyButtonComponent } from '@osf/shared/components/copy-button/copy-but import { InfoIconComponent } from '@osf/shared/components/info-icon/info-icon.component'; import { StopPropagationDirective } from '@osf/shared/directives/stop-propagation.directive'; -import { OsfFileRevision } from '../../models'; +import { OsfFileRevision } from '../../models/file-revisions.model'; @Component({ selector: 'osf-file-revisions', diff --git a/src/app/features/files/constants/file-browser-info.constants.ts b/src/app/features/files/constants/file-browser-info.constants.ts index 9e0c95ec0..1de435ecb 100644 --- a/src/app/features/files/constants/file-browser-info.constants.ts +++ b/src/app/features/files/constants/file-browser-info.constants.ts @@ -1,6 +1,6 @@ import { ResourceType } from '@osf/shared/enums/resource-type.enum'; -import { FileInfoItem } from '../models'; +import { FileInfoItem } from '../models/info-item.model'; export const FILE_BROWSER_INFO_ITEMS: FileInfoItem[] = [ { diff --git a/src/app/features/files/constants/file-metadata-fields.constants.ts b/src/app/features/files/constants/file-metadata-fields.constants.ts index d67123b6a..e2c4e5a6f 100644 --- a/src/app/features/files/constants/file-metadata-fields.constants.ts +++ b/src/app/features/files/constants/file-metadata-fields.constants.ts @@ -1,4 +1,4 @@ -import { MetadataField } from '../models'; +import { MetadataField } from '../models/files-metadata-fields.model'; export const FileMetadataFields: MetadataField[] = [ { key: 'title', label: 'common.labels.title' }, diff --git a/src/app/features/files/mappers/file-custom-metadata.mapper.ts b/src/app/features/files/mappers/file-custom-metadata.mapper.ts index 3af893193..1292bd004 100644 --- a/src/app/features/files/mappers/file-custom-metadata.mapper.ts +++ b/src/app/features/files/mappers/file-custom-metadata.mapper.ts @@ -1,7 +1,8 @@ import { ApiData } from '@osf/shared/models/common/json-api.model'; import { replaceBadEncodedChars } from '@shared/helpers/format-bad-encoding.helper'; -import { FileCustomMetadata, OsfFileCustomMetadata } from '../models'; +import { OsfFileCustomMetadata } from '../models/file-custom-metadata.model'; +import { FileCustomMetadata } from '../models/get-file-metadata-response.model'; export function MapFileCustomMetadata(data: ApiData): OsfFileCustomMetadata { return { diff --git a/src/app/features/files/mappers/file-revision.mapper.ts b/src/app/features/files/mappers/file-revision.mapper.ts index 19046d20f..0adb0231f 100644 --- a/src/app/features/files/mappers/file-revision.mapper.ts +++ b/src/app/features/files/mappers/file-revision.mapper.ts @@ -1,6 +1,7 @@ import { ApiData } from '@osf/shared/models/common/json-api.model'; -import { FileRevisionJsonApi, OsfFileRevision } from '../models'; +import { OsfFileRevision } from '../models/file-revisions.model'; +import { FileRevisionJsonApi } from '../models/get-file-revisions-response.model'; export function MapFileRevision(data: ApiData[]): OsfFileRevision[] { const revision = data.map((revision) => ({ diff --git a/src/app/features/files/models/files-actions-options.model.ts b/src/app/features/files/models/files-actions-options.model.ts new file mode 100644 index 000000000..cfe735208 --- /dev/null +++ b/src/app/features/files/models/files-actions-options.model.ts @@ -0,0 +1,24 @@ +import { Observable } from 'rxjs'; + +import { FileModel } from '@shared/models/files/file.model'; +import { FileFolderModel } from '@shared/models/files/file-folder.model'; + +export interface DeleteSelectedOptions { + files: FileModel[]; + deleteEntry: (link: string) => Observable; + onSuccess: () => void; +} + +export interface MoveFilesOptions { + files: FileModel[]; + action: 'move' | 'copy'; + resourceId: string; + storageProvider: string; + foldersStack: FileFolderModel[]; + initialFolder: FileFolderModel | null | undefined; +} + +export interface CreateFolderOptions { + newFolderLink: string; + createFolder: (newFolderLink: string, folderName: string) => Observable; +} diff --git a/src/app/features/files/models/files-upload-options.model.ts b/src/app/features/files/models/files-upload-options.model.ts new file mode 100644 index 000000000..94b3eccd8 --- /dev/null +++ b/src/app/features/files/models/files-upload-options.model.ts @@ -0,0 +1,16 @@ +import { FileLinkModel } from '@osf/shared/models/files/file-link.model'; + +export interface UploadFilesOptions { + files: File | File[]; + uploadLink: string; + allowRevisions: boolean; + onStart: (fileName: string) => void; + onProgress: (progress: number) => void; + onComplete: () => void; +} + +export interface UploadState { + completedUploads: number; + totalFiles: number; + conflictFiles: FileLinkModel[]; +} diff --git a/src/app/features/files/models/index.ts b/src/app/features/files/models/index.ts deleted file mode 100644 index 29d50dfc6..000000000 --- a/src/app/features/files/models/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './file-custom-metadata.model'; -export * from './file-revisions.model'; -export * from './files-metadata-fields.model'; -export * from './get-custom-metadata-response.model'; -export * from './get-file-metadata-response.model'; -export * from './get-file-revisions-response.model'; -export * from './get-short-info-response.model'; -export * from './info-item.model'; -export * from './patch-file-metadata.model'; diff --git a/src/app/features/files/pages/file-detail/file-detail.component.html b/src/app/features/files/pages/file-detail/file-detail.component.html index fc44a06e3..1e72e92f0 100644 --- a/src/app/features/files/pages/file-detail/file-detail.component.html +++ b/src/app/features/files/pages/file-detail/file-detail.component.html @@ -1,9 +1,6 @@ - +@let currentFile = file(); + + @@ -13,7 +10,7 @@ -
+
@@ -23,7 +20,7 @@
- @if (!isAnonymous() && !hasViewOnly() && hasWriteAccess()) { + @if (canManageFileActions()) { } - @if (file()?.links?.download) { + @if (currentFile?.links?.download) { } - @if (file()?.links?.render) { + @if (currentFile?.links?.render) {
@@ -57,7 +54,7 @@
} - @if (file()?.links?.html) { + @if (currentFile?.links?.html) {
} - @if (showDeleteButton()) { + @if (canManageFileActions()) { }
@@ -121,7 +118,7 @@ } @else {