From 8137f041f2583d43cad83c60ab32185bd7c50110 Mon Sep 17 00:00:00 2001 From: NazarMykhalkevych Date: Wed, 13 Aug 2025 11:03:48 +0300 Subject: [PATCH 1/7] feat(newbranch): test push --- .../files-control/files-control.component.ts | 13 ++++++++----- .../registry-files/registry-files.component.ts | 3 ++- src/app/shared/services/files.service.ts | 10 ++++++++++ 3 files changed, 20 insertions(+), 6 deletions(-) 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 978662dfc..587aefbe8 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 @@ -15,7 +15,6 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { CreateFolderDialogComponent } from '@osf/features/project/files/components'; -import { approveFile } from '@osf/features/project/files/utils'; import { FilesTreeComponent, LoadingSpinnerComponent } from '@osf/shared/components'; import { FilesTreeActions, OsfFile } from '@osf/shared/models'; import { FilesService } from '@osf/shared/services'; @@ -172,10 +171,14 @@ export class FilesControlComponent { if (event.type === HttpEventType.Response) { if (event.body) { - const fileId = event?.body?.data.id; - const branchedFromId = this.projectId(); - if (fileId && branchedFromId) { - approveFile(fileId, branchedFromId); + const fileId = event?.body?.data?.id?.split('/').pop(); + if (fileId) { + this.filesService + .getFileGuid(fileId) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((file) => { + this.selectFile(file); + }); } } } diff --git a/src/app/features/registry/pages/registry-files/registry-files.component.ts b/src/app/features/registry/pages/registry-files/registry-files.component.ts index a4bf463d2..37c778d10 100644 --- a/src/app/features/registry/pages/registry-files/registry-files.component.ts +++ b/src/app/features/registry/pages/registry-files/registry-files.component.ts @@ -13,6 +13,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormControl } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; +import { FileProvider } from '@osf/features/project/files/models'; import { FilesTreeComponent, FormSelectComponent, @@ -83,7 +84,7 @@ export class RegistryFilesComponent { protected dataLoaded = signal(false); protected readonly sortOptions = ALL_SORT_OPTIONS; - protected readonly provider = 'osfstorage'; + protected readonly provider = FileProvider.OsfStorage; constructor() { this.route.parent?.params.subscribe((params) => { diff --git a/src/app/shared/services/files.service.ts b/src/app/shared/services/files.service.ts index 7f99e58b9..1e961e766 100644 --- a/src/app/shared/services/files.service.ts +++ b/src/app/shared/services/files.service.ts @@ -154,6 +154,16 @@ export class FilesService { .pipe(map((response) => MapFile(response.data))); } + getFileGuid(id: string): Observable { + const params = { + create_guid: 'true', + }; + + return this.jsonApiService + .get(`${environment.apiUrl}/files/${id}`, params) + .pipe(map((response) => MapFile(response.data))); + } + getFileById(fileGuid: string): Observable { return this.jsonApiService .get(`${environment.apiUrl}/files/${fileGuid}/`) From cd0bf5ac6291793418c36f412bbaf0c590544239 Mon Sep 17 00:00:00 2001 From: NazarMykhalkevych Date: Thu, 14 Aug 2025 14:40:57 +0300 Subject: [PATCH 2/7] fix(file): add quid for file --- .../project-files/project-files.component.ts | 7 +++-- .../files/utils/approve-file.helper.ts | 15 ---------- src/app/features/project/files/utils/index.ts | 1 - .../custom-step/custom-step.component.ts | 28 +++++++++++++++---- .../files-tree/files-tree.component.ts | 8 +++++- src/app/shared/mappers/index.ts | 1 + src/app/shared/services/files.service.ts | 3 +- src/assets/styles/overrides/tree.scss | 3 +- 8 files changed, 37 insertions(+), 29 deletions(-) delete mode 100644 src/app/features/project/files/utils/approve-file.helper.ts delete mode 100644 src/app/features/project/files/utils/index.ts diff --git a/src/app/features/project/files/pages/project-files/project-files.component.ts b/src/app/features/project/files/pages/project-files/project-files.component.ts index 01463158c..23cb70d45 100644 --- a/src/app/features/project/files/pages/project-files/project-files.component.ts +++ b/src/app/features/project/files/pages/project-files/project-files.component.ts @@ -44,7 +44,6 @@ import { SetSearch, SetSort, } from '@osf/features/project/files/store'; -import { approveFile } from '@osf/features/project/files/utils'; import { GetProjectById, ProjectOverviewSelectors } from '@osf/features/project/overview/store'; import { ALL_SORT_OPTIONS } from '@osf/shared/constants'; import { @@ -246,8 +245,10 @@ export class ProjectFilesComponent { if (event.type === HttpEventType.Response) { if (event.body) { - const fileId = event?.body?.data.id; - approveFile(fileId, this.projectId()); + const fileId = event?.body?.data?.id?.split('/').pop(); + if (fileId) { + this.filesService.getFileGuid(fileId).pipe(takeUntilDestroyed(this.destroyRef)).subscribe(); + } } } }); diff --git a/src/app/features/project/files/utils/approve-file.helper.ts b/src/app/features/project/files/utils/approve-file.helper.ts deleted file mode 100644 index 87af40df6..000000000 --- a/src/app/features/project/files/utils/approve-file.helper.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { environment } from 'src/environments/environment'; - -export function approveFile(fileId: string, projectId: string): void { - const link = `${environment.apiUrlV1}/${projectId}/files/${fileId}/`; - - const iframe = document.createElement('iframe'); - iframe.style.display = 'none'; - iframe.src = link; - - iframe.onload = () => { - setTimeout(() => iframe.remove(), 3000); - }; - - document.body.appendChild(iframe); -} diff --git a/src/app/features/project/files/utils/index.ts b/src/app/features/project/files/utils/index.ts deleted file mode 100644 index 22377b7a2..000000000 --- a/src/app/features/project/files/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './approve-file.helper'; diff --git a/src/app/features/registries/components/custom-step/custom-step.component.ts b/src/app/features/registries/components/custom-step/custom-step.component.ts index fcc1aeb05..47fbf4669 100644 --- a/src/app/features/registries/components/custom-step/custom-step.component.ts +++ b/src/app/features/registries/components/custom-step/custom-step.component.ts @@ -96,7 +96,7 @@ export class CustomStepComponent implements OnDestroy { stepForm!: FormGroup; - attachedFiles: Record[]> = {}; + attachedFiles: Record[]> = {}; constructor() { this.route.params.pipe(takeUntilDestroyed()).subscribe((params) => { @@ -181,7 +181,7 @@ export class CustomStepComponent implements OnDestroy { onAttachFile(file: OsfFile, questionKey: string): void { this.attachedFiles[questionKey] = this.attachedFiles[questionKey] || []; - if (!this.attachedFiles[questionKey].some((f) => f.id === file.id)) { + if (!this.attachedFiles[questionKey].some((f) => f.file_id === file.id)) { this.attachedFiles[questionKey].push(file); this.stepForm.patchValue({ [questionKey]: [...(this.attachedFiles[questionKey] || []), file], @@ -189,20 +189,36 @@ export class CustomStepComponent implements OnDestroy { const otherFormValues = { ...this.stepForm.value }; delete otherFormValues[questionKey]; this.updateAction.emit({ - [questionKey]: [...this.attachedFiles[questionKey].map((f) => FilesMapper.toFilePayload(f as OsfFile))], + [questionKey]: [ + ...this.attachedFiles[questionKey].map((f) => { + if (f.file_id) { + const { name, ...payload } = f; + return payload; + } + return FilesMapper.toFilePayload(f as OsfFile); + }), + ], ...otherFormValues, }); } } - removeFromAttachedFiles(file: Partial, questionKey: string): void { + removeFromAttachedFiles(file: Partial, questionKey: string): void { if (this.attachedFiles[questionKey]) { - this.attachedFiles[questionKey] = this.attachedFiles[questionKey].filter((f) => f.id !== file.id); + this.attachedFiles[questionKey] = this.attachedFiles[questionKey].filter((f) => f.file_id !== file.file_id); this.stepForm.patchValue({ [questionKey]: this.attachedFiles[questionKey], }); this.updateAction.emit({ - [questionKey]: [...this.attachedFiles[questionKey].map((f) => FilesMapper.toFilePayload(f as OsfFile))], + [questionKey]: [ + ...this.attachedFiles[questionKey].map((f) => { + if (f.file_id) { + const { name, ...payload } = f; + return payload; + } + return FilesMapper.toFilePayload(f as OsfFile); + }), + ], }); } } 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 a27ce8fdc..526546184 100644 --- a/src/app/shared/components/files-tree/files-tree.component.ts +++ b/src/app/shared/components/files-tree/files-tree.component.ts @@ -149,7 +149,13 @@ export class FilesTreeComponent implements OnDestroy, AfterViewInit { openEntry(file: OsfFile) { if (file.kind === 'file') { - this.entryFileClicked.emit(file); + if (!file.guid) { + this.filesService.getFileGuid(file.id).subscribe((file) => { + this.entryFileClicked.emit(file); + }); + } else { + this.entryFileClicked.emit(file); + } } else { this.actions().setFilesIsLoading?.(true); this.folderIsOpening.emit(true); diff --git a/src/app/shared/mappers/index.ts b/src/app/shared/mappers/index.ts index 3eb5cee81..fcc201706 100644 --- a/src/app/shared/mappers/index.ts +++ b/src/app/shared/mappers/index.ts @@ -3,6 +3,7 @@ export * from './citations.mapper'; export * from './collections'; export * from './components'; export * from './contributors'; +export * from './files/files.mapper'; export * from './filters'; export * from './institutions'; export * from './licenses.mapper'; diff --git a/src/app/shared/services/files.service.ts b/src/app/shared/services/files.service.ts index 1e961e766..66cffc62a 100644 --- a/src/app/shared/services/files.service.ts +++ b/src/app/shared/services/files.service.ts @@ -160,7 +160,7 @@ export class FilesService { }; return this.jsonApiService - .get(`${environment.apiUrl}/files/${id}`, params) + .get(`${environment.apiUrl}/files/${id}/`, params) .pipe(map((response) => MapFile(response.data))); } @@ -190,7 +190,6 @@ export class FilesService { getProjectShortInfo(resourceId: string): Observable { const params = { 'fields[nodes]': 'title,description,date_created,date_modified', - embed: 'bibliographic_contributors', }; return this.jsonApiService.get(`${environment.apiUrl}/nodes/${resourceId}/`, params); } diff --git a/src/assets/styles/overrides/tree.scss b/src/assets/styles/overrides/tree.scss index c8edbddf1..60b70baff 100644 --- a/src/assets/styles/overrides/tree.scss +++ b/src/assets/styles/overrides/tree.scss @@ -35,7 +35,8 @@ } .p-tree-root { - overflow: hidden; + overflow: auto; + min-height: 180px; } .p-tree-node-content { From b989003ea37c819160a410c1d12383545a5640e9 Mon Sep 17 00:00:00 2001 From: NazarMykhalkevych Date: Mon, 18 Aug 2025 09:49:25 +0300 Subject: [PATCH 3/7] fix(files): add file link component, refactoring --- src/app/app.routes.ts | 5 + .../core/constants/ngxs-states.constant.ts | 2 + .../edit-file-metadata-dialog.component.html | 56 +++++ .../edit-file-metadata-dialog.component.scss | 0 ...dit-file-metadata-dialog.component.spec.ts | 22 ++ .../edit-file-metadata-dialog.component.ts | 69 ++++++ .../file-keywords.component.html | 24 ++ .../file-keywords.component.scss | 0 .../file-keywords.component.spec.ts | 22 ++ .../file-keywords/file-keywords.component.ts | 64 ++++++ .../file-metadata.component.html | 33 +++ .../file-metadata.component.scss | 0 .../file-metadata.component.spec.ts | 22 ++ .../file-metadata/file-metadata.component.ts | 70 ++++++ .../file-revisions.component.html | 60 +++++ .../file-revisions.component.scss | 0 .../file-revisions.component.spec.ts | 22 ++ .../file-revisions.component.ts | 52 +++++ src/app/features/files/components/index.ts | 4 + .../constants/default-state.constants.ts | 61 +++++ .../file-metadata-fields.constants.ts | 8 + .../constants/file-provider.constants.ts | 9 + src/app/features/files/constants/index.ts | 3 + .../files/enums/file-detail-tab.enum.ts | 5 + src/app/features/files/enums/index.ts | 1 + .../mappers/file-custom-metadata.mapper.ts | 12 + .../files/mappers/file-revision.mapper.ts | 12 + src/app/features/files/mappers/index.ts | 4 + .../files/mappers/resource-metadata.mapper.ts | 26 +++ .../models/file-custom-metadata.model.ts | 7 + .../files/models/file-revisions.model.ts | 9 + .../files/models/file-target.model.ts | 21 ++ .../models/files-metadata-fields.model.ts | 6 + ...resource-custom-metadata-response.model.ts | 29 +++ .../get-resource-short-info-response.model.ts | 16 ++ src/app/features/files/models/index.ts | 5 + .../files/models/patch-file-metadata.model.ts | 6 + .../file-detail/file-detail.component.html | 2 +- .../file-detail/file-detail.component.scss | 0 .../file-detail/file-detail.component.spec.ts | 0 .../file-detail/file-detail.component.ts | 92 ++++---- src/app/features/files/store/files.actions.ts | 84 +++++++ src/app/features/files/store/files.model.ts | 24 ++ .../features/files/store/files.selectors.ts | 125 ++++++++++ src/app/features/files/store/files.state.ts | 216 ++++++++++++++++++ src/app/features/files/store/index.ts | 3 + .../project/files/components/index.ts | 1 - .../models/osf-models/file-target.model.ts | 2 + .../project/files/project-files.routes.ts | 4 +- .../files/store/project-files.state.ts | 6 +- .../file-link/file-link.component.html | 14 ++ .../file-link/file-link.component.scss | 0 .../file-link/file-link.component.spec.ts | 22 ++ .../file-link/file-link.component.ts | 46 ++++ .../files-tree/files-tree.component.ts | 6 +- src/app/shared/components/index.ts | 1 + .../registration-blocks-data.component.html | 2 +- .../registration-blocks-data.component.ts | 4 +- src/app/shared/mappers/files/files.mapper.ts | 2 + src/app/shared/models/index.ts | 1 + .../shared/models/resource-metadata.model.ts | 16 ++ src/app/shared/services/files.service.ts | 19 +- 62 files changed, 1391 insertions(+), 68 deletions(-) create mode 100644 src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.html create mode 100644 src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.scss create mode 100644 src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.spec.ts create mode 100644 src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.ts create mode 100644 src/app/features/files/components/file-keywords/file-keywords.component.html create mode 100644 src/app/features/files/components/file-keywords/file-keywords.component.scss create mode 100644 src/app/features/files/components/file-keywords/file-keywords.component.spec.ts create mode 100644 src/app/features/files/components/file-keywords/file-keywords.component.ts create mode 100644 src/app/features/files/components/file-metadata/file-metadata.component.html create mode 100644 src/app/features/files/components/file-metadata/file-metadata.component.scss create mode 100644 src/app/features/files/components/file-metadata/file-metadata.component.spec.ts create mode 100644 src/app/features/files/components/file-metadata/file-metadata.component.ts create mode 100644 src/app/features/files/components/file-revisions/file-revisions.component.html create mode 100644 src/app/features/files/components/file-revisions/file-revisions.component.scss create mode 100644 src/app/features/files/components/file-revisions/file-revisions.component.spec.ts create mode 100644 src/app/features/files/components/file-revisions/file-revisions.component.ts create mode 100644 src/app/features/files/components/index.ts create mode 100644 src/app/features/files/constants/default-state.constants.ts create mode 100644 src/app/features/files/constants/file-metadata-fields.constants.ts create mode 100644 src/app/features/files/constants/file-provider.constants.ts create mode 100644 src/app/features/files/constants/index.ts create mode 100644 src/app/features/files/enums/file-detail-tab.enum.ts create mode 100644 src/app/features/files/enums/index.ts create mode 100644 src/app/features/files/mappers/file-custom-metadata.mapper.ts create mode 100644 src/app/features/files/mappers/file-revision.mapper.ts create mode 100644 src/app/features/files/mappers/index.ts create mode 100644 src/app/features/files/mappers/resource-metadata.mapper.ts create mode 100644 src/app/features/files/models/file-custom-metadata.model.ts create mode 100644 src/app/features/files/models/file-revisions.model.ts create mode 100644 src/app/features/files/models/file-target.model.ts create mode 100644 src/app/features/files/models/files-metadata-fields.model.ts create mode 100644 src/app/features/files/models/get-resource-custom-metadata-response.model.ts create mode 100644 src/app/features/files/models/get-resource-short-info-response.model.ts create mode 100644 src/app/features/files/models/index.ts create mode 100644 src/app/features/files/models/patch-file-metadata.model.ts rename src/app/features/{project => }/files/pages/file-detail/file-detail.component.html (97%) rename src/app/features/{project => }/files/pages/file-detail/file-detail.component.scss (100%) rename src/app/features/{project => }/files/pages/file-detail/file-detail.component.spec.ts (100%) rename src/app/features/{project => }/files/pages/file-detail/file-detail.component.ts (72%) create mode 100644 src/app/features/files/store/files.actions.ts create mode 100644 src/app/features/files/store/files.model.ts create mode 100644 src/app/features/files/store/files.selectors.ts create mode 100644 src/app/features/files/store/files.state.ts create mode 100644 src/app/features/files/store/index.ts create mode 100644 src/app/shared/components/file-link/file-link.component.html create mode 100644 src/app/shared/components/file-link/file-link.component.scss create mode 100644 src/app/shared/components/file-link/file-link.component.spec.ts create mode 100644 src/app/shared/components/file-link/file-link.component.ts create mode 100644 src/app/shared/models/resource-metadata.model.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 6f8e34e0f..35d77fc43 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -172,6 +172,11 @@ export const routes: Routes = [ import('./core/components/request-access/request-access.component').then((mod) => mod.RequestAccessComponent), data: { skipBreadcrumbs: true }, }, + { + path: 'files/:fileGuid', + loadComponent: () => + import('@osf/features/files/pages/file-detail/file-detail.component').then((c) => c.FileDetailComponent), + }, { path: '**', loadComponent: () => diff --git a/src/app/core/constants/ngxs-states.constant.ts b/src/app/core/constants/ngxs-states.constant.ts index de53c3aee..800a31029 100644 --- a/src/app/core/constants/ngxs-states.constant.ts +++ b/src/app/core/constants/ngxs-states.constant.ts @@ -1,6 +1,7 @@ import { ProviderState } from '@core/store/provider'; import { UserState } from '@core/store/user'; import { AuthState } from '@osf/features/auth/store'; +import { FilesState } from '@osf/features/files/store'; import { MeetingsState } from '@osf/features/meetings/store'; import { ProjectMetadataState } from '@osf/features/project/metadata/store'; import { ProjectOverviewState } from '@osf/features/project/overview/store'; @@ -30,4 +31,5 @@ export const STATES = [ ProjectMetadataState, LicensesState, RegionsState, + FilesState, ]; diff --git a/src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.html b/src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.html new file mode 100644 index 000000000..9abef56b5 --- /dev/null +++ b/src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.html @@ -0,0 +1,56 @@ +
+
+

{{ 'project.files.detail.fileMetadata.fields.title' | translate }}

+ +
+ +
+

{{ 'project.files.detail.fileMetadata.fields.description' | translate }}

+ +
+ +
+

{{ 'project.files.detail.fileMetadata.fields.resourceType' | translate }}

+ +
+ +
+

{{ 'project.files.detail.fileMetadata.fields.resourceLanguage' | translate }}

+ +
+ +
+ + +
+
diff --git a/src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.scss b/src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.spec.ts b/src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.spec.ts new file mode 100644 index 000000000..301630a95 --- /dev/null +++ b/src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EditFileMetadataDialogComponent } from './edit-file-metadata-dialog.component'; + +describe('EditFileMetadataDialogComponent', () => { + let component: EditFileMetadataDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [EditFileMetadataDialogComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(EditFileMetadataDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 000000000..03d360374 --- /dev/null +++ b/src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.ts @@ -0,0 +1,69 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; +import { DynamicDialogRef } from 'primeng/dynamicdialog'; +import { InputText } from 'primeng/inputtext'; +import { Select } from 'primeng/select'; + +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; + +import { resourceLanguages, resourceTypes } from '@osf/shared/constants'; + +import { PatchFileMetadata } from '../../models'; + +@Component({ + selector: 'osf-edit-file-metadata-dialog', + imports: [Button, InputText, Select, ReactiveFormsModule, TranslatePipe], + templateUrl: './edit-file-metadata-dialog.component.html', + styleUrl: './edit-file-metadata-dialog.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class EditFileMetadataDialogComponent { + protected readonly resourceTypes = resourceTypes; + protected readonly languages = resourceLanguages; + + private readonly dialogRef = inject(DynamicDialogRef); + + fileMetadataForm = new FormGroup({ + title: new FormControl(null), + description: new FormControl(null), + resourceType: new FormControl(null), + resourceLanguage: new FormControl(null), + }); + + get titleControl(): FormControl { + return this.fileMetadataForm.get('title') as FormControl; + } + + get descriptionControl(): FormControl { + return this.fileMetadataForm.get('description') as FormControl; + } + + get resourceTypeControl(): FormControl { + return this.fileMetadataForm.get('resourceType') as FormControl; + } + + get resourceLanguageControl(): FormControl { + return this.fileMetadataForm.get('resourceLanguage') as FormControl; + } + + setFileMetadata() { + if (this.fileMetadataForm.invalid) { + return; + } + + const formValues: PatchFileMetadata = { + title: this.fileMetadataForm.get('title')?.value ?? null, + description: this.fileMetadataForm.get('description')?.value ?? null, + resource_type_general: this.fileMetadataForm.get('resourceType')?.value ?? null, + language: this.fileMetadataForm.get('resourceLanguage')?.value ?? null, + }; + + this.dialogRef.close(formValues); + } + + cancel() { + this.dialogRef.close(); + } +} diff --git a/src/app/features/files/components/file-keywords/file-keywords.component.html b/src/app/features/files/components/file-keywords/file-keywords.component.html new file mode 100644 index 000000000..2b1bc0d9c --- /dev/null +++ b/src/app/features/files/components/file-keywords/file-keywords.component.html @@ -0,0 +1,24 @@ +
+

{{ 'project.files.detail.keywords.title' | translate }}

+ +
+ + + + +
+ + @if (!isTagsLoading()) { +
+ @for (tag of tags(); track $index) { + + } +
+ } @else { + + } +
diff --git a/src/app/features/files/components/file-keywords/file-keywords.component.scss b/src/app/features/files/components/file-keywords/file-keywords.component.scss new file mode 100644 index 000000000..e69de29bb 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 new file mode 100644 index 000000000..aeb0114a5 --- /dev/null +++ b/src/app/features/files/components/file-keywords/file-keywords.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FileKeywordsComponent } from './file-keywords.component'; + +describe('FileKeywordsComponent', () => { + let component: FileKeywordsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FileKeywordsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(FileKeywordsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/files/components/file-keywords/file-keywords.component.ts b/src/app/features/files/components/file-keywords/file-keywords.component.ts new file mode 100644 index 000000000..02fc592b8 --- /dev/null +++ b/src/app/features/files/components/file-keywords/file-keywords.component.ts @@ -0,0 +1,64 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; +import { Chip } from 'primeng/chip'; +import { InputText } from 'primeng/inputtext'; +import { Skeleton } from 'primeng/skeleton'; + +import { ChangeDetectionStrategy, Component, DestroyRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; + +import { InputLimits } from '@shared/constants'; +import { CustomValidators } from '@shared/utils'; + +import { FilesSelectors, UpdateTags } from '../../store'; + +@Component({ + selector: 'osf-file-keywords', + imports: [Button, Chip, Skeleton, InputText, ReactiveFormsModule, TranslatePipe], + templateUrl: './file-keywords.component.html', + styleUrls: ['./file-keywords.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FileKeywordsComponent { + private readonly actions = createDispatchMap({ updateTags: UpdateTags }); + private readonly destroyRef = inject(DestroyRef); + + readonly tags = select(FilesSelectors.getFileTags); + readonly isTagsLoading = select(FilesSelectors.isFileTagsLoading); + readonly file = select(FilesSelectors.getOpenedFile); + + keywordControl = new FormControl('', { + nonNullable: true, + validators: [CustomValidators.requiredTrimmed(), Validators.maxLength(InputLimits.name.maxLength)], + }); + + addTag(): void { + const fileGuid = this.file()?.guid; + + if (this.keywordControl.value && fileGuid) { + const updatedTags = [...this.tags(), this.keywordControl.value!]; + this.updateTags(updatedTags, fileGuid); + } + } + + deleteTag(value: string): void { + const fileGuid = this.file()?.guid; + + if (fileGuid) { + const updatedTags = [...this.tags()]; + updatedTags.splice(updatedTags.indexOf(value), 1); + this.updateTags(updatedTags, fileGuid); + } + } + + private updateTags(updatedTags: string[], fileGuid: string) { + this.actions + .updateTags(updatedTags, fileGuid) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => this.keywordControl.reset()); + } +} diff --git a/src/app/features/files/components/file-metadata/file-metadata.component.html b/src/app/features/files/components/file-metadata/file-metadata.component.html new file mode 100644 index 000000000..926738703 --- /dev/null +++ b/src/app/features/files/components/file-metadata/file-metadata.component.html @@ -0,0 +1,33 @@ +
+
+

{{ 'project.files.detail.fileMetadata.title' | translate }}

+ +
+ + + + + +
+
+ + @if (isLoading()) { + + + + + } @else { + @for (field of metadataFields; track field.key) { + @if (fileMetadata()?.[field.key]) { +
+

{{ field.label | translate }}

+

{{ fileMetadata()?.[field.key] }}

+
+ } + } + } +
diff --git a/src/app/features/files/components/file-metadata/file-metadata.component.scss b/src/app/features/files/components/file-metadata/file-metadata.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/files/components/file-metadata/file-metadata.component.spec.ts b/src/app/features/files/components/file-metadata/file-metadata.component.spec.ts new file mode 100644 index 000000000..c2f41a862 --- /dev/null +++ b/src/app/features/files/components/file-metadata/file-metadata.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FileMetadataComponent } from './file-metadata.component'; + +describe('FileMetadataComponent', () => { + let component: FileMetadataComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FileMetadataComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(FileMetadataComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 000000000..9c1a8a18b --- /dev/null +++ b/src/app/features/files/components/file-metadata/file-metadata.component.ts @@ -0,0 +1,70 @@ +import { createDispatchMap, select } from '@ngxs/store'; + +import { TranslatePipe, TranslateService } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; +import { DialogService } from 'primeng/dynamicdialog'; +import { Skeleton } from 'primeng/skeleton'; + +import { filter, map, of } from 'rxjs'; + +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { ActivatedRoute } from '@angular/router'; + +import { FileMetadataFields } from '../../constants'; +import { PatchFileMetadata } from '../../models'; +import { FilesSelectors, SetFileMetadata } from '../../store'; +import { EditFileMetadataDialogComponent } from '../edit-file-metadata-dialog/edit-file-metadata-dialog.component'; + +import { environment } from 'src/environments/environment'; + +@Component({ + selector: 'osf-file-metadata', + imports: [Button, Skeleton, TranslatePipe], + templateUrl: './file-metadata.component.html', + styleUrl: './file-metadata.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [DialogService], +}) +export class FileMetadataComponent { + private readonly actions = createDispatchMap({ setFileMetadata: SetFileMetadata }); + private readonly route = inject(ActivatedRoute); + private readonly dialogService = inject(DialogService); + private readonly translateService = inject(TranslateService); + + fileMetadata = select(FilesSelectors.getFileCustomMetadata); + isLoading = select(FilesSelectors.isFileMetadataLoading); + + readonly fileGuid = toSignal(this.route.params.pipe(map((params) => params['fileGuid'])) ?? of(undefined)); + + metadataFields = FileMetadataFields; + + setFileMetadata(formValues: PatchFileMetadata) { + const fileId = this.fileMetadata()?.id; + + if (fileId) { + this.actions.setFileMetadata(formValues, fileId); + } + } + + downloadFileMetadata(): void { + if (this.fileGuid()) { + window.open(`${environment.webUrl}/${this.fileGuid()}/metadata/?format=datacite-json`)?.focus(); + } + } + + openEditFileMetadataDialog() { + this.dialogService + .open(EditFileMetadataDialogComponent, { + width: '448px', + focusOnShow: false, + header: this.translateService.instant('project.files.detail.fileMetadata.edit'), + closeOnEscape: true, + modal: true, + closable: true, + }) + .onClose.pipe(filter((res: PatchFileMetadata) => !!res)) + .subscribe((res) => this.setFileMetadata(res)); + } +} diff --git a/src/app/features/files/components/file-revisions/file-revisions.component.html b/src/app/features/files/components/file-revisions/file-revisions.component.html new file mode 100644 index 000000000..7fb8c95a3 --- /dev/null +++ b/src/app/features/files/components/file-revisions/file-revisions.component.html @@ -0,0 +1,60 @@ +
+
+

{{ 'project.files.detail.revisions.title' | translate }}

+
+ + @if (isLoading()) { + + } @else { + + @for (item of fileRevisions(); track item.version) { + + + {{ item.version }}. {{ item.dateTime | date: 'MMM d, yyyy HH:mm' }} + + + +
    +
  • + + + +
  • + +
  • + + + +
  • + +
  • + + +

    {{ item.downloads }}

    +
  • +
+
+
+ } +
+ } +
diff --git a/src/app/features/files/components/file-revisions/file-revisions.component.scss b/src/app/features/files/components/file-revisions/file-revisions.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/features/files/components/file-revisions/file-revisions.component.spec.ts b/src/app/features/files/components/file-revisions/file-revisions.component.spec.ts new file mode 100644 index 000000000..980be66eb --- /dev/null +++ b/src/app/features/files/components/file-revisions/file-revisions.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FileRevisionsComponent } from './file-revisions.component'; + +describe('FileRevisionsComponent', () => { + let component: FileRevisionsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FileRevisionsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(FileRevisionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 000000000..c2f931caf --- /dev/null +++ b/src/app/features/files/components/file-revisions/file-revisions.component.ts @@ -0,0 +1,52 @@ +import { select } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; + +import { Accordion, AccordionContent, AccordionHeader, AccordionPanel } from 'primeng/accordion'; +import { Button } from 'primeng/button'; +import { Skeleton } from 'primeng/skeleton'; + +import { map, of } from 'rxjs'; + +import { DatePipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { ActivatedRoute } from '@angular/router'; + +import { CopyButtonComponent, InfoIconComponent } from '@shared/components'; + +import { FilesSelectors } from '../../store'; + +import { environment } from 'src/environments/environment'; + +@Component({ + selector: 'osf-file-revisions', + templateUrl: './file-revisions.component.html', + styleUrls: ['./file-revisions.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + Accordion, + AccordionPanel, + AccordionHeader, + AccordionContent, + Button, + DatePipe, + TranslatePipe, + InfoIconComponent, + CopyButtonComponent, + Skeleton, + ], +}) +export class FileRevisionsComponent { + private readonly route = inject(ActivatedRoute); + + readonly fileRevisions = select(FilesSelectors.getFileRevisions); + readonly isLoading = select(FilesSelectors.isFileRevisionsLoading); + readonly fileGuid = toSignal(this.route.params.pipe(map((params) => params['fileGuid'])) ?? of(undefined)); + + downloadRevision(version: string): void { + if (this.fileGuid()) { + window.open(`${environment.downloadUrl}/${this.fileGuid()}/?revision=${version}`)?.focus(); + } + } +} diff --git a/src/app/features/files/components/index.ts b/src/app/features/files/components/index.ts new file mode 100644 index 000000000..7925a2597 --- /dev/null +++ b/src/app/features/files/components/index.ts @@ -0,0 +1,4 @@ +export { EditFileMetadataDialogComponent } from './edit-file-metadata-dialog/edit-file-metadata-dialog.component'; +export { FileKeywordsComponent } from './file-keywords/file-keywords.component'; +export { FileMetadataComponent } from './file-metadata/file-metadata.component'; +export { FileRevisionsComponent } from './file-revisions/file-revisions.component'; diff --git a/src/app/features/files/constants/default-state.constants.ts b/src/app/features/files/constants/default-state.constants.ts new file mode 100644 index 000000000..465951ae3 --- /dev/null +++ b/src/app/features/files/constants/default-state.constants.ts @@ -0,0 +1,61 @@ +import { FilesStateModel } from '../store/files.model'; + +import { FileProvider } from './file-provider.constants'; + +export const filesStateDefaults: FilesStateModel = { + files: { + data: [], + isLoading: false, + error: null, + }, + moveFileFiles: { + data: [], + isLoading: false, + error: null, + }, + currentFolder: null, + moveFileCurrentFolder: null, + search: '', + sort: 'name', + provider: FileProvider.OsfStorage, + openedFile: { + data: null, + isLoading: false, + error: null, + }, + fileMetadata: { + data: null, + isLoading: false, + error: null, + }, + resourceMetadata: { + data: null, + isLoading: false, + error: null, + }, + contributors: { + data: null, + isLoading: false, + error: null, + }, + fileRevisions: { + data: null, + isLoading: false, + error: null, + }, + tags: { + data: [], + isLoading: false, + error: null, + }, + rootFolders: { + data: [], + isLoading: true, + error: null, + }, + configuredStorageAddons: { + data: [], + isLoading: true, + error: null, + }, +}; diff --git a/src/app/features/files/constants/file-metadata-fields.constants.ts b/src/app/features/files/constants/file-metadata-fields.constants.ts new file mode 100644 index 000000000..6aadeb040 --- /dev/null +++ b/src/app/features/files/constants/file-metadata-fields.constants.ts @@ -0,0 +1,8 @@ +import { MetadataField } from '../models'; + +export const FileMetadataFields: MetadataField[] = [ + { key: 'title', label: 'project.files.detail.fileMetadata.fields.title' }, + { key: 'description', label: 'project.files.detail.fileMetadata.fields.description' }, + { key: 'resourceTypeGeneral', label: 'project.files.detail.fileMetadata.fields.resourceType' }, + { key: 'language', label: 'project.files.detail.fileMetadata.fields.resourceLanguage' }, +]; diff --git a/src/app/features/files/constants/file-provider.constants.ts b/src/app/features/files/constants/file-provider.constants.ts new file mode 100644 index 000000000..546a640d5 --- /dev/null +++ b/src/app/features/files/constants/file-provider.constants.ts @@ -0,0 +1,9 @@ +export const FileProvider = { + OsfStorage: 'osfstorage', + GoogleDrive: 'google-drive', + Box: 'box', + DropBox: 'dropbox', + OneDrive: 'onedrive', + WebDav: 'webdav', + S3: 's3', +}; diff --git a/src/app/features/files/constants/index.ts b/src/app/features/files/constants/index.ts new file mode 100644 index 000000000..c1f029a52 --- /dev/null +++ b/src/app/features/files/constants/index.ts @@ -0,0 +1,3 @@ +export * from './default-state.constants'; +export * from './file-metadata-fields.constants'; +export * from './file-provider.constants'; diff --git a/src/app/features/files/enums/file-detail-tab.enum.ts b/src/app/features/files/enums/file-detail-tab.enum.ts new file mode 100644 index 000000000..4c6c7151d --- /dev/null +++ b/src/app/features/files/enums/file-detail-tab.enum.ts @@ -0,0 +1,5 @@ +export enum FileDetailTab { + Details = 1, + Revisions, + Keywords, +} diff --git a/src/app/features/files/enums/index.ts b/src/app/features/files/enums/index.ts new file mode 100644 index 000000000..01282d755 --- /dev/null +++ b/src/app/features/files/enums/index.ts @@ -0,0 +1 @@ +export * from './file-detail-tab.enum'; diff --git a/src/app/features/files/mappers/file-custom-metadata.mapper.ts b/src/app/features/files/mappers/file-custom-metadata.mapper.ts new file mode 100644 index 000000000..2611fe4c8 --- /dev/null +++ b/src/app/features/files/mappers/file-custom-metadata.mapper.ts @@ -0,0 +1,12 @@ +import { ApiData } from '@osf/core/models'; +import { FileCustomMetadata, OsfFileCustomMetadata } from '@osf/features/project/files/models'; + +export function MapFileCustomMetadata(data: ApiData): OsfFileCustomMetadata { + return { + id: data.id, + description: data.attributes.description, + language: data.attributes.language, + resourceTypeGeneral: data.attributes.resource_type_general, + title: data.attributes.title, + }; +} diff --git a/src/app/features/files/mappers/file-revision.mapper.ts b/src/app/features/files/mappers/file-revision.mapper.ts new file mode 100644 index 000000000..9b340934f --- /dev/null +++ b/src/app/features/files/mappers/file-revision.mapper.ts @@ -0,0 +1,12 @@ +import { ApiData } from '@core/models'; +import { OsfFileRevision } from '@osf/features/project/files/models/osf-models/file-revisions.model'; +import { FileRevisionJsonApi } from '@osf/features/project/files/models/responses/get-file-revisions-response.model'; + +export function MapFileRevision(data: ApiData[]): OsfFileRevision[] { + return data.map((revision) => ({ + downloads: revision.attributes.extra.downloads, + hashes: { md5: revision.attributes.extra.hashes.md5, sha256: revision.attributes.extra.hashes.sha256 }, + dateTime: new Date(revision.attributes.modified_utc), + version: revision.attributes.version, + })); +} diff --git a/src/app/features/files/mappers/index.ts b/src/app/features/files/mappers/index.ts new file mode 100644 index 000000000..bfa419062 --- /dev/null +++ b/src/app/features/files/mappers/index.ts @@ -0,0 +1,4 @@ +export * from './file-custom-metadata.mapper'; +export * from './file-revision.mapper'; +export * from './project-metadata.mapper'; +export * from '@shared/mappers/files/files.mapper'; diff --git a/src/app/features/files/mappers/resource-metadata.mapper.ts b/src/app/features/files/mappers/resource-metadata.mapper.ts new file mode 100644 index 000000000..c4f1bf7bf --- /dev/null +++ b/src/app/features/files/mappers/resource-metadata.mapper.ts @@ -0,0 +1,26 @@ +import { ResourceMetadata } from '@osf/shared/models'; + +import { GetResourceCustomMetadataResponse } from '../models/get-resource-custom-metadata-response.model'; +import { GetResourceShortInfoResponse } from '../models/get-resource-short-info-response.model'; + +export function MapResourceMetadata( + shortInfo: GetResourceShortInfoResponse, + customMetadata: GetResourceCustomMetadataResponse +): ResourceMetadata { + return { + title: shortInfo.data.attributes.title, + description: shortInfo.data.attributes.description, + dateCreated: new Date(shortInfo.data.attributes.date_created), + dateModified: new Date(shortInfo.data.attributes.date_modified), + funders: customMetadata.data.embeds.custom_metadata.data.attributes.funders.map((funder) => ({ + funderName: funder.funder_name, + funderIdentifier: funder.funder_identifier, + funderIdentifierType: funder.funder_identifier_type, + awardNumber: funder.award_number, + awardUri: funder.award_uri, + awardTitle: funder.award_title, + })), + language: customMetadata.data.embeds.custom_metadata.data.attributes.language, + resourceTypeGeneral: customMetadata.data.embeds.custom_metadata.data.attributes.resource_type_general, + }; +} diff --git a/src/app/features/files/models/file-custom-metadata.model.ts b/src/app/features/files/models/file-custom-metadata.model.ts new file mode 100644 index 000000000..aace1c784 --- /dev/null +++ b/src/app/features/files/models/file-custom-metadata.model.ts @@ -0,0 +1,7 @@ +export interface OsfFileCustomMetadata { + id: string; + language: string; + resourceTypeGeneral: string; + title: string; + description: string; +} diff --git a/src/app/features/files/models/file-revisions.model.ts b/src/app/features/files/models/file-revisions.model.ts new file mode 100644 index 000000000..84148c1d7 --- /dev/null +++ b/src/app/features/files/models/file-revisions.model.ts @@ -0,0 +1,9 @@ +export interface OsfFileRevision { + downloads: 0; + hashes: { + md5: string; + sha256: string; + }; + version: string; + dateTime: Date; +} diff --git a/src/app/features/files/models/file-target.model.ts b/src/app/features/files/models/file-target.model.ts new file mode 100644 index 000000000..84485a11e --- /dev/null +++ b/src/app/features/files/models/file-target.model.ts @@ -0,0 +1,21 @@ +export interface OsfFileTarget { + title: string; + description: string; + category: string; + customCitation: string | null; + dateCreated: string; + dateModified: string; + registration: boolean; + preprint: boolean; + fork: boolean; + collection: boolean; + tags: string[]; + nodeLicense: string | null; + analyticsKey: string; + currentUserCanComment: boolean; + currentUserPermissions: string[]; + currentUserIsContributor: boolean; + currentUserIsContributorOrGroupMember: boolean; + wikiEnabled: boolean; + public: boolean; +} diff --git a/src/app/features/files/models/files-metadata-fields.model.ts b/src/app/features/files/models/files-metadata-fields.model.ts new file mode 100644 index 000000000..7d06ec9a1 --- /dev/null +++ b/src/app/features/files/models/files-metadata-fields.model.ts @@ -0,0 +1,6 @@ +import { OsfFileCustomMetadata } from './file-custom-metadata.model'; + +export interface MetadataField { + key: keyof OsfFileCustomMetadata; + label: string; +} diff --git a/src/app/features/files/models/get-resource-custom-metadata-response.model.ts b/src/app/features/files/models/get-resource-custom-metadata-response.model.ts new file mode 100644 index 000000000..5247896ae --- /dev/null +++ b/src/app/features/files/models/get-resource-custom-metadata-response.model.ts @@ -0,0 +1,29 @@ +import { ApiData, JsonApiResponse } from '@core/models'; + +export type GetResourceCustomMetadataResponse = JsonApiResponse< + ApiData, + null +>; + +export interface ResourceMetadataEmbedResponse { + custom_metadata: JsonApiResponse< + ApiData< + { + language: string; + resource_type_general: string; + funders: { + funder_name: string; + funder_identifier: string; + funder_identifier_type: string; + award_number: string; + award_uri: string; + award_title: string; + }[]; + }, + null, + null, + null + >, + null + >; +} diff --git a/src/app/features/files/models/get-resource-short-info-response.model.ts b/src/app/features/files/models/get-resource-short-info-response.model.ts new file mode 100644 index 000000000..8e38daf5f --- /dev/null +++ b/src/app/features/files/models/get-resource-short-info-response.model.ts @@ -0,0 +1,16 @@ +import { ApiData, JsonApiResponse } from '@core/models'; + +export type GetResourceShortInfoResponse = JsonApiResponse< + ApiData< + { + title: string; + description: string; + date_created: string; + date_modified: string; + }, + null, + null, + null + >, + null +>; diff --git a/src/app/features/files/models/index.ts b/src/app/features/files/models/index.ts new file mode 100644 index 000000000..d66cc146f --- /dev/null +++ b/src/app/features/files/models/index.ts @@ -0,0 +1,5 @@ +export * from './file-custom-metadata.model'; +export * from './file-revisions.model'; +export * from './file-target.model'; +export * from './files-metadata-fields.model'; +export * from './patch-file-metadata.model'; diff --git a/src/app/features/files/models/patch-file-metadata.model.ts b/src/app/features/files/models/patch-file-metadata.model.ts new file mode 100644 index 000000000..b2a9869dc --- /dev/null +++ b/src/app/features/files/models/patch-file-metadata.model.ts @@ -0,0 +1,6 @@ +export interface PatchFileMetadata { + description: string | null; + language: string | null; + title: string | null; + resource_type_general: string | null; +} diff --git a/src/app/features/project/files/pages/file-detail/file-detail.component.html b/src/app/features/files/pages/file-detail/file-detail.component.html similarity index 97% rename from src/app/features/project/files/pages/file-detail/file-detail.component.html rename to src/app/features/files/pages/file-detail/file-detail.component.html index 0ef4b7ea3..bd88a1098 100644 --- a/src/app/features/project/files/pages/file-detail/file-detail.component.html +++ b/src/app/features/files/pages/file-detail/file-detail.component.html @@ -91,7 +91,7 @@ } @else { - + } diff --git a/src/app/features/project/files/pages/file-detail/file-detail.component.scss b/src/app/features/files/pages/file-detail/file-detail.component.scss similarity index 100% rename from src/app/features/project/files/pages/file-detail/file-detail.component.scss rename to src/app/features/files/pages/file-detail/file-detail.component.scss diff --git a/src/app/features/project/files/pages/file-detail/file-detail.component.spec.ts b/src/app/features/files/pages/file-detail/file-detail.component.spec.ts similarity index 100% rename from src/app/features/project/files/pages/file-detail/file-detail.component.spec.ts rename to src/app/features/files/pages/file-detail/file-detail.component.spec.ts diff --git a/src/app/features/project/files/pages/file-detail/file-detail.component.ts b/src/app/features/files/pages/file-detail/file-detail.component.ts similarity index 72% rename from src/app/features/project/files/pages/file-detail/file-detail.component.ts rename to src/app/features/files/pages/file-detail/file-detail.component.ts index bd8cb5be6..d5db8aa4a 100644 --- a/src/app/features/project/files/pages/file-detail/file-detail.component.ts +++ b/src/app/features/files/pages/file-detail/file-detail.component.ts @@ -1,4 +1,4 @@ -import { select, Store } from '@ngxs/store'; +import { createDispatchMap, select, Store } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; @@ -6,33 +6,28 @@ import { Button } from 'primeng/button'; import { Menu } from 'primeng/menu'; import { Tab, TabList, Tabs } from 'primeng/tabs'; -import { EMPTY, switchMap } from 'rxjs'; +import { switchMap } from 'rxjs'; import { ChangeDetectionStrategy, Component, DestroyRef, HostBinding, inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; -import { - FileKeywordsComponent, - FileMetadataComponent, - FileProjectMetadataComponent, - FileRevisionsComponent, -} from '@osf/features/project/files/components'; -import { FileDetailTab } from '@osf/features/project/files/enums/file-detail-tab.enum'; import { embedDynamicJs, embedStaticHtml } from '@osf/features/project/files/models'; +import { LoadingSpinnerComponent, SubHeaderComponent } from '@shared/components'; +import { OsfFile } from '@shared/models'; +import { CustomConfirmationService, ToastService } from '@shared/services'; + +import { FileKeywordsComponent, FileMetadataComponent, FileRevisionsComponent } from '../../components'; +import { FileDetailTab } from '../../enums'; import { - DeleteEntry, + FilesSelectors, GetFile, GetFileMetadata, - GetFileProjectContributors, - GetFileProjectMetadata, + GetFileResourceContributors, + GetFileResourceMetadata, GetFileRevisions, - ProjectFilesSelectors, -} from '@osf/features/project/files/store'; -import { LoadingSpinnerComponent, SubHeaderComponent } from '@shared/components'; -import { OsfFile } from '@shared/models'; -import { CustomConfirmationService, ToastService } from '@shared/services'; +} from '../../store'; @Component({ selector: 'osf-file-detail', @@ -49,7 +44,6 @@ import { CustomConfirmationService, ToastService } from '@shared/services'; FileKeywordsComponent, FileRevisionsComponent, FileMetadataComponent, - FileProjectMetadataComponent, ], templateUrl: './file-detail.component.html', styleUrl: './file-detail.component.scss', @@ -66,10 +60,19 @@ export class FileDetailComponent { readonly toastService = inject(ToastService); readonly customConfirmationService = inject(CustomConfirmationService); - file = select(ProjectFilesSelectors.getOpenedFile); - isFileLoading = select(ProjectFilesSelectors.isOpenedFileLoading); + private readonly actions = createDispatchMap({ + getFile: GetFile, + getFileRevisions: GetFileRevisions, + getFileMetadata: GetFileMetadata, + getFileResourceMetadata: GetFileResourceMetadata, + getFileResourceContributors: GetFileResourceContributors, + }); + + file = select(FilesSelectors.getOpenedFile); + isFileLoading = select(FilesSelectors.isOpenedFileLoading); safeLink: SafeResourceUrl | null = null; - projectId: string | null = null; + resourceId = ''; + resourceType = ''; isIframeLoading = true; @@ -106,20 +109,18 @@ export class FileDetailComponent { ]; constructor() { - this.route.parent?.parent?.parent?.parent?.params.subscribe((params) => { - if (params['id']) { - this.projectId = params['id']; - } - }); + // this.route.parent?.parent?.parent?.parent?.params.subscribe((params) => { + // if (params['id']) { + // this.projectId = params['id']; + // } + // }); this.route.params .pipe( takeUntilDestroyed(this.destroyRef), switchMap((params) => { this.fileGuid = params['fileGuid']; - return this.store - .dispatch(new GetFile(this.fileGuid)) - .pipe(switchMap(() => this.route.parent?.parent?.parent?.params || EMPTY)); + return this.actions.getFile(this.fileGuid); }) ) .subscribe((parentParams) => { @@ -127,20 +128,22 @@ export class FileDetailComponent { if (link) { this.safeLink = this.sanitizer.bypassSecurityTrustResourceUrl(link); } + this.resourceId = this.file()?.target.id || ''; + this.resourceType = this.file()?.target.type || ''; + const fileId = this.file()?.path.replaceAll('/', ''); + // this.projectId = parentParams['id']; + if (this.resourceId && this.resourceType) { + this.actions.getFileResourceMetadata(this.resourceId, this.resourceType); + this.actions.getFileResourceContributors(this.resourceId, this.resourceType); - this.projectId = parentParams['id']; - if (this.projectId) { - this.store.dispatch(new GetFileProjectMetadata(this.projectId)); - this.store.dispatch(new GetFileProjectContributors(this.projectId)); - const fileId = this.file()?.path.replaceAll('/', ''); if (fileId) { - this.store.dispatch(new GetFileRevisions(this.projectId, fileId)); + this.actions.getFileRevisions(this.resourceId, this.resourceType, fileId); } } }); this.route.params.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params) => { - this.store.dispatch(new GetFileMetadata(params['fileGuid'])); + this.actions.getFileMetadata(params['fileGuid']); }); } @@ -160,14 +163,15 @@ export class FileDetailComponent { } deleteEntry(link: string): void { - if (this.projectId) { - this.store - .dispatch(new DeleteEntry(this.projectId, link)) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(() => { - this.router.navigate(['/project', this.projectId, 'files']); - }); - } + console.log('Delete entry link:', link); + // if (this.projectId) { + // this.store + // .dispatch(new DeleteEntry(this.projectId, link)) + // .pipe(takeUntilDestroyed(this.destroyRef)) + // .subscribe(() => { + // this.router.navigate(['/project', this.projectId, 'files']); + // }); + // } } confirmDelete(file: OsfFile): void { diff --git a/src/app/features/files/store/files.actions.ts b/src/app/features/files/store/files.actions.ts new file mode 100644 index 000000000..4723d621e --- /dev/null +++ b/src/app/features/files/store/files.actions.ts @@ -0,0 +1,84 @@ +import { PatchFileMetadata } from '../models'; + +export class GetFile { + static readonly type = '[Files] Get File'; + + constructor(public fileGuid: string) {} +} + +export class GetFileMetadata { + static readonly type = '[Files] Get File Metadata'; + + constructor(public fileGuid: string) {} +} + +export class GetFileResourceMetadata { + static readonly type = '[Files] Get File Resource Metadata'; + + constructor( + public resourceId: string, + public resourceType: string + ) {} +} + +export class GetFileResourceContributors { + static readonly type = '[Files] Get File Resource Contributors'; + + constructor( + public resourceId: string, + public resourceType: string + ) {} +} + +export class SetFileMetadata { + static readonly type = '[Files] Set File Metadata'; + + constructor( + public payload: PatchFileMetadata, + public fileGuid: string + ) {} +} + +export class GetFileRevisions { + static readonly type = '[Files] Get Revisions'; + + constructor( + public resourceId: string, + public resourceType: string, + public fileId: string + ) {} +} + +export class UpdateTags { + static readonly type = '[Files] Update Tags'; + + constructor( + public tags: string[], + public fileGuid: string + ) {} +} + +export class DeleteEntry { + static readonly type = '[Files] Delete entry'; + + constructor( + public resourceId: string, + public link: string + ) {} +} + +export class GetRootFolders { + static readonly type = '[Files] Get Folders'; + + constructor(public folderLink: string) {} +} + +export class GetConfiguredStorageAddons { + static readonly type = '[Files] Get ConfiguredStorageAddons'; + + constructor(public resourceUri: string) {} +} + +export class ResetState { + static readonly type = '[Files] Reset State'; +} diff --git a/src/app/features/files/store/files.model.ts b/src/app/features/files/store/files.model.ts new file mode 100644 index 000000000..5d9fe51a4 --- /dev/null +++ b/src/app/features/files/store/files.model.ts @@ -0,0 +1,24 @@ +import { ContributorModel, OsfFile, ResourceMetadata } from '@shared/models'; +import { ConfiguredStorageAddon } from '@shared/models/addons'; +import { AsyncStateModel } from '@shared/models/store'; + +import { FileProvider } from '../constants'; +import { OsfFileCustomMetadata, OsfFileRevision } from '../models'; + +export interface FilesStateModel { + files: AsyncStateModel; + moveFileFiles: AsyncStateModel; + currentFolder: OsfFile | null; + moveFileCurrentFolder: OsfFile | null; + search: string; + sort: string; + provider: (typeof FileProvider)[keyof typeof FileProvider]; + openedFile: AsyncStateModel; + fileMetadata: AsyncStateModel; + resourceMetadata: AsyncStateModel; + contributors: AsyncStateModel[] | null>; + fileRevisions: AsyncStateModel; + tags: AsyncStateModel; + rootFolders: AsyncStateModel; + configuredStorageAddons: AsyncStateModel; +} diff --git a/src/app/features/files/store/files.selectors.ts b/src/app/features/files/store/files.selectors.ts new file mode 100644 index 000000000..da448f067 --- /dev/null +++ b/src/app/features/files/store/files.selectors.ts @@ -0,0 +1,125 @@ +import { Selector } from '@ngxs/store'; + +import { ConfiguredStorageAddon, ContributorModel, OsfFile, ResourceMetadata } from '@shared/models'; + +import { OsfFileCustomMetadata, OsfFileRevision } from '../models'; + +import { FilesStateModel } from './files.model'; +import { FilesState } from './files.state'; + +export class FilesSelectors { + @Selector([FilesState]) + static getFiles(state: FilesStateModel): OsfFile[] { + return state.files.data; + } + + @Selector([FilesState]) + static isFilesLoading(state: FilesStateModel): boolean { + return state.files.isLoading; + } + + @Selector([FilesState]) + static getMoveFileFiles(state: FilesStateModel): OsfFile[] { + return state.moveFileFiles.data; + } + + @Selector([FilesState]) + static isMoveFileFilesLoading(state: FilesStateModel): boolean { + return state.moveFileFiles.isLoading; + } + + @Selector([FilesState]) + static getCurrentFolder(state: FilesStateModel): OsfFile | null { + return state.currentFolder; + } + + @Selector([FilesState]) + static getMoveFileCurrentFolder(state: FilesStateModel): OsfFile | null { + return state.moveFileCurrentFolder; + } + + @Selector([FilesState]) + static getProvider(state: FilesStateModel): string { + return state.provider; + } + + @Selector([FilesState]) + static getOpenedFile(state: FilesStateModel): OsfFile | null { + return state.openedFile.data; + } + + @Selector([FilesState]) + static isOpenedFileLoading(state: FilesStateModel): boolean { + return state.openedFile.isLoading; + } + + @Selector([FilesState]) + static getFileCustomMetadata(state: FilesStateModel): OsfFileCustomMetadata | null { + return state.fileMetadata.data; + } + + @Selector([FilesState]) + static isFileMetadataLoading(state: FilesStateModel): boolean { + return state.fileMetadata.isLoading; + } + + @Selector([FilesState]) + static getMetadata(state: FilesStateModel): ResourceMetadata | null { + return state.resourceMetadata.data; + } + + @Selector([FilesState]) + static isMetadataLoading(state: FilesStateModel): boolean { + return state.resourceMetadata.isLoading; + } + + @Selector([FilesState]) + static getContributors(state: FilesStateModel): Partial[] | null { + return state.contributors.data; + } + + @Selector([FilesState]) + static isContributorsLoading(state: FilesStateModel): boolean { + return state.contributors.isLoading; + } + + @Selector([FilesState]) + static getFileRevisions(state: FilesStateModel): OsfFileRevision[] | null { + return state.fileRevisions.data; + } + + @Selector([FilesState]) + static isFileRevisionsLoading(state: FilesStateModel): boolean { + return state.fileRevisions.isLoading; + } + + @Selector([FilesState]) + static getFileTags(state: FilesStateModel): string[] { + return state.tags.data; + } + + @Selector([FilesState]) + static isFileTagsLoading(state: FilesStateModel): boolean { + return state.tags.isLoading; + } + + @Selector([FilesState]) + static getRootFolders(state: FilesStateModel): OsfFile[] | null { + return state.rootFolders.data; + } + + @Selector([FilesState]) + static isRootFoldersLoading(state: FilesStateModel): boolean { + return state.rootFolders.isLoading; + } + + @Selector([FilesState]) + static getConfiguredStorageAddons(state: FilesStateModel): ConfiguredStorageAddon[] | null { + return state.configuredStorageAddons.data; + } + + @Selector([FilesState]) + static isConfiguredStorageAddonsLoading(state: FilesStateModel): boolean { + return state.configuredStorageAddons.isLoading; + } +} diff --git a/src/app/features/files/store/files.state.ts b/src/app/features/files/store/files.state.ts new file mode 100644 index 000000000..3d4d615f5 --- /dev/null +++ b/src/app/features/files/store/files.state.ts @@ -0,0 +1,216 @@ +import { Action, State, StateContext } from '@ngxs/store'; + +import { catchError, finalize, forkJoin, tap } from 'rxjs'; + +import { inject, Injectable } from '@angular/core'; + +import { handleSectionError } from '@osf/core/handlers'; +import { FilesService, ToastService } from '@shared/services'; + +import { filesStateDefaults } from '../constants'; +import { MapResourceMetadata } from '../mappers/resource-metadata.mapper'; + +import { + DeleteEntry, + GetConfiguredStorageAddons, + GetFile, + GetFileMetadata, + GetFileResourceContributors, + GetFileResourceMetadata, + GetFileRevisions, + GetRootFolders, + ResetState, + SetFileMetadata, + UpdateTags, +} from './files.actions'; +import { FilesStateModel } from './files.model'; + +@Injectable() +@State({ + name: 'filesState', + defaults: filesStateDefaults, +}) +export class FilesState { + filesService = inject(FilesService); + toastService = inject(ToastService); + + @Action(GetFile) + getFile(ctx: StateContext, action: GetFile) { + const state = ctx.getState(); + ctx.patchState({ openedFile: { ...state.openedFile, isLoading: true, error: null } }); + ctx.patchState({ tags: { ...state.tags, isLoading: true, error: null } }); + + return this.filesService.getFileTarget(action.fileGuid).pipe( + tap({ + next: (file) => { + ctx.patchState({ openedFile: { data: file, isLoading: false, error: null } }); + ctx.patchState({ tags: { data: file.tags, isLoading: false, error: null } }); + }, + }), + catchError((error) => handleSectionError(ctx, 'openedFile', error)) + ); + } + + @Action(GetFileMetadata) + getFileMetadata(ctx: StateContext, action: GetFileMetadata) { + const state = ctx.getState(); + ctx.patchState({ fileMetadata: { ...state.fileMetadata, isLoading: true, error: null } }); + + return this.filesService.getFileMetadata(action.fileGuid).pipe( + tap({ + next: (metadata) => { + ctx.patchState({ fileMetadata: { data: metadata, isLoading: false, error: null } }); + }, + }), + catchError((error) => handleSectionError(ctx, 'fileMetadata', error)) + ); + } + + @Action(SetFileMetadata) + setFileMetadata(ctx: StateContext, action: SetFileMetadata) { + const state = ctx.getState(); + ctx.patchState({ fileMetadata: { ...state.fileMetadata, isLoading: true, error: null } }); + + return this.filesService.patchFileMetadata(action.payload, action.fileGuid).pipe( + tap({ + next: (fileMetadata) => { + if (fileMetadata.id) { + ctx.patchState({ fileMetadata: { data: fileMetadata, isLoading: false, error: null } }); + } + }, + }), + catchError((error) => handleSectionError(ctx, 'fileMetadata', error)) + ); + } + + @Action(DeleteEntry) + deleteEntry(ctx: StateContext, action: DeleteEntry) { + return this.filesService.deleteEntry(action.link).pipe( + tap({ + next: () => { + const selectedFolder = ctx.getState().currentFolder; + // if (selectedFolder?.relationships.filesLink) { + // ctx.dispatch(new GetFiles(selectedFolder?.relationships.filesLink)); + // } else { + // ctx.dispatch(new GetRootFolderFiles(action.resourceId)); + // } + }, + }) + ); + } + + @Action(GetFileResourceMetadata) + getFileResourceMetadata(ctx: StateContext, action: GetFileResourceMetadata) { + const state = ctx.getState(); + ctx.patchState({ resourceMetadata: { ...state.resourceMetadata, isLoading: true, error: null } }); + + forkJoin({ + projectShortInfo: this.filesService.getResourceShortInfo(action.resourceId, action.resourceType), + resourceMetadata: this.filesService.getCustomMetadata(action.resourceId), + }) + .pipe(catchError((error) => handleSectionError(ctx, 'resourceMetadata', error))) + .subscribe((results) => { + const resourceMetadata = MapResourceMetadata(results.projectShortInfo, results.resourceMetadata); + ctx.patchState({ + resourceMetadata: { + data: resourceMetadata, + isLoading: false, + error: null, + }, + }); + }); + } + + @Action(GetFileResourceContributors) + getFileResourceContributors(ctx: StateContext, action: GetFileResourceContributors) { + const state = ctx.getState(); + ctx.patchState({ contributors: { ...state.contributors, isLoading: true, error: null } }); + + return this.filesService.getResourceContributors(action.resourceId, action.resourceType).pipe( + tap({ + next: (contributors) => { + ctx.patchState({ contributors: { data: contributors, isLoading: false, error: null } }); + }, + }), + catchError((error) => handleSectionError(ctx, 'contributors', error)) + ); + } + + @Action(GetFileRevisions) + getFileRevisions(ctx: StateContext, action: GetFileRevisions) { + const state = ctx.getState(); + ctx.patchState({ fileRevisions: { ...state.fileRevisions, isLoading: true, error: null } }); + + return this.filesService.getFileRevisions(action.resourceId, state.provider, action.fileId).pipe( + tap({ + next: (revisions) => { + ctx.patchState({ fileRevisions: { data: revisions, isLoading: false, error: null } }); + }, + }), + catchError((error) => handleSectionError(ctx, 'fileRevisions', error)) + ); + } + + @Action(UpdateTags) + updateTags(ctx: StateContext, action: UpdateTags) { + const state = ctx.getState(); + ctx.patchState({ tags: { ...state.tags, isLoading: true, error: null } }); + + return this.filesService.updateTags(action.tags, action.fileGuid).pipe( + tap({ + next: (file) => { + ctx.patchState({ tags: { data: file.tags, isLoading: false, error: null } }); + }, + }), + catchError((error) => handleSectionError(ctx, 'tags', error)) + ); + } + + @Action(GetRootFolders) + getRootFolders(ctx: StateContext, action: GetRootFolders) { + const state = ctx.getState(); + ctx.patchState({ rootFolders: { ...state.rootFolders, isLoading: true } }); + + return this.filesService.getFolders(action.folderLink).pipe( + tap({ + next: (folders) => + ctx.patchState({ + rootFolders: { + data: folders, + isLoading: false, + error: null, + }, + }), + }), + catchError((error) => handleSectionError(ctx, 'rootFolders', error)) + ); + } + + @Action(GetConfiguredStorageAddons) + getConfiguredStorageAddons(ctx: StateContext, action: GetConfiguredStorageAddons) { + const state = ctx.getState(); + ctx.patchState({ configuredStorageAddons: { ...state.configuredStorageAddons, isLoading: true } }); + + return this.filesService.getConfiguredStorageAddons(action.resourceUri).pipe( + tap({ + next: (addons) => + ctx.patchState({ + configuredStorageAddons: { + data: addons, + isLoading: false, + error: null, + }, + }), + }), + finalize(() => { + ctx.patchState({ configuredStorageAddons: { ...state.configuredStorageAddons, isLoading: false } }); + }), + catchError((error) => handleSectionError(ctx, 'configuredStorageAddons', error)) + ); + } + + @Action(ResetState) + resetState(ctx: StateContext) { + ctx.patchState(filesStateDefaults); + } +} diff --git a/src/app/features/files/store/index.ts b/src/app/features/files/store/index.ts new file mode 100644 index 000000000..04f101f76 --- /dev/null +++ b/src/app/features/files/store/index.ts @@ -0,0 +1,3 @@ +export * from './files.actions'; +export * from './files.selectors'; +export * from './files.state'; diff --git a/src/app/features/project/files/components/index.ts b/src/app/features/project/files/components/index.ts index 3325c793f..caec537b7 100644 --- a/src/app/features/project/files/components/index.ts +++ b/src/app/features/project/files/components/index.ts @@ -6,5 +6,4 @@ export { FileProjectMetadataComponent } from './file-project-metadata/file-proje export { MoveFileDialogComponent } from './move-file-dialog/move-file-dialog.component'; export { RenameFileDialogComponent } from './rename-file-dialog/rename-file-dialog.component'; export { FileRevisionsComponent } from '@osf/features/project/files/components/file-revisions/file-revisions.component'; -export { FileDetailComponent } from '@osf/features/project/files/pages/file-detail/file-detail.component'; export { FilesTreeComponent } from '@shared/components/files-tree/files-tree.component'; diff --git a/src/app/features/project/files/models/osf-models/file-target.model.ts b/src/app/features/project/files/models/osf-models/file-target.model.ts index 84485a11e..85858e99d 100644 --- a/src/app/features/project/files/models/osf-models/file-target.model.ts +++ b/src/app/features/project/files/models/osf-models/file-target.model.ts @@ -1,4 +1,5 @@ export interface OsfFileTarget { + id: string; title: string; description: string; category: string; @@ -18,4 +19,5 @@ export interface OsfFileTarget { currentUserIsContributorOrGroupMember: boolean; wikiEnabled: boolean; public: boolean; + type: string; } diff --git a/src/app/features/project/files/project-files.routes.ts b/src/app/features/project/files/project-files.routes.ts index 44f13d430..754aacd24 100644 --- a/src/app/features/project/files/project-files.routes.ts +++ b/src/app/features/project/files/project-files.routes.ts @@ -17,9 +17,7 @@ export const projectFilesRoutes: Routes = [ { path: ':fileGuid', loadComponent: () => - import('@osf/features/project/files/pages/file-detail/file-detail.component').then( - (c) => c.FileDetailComponent - ), + import('@osf/features/files/pages/file-detail/file-detail.component').then((c) => c.FileDetailComponent), children: [ { path: 'metadata', diff --git a/src/app/features/project/files/store/project-files.state.ts b/src/app/features/project/files/store/project-files.state.ts index 413079895..1553a5d43 100644 --- a/src/app/features/project/files/store/project-files.state.ts +++ b/src/app/features/project/files/store/project-files.state.ts @@ -213,8 +213,8 @@ export class ProjectFilesState { ctx.patchState({ projectMetadata: { ...state.projectMetadata, isLoading: true, error: null } }); forkJoin({ - projectShortInfo: this.filesService.getProjectShortInfo(action.projectId), - projectMetadata: this.filesService.getProjectCustomMetadata(action.projectId), + projectShortInfo: this.filesService.getResourceShortInfo(action.projectId, 'nodes'), + projectMetadata: this.filesService.getCustomMetadata(action.projectId), }) .pipe(catchError((error) => this.handleError(ctx, 'projectMetadata', error))) .subscribe((results) => { @@ -234,7 +234,7 @@ export class ProjectFilesState { const state = ctx.getState(); ctx.patchState({ contributors: { ...state.contributors, isLoading: true, error: null } }); - return this.filesService.getProjectContributors(action.projectId).pipe( + return this.filesService.getResourceContributors(action.projectId, 'nodes').pipe( tap({ next: (contributors) => { ctx.patchState({ contributors: { data: contributors, isLoading: false, error: null } }); diff --git a/src/app/shared/components/file-link/file-link.component.html b/src/app/shared/components/file-link/file-link.component.html new file mode 100644 index 000000000..372199538 --- /dev/null +++ b/src/app/shared/components/file-link/file-link.component.html @@ -0,0 +1,14 @@ +
+ @if (hasProjectedContent()) { + + } @else { + + } +
diff --git a/src/app/shared/components/file-link/file-link.component.scss b/src/app/shared/components/file-link/file-link.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/shared/components/file-link/file-link.component.spec.ts b/src/app/shared/components/file-link/file-link.component.spec.ts new file mode 100644 index 000000000..3e88dedfe --- /dev/null +++ b/src/app/shared/components/file-link/file-link.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FileLinkComponent } from './file-link.component'; + +describe('FileLinkComponent', () => { + let component: FileLinkComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FileLinkComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(FileLinkComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/file-link/file-link.component.ts b/src/app/shared/components/file-link/file-link.component.ts new file mode 100644 index 000000000..d494d838a --- /dev/null +++ b/src/app/shared/components/file-link/file-link.component.ts @@ -0,0 +1,46 @@ +import { Tag } from 'primeng/tag'; + +import { + ChangeDetectionStrategy, + Component, + computed, + contentChild, + DestroyRef, + ElementRef, + inject, + input, +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Router } from '@angular/router'; + +import { FilesService } from '@osf/shared/services'; + +@Component({ + selector: 'osf-file-link', + imports: [Tag], + templateUrl: './file-link.component.html', + styleUrl: './file-link.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FileLinkComponent { + file = input.required<{ file_id: string; file_name: string }>(); + content = contentChild('content'); + + private readonly filesService = inject(FilesService); + private readonly router = inject(Router); + private readonly destroyRef = inject(DestroyRef); + + hasProjectedContent = computed(() => !!this.content()); + + navigateToFile() { + const fileId = this.file().file_id; + if (fileId) { + this.filesService + .getFileGuid(fileId) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((file) => { + this.router.navigate(['/files', file.guid]); + }); + } + } +} 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 526546184..db75c9d64 100644 --- a/src/app/shared/components/files-tree/files-tree.component.ts +++ b/src/app/shared/components/files-tree/files-tree.component.ts @@ -149,12 +149,12 @@ export class FilesTreeComponent implements OnDestroy, AfterViewInit { openEntry(file: OsfFile) { if (file.kind === 'file') { - if (!file.guid) { + if (file.guid) { + this.entryFileClicked.emit(file); + } else { this.filesService.getFileGuid(file.id).subscribe((file) => { this.entryFileClicked.emit(file); }); - } else { - this.entryFileClicked.emit(file); } } else { this.actions().setFilesIsLoading?.(true); diff --git a/src/app/shared/components/index.ts b/src/app/shared/components/index.ts index a1c1589d2..e5eabe591 100644 --- a/src/app/shared/components/index.ts +++ b/src/app/shared/components/index.ts @@ -7,6 +7,7 @@ export { EducationHistoryComponent } from './education-history/education-history export { EducationHistoryDialogComponent } from './education-history-dialog/education-history-dialog.component'; export { EmploymentHistoryComponent } from './employment-history/employment-history.component'; export { EmploymentHistoryDialogComponent } from './employment-history-dialog/employment-history-dialog.component'; +export { FileLinkComponent } from './file-link/file-link.component'; export { FileMenuComponent } from './file-menu/file-menu.component'; export { FilesTreeComponent } from './files-tree/files-tree.component'; export { FilterChipsComponent } from './filter-chips/filter-chips.component'; diff --git a/src/app/shared/components/registration-blocks-data/registration-blocks-data.component.html b/src/app/shared/components/registration-blocks-data/registration-blocks-data.component.html index d4392c874..bf98e1db7 100644 --- a/src/app/shared/components/registration-blocks-data/registration-blocks-data.component.html +++ b/src/app/shared/components/registration-blocks-data/registration-blocks-data.component.html @@ -22,7 +22,7 @@

@if (reviewData()[question.responseKey!].length) {
@for (file of reviewData()[question.responseKey!]; track file.id) { - + }
} @else { diff --git a/src/app/shared/components/registration-blocks-data/registration-blocks-data.component.ts b/src/app/shared/components/registration-blocks-data/registration-blocks-data.component.ts index 846aa407e..080c02764 100644 --- a/src/app/shared/components/registration-blocks-data/registration-blocks-data.component.ts +++ b/src/app/shared/components/registration-blocks-data/registration-blocks-data.component.ts @@ -9,9 +9,11 @@ import { INPUT_VALIDATION_MESSAGES } from '@osf/shared/constants'; import { FieldType } from '@osf/shared/enums'; import { Question } from '@osf/shared/models'; +import { FileLinkComponent } from '../file-link/file-link.component'; + @Component({ selector: 'osf-registration-blocks-data', - imports: [Tag, TranslatePipe, Message], + imports: [Tag, TranslatePipe, Message, FileLinkComponent], templateUrl: './registration-blocks-data.component.html', styleUrl: './registration-blocks-data.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/shared/mappers/files/files.mapper.ts b/src/app/shared/mappers/files/files.mapper.ts index 7ed7e83d8..838032bf3 100644 --- a/src/app/shared/mappers/files/files.mapper.ts +++ b/src/app/shared/mappers/files/files.mapper.ts @@ -48,6 +48,7 @@ export function MapFile( filesLink: file?.relationships?.files?.links?.related?.href, }, target: { + id: file?.embeds?.target.data.id, title: file?.embeds?.target.data.attributes.title, description: file?.embeds?.target.data.attributes.description, category: file?.embeds?.target.data.attributes.category, @@ -61,6 +62,7 @@ export function MapFile( tags: file?.embeds?.target.data.attributes.tags, nodeLicense: file?.embeds?.target.data.attributes.node_license, analyticsKey: file?.embeds?.target.data.attributes.analytics_key, + type: file?.embeds?.target.data.type, }, } as OsfFile; } diff --git a/src/app/shared/models/index.ts b/src/app/shared/models/index.ts index 154a8a219..d0cb7b98b 100644 --- a/src/app/shared/models/index.ts +++ b/src/app/shared/models/index.ts @@ -31,6 +31,7 @@ export * from './provider'; export * from './query-params.model'; export * from './registration'; export * from './resource-card'; +export * from './resource-metadata.model'; export * from './resource-overview.model'; export * from './search'; export * from './select-option.model'; diff --git a/src/app/shared/models/resource-metadata.model.ts b/src/app/shared/models/resource-metadata.model.ts new file mode 100644 index 000000000..46fcd5f37 --- /dev/null +++ b/src/app/shared/models/resource-metadata.model.ts @@ -0,0 +1,16 @@ +export interface ResourceMetadata { + title: string; + description: string; + dateCreated: Date; + dateModified: Date; + language: string; + resourceTypeGeneral: string; + funders: { + funderName: string; + funderIdentifier: string; + funderIdentifierType: string; + awardNumber: string; + awardUri: string; + awardTitle: string; + }[]; +} diff --git a/src/app/shared/services/files.service.ts b/src/app/shared/services/files.service.ts index 66cffc62a..71c26d027 100644 --- a/src/app/shared/services/files.service.ts +++ b/src/app/shared/services/files.service.ts @@ -187,29 +187,26 @@ export class FilesService { .pipe(map((response) => MapFileCustomMetadata(response.data))); } - getProjectShortInfo(resourceId: string): Observable { + getResourceShortInfo(resourceId: string, resourceType: string): Observable { const params = { 'fields[nodes]': 'title,description,date_created,date_modified', }; - return this.jsonApiService.get(`${environment.apiUrl}/nodes/${resourceId}/`, params); + return this.jsonApiService.get( + `${environment.apiUrl}/${resourceType}/${resourceId}/`, + params + ); } - getProjectCustomMetadata(resourceId: string): Observable { + getCustomMetadata(resourceId: string): Observable { return this.jsonApiService.get( `${environment.apiUrl}/guids/${resourceId}/?embed=custom_metadata&resolve=false` ); } - getProjectContributors(resourceId: string): Observable { - const params = { - 'page[size]': '50', - 'fields[users]': 'full_name,active', - }; - + getResourceContributors(resourceId: string, resourceType: string): Observable { return this.jsonApiService .get( - `${environment.apiUrl}/nodes/${resourceId}/contributors_and_group_members/`, - params + `${environment.apiUrl}/${resourceType}/${resourceId}/bibliographic_contributors/` ) .pipe( map((response) => From ec7d703733dc4fe3dd2aa7ff52f139251497670e Mon Sep 17 00:00:00 2001 From: NazarMykhalkevych Date: Mon, 18 Aug 2025 12:03:29 +0300 Subject: [PATCH 4/7] fix(files): update imports --- .../files/components/file-keywords/file-keywords.component.ts | 2 +- .../files/models/get-resource-custom-metadata-response.model.ts | 2 +- .../files/models/get-resource-short-info-response.model.ts | 2 +- src/app/features/files/store/files.state.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/features/files/components/file-keywords/file-keywords.component.ts b/src/app/features/files/components/file-keywords/file-keywords.component.ts index 02fc592b8..7991a5740 100644 --- a/src/app/features/files/components/file-keywords/file-keywords.component.ts +++ b/src/app/features/files/components/file-keywords/file-keywords.component.ts @@ -11,8 +11,8 @@ import { ChangeDetectionStrategy, Component, DestroyRef, inject } from '@angular import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; +import { CustomValidators } from '@osf/shared/helpers'; import { InputLimits } from '@shared/constants'; -import { CustomValidators } from '@shared/utils'; import { FilesSelectors, UpdateTags } from '../../store'; diff --git a/src/app/features/files/models/get-resource-custom-metadata-response.model.ts b/src/app/features/files/models/get-resource-custom-metadata-response.model.ts index 5247896ae..34b6a7f89 100644 --- a/src/app/features/files/models/get-resource-custom-metadata-response.model.ts +++ b/src/app/features/files/models/get-resource-custom-metadata-response.model.ts @@ -1,4 +1,4 @@ -import { ApiData, JsonApiResponse } from '@core/models'; +import { ApiData, JsonApiResponse } from '@osf/shared/models'; export type GetResourceCustomMetadataResponse = JsonApiResponse< ApiData, diff --git a/src/app/features/files/models/get-resource-short-info-response.model.ts b/src/app/features/files/models/get-resource-short-info-response.model.ts index 8e38daf5f..8b50228f3 100644 --- a/src/app/features/files/models/get-resource-short-info-response.model.ts +++ b/src/app/features/files/models/get-resource-short-info-response.model.ts @@ -1,4 +1,4 @@ -import { ApiData, JsonApiResponse } from '@core/models'; +import { ApiData, JsonApiResponse } from '@osf/shared/models'; export type GetResourceShortInfoResponse = JsonApiResponse< ApiData< diff --git a/src/app/features/files/store/files.state.ts b/src/app/features/files/store/files.state.ts index 3d4d615f5..1a9b29f99 100644 --- a/src/app/features/files/store/files.state.ts +++ b/src/app/features/files/store/files.state.ts @@ -4,7 +4,7 @@ import { catchError, finalize, forkJoin, tap } from 'rxjs'; import { inject, Injectable } from '@angular/core'; -import { handleSectionError } from '@osf/core/handlers'; +import { handleSectionError } from '@osf/shared/helpers'; import { FilesService, ToastService } from '@shared/services'; import { filesStateDefaults } from '../constants'; From ad093b45e741b405a862d122f74269be2754d869 Mon Sep 17 00:00:00 2001 From: NazarMykhalkevych Date: Tue, 19 Aug 2025 01:24:42 +0300 Subject: [PATCH 5/7] fix(files): refactoring files --- .../create-folder-dialog.component.html | 4 +- .../create-folder-dialog.component.spec.ts | 0 .../create-folder-dialog.component.ts | 0 .../edit-file-metadata-dialog.component.html | 8 +- .../file-keywords.component.html | 2 +- .../file-metadata.component.html | 4 +- .../file-metadata/file-metadata.component.ts | 2 +- .../file-resource-metadata.component.html} | 54 +-- .../file-resource-metadata.component.scss} | 0 .../file-resource-metadata.component.spec.ts | 22 ++ .../file-resource-metadata.component.ts | 24 ++ .../file-revisions.component.html | 12 +- src/app/features/files/components/index.ts | 4 + .../move-file-dialog.component.html | 9 +- .../move-file-dialog.component.scss | 0 .../move-file-dialog.component.spec.ts | 0 .../move-file-dialog.component.ts | 2 +- .../rename-file-dialog.component.html | 4 +- .../rename-file-dialog.component.spec.ts | 0 .../rename-file-dialog.component.ts | 0 .../constants/embed-content.constants.ts | 42 +++ .../file-metadata-fields.constants.ts | 8 +- src/app/features/files/constants/index.ts | 1 + .../files.routes.ts} | 13 +- .../community-metadata.component.html | 0 .../community-metadata.component.scss | 0 .../community-metadata.component.spec.ts | 0 .../community-metadata.component.ts | 0 .../file-detail/file-detail.component.html | 10 +- .../file-detail/file-detail.component.ts | 16 +- .../files-container.component.html | 1 + .../files-container.component.spec.ts | 26 ++ .../files-container.component.ts | 10 + .../files/pages/files/files.component.html | 114 ++++++ .../files/pages/files/files.component.scss | 24 ++ .../files/pages/files/files.component.spec.ts | 26 ++ .../files/pages/files/files.component.ts | 341 ++++++++++++++++++ src/app/features/files/store/files.actions.ts | 69 ++++ .../features/files/store/files.selectors.ts | 6 +- src/app/features/files/store/files.state.ts | 144 +++++++- .../bulk-upload/bulk-upload.component.html | 2 +- ...tion-moderation-submissions.component.html | 2 +- .../preprint-submissions.component.html | 2 +- ...rint-withdrawal-submissions.component.html | 2 +- ...egistry-pending-submissions.component.html | 2 +- .../registry-submissions.component.html | 2 +- .../edit-file-metadata-dialog.component.html | 56 --- ...dit-file-metadata-dialog.component.spec.ts | 22 -- .../edit-file-metadata-dialog.component.ts | 69 ---- .../file-keywords.component.html | 24 -- .../file-keywords.component.scss | 0 .../file-keywords.component.spec.ts | 22 -- .../file-keywords/file-keywords.component.ts | 63 ---- .../file-metadata.component.html | 33 -- .../file-metadata.component.scss | 0 .../file-metadata.component.spec.ts | 22 -- .../file-metadata/file-metadata.component.ts | 71 ---- .../file-project-metadata.component.scss | 0 .../file-project-metadata.component.spec.ts | 22 -- .../file-project-metadata.component.ts | 24 -- .../file-revisions.component.html | 60 --- .../file-revisions.component.scss | 0 .../file-revisions.component.spec.ts | 22 -- .../file-revisions.component.ts | 51 --- .../project/files/components/index.ts | 9 - .../constants/file-metadata-fields.const.ts | 8 +- .../project-files.component.html | 16 +- .../project-files/project-files.component.ts | 5 +- .../files/store/project-files.state.ts | 3 - src/app/features/project/project.routes.ts | 8 +- .../custom-step/custom-step.component.html | 2 +- .../files-control.component.html | 6 +- .../files-control/files-control.component.ts | 4 +- .../registry-files.component.html | 8 +- src/app/features/registry/registry.routes.ts | 2 +- .../file-menu/file-menu.component.ts | 10 +- .../files-tree/files-tree.component.html | 6 +- .../files-tree/files-tree.component.ts | 24 +- .../project-metadata-funding.component.html | 10 +- ...resource-information-dialog.component.html | 4 +- .../shared/constants/sort-options.const.ts | 16 +- src/app/shared/models/files/index.ts | 1 + .../files/resource-files-links.model.ts | 4 + src/app/shared/services/files.service.ts | 15 + src/assets/i18n/en.json | 228 ++++++------ 85 files changed, 1104 insertions(+), 860 deletions(-) rename src/app/features/{project => }/files/components/create-folder-dialog/create-folder-dialog.component.html (81%) rename src/app/features/{project => }/files/components/create-folder-dialog/create-folder-dialog.component.spec.ts (100%) rename src/app/features/{project => }/files/components/create-folder-dialog/create-folder-dialog.component.ts (100%) rename src/app/features/{project/files/components/file-project-metadata/file-project-metadata.component.html => files/components/file-resource-metadata/file-resource-metadata.component.html} (51%) rename src/app/features/{project/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.scss => files/components/file-resource-metadata/file-resource-metadata.component.scss} (100%) create mode 100644 src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.spec.ts create mode 100644 src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.ts rename src/app/features/{project => }/files/components/move-file-dialog/move-file-dialog.component.html (87%) rename src/app/features/{project => }/files/components/move-file-dialog/move-file-dialog.component.scss (100%) rename src/app/features/{project => }/files/components/move-file-dialog/move-file-dialog.component.spec.ts (100%) rename src/app/features/{project => }/files/components/move-file-dialog/move-file-dialog.component.ts (98%) rename src/app/features/{project => }/files/components/rename-file-dialog/rename-file-dialog.component.html (82%) rename src/app/features/{project => }/files/components/rename-file-dialog/rename-file-dialog.component.spec.ts (100%) rename src/app/features/{project => }/files/components/rename-file-dialog/rename-file-dialog.component.ts (100%) create mode 100644 src/app/features/files/constants/embed-content.constants.ts rename src/app/features/{project/files/project-files.routes.ts => files/files.routes.ts} (50%) rename src/app/features/{project => }/files/pages/community-metadata/community-metadata.component.html (100%) rename src/app/features/{project => }/files/pages/community-metadata/community-metadata.component.scss (100%) rename src/app/features/{project => }/files/pages/community-metadata/community-metadata.component.spec.ts (100%) rename src/app/features/{project => }/files/pages/community-metadata/community-metadata.component.ts (100%) create mode 100644 src/app/features/files/pages/files-container/files-container.component.html create mode 100644 src/app/features/files/pages/files-container/files-container.component.spec.ts create mode 100644 src/app/features/files/pages/files-container/files-container.component.ts create mode 100644 src/app/features/files/pages/files/files.component.html create mode 100644 src/app/features/files/pages/files/files.component.scss create mode 100644 src/app/features/files/pages/files/files.component.spec.ts create mode 100644 src/app/features/files/pages/files/files.component.ts delete mode 100644 src/app/features/project/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.html delete mode 100644 src/app/features/project/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.spec.ts delete mode 100644 src/app/features/project/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.ts delete mode 100644 src/app/features/project/files/components/file-keywords/file-keywords.component.html delete mode 100644 src/app/features/project/files/components/file-keywords/file-keywords.component.scss delete mode 100644 src/app/features/project/files/components/file-keywords/file-keywords.component.spec.ts delete mode 100644 src/app/features/project/files/components/file-keywords/file-keywords.component.ts delete mode 100644 src/app/features/project/files/components/file-metadata/file-metadata.component.html delete mode 100644 src/app/features/project/files/components/file-metadata/file-metadata.component.scss delete mode 100644 src/app/features/project/files/components/file-metadata/file-metadata.component.spec.ts delete mode 100644 src/app/features/project/files/components/file-metadata/file-metadata.component.ts delete mode 100644 src/app/features/project/files/components/file-project-metadata/file-project-metadata.component.scss delete mode 100644 src/app/features/project/files/components/file-project-metadata/file-project-metadata.component.spec.ts delete mode 100644 src/app/features/project/files/components/file-project-metadata/file-project-metadata.component.ts delete mode 100644 src/app/features/project/files/components/file-revisions/file-revisions.component.html delete mode 100644 src/app/features/project/files/components/file-revisions/file-revisions.component.scss delete mode 100644 src/app/features/project/files/components/file-revisions/file-revisions.component.spec.ts delete mode 100644 src/app/features/project/files/components/file-revisions/file-revisions.component.ts delete mode 100644 src/app/features/project/files/components/index.ts create mode 100644 src/app/shared/models/files/resource-files-links.model.ts diff --git a/src/app/features/project/files/components/create-folder-dialog/create-folder-dialog.component.html b/src/app/features/files/components/create-folder-dialog/create-folder-dialog.component.html similarity index 81% rename from src/app/features/project/files/components/create-folder-dialog/create-folder-dialog.component.html rename to src/app/features/files/components/create-folder-dialog/create-folder-dialog.component.html index 6d45824fe..cbeb82b17 100644 --- a/src/app/features/project/files/components/create-folder-dialog/create-folder-dialog.component.html +++ b/src/app/features/files/components/create-folder-dialog/create-folder-dialog.component.html @@ -2,8 +2,8 @@
diff --git a/src/app/features/project/files/components/create-folder-dialog/create-folder-dialog.component.spec.ts b/src/app/features/files/components/create-folder-dialog/create-folder-dialog.component.spec.ts similarity index 100% rename from src/app/features/project/files/components/create-folder-dialog/create-folder-dialog.component.spec.ts rename to src/app/features/files/components/create-folder-dialog/create-folder-dialog.component.spec.ts diff --git a/src/app/features/project/files/components/create-folder-dialog/create-folder-dialog.component.ts b/src/app/features/files/components/create-folder-dialog/create-folder-dialog.component.ts similarity index 100% rename from src/app/features/project/files/components/create-folder-dialog/create-folder-dialog.component.ts rename to src/app/features/files/components/create-folder-dialog/create-folder-dialog.component.ts diff --git a/src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.html b/src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.html index 9abef56b5..7d1ac831c 100644 --- a/src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.html +++ b/src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.html @@ -1,16 +1,16 @@
-

{{ 'project.files.detail.fileMetadata.fields.title' | translate }}

+

{{ 'files.detail.fileMetadata.fields.title' | translate }}

-

{{ 'project.files.detail.fileMetadata.fields.description' | translate }}

+

{{ 'files.detail.fileMetadata.fields.description' | translate }}

-

{{ 'project.files.detail.fileMetadata.fields.resourceType' | translate }}

+

{{ 'files.detail.fileMetadata.fields.resourceType' | translate }}

-

{{ 'project.files.detail.fileMetadata.fields.resourceLanguage' | translate }}

+

{{ 'files.detail.fileMetadata.fields.resourceLanguage' | translate }}

-

{{ 'project.files.detail.keywords.title' | translate }}

+

{{ 'files.detail.keywords.title' | translate }}

diff --git a/src/app/features/files/components/file-metadata/file-metadata.component.html b/src/app/features/files/components/file-metadata/file-metadata.component.html index 926738703..e6038fa22 100644 --- a/src/app/features/files/components/file-metadata/file-metadata.component.html +++ b/src/app/features/files/components/file-metadata/file-metadata.component.html @@ -1,6 +1,6 @@
-

{{ 'project.files.detail.fileMetadata.title' | translate }}

+

{{ 'files.detail.fileMetadata.title' | translate }}

@@ -10,7 +10,7 @@

{{ 'project.files.detail.fileMetadata.title' | translate }}

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 9c1a8a18b..0e87413ed 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 @@ -59,7 +59,7 @@ export class FileMetadataComponent { .open(EditFileMetadataDialogComponent, { width: '448px', focusOnShow: false, - header: this.translateService.instant('project.files.detail.fileMetadata.edit'), + header: this.translateService.instant('files.detail.fileMetadata.edit'), closeOnEscape: true, modal: true, closable: true, diff --git a/src/app/features/project/files/components/file-project-metadata/file-project-metadata.component.html b/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.html similarity index 51% rename from src/app/features/project/files/components/file-project-metadata/file-project-metadata.component.html rename to src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.html index b71dd21f0..de4433eb5 100644 --- a/src/app/features/project/files/components/file-project-metadata/file-project-metadata.component.html +++ b/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.html @@ -1,35 +1,35 @@
-

{{ 'project.files.detail.projectMetadata.title' | translate }}

+

{{ 'files.detail.resourceMetadata.title' | translate }}

- @if (isProjectMetadataLoading()) { + @if (isResourceMetadataLoading()) { } @else { - @for (funder of projectMetadata()?.funders; track $index) { + @for (funder of resourceMetadata()?.funders; track $index) {
@if (funder.funderName) {
-

{{ 'project.files.detail.projectMetadata.fields.funder' | translate }}

+

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

{{ funder.funderName }}
} @if (funder.awardTitle) {
-

{{ 'project.files.detail.projectMetadata.fields.awardTitle' | translate }}

+

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

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

{{ 'project.files.detail.projectMetadata.fields.awardNumber' | translate }}

+

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

{{ funder.awardTitle }}
} @if (funder.awardUri) {
-

{{ 'project.files.detail.projectMetadata.fields.awardUri' | translate }}

+

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

{{ funder.awardUri }}
} @@ -37,72 +37,72 @@

{{ 'project.files.detail.projectMetadata.fields.awardUri' | translate }}

-

{{ 'project.files.detail.projectMetadata.fields.title' | translate }}

+

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

- {{ projectMetadata()?.title }} + {{ resourceMetadata()?.title }}
- @if (projectMetadata()?.description) { + @if (resourceMetadata()?.description) {
-

{{ 'project.files.detail.projectMetadata.fields.description' | translate }}

+

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

- {{ projectMetadata()?.description }} + {{ resourceMetadata()?.description }}
} - @if (projectMetadata()?.resourceTypeGeneral) { + @if (resourceMetadata()?.resourceTypeGeneral) {
-

{{ 'project.files.detail.projectMetadata.fields.resourceType' | translate }}

+

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

- {{ projectMetadata()?.resourceTypeGeneral }} + {{ resourceMetadata()?.resourceTypeGeneral }}
} - @if (projectMetadata()?.language) { + @if (resourceMetadata()?.language) {
-

{{ 'project.files.detail.projectMetadata.fields.resourceLanguage' | translate }}

+

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

- {{ projectMetadata()?.language }} + {{ resourceMetadata()?.language }}
} - @if (projectMetadata()?.dateCreated) { + @if (resourceMetadata()?.dateCreated) {
-

{{ 'project.files.detail.projectMetadata.fields.dateCreated' | translate }}

+

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

- {{ projectMetadata()?.dateCreated | date: 'MMMM d, y' }} + {{ resourceMetadata()?.dateCreated | date: 'MMMM d, y' }}
} - @if (projectMetadata()?.dateModified) { + @if (resourceMetadata()?.dateModified) {
-

{{ 'project.files.detail.projectMetadata.fields.dateModified' | translate }}

+

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

- {{ projectMetadata()?.dateModified | date: 'MMMM d, y' }} + {{ resourceMetadata()?.dateModified | date: 'MMMM d, y' }}
} } - @if (isProjectContributorsLoading()) { + @if (isResourceContributorsLoading()) { } @else { @if (contributors()?.length) {
-

{{ 'project.files.detail.projectMetadata.fields.contributors' | translate }}

+

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

@for (contributor of contributors(); track $index) { - {{ contributor.name }} + {{ contributor.fullName }} @if (!$last) { } diff --git a/src/app/features/project/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.scss b/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.scss similarity index 100% rename from src/app/features/project/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.scss rename to src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.scss diff --git a/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.spec.ts b/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.spec.ts new file mode 100644 index 000000000..ca59b6b86 --- /dev/null +++ b/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FileResourceMetadataComponent } from './file-resource-metadata.component'; + +describe('FileResourceMetadataComponent', () => { + let component: FileResourceMetadataComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FileResourceMetadataComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(FileResourceMetadataComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.ts b/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.ts new file mode 100644 index 000000000..25a00edf8 --- /dev/null +++ b/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.ts @@ -0,0 +1,24 @@ +import { select } from '@ngxs/store'; + +import { TranslatePipe } from '@ngx-translate/core'; + +import { Skeleton } from 'primeng/skeleton'; + +import { DatePipe } from '@angular/common'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +import { FilesSelectors } from '../../store'; + +@Component({ + selector: 'osf-file-resource-metadata', + imports: [DatePipe, TranslatePipe, Skeleton], + templateUrl: './file-resource-metadata.component.html', + styleUrl: './file-resource-metadata.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FileResourceMetadataComponent { + resourceMetadata = select(FilesSelectors.getResourceMetadata); + contributors = select(FilesSelectors.getContributors); + isResourceMetadataLoading = select(FilesSelectors.isResourceMetadataLoading); + isResourceContributorsLoading = select(FilesSelectors.isResourceContributorsLoading); +} diff --git a/src/app/features/files/components/file-revisions/file-revisions.component.html b/src/app/features/files/components/file-revisions/file-revisions.component.html index 7fb8c95a3..2c7e02e54 100644 --- a/src/app/features/files/components/file-revisions/file-revisions.component.html +++ b/src/app/features/files/components/file-revisions/file-revisions.component.html @@ -1,6 +1,6 @@
-

{{ 'project.files.detail.revisions.title' | translate }}

+

{{ 'files.detail.revisions.title' | translate }}

@if (isLoading()) { @@ -18,12 +18,12 @@

{{ 'project.files.detail.revisions.title' | translate }}

  • @@ -31,12 +31,12 @@

    {{ 'project.files.detail.revisions.title' | translate }}

  • @@ -45,7 +45,7 @@

    {{ 'project.files.detail.revisions.title' | translate }}

    diff --git a/src/app/features/files/components/index.ts b/src/app/features/files/components/index.ts index 7925a2597..80c02901d 100644 --- a/src/app/features/files/components/index.ts +++ b/src/app/features/files/components/index.ts @@ -1,4 +1,8 @@ +export { CreateFolderDialogComponent } from './create-folder-dialog/create-folder-dialog.component'; export { EditFileMetadataDialogComponent } from './edit-file-metadata-dialog/edit-file-metadata-dialog.component'; export { FileKeywordsComponent } from './file-keywords/file-keywords.component'; export { FileMetadataComponent } from './file-metadata/file-metadata.component'; +export { FileResourceMetadataComponent } from './file-resource-metadata/file-resource-metadata.component'; export { FileRevisionsComponent } from './file-revisions/file-revisions.component'; +export { MoveFileDialogComponent } from './move-file-dialog/move-file-dialog.component'; +export { RenameFileDialogComponent } from './rename-file-dialog/rename-file-dialog.component'; diff --git a/src/app/features/project/files/components/move-file-dialog/move-file-dialog.component.html b/src/app/features/files/components/move-file-dialog/move-file-dialog.component.html similarity index 87% rename from src/app/features/project/files/components/move-file-dialog/move-file-dialog.component.html rename to src/app/features/files/components/move-file-dialog/move-file-dialog.component.html index 563817c83..fb2589340 100644 --- a/src/app/features/project/files/components/move-file-dialog/move-file-dialog.component.html +++ b/src/app/features/files/components/move-file-dialog/move-file-dialog.component.html @@ -6,7 +6,7 @@
    cost-shield -

    {{ 'project.files.dialogs.moveFile.storage' | translate }}

    +

    {{ 'files.dialogs.moveFile.storage' | translate }}

    @@ -37,10 +37,7 @@

    {{ 'project.files.dialogs.moveFile.storage' | translate } @else if (config.data.file.id === file.id) { - diff --git a/src/app/features/project/files/components/move-file-dialog/move-file-dialog.component.scss b/src/app/features/files/components/move-file-dialog/move-file-dialog.component.scss similarity index 100% rename from src/app/features/project/files/components/move-file-dialog/move-file-dialog.component.scss rename to src/app/features/files/components/move-file-dialog/move-file-dialog.component.scss diff --git a/src/app/features/project/files/components/move-file-dialog/move-file-dialog.component.spec.ts b/src/app/features/files/components/move-file-dialog/move-file-dialog.component.spec.ts similarity index 100% rename from src/app/features/project/files/components/move-file-dialog/move-file-dialog.component.spec.ts rename to src/app/features/files/components/move-file-dialog/move-file-dialog.component.spec.ts diff --git a/src/app/features/project/files/components/move-file-dialog/move-file-dialog.component.ts b/src/app/features/files/components/move-file-dialog/move-file-dialog.component.ts similarity index 98% rename from src/app/features/project/files/components/move-file-dialog/move-file-dialog.component.ts rename to src/app/features/files/components/move-file-dialog/move-file-dialog.component.ts index 2cb2615be..9bfea9c1d 100644 --- a/src/app/features/project/files/components/move-file-dialog/move-file-dialog.component.ts +++ b/src/app/features/files/components/move-file-dialog/move-file-dialog.component.ts @@ -105,7 +105,7 @@ export class MoveFileDialogComponent { let path = this.currentFolder()?.path; if (!path) { - throw new Error(this.translateService.instant('project.files.dialogs.moveFile.pathError')); + throw new Error(this.translateService.instant('files.dialogs.moveFile.pathError')); } if (!this.currentFolder()?.relationships.parentFolderLink) { diff --git a/src/app/features/project/files/components/rename-file-dialog/rename-file-dialog.component.html b/src/app/features/files/components/rename-file-dialog/rename-file-dialog.component.html similarity index 82% rename from src/app/features/project/files/components/rename-file-dialog/rename-file-dialog.component.html rename to src/app/features/files/components/rename-file-dialog/rename-file-dialog.component.html index 03cf87dde..2abb1b505 100644 --- a/src/app/features/project/files/components/rename-file-dialog/rename-file-dialog.component.html +++ b/src/app/features/files/components/rename-file-dialog/rename-file-dialog.component.html @@ -2,8 +2,8 @@
    diff --git a/src/app/features/project/files/components/rename-file-dialog/rename-file-dialog.component.spec.ts b/src/app/features/files/components/rename-file-dialog/rename-file-dialog.component.spec.ts similarity index 100% rename from src/app/features/project/files/components/rename-file-dialog/rename-file-dialog.component.spec.ts rename to src/app/features/files/components/rename-file-dialog/rename-file-dialog.component.spec.ts diff --git a/src/app/features/project/files/components/rename-file-dialog/rename-file-dialog.component.ts b/src/app/features/files/components/rename-file-dialog/rename-file-dialog.component.ts similarity index 100% rename from src/app/features/project/files/components/rename-file-dialog/rename-file-dialog.component.ts rename to src/app/features/files/components/rename-file-dialog/rename-file-dialog.component.ts diff --git a/src/app/features/files/constants/embed-content.constants.ts b/src/app/features/files/constants/embed-content.constants.ts new file mode 100644 index 000000000..d539e5868 --- /dev/null +++ b/src/app/features/files/constants/embed-content.constants.ts @@ -0,0 +1,42 @@ +export const embedDynamicJs = ` + + +
    + + +`.trim(); + +export const embedStaticHtml = ` +