Skip to content

Commit fde004e

Browse files
committed
Merge remote-tracking branch 'upstream/feature/pbs-25-24' into fix/ENG-9838
2 parents 41e1ef7 + 063a1f2 commit fde004e

20 files changed

+334
-135
lines changed

src/app/app.routes.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,13 @@ export const routes: Routes = [
156156
import('./core/components/forbidden-page/forbidden-page.component').then((mod) => mod.ForbiddenPageComponent),
157157
data: { skipBreadcrumbs: true },
158158
},
159+
{
160+
path: 'preprints/:providerId/:id/pending-moderation',
161+
loadComponent: () =>
162+
import(
163+
'@osf/features/preprints/pages/preprint-pending-moderation/preprint-pending-moderation.component'
164+
).then((mod) => mod.PreprintPendingModerationComponent),
165+
},
159166
{
160167
path: 'request-access/:id',
161168
loadComponent: () =>

src/app/features/preprints/pages/preprint-details/preprint-details.component.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,15 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy {
383383
}
384384
}
385385
},
386+
error: (error) => {
387+
if (
388+
error instanceof HttpErrorResponse &&
389+
error.status === 403 &&
390+
error?.error?.errors[0]?.detail === 'This preprint is pending moderation and is not yet publicly available.'
391+
) {
392+
this.router.navigate(['/preprints', this.providerId(), preprintId, 'pending-moderation']);
393+
}
394+
},
386395
});
387396
}
388397

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<section class="container text-center flex flex-column flex-1 my-7 mx-3 p-3 md:p-4">
2+
<h2>{{ 'preprints.details.moderationStatusBanner.pendingDetails.title' | translate }}</h2>
3+
<p class="mt-4">{{ 'preprints.details.moderationStatusBanner.pendingDetails.body' | translate }}</p>
4+
</section>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
@use "styles/mixins" as mix;
2+
3+
:host {
4+
@include mix.flex-center;
5+
flex: 1;
6+
background: var(--gradient-3);
7+
}
8+
9+
.container {
10+
max-width: mix.rem(448px);
11+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { PreprintPendingModerationComponent } from './preprint-pending-moderation.component';
4+
5+
import { OSFTestingModule } from '@testing/osf.testing.module';
6+
7+
describe('PreprintPendingModerationComponent', () => {
8+
let component: PreprintPendingModerationComponent;
9+
let fixture: ComponentFixture<PreprintPendingModerationComponent>;
10+
11+
beforeEach(async () => {
12+
await TestBed.configureTestingModule({
13+
imports: [PreprintPendingModerationComponent, OSFTestingModule],
14+
}).compileComponents();
15+
16+
fixture = TestBed.createComponent(PreprintPendingModerationComponent);
17+
component = fixture.componentInstance;
18+
fixture.detectChanges();
19+
});
20+
21+
it('should create', () => {
22+
expect(component).toBeTruthy();
23+
});
24+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { TranslatePipe } from '@ngx-translate/core';
2+
3+
import { ChangeDetectionStrategy, Component } from '@angular/core';
4+
5+
@Component({
6+
selector: 'osf-preprint-pending-moderation',
7+
templateUrl: './preprint-pending-moderation.component.html',
8+
styleUrl: './preprint-pending-moderation.component.scss',
9+
changeDetection: ChangeDetectionStrategy.OnPush,
10+
imports: [TranslatePipe],
11+
})
12+
export class PreprintPendingModerationComponent {}

src/app/features/settings/account-settings/components/add-email/add-email.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
styleClass="w-full"
1313
(onClick)="dialogRef.close()"
1414
severity="info"
15-
[label]="'settings.accountSettings.addEmail.buttons.cancel' | translate"
15+
[label]="'common.buttons.cancel' | translate"
1616
>
1717
</p-button>
1818

@@ -22,7 +22,7 @@
2222
[disabled]="emailControl.invalid"
2323
(click)="addEmail()"
2424
[loading]="isSubmitting()"
25-
[label]="'settings.accountSettings.addEmail.buttons.add' | translate"
25+
[label]="'common.buttons.add' | translate"
2626
>
2727
</p-button>
2828
</div>

src/app/shared/components/wiki/compare-section/compare-section.component.html

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,20 @@
88
<ng-template #header>
99
<h2 class="mr-2">{{ 'project.wiki.compare' | translate }}</h2>
1010
</ng-template>
11-
<div class="flex flex-wrap align-items-center mb-2 lg:flex-nowrap">
12-
<span class="mr-2">Live preview to</span>
11+
12+
<div class="flex flex-wrap align-items-center mb-4 lg:flex-nowrap">
13+
<span class="min-w-max mr-2">{{ 'project.wiki.livePreviewTo' | translate }}:</span>
14+
1315
<p-select
16+
class="w-full"
17+
styleClass="select-version"
1418
[options]="mappedVersions()"
1519
[ngModel]="selectedVersion"
1620
(onChange)="onVersionChange($event.value)"
17-
placeholder="Version"
18-
class="w-full"
19-
styleClass="select-version"
21+
[placeholder]="'project.wiki.version.title' | translate"
2022
/>
2123
</div>
24+
2225
<p-panel styleClass="compare-view" showHeader="false">
2326
<div lass="mt-3" [innerHTML]="content()"></div>
2427
</p-panel>

src/app/shared/components/wiki/compare-section/compare-section.component.spec.ts

Lines changed: 119 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import { ComponentFixture, TestBed } from '@angular/core/testing';
2-
import { provideNoopAnimations } from '@angular/platform-browser/animations';
32

43
import { WikiVersion } from '@shared/models/wiki/wiki.model';
54

65
import { CompareSectionComponent } from './compare-section.component';
76

87
import { TranslateServiceMock } from '@testing/mocks/translate.service.mock';
8+
import { OSFTestingModule } from '@testing/osf.testing.module';
99

1010
describe('CompareSectionComponent', () => {
1111
let component: CompareSectionComponent;
1212
let fixture: ComponentFixture<CompareSectionComponent>;
13+
let translateServiceMock: any;
1314

1415
const mockVersions: WikiVersion[] = [
1516
{
@@ -22,32 +23,35 @@ describe('CompareSectionComponent', () => {
2223
createdAt: '2024-01-02T10:00:00Z',
2324
createdBy: 'Jane Smith',
2425
},
26+
{
27+
id: 'version-3',
28+
createdAt: '2024-01-03T10:00:00Z',
29+
createdBy: 'Bob Johnson',
30+
},
2531
];
2632

2733
const mockVersionContent = 'Original content';
2834
const mockPreviewContent = 'Updated content with changes';
2935

3036
beforeEach(async () => {
3137
await TestBed.configureTestingModule({
32-
imports: [CompareSectionComponent],
33-
providers: [TranslateServiceMock, provideNoopAnimations()],
38+
imports: [CompareSectionComponent, OSFTestingModule],
39+
providers: [TranslateServiceMock],
3440
}).compileComponents();
3541

42+
translateServiceMock = TestBed.inject(TranslateServiceMock.provide);
43+
translateServiceMock.instant.mockReturnValue('Current');
44+
3645
fixture = TestBed.createComponent(CompareSectionComponent);
3746
component = fixture.componentInstance;
3847

3948
fixture.componentRef.setInput('versions', mockVersions);
4049
fixture.componentRef.setInput('versionContent', mockVersionContent);
4150
fixture.componentRef.setInput('previewContent', mockPreviewContent);
4251
fixture.componentRef.setInput('isLoading', false);
43-
4452
fixture.detectChanges();
4553
});
4654

47-
it('should create', () => {
48-
expect(component).toBeTruthy();
49-
});
50-
5155
it('should set versions input', () => {
5256
expect(component.versions()).toEqual(mockVersions);
5357
});
@@ -64,14 +68,60 @@ describe('CompareSectionComponent', () => {
6468
expect(component.isLoading()).toBe(false);
6569
});
6670

67-
it('should emit selectVersion when version changes', () => {
71+
it('should handle empty versions array', () => {
72+
fixture.componentRef.setInput('versions', []);
73+
fixture.detectChanges();
74+
75+
expect(component.versions()).toEqual([]);
76+
expect(component.selectedVersion).toBeUndefined();
77+
});
78+
79+
it('should initialize with first version selected and emit selectVersion', () => {
80+
expect(component.selectedVersion).toBe(mockVersions[0].id);
81+
});
82+
83+
it('should not emit when no versions available', () => {
6884
const emitSpy = jest.spyOn(component.selectVersion, 'emit');
69-
const versionId = 'version-2';
7085

71-
component.onVersionChange(versionId);
86+
fixture.componentRef.setInput('versions', []);
87+
fixture.detectChanges();
7288

73-
expect(component.selectedVersion).toBe(versionId);
74-
expect(emitSpy).toHaveBeenCalledWith(versionId);
89+
expect(component.selectedVersion).toBeUndefined();
90+
expect(emitSpy).not.toHaveBeenCalled();
91+
});
92+
93+
it('should map versions correctly', () => {
94+
const mappedVersions = component.mappedVersions();
95+
96+
expect(mappedVersions).toHaveLength(3);
97+
expect(mappedVersions[0].value).toBe('version-1');
98+
expect(mappedVersions[0].label).toContain('(Current)');
99+
expect(mappedVersions[0].label).toContain('John Doe');
100+
expect(mappedVersions[1].value).toBe('version-2');
101+
expect(mappedVersions[1].label).toContain('(2)');
102+
expect(mappedVersions[1].label).toContain('Jane Smith');
103+
expect(mappedVersions[2].value).toBe('version-3');
104+
expect(mappedVersions[2].label).toContain('(1)');
105+
expect(mappedVersions[2].label).toContain('Bob Johnson');
106+
});
107+
108+
it('should handle version with undefined createdBy', () => {
109+
const versionsWithUndefinedCreator: WikiVersion[] = [
110+
{
111+
id: 'version-1',
112+
createdAt: '2024-01-01T10:00:00Z',
113+
createdBy: undefined,
114+
},
115+
];
116+
117+
fixture.componentRef.setInput('versions', versionsWithUndefinedCreator);
118+
fixture.detectChanges();
119+
120+
const mappedVersions = component.mappedVersions();
121+
expect(mappedVersions).toHaveLength(1);
122+
expect(mappedVersions[0].value).toBe('version-1');
123+
expect(mappedVersions[0].label).toContain('(Current)');
124+
expect(mappedVersions[0].label).toContain('1/1/2024');
75125
});
76126

77127
it('should handle single version', () => {
@@ -84,7 +134,61 @@ describe('CompareSectionComponent', () => {
84134
expect(mappedVersions[0].label).toContain('(Current)');
85135
});
86136

87-
it('should initialize with first version selected', () => {
88-
expect(component.selectedVersion).toBe(mockVersions[0].id);
137+
it('should compute content diff correctly', () => {
138+
const content = component.content();
139+
140+
expect(content).toContain('<span class="removed">Original</span>');
141+
expect(content).toContain('<span class="added">Updated</span>');
142+
expect(content).toContain('content');
143+
expect(content).toContain('<span class="added">with changes</span>');
144+
});
145+
146+
it('should handle identical content', () => {
147+
fixture.componentRef.setInput('previewContent', mockVersionContent);
148+
fixture.detectChanges();
149+
150+
const content = component.content();
151+
expect(content).toBe(mockVersionContent);
152+
});
153+
154+
it('should handle empty version content', () => {
155+
fixture.componentRef.setInput('versionContent', '');
156+
fixture.detectChanges();
157+
158+
const content = component.content();
159+
expect(content).toContain('<span class="added">Updated content with changes</span>');
160+
});
161+
162+
it('should handle empty preview content', () => {
163+
fixture.componentRef.setInput('previewContent', '');
164+
fixture.detectChanges();
165+
166+
const content = component.content();
167+
expect(content).toContain('<span class="removed">Original content</span>');
168+
});
169+
170+
it('should update selectedVersion and emit selectVersion', () => {
171+
const emitSpy = jest.spyOn(component.selectVersion, 'emit');
172+
const versionId = 'version-2';
173+
174+
component.onVersionChange(versionId);
175+
176+
expect(component.selectedVersion).toBe(versionId);
177+
expect(emitSpy).toHaveBeenCalledWith(versionId);
178+
expect(emitSpy).toHaveBeenCalledTimes(1);
179+
});
180+
181+
it('should emit correct version id when called multiple times', () => {
182+
const emitSpy = jest.spyOn(component.selectVersion, 'emit');
183+
184+
component.onVersionChange('version-2');
185+
component.onVersionChange('version-3');
186+
component.onVersionChange('version-1');
187+
188+
expect(component.selectedVersion).toBe('version-1');
189+
expect(emitSpy).toHaveBeenCalledTimes(3);
190+
expect(emitSpy).toHaveBeenNthCalledWith(1, 'version-2');
191+
expect(emitSpy).toHaveBeenNthCalledWith(2, 'version-3');
192+
expect(emitSpy).toHaveBeenNthCalledWith(3, 'version-1');
89193
});
90194
});

src/app/shared/components/wiki/compare-section/compare-section.component.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { TranslatePipe } from '@ngx-translate/core';
1+
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
22

33
import { Panel } from 'primeng/panel';
44
import { Select } from 'primeng/select';
55
import { Skeleton } from 'primeng/skeleton';
66

7-
import { ChangeDetectionStrategy, Component, computed, effect, input, output } from '@angular/core';
7+
import { ChangeDetectionStrategy, Component, computed, effect, inject, input, output } from '@angular/core';
88
import { FormsModule } from '@angular/forms';
99

1010
import { WikiVersion } from '@osf/shared/models/wiki/wiki.model';
@@ -25,20 +25,23 @@ export class CompareSectionComponent {
2525
isLoading = input.required<boolean>();
2626
selectVersion = output<string>();
2727

28+
translateService = inject(TranslateService);
29+
2830
selectedVersion: string | null = null;
2931

32+
private readonly currentLabel = this.translateService.instant('project.wiki.version.current');
33+
private readonly unknownAuthorLabel = this.translateService.instant('project.wiki.version.unknownAuthor');
34+
3035
mappedVersions = computed(() => [
31-
...this.versions().map((version, index) => {
32-
const labelPrefix = index === 0 ? '(Current)' : `(${this.versions().length - index})`;
33-
return {
34-
label: `${labelPrefix} ${version.createdBy}: (${new Date(version.createdAt).toLocaleString()})`,
35-
value: version.id,
36-
};
37-
}),
36+
...this.versions().map((version, index) => ({
37+
label: this.formatVersionLabel(version, index),
38+
value: version.id,
39+
})),
3840
]);
3941

4042
content = computed(() => {
4143
const changes = Diff.diffWords(this.versionContent(), this.previewContent());
44+
4245
return changes
4346
.map((change) => {
4447
if (change.added) {
@@ -54,13 +57,21 @@ export class CompareSectionComponent {
5457
constructor() {
5558
effect(() => {
5659
this.selectedVersion = this.versions()[0]?.id;
60+
5761
if (this.selectedVersion) {
5862
this.selectVersion.emit(this.selectedVersion);
5963
}
6064
});
6165
}
66+
6267
onVersionChange(versionId: string): void {
6368
this.selectedVersion = versionId;
6469
this.selectVersion.emit(versionId);
6570
}
71+
72+
private formatVersionLabel(version: WikiVersion, index: number): string {
73+
const prefix = index === 0 ? `(${this.currentLabel})` : `(${this.versions().length - index})`;
74+
const creator = version.createdBy || this.unknownAuthorLabel;
75+
return `${prefix} ${creator}: (${new Date(version.createdAt).toLocaleString()})`;
76+
}
6677
}

0 commit comments

Comments
 (0)