Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ <h1 class="collections-heading flex align-items-center">{{ collectionProvider()?
[targetStepValue]="AddToCollectionSteps.CollectionMetadata"
[isDisabled]="isCollectionMetadataDisabled()"
[primaryCollectionId]="primaryCollectionId()"
[isCedarMode]="isCedarMode()"
[cedarTemplate]="requiredMetadataTemplate()"
[existingCedarRecord]="existingCedarRecord()"
(metadataSaved)="handleCollectionMetadataSaved($event)"
(cedarDataSaved)="handleCedarDataSaved($event)"
(stepChange)="handleChangeStep($event)"
/>
</p-stepper>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { ProjectContributorsStepComponent } from '@osf/features/collections/comp
import { ProjectMetadataStepComponent } from '@osf/features/collections/components/add-to-collection/project-metadata-step/project-metadata-step.component';
import { SelectProjectStepComponent } from '@osf/features/collections/components/add-to-collection/select-project-step/select-project-step.component';
import { AddToCollectionSteps } from '@osf/features/collections/enums';
import { CedarRecordDataBinding } from '@osf/features/metadata/models';
import { MetadataSelectors } from '@osf/features/metadata/store';
import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component';
import { CustomDialogService } from '@osf/shared/services/custom-dialog.service';
import { ToastService } from '@osf/shared/services/toast.service';
Expand Down Expand Up @@ -62,8 +64,10 @@ describe('AddToCollectionComponent', () => {
signals: [
{ selector: CollectionsSelectors.getCollectionProviderLoading, value: false },
{ selector: CollectionsSelectors.getCollectionProvider, value: mockCollectionProvider },
{ selector: CollectionsSelectors.getRequiredMetadataTemplate, value: null },
{ selector: ProjectsSelectors.getSelectedProject, value: MOCK_PROJECT },
{ selector: UserSelectors.getCurrentUser, value: MOCK_USER },
{ selector: MetadataSelectors.getCedarRecords, value: [] },
],
}),
],
Expand Down Expand Up @@ -123,6 +127,19 @@ describe('AddToCollectionComponent', () => {
expect(component.stepperActiveValue()).toBe(AddToCollectionSteps.Complete);
});

it('should handle cedar data saved', () => {
const mockCedarData: CedarRecordDataBinding = {
data: {} as CedarRecordDataBinding['data'],
id: 'template-123',
isPublished: false,
};
component.handleCedarDataSaved(mockCedarData);

expect(component.pendingCedarData()).toEqual(mockCedarData);
expect(component.collectionMetadataSaved()).toBe(true);
expect(component.stepperActiveValue()).toBe(AddToCollectionSteps.Complete);
});

it('should have actions defined', () => {
expect(component.actions).toBeDefined();
expect(component.actions.getCollectionProvider).toBeDefined();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { TranslatePipe } from '@ngx-translate/core';
import { Button } from 'primeng/button';
import { Stepper } from 'primeng/stepper';

import { filter, map, Observable, of, switchMap } from 'rxjs';
import { filter, finalize, map, Observable, of, switchMap } from 'rxjs';

import { isPlatformBrowser } from '@angular/common';
import {
Expand All @@ -23,9 +23,18 @@ import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { FormGroup } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';

import { ENVIRONMENT } from '@core/provider/environment.provider';
import { UserSelectors } from '@core/store/user';
import { CedarMetadataRecordData, CedarRecordDataBinding } from '@osf/features/metadata/models';
import {
CreateCedarMetadataRecord,
GetCedarMetadataRecords,
MetadataSelectors,
UpdateCedarMetadataRecord,
} from '@osf/features/metadata/store';
import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component';
import { CollectionSubmissionReviewState } from '@osf/shared/enums/collection-submission-review-state.enum';
import { ResourceType } from '@osf/shared/enums/resource-type.enum';
import { CanDeactivateComponent } from '@osf/shared/models/can-deactivate.interface';
import { BrandService } from '@osf/shared/services/brand.service';
import { CustomDialogService } from '@osf/shared/services/custom-dialog.service';
Expand Down Expand Up @@ -81,6 +90,7 @@ export class AddToCollectionComponent implements CanDeactivateComponent {
private readonly headerStyleHelper = inject(HeaderStyleService);
private readonly platformId = inject(PLATFORM_ID);
private readonly isBrowser = isPlatformBrowser(this.platformId);
private readonly environment = inject(ENVIRONMENT);

readonly selectedProjectId = toSignal<string | null>(
this.route.params.pipe(map((params) => params['id'])) ?? of(null)
Expand All @@ -92,15 +102,18 @@ export class AddToCollectionComponent implements CanDeactivateComponent {

isProviderLoading = select(CollectionsSelectors.getCollectionProviderLoading);
collectionProvider = select(CollectionsSelectors.getCollectionProvider);
requiredMetadataTemplate = select(CollectionsSelectors.getRequiredMetadataTemplate);
selectedProject = select(ProjectsSelectors.getSelectedProject);
currentUser = select(UserSelectors.getCurrentUser);
currentCollectionSubmission = select(AddToCollectionSelectors.getCurrentCollectionSubmission);
cedarRecords = select(MetadataSelectors.getCedarRecords);

providerId = signal<string>('');
allowNavigation = signal<boolean>(false);
projectMetadataSaved = signal<boolean>(false);
projectContributorsSaved = signal<boolean>(false);
collectionMetadataSaved = signal<boolean>(false);
pendingCedarData = signal<CedarRecordDataBinding | null>(null);
stepperActiveValue = signal<number>(AddToCollectionSteps.SelectProject);

primaryCollectionId = computed(() => this.collectionProvider()?.primaryCollection?.id);
Expand All @@ -110,14 +123,24 @@ export class AddToCollectionComponent implements CanDeactivateComponent {
isCollectionMetadataDisabled = computed(
() => !this.selectedProject() || !this.projectMetadataSaved() || !this.projectContributorsSaved()
);
isCedarMode = computed(() => this.environment.collectionSubmissionWithCedar && !!this.requiredMetadataTemplate());
existingCedarRecord = computed<CedarMetadataRecordData | null>(() => {
const records = this.cedarRecords();
const templateId = this.requiredMetadataTemplate()?.id;
if (!records?.length || !templateId) return null;
return records.find((r) => r.relationships?.template?.data?.id === templateId) ?? null;
});

actions = createDispatchMap({
readonly actions = createDispatchMap({
getCollectionProvider: GetCollectionProvider,
clearAddToCollectionState: ClearAddToCollectionState,
updateCollectionSubmission: UpdateCollectionSubmission,
deleteCollectionSubmission: RemoveCollectionSubmission,
setSelectedProject: SetSelectedProject,
getCurrentCollectionSubmission: GetCurrentCollectionSubmission,
getCedarRecords: GetCedarMetadataRecords,
createCedarRecord: CreateCedarMetadataRecord,
updateCedarRecord: UpdateCedarMetadataRecord,
});

showRemoveButton = computed(
Expand All @@ -133,7 +156,10 @@ export class AddToCollectionComponent implements CanDeactivateComponent {
}

@HostListener('window:beforeunload', ['$event'])
onBeforeUnload($event: BeforeUnloadEvent): boolean {
onBeforeUnload($event: BeforeUnloadEvent): boolean | undefined {
if (this.allowNavigation() || !this.hasUnsavedChanges()) {
return undefined;
}
$event.preventDefault();
return false;
}
Expand Down Expand Up @@ -171,11 +197,17 @@ export class AddToCollectionComponent implements CanDeactivateComponent {
this.stepperActiveValue.set(AddToCollectionSteps.Complete);
}

handleCedarDataSaved(data: CedarRecordDataBinding): void {
this.pendingCedarData.set(data);
this.collectionMetadataSaved.set(true);
this.stepperActiveValue.set(AddToCollectionSteps.Complete);
}

handleAddToCollection() {
const payload = {
collectionId: this.primaryCollectionId() || '',
projectId: this.selectedProject()?.id || '',
collectionMetadata: this.collectionMetadataForm.value || {},
collectionMetadata: this.isCedarMode() ? {} : this.collectionMetadataForm.value || {},
userId: this.currentUser()?.id || '',
};

Expand All @@ -186,13 +218,20 @@ export class AddToCollectionComponent implements CanDeactivateComponent {

this.actions
.updateCollectionSubmission(payload)
.pipe(takeUntilDestroyed(this.destroyRef))
.pipe(
switchMap(() => this.saveCedarRecordIfNeeded()),
finalize(() => this.loaderService.hide()),
takeUntilDestroyed(this.destroyRef)
)
.subscribe({
next: () => {
this.toastService.showSuccess('collections.addToCollection.confirmationDialogToastMessage');
this.allowNavigation.set(true);
this.router.navigate([this.selectedProject()?.id, 'overview']);
},
error: () => {
this.toastService.showError('collections.addToCollection.updateError');
},
});
} else {
this.customDialogService
Expand All @@ -203,11 +242,17 @@ export class AddToCollectionComponent implements CanDeactivateComponent {
})
.onClose.pipe(
filter((res) => !!res),
switchMap(() => this.saveCedarRecordIfNeeded()),
takeUntilDestroyed(this.destroyRef)
)
.subscribe(() => {
this.allowNavigation.set(true);
this.router.navigate([this.selectedProject()?.id, 'overview']);
.subscribe({
next: () => {
this.allowNavigation.set(true);
this.router.navigate([this.selectedProject()?.id, 'overview']);
},
error: () => {
this.toastService.showError('collections.addToCollection.updateError');
},
});
}
}
Expand Down Expand Up @@ -248,6 +293,20 @@ export class AddToCollectionComponent implements CanDeactivateComponent {
});
}

private saveCedarRecordIfNeeded(): Observable<unknown> {
if (!this.isCedarMode()) return of(null);

const cedarData = this.pendingCedarData();
const projectId = this.selectedProject()?.id;
const templateId = this.requiredMetadataTemplate()?.id;
if (!cedarData || !projectId || !templateId) return of(null);

const existingId = this.existingCedarRecord()?.id;
return existingId
? this.actions.updateCedarRecord(cedarData, existingId, projectId, ResourceType.Project)
: this.actions.createCedarRecord(cedarData, projectId, ResourceType.Project);
}
Comment thread
nsemets marked this conversation as resolved.

private initializeProvider(): void {
const id = this.route.snapshot.paramMap.get('providerId');
if (!id) {
Expand Down Expand Up @@ -286,6 +345,14 @@ export class AddToCollectionComponent implements CanDeactivateComponent {
this.actions.setSelectedProject(submission.project);
}
});

effect(() => {
const projectId = this.selectedProjectId();
const isCedar = this.isCedarMode();
if (isCedar && projectId) {
this.actions.getCedarRecords(projectId, ResourceType.Project);
}
});
}

private setupCleanup() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,25 @@ <h3>{{ 'collections.addToCollection.collectionMetadata' | translate }}</h3>

@if (!isDisabled() && stepperActiveValue() !== targetStepValue()) {
@if (collectionMetadataSaved()) {
@for (filterEntry of availableFilterEntries(); track filterEntry.key) {
<div>
<p class="font-bold">{{ filterEntry.labelKey | translate }}</p>
@if (isCedarMode()) {
@if (cedarTemplate()) {
<cedar-artifact-viewer
#cedarViewer
[config]="cedarViewerConfig"
[templateObject]="cedarTemplate()!.attributes.template"
[instanceObject]="cedarFormData()"
></cedar-artifact-viewer>
}
} @else {
@for (filterEntry of availableFilterEntries(); track filterEntry.key) {
<div>
<p class="font-bold">{{ filterEntry.labelKey | translate }}</p>

<p class="mt-2">
{{ collectionMetadataForm().get(filterEntry.key)?.value }}
</p>
</div>
<p class="mt-2">
{{ collectionMetadataForm().get(filterEntry.key)?.value }}
</p>
</div>
}
}
}

Expand All @@ -35,33 +46,59 @@ <h3>{{ 'collections.addToCollection.collectionMetadata' | translate }}</h3>

<p-step-panel [value]="targetStepValue()" class="p-0">
<ng-template class="m-0" #content let-activateCallback="activateCallback">
<form [formGroup]="collectionMetadataForm()" class="grid row-gap-2 mt-3">
@for (filterEntry of availableFilterEntries(); track filterEntry.key) {
<div class="flex flex-column gap-1 col-12 md:col-6">
<label [for]="filterEntry.key">{{ filterEntry.labelKey | translate }}</label>
<p-select
[id]="filterEntry.key"
[options]="filterEntry.options"
[formControlName]="filterEntry.key"
[placeholder]="filterEntry.labelKey | translate"
appendTo="body"
></p-select>
@if (isCedarMode()) {
@if (cedarTemplate()) {
<div class="cedar-editor-container mt-3">
<cedar-embeddable-editor
#cedarEditor
[config]="cedarConfig"
[templateObject]="cedarTemplate()!.attributes.template"
[metadata]="cedarFormData()"
(change)="onCedarChange($event)"
(keyup)="onCedarChange($event)"
></cedar-embeddable-editor>
</div>

<div class="flex justify-content-end gap-3 mt-4">
<p-button
severity="info"
[label]="'common.buttons.discardChanges' | translate"
(onClick)="handleDiscardChanges()"
/>
<p-button [label]="'common.buttons.saveAndContinue' | translate" (onClick)="handleSaveCedarMetadata()" />
</div>
} @else {
<p class="mt-3">{{ 'collections.addToCollection.cedarFormNotAvailable' | translate }}</p>
}
</form>
} @else {
<form [formGroup]="collectionMetadataForm()" class="grid row-gap-2 mt-3">
@for (filterEntry of availableFilterEntries(); track filterEntry.key) {
<div class="flex flex-column gap-1 col-12 md:col-6">
<label [for]="filterEntry.key">{{ filterEntry.labelKey | translate }}</label>
<p-select
[id]="filterEntry.key"
[options]="filterEntry.options"
[formControlName]="filterEntry.key"
[placeholder]="filterEntry.labelKey | translate"
appendTo="body"
></p-select>
</div>
}
</form>

<div class="flex justify-content-end gap-3 mt-4">
<p-button
severity="info"
[label]="'common.buttons.discardChanges' | translate"
(onClick)="handleDiscardChanges()"
/>
<p-button
[label]="'common.buttons.saveAndContinue' | translate"
[disabled]="!collectionMetadataForm().valid"
(onClick)="handleSaveMetadata()"
/>
</div>
<div class="flex justify-content-end gap-3 mt-4">
<p-button
severity="info"
[label]="'common.buttons.discardChanges' | translate"
(onClick)="handleDiscardChanges()"
/>
<p-button
[label]="'common.buttons.saveAndContinue' | translate"
[disabled]="!collectionMetadataForm().valid"
(onClick)="handleSaveMetadata()"
/>
</div>
}
</ng-template>
</p-step-panel>
</p-step-item>
Loading
Loading