Skip to content

Commit 6ec5ed6

Browse files
feat(metadata): add metadata to file detail page
1 parent 258d539 commit 6ec5ed6

66 files changed

Lines changed: 324 additions & 3868 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/app/app.routes.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { provideStates } from '@ngxs/store';
22

33
import { Routes } from '@angular/router';
44

5+
import { isFileGuard } from '@core/guards/is-file.guard';
56
import { BookmarksState, ProjectsState } from '@shared/stores';
67

78
import { authGuard, redirectIfLoggedInGuard } from './core/guards';
@@ -164,9 +165,9 @@ export const routes: Routes = [
164165
data: { skipBreadcrumbs: true },
165166
},
166167
{
167-
path: 'files/:fileGuid',
168-
loadComponent: () =>
169-
import('@osf/features/files/pages/file-detail/file-detail.component').then((c) => c.FileDetailComponent),
168+
path: ':id',
169+
canMatch: [isFileGuard],
170+
loadChildren: () => import('./features/files/files.routes').then((m) => m.filesRoutes),
170171
},
171172
{
172173
path: ':id',

src/app/core/constants/ngxs-states.constant.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { ProviderState } from '@core/store/provider';
22
import { UserState } from '@core/store/user';
33
import { FilesState } from '@osf/features/files/store';
44
import { MetadataState } from '@osf/features/metadata/store';
5-
import { ProjectMetadataState } from '@osf/features/project/metadata/store';
65
import { ProjectOverviewState } from '@osf/features/project/overview/store';
76
import { RegistrationsState } from '@osf/features/project/registrations/store';
87
import { AddonsState, CurrentResourceState, WikiState } from '@osf/shared/stores';
@@ -20,7 +19,6 @@ export const STATES = [
2019
ProjectOverviewState,
2120
WikiState,
2221
RegistrationsState,
23-
ProjectMetadataState,
2422
LicensesState,
2523
RegionsState,
2624
FilesState,
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Store } from '@ngxs/store';
2+
3+
import { map, switchMap } from 'rxjs/operators';
4+
5+
import { inject } from '@angular/core';
6+
import { CanMatchFn, Route, UrlSegment } from '@angular/router';
7+
8+
import { CurrentResourceType } from '../../shared/enums';
9+
import { CurrentResourceSelectors, GetResource } from '../../shared/stores';
10+
11+
export const isFileGuard: CanMatchFn = (route: Route, segments: UrlSegment[]) => {
12+
const store = inject(Store);
13+
14+
const id = segments[0]?.path;
15+
if (!id) {
16+
return false;
17+
}
18+
19+
const currentResource = store.selectSnapshot(CurrentResourceSelectors.getCurrentResource);
20+
21+
if (currentResource && currentResource.id === id) {
22+
if (currentResource.type === CurrentResourceType.Files) {
23+
return true;
24+
}
25+
26+
return currentResource.type === CurrentResourceType.Files;
27+
}
28+
29+
return store.dispatch(new GetResource(id)).pipe(
30+
switchMap(() => store.select(CurrentResourceSelectors.getCurrentResource)),
31+
map((resource) => {
32+
if (!resource || resource.id !== id) {
33+
return false;
34+
}
35+
36+
if (resource.type === CurrentResourceType.Files) {
37+
return true;
38+
}
39+
return resource.type === CurrentResourceType.Files;
40+
})
41+
);
42+
};

src/app/features/files/files.routes.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { Routes } from '@angular/router';
22

3+
import { ResourceType } from '@osf/shared/enums';
4+
35
import { FilesContainerComponent } from './pages/files-container/files-container.component';
46

57
export const filesRoutes: Routes = [
@@ -11,17 +13,24 @@ export const filesRoutes: Routes = [
1113
path: '',
1214
loadComponent: () => import('@osf/features/files/pages/files/files.component').then((c) => c.FilesComponent),
1315
},
16+
{
17+
path: 'metadata',
18+
loadChildren: () => import('@osf/features/metadata/metadata.routes').then((mod) => mod.metadataRoutes),
19+
data: { resourceType: ResourceType.File },
20+
},
1421
{
1522
path: ':fileGuid',
16-
loadComponent: () =>
17-
import('@osf/features/files/pages/file-detail/file-detail.component').then((c) => c.FileDetailComponent),
23+
loadComponent: () => {
24+
return import('@osf/features/files/pages/file-detail/file-detail.component').then(
25+
(c) => c.FileDetailComponent
26+
);
27+
},
28+
1829
children: [
1930
{
2031
path: 'metadata',
21-
loadComponent: () =>
22-
import('@osf/features/files/pages/community-metadata/community-metadata.component').then(
23-
(c) => c.CommunityMetadataComponent
24-
),
32+
loadChildren: () => import('@osf/features/metadata/metadata.routes').then((mod) => mod.metadataRoutes),
33+
data: { resourceType: ResourceType.File },
2534
},
2635
],
2736
},

src/app/features/files/pages/file-detail/file-detail.component.html

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<p-button
2222
severity="secondary"
2323
[label]="'files.detail.actions.addCommunityMetadata' | translate"
24-
[routerLink]="['/metadata']"
24+
[routerLink]="['/', fileGuid, 'metadata', 'add']"
2525
></p-button>
2626

2727
@if (file()?.links?.download) {
@@ -90,8 +90,25 @@
9090
} @else if (selectedTab === FileDetailTab.Keywords) {
9191
<osf-file-keywords class="metadata"></osf-file-keywords>
9292
} @else {
93-
<osf-file-metadata class="metadata"></osf-file-metadata>
94-
<osf-file-resource-metadata class="metadata" [resourceType]="resourceType"></osf-file-resource-metadata>
93+
<div class="file-metadata-tabs">
94+
<osf-metadata-tabs
95+
[loading]="isLoading()"
96+
[tabs]="tabs()"
97+
[selectedTab]="selectedMetadataTab()"
98+
[selectedCedarTemplate]="selectedCedarTemplate()!"
99+
[selectedCedarRecord]="selectedCedarRecord()!"
100+
[cedarFormReadonly]="cedarFormReadonly()"
101+
(changeTab)="onMetadataTabChange($event)"
102+
(formSubmit)="onCedarFormSubmit($event)"
103+
(cedarFormChangeTemplate)="onCedarFormChangeTemplate()"
104+
(cedarFormEdit)="onCedarFormEdit()"
105+
>
106+
<div class="w-full flex flex-column gap-4 flex-1 -mt-4">
107+
<osf-file-metadata class="metadata"></osf-file-metadata>
108+
<osf-file-resource-metadata class="metadata" [resourceType]="resourceType"></osf-file-resource-metadata>
109+
</div>
110+
</osf-metadata-tabs>
111+
</div>
95112
}
96113
</div>
97114
</div>

src/app/features/files/pages/file-detail/file-detail.component.ts

Lines changed: 146 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,41 @@ import { TranslatePipe } from '@ngx-translate/core';
44

55
import { Button } from 'primeng/button';
66
import { Menu } from 'primeng/menu';
7+
import { TableModule } from 'primeng/table';
78
import { Tab, TabList, Tabs } from 'primeng/tabs';
89

910
import { switchMap } from 'rxjs';
1011

11-
import { ChangeDetectionStrategy, Component, DestroyRef, HostBinding, inject } from '@angular/core';
12+
import {
13+
ChangeDetectionStrategy,
14+
Component,
15+
computed,
16+
DestroyRef,
17+
effect,
18+
HostBinding,
19+
inject,
20+
signal,
21+
} from '@angular/core';
1222
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
1323
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
1424
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
1525

16-
import { LoadingSpinnerComponent, SubHeaderComponent } from '@osf/shared/components';
17-
import { OsfFile } from '@shared/models';
18-
import { CustomConfirmationService, ToastService } from '@shared/services';
26+
import {
27+
CedarMetadataDataTemplateJsonApi,
28+
CedarMetadataRecordData,
29+
CedarRecordDataBinding,
30+
} from '@osf/features/metadata/models';
31+
import {
32+
CreateCedarMetadataRecord,
33+
GetCedarMetadataRecords,
34+
GetCedarMetadataTemplates,
35+
MetadataSelectors,
36+
UpdateCedarMetadataRecord,
37+
} from '@osf/features/metadata/store';
38+
import { LoadingSpinnerComponent, MetadataTabsComponent, SubHeaderComponent } from '@osf/shared/components';
39+
import { MetadataResourceEnum, ResourceType } from '@osf/shared/enums';
40+
import { MetadataTabsModel, OsfFile } from '@osf/shared/models';
41+
import { CustomConfirmationService, ToastService } from '@osf/shared/services';
1942

2043
import {
2144
FileKeywordsComponent,
@@ -51,6 +74,8 @@ import {
5174
FileRevisionsComponent,
5275
FileMetadataComponent,
5376
FileResourceMetadataComponent,
77+
MetadataTabsComponent,
78+
TableModule,
5479
],
5580
templateUrl: './file-detail.component.html',
5681
styleUrl: './file-detail.component.scss',
@@ -74,19 +99,27 @@ export class FileDetailComponent {
7499
getFileResourceMetadata: GetFileResourceMetadata,
75100
getFileResourceContributors: GetFileResourceContributors,
76101
deleteEntry: DeleteEntry,
102+
103+
getCedarRecords: GetCedarMetadataRecords,
104+
getCedarTemplates: GetCedarMetadataTemplates,
105+
createCedarRecord: CreateCedarMetadataRecord,
106+
updateCedarRecord: UpdateCedarMetadataRecord,
77107
});
78108

79109
file = select(FilesSelectors.getOpenedFile);
80110
isFileLoading = select(FilesSelectors.isOpenedFileLoading);
111+
cedarRecords = select(MetadataSelectors.getCedarRecords);
112+
cedarTemplates = select(MetadataSelectors.getCedarTemplates);
113+
81114
safeLink: SafeResourceUrl | null = null;
82115
resourceId = '';
83116
resourceType = '';
84117

85118
isIframeLoading = true;
86119

87-
protected readonly FileDetailTab = FileDetailTab;
120+
readonly FileDetailTab = FileDetailTab;
88121

89-
protected selectedTab: FileDetailTab = FileDetailTab.Details;
122+
selectedTab: FileDetailTab = FileDetailTab.Details;
90123

91124
fileGuid = '';
92125

@@ -116,6 +149,18 @@ export class FileDetailComponent {
116149
},
117150
];
118151

152+
tabs = signal<MetadataTabsModel[]>([]);
153+
154+
isLoading = computed(() => {
155+
return this.isFileLoading();
156+
});
157+
158+
selectedMetadataTab = signal('osf');
159+
160+
selectedCedarRecord = signal<CedarMetadataRecordData | null>(null);
161+
selectedCedarTemplate = signal<CedarMetadataDataTemplateJsonApi | null>(null);
162+
cedarFormReadonly = signal<boolean>(true);
163+
119164
constructor() {
120165
this.route.params
121166
.pipe(
@@ -136,14 +181,31 @@ export class FileDetailComponent {
136181
if (this.resourceId && this.resourceType) {
137182
this.actions.getFileResourceMetadata(this.resourceId, this.resourceType);
138183
this.actions.getFileResourceContributors(this.resourceId, this.resourceType);
139-
140184
if (fileId) {
141185
const fileProvider = this.file()?.provider || '';
142186
this.actions.getFileRevisions(this.resourceId, fileProvider, fileId);
187+
this.actions.getCedarTemplates();
188+
this.actions.getCedarRecords(fileId, ResourceType.File);
143189
}
144190
}
145191
});
146192

193+
effect(() => {
194+
const records = this.cedarRecords();
195+
196+
const baseTabs = [{ id: 'osf', label: 'OSF', type: MetadataResourceEnum.PROJECT }];
197+
198+
const cedarTabs =
199+
records?.map((record) => ({
200+
id: record.id || '',
201+
label: record.embeds?.template?.data?.attributes?.schema_name || `Record ${record.id}`,
202+
type: MetadataResourceEnum.CEDAR,
203+
})) || [];
204+
205+
this.tabs.set([...baseTabs, ...cedarTabs]);
206+
// this.handleRouteBasedTabSelection();
207+
});
208+
147209
this.route.params.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params) => {
148210
this.actions.getFileMetadata(params['fileGuid']);
149211
});
@@ -189,28 +251,100 @@ export class FileDetailComponent {
189251
this.selectedTab = index;
190252
}
191253

192-
protected handleEmailShare(): void {
254+
handleEmailShare(): void {
193255
const link = `mailto:?subject=${this.file()?.name ?? ''}&body=${this.file()?.links?.html ?? ''}`;
194256
window.location.href = link;
195257
}
196258

197-
protected handleXShare(): void {
259+
handleXShare(): void {
198260
const link = `https://x.com/intent/tweet?url=${this.file()?.links?.html ?? ''}&text=${this.file()?.name ?? ''}&via=OSFramework`;
199261
window.open(link, '_blank', 'noopener,noreferrer');
200262
}
201263

202-
protected handleFacebookShare(): void {
264+
handleFacebookShare(): void {
203265
const link = `https://www.facebook.com/dialog/share?app_id=1022273774556662&display=popup&href=${this.file()?.links?.html ?? ''}&redirect_uri=${this.file()?.links?.html ?? ''}`;
204266
window.open(link, '_blank', 'noopener,noreferrer');
205267
}
206268

207-
protected handleCopyDynamicEmbed(): void {
269+
handleCopyDynamicEmbed(): void {
208270
const data = embedDynamicJs.replace('ENCODED_URL', this.file()?.links?.render ?? '');
209271
this.copyToClipboard(data);
210272
}
211273

212-
protected handleCopyStaticEmbed(): void {
274+
handleCopyStaticEmbed(): void {
213275
const data = embedStaticHtml.replace('ENCODED_URL', this.file()?.links?.render ?? '');
214276
this.copyToClipboard(data);
215277
}
278+
279+
onMetadataTabChange(tabId: string | number): void {
280+
const tab = this.tabs().find((x) => x.id === tabId.toString());
281+
282+
if (!tab) {
283+
return;
284+
}
285+
286+
this.selectedMetadataTab.set(tab.id as MetadataResourceEnum);
287+
if (tab.type === 'cedar') {
288+
this.selectedCedarRecord.set(null);
289+
this.selectedCedarTemplate.set(null);
290+
if (tab.id) {
291+
this.loadCedarRecord(tab.id);
292+
}
293+
} else {
294+
this.selectedCedarRecord.set(null);
295+
this.selectedCedarTemplate.set(null);
296+
}
297+
}
298+
299+
onCedarFormEdit(): void {
300+
this.cedarFormReadonly.set(false);
301+
}
302+
303+
onCedarFormSubmit(data: CedarRecordDataBinding): void {
304+
// const selectedRecord = this.selectedCedarRecord();
305+
// if (!this.resourceId || !selectedRecord) return;
306+
// if (selectedRecord.id) {
307+
// this.actions
308+
// .updateCedarRecord(data, selectedRecord.id, this.resourceId, this.resourceType())
309+
// .pipe(takeUntilDestroyed(this.destroyRef))
310+
// .subscribe({
311+
// next: () => {
312+
// this.cedarFormReadonly.set(true);
313+
// this.toastService.showSuccess('CEDAR record updated successfully');
314+
// this.actions.getCedarRecords(this.resourceId, this.resourceType());
315+
// },
316+
// });
317+
// }
318+
}
319+
320+
onCedarFormChangeTemplate(): void {
321+
// this.router.navigate(['add'], { relativeTo: this.activeRoute });
322+
}
323+
324+
private loadCedarRecord(recordId: string): void {
325+
const records = this.cedarRecords();
326+
const templates = this.cedarTemplates();
327+
if (!records) {
328+
return;
329+
}
330+
const record = records.find((r) => r.id === recordId);
331+
if (!record) {
332+
return;
333+
}
334+
this.selectedCedarRecord.set(record);
335+
this.cedarFormReadonly.set(true);
336+
const templateId = record.relationships?.template?.data?.id;
337+
if (templateId && templates?.data) {
338+
const template = templates.data.find((t) => t.id === templateId);
339+
if (template) {
340+
this.selectedCedarTemplate.set(template);
341+
} else {
342+
this.selectedCedarTemplate.set(null);
343+
this.actions.getCedarTemplates();
344+
}
345+
} else {
346+
this.selectedCedarTemplate.set(null);
347+
this.actions.getCedarTemplates();
348+
}
349+
}
216350
}

0 commit comments

Comments
 (0)