Skip to content

Commit 49e2652

Browse files
authored
[ENG-9548] fix(wiki-list): Make rename wiki (#738)
- Ticket: [https://openscience.atlassian.net/browse/ENG-9548] - Feature flag: n/a ## Purpose Enable to rename a wiki
1 parent d80153a commit 49e2652

File tree

13 files changed

+284
-10
lines changed

13 files changed

+284
-10
lines changed

src/app/features/project/wiki/wiki.component.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
[canEdit]="hasWriteAccess()"
3737
(createWiki)="onCreateWiki()"
3838
(deleteWiki)="onDeleteWiki()"
39+
(renameWiki)="onRenameWiki()"
3940
></osf-wiki-list>
4041
@if (wikiModes().view) {
4142
<osf-view-section

src/app/features/project/wiki/wiki.component.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@ export class WikiComponent {
151151
this.navigateToWiki(this.currentWikiId());
152152
}
153153

154+
onRenameWiki() {
155+
this.navigateToWiki(this.currentWikiId());
156+
}
157+
154158
onDeleteWiki() {
155159
this.actions.deleteWiki(this.currentWikiId()).pipe(tap(() => this.navigateToWiki(this.currentWikiId())));
156160
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<form [formGroup]="renameWikiForm" (ngSubmit)="submitForm()">
2+
<osf-text-input
3+
[control]="renameWikiForm.controls['name']"
4+
[placeholder]="'project.wiki.addNewWikiPlaceholder'"
5+
[maxLength]="inputLimits.fullName.maxLength"
6+
>
7+
</osf-text-input>
8+
9+
<div class="flex justify-content-end gap-2 mt-4">
10+
<p-button [label]="'common.buttons.cancel' | translate" severity="info" (onClick)="dialogRef.close()" />
11+
<p-button
12+
[label]="'common.buttons.rename' | translate"
13+
type="submit"
14+
[loading]="isSubmitting()"
15+
[disabled]="renameWikiForm.invalid"
16+
/>
17+
</div>
18+
</form>

src/app/shared/components/wiki/rename-wiki-dialog/rename-wiki-dialog.component.scss

Whitespace-only changes.
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { MockComponent, MockProvider } from 'ng-mocks';
2+
3+
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
4+
5+
import { ComponentFixture, TestBed } from '@angular/core/testing';
6+
7+
import { ToastService } from '@osf/shared/services/toast.service';
8+
import { WikiSelectors } from '@osf/shared/stores/wiki';
9+
10+
import { TextInputComponent } from '../../text-input/text-input.component';
11+
12+
import { RenameWikiDialogComponent } from './rename-wiki-dialog.component';
13+
14+
import { TranslateServiceMock } from '@testing/mocks/translate.service.mock';
15+
import { provideMockStore } from '@testing/providers/store-provider.mock';
16+
17+
describe('RenameWikiDialogComponent', () => {
18+
let component: RenameWikiDialogComponent;
19+
let fixture: ComponentFixture<RenameWikiDialogComponent>;
20+
21+
beforeEach(async () => {
22+
await TestBed.configureTestingModule({
23+
imports: [RenameWikiDialogComponent, MockComponent(TextInputComponent)],
24+
providers: [
25+
TranslateServiceMock,
26+
MockProvider(DynamicDialogRef),
27+
MockProvider(DynamicDialogConfig, {
28+
data: {
29+
resourceId: 'project-123',
30+
wikiName: 'Wiki Name',
31+
},
32+
}),
33+
MockProvider(ToastService),
34+
provideMockStore({
35+
selectors: [{ selector: WikiSelectors.getWikiSubmitting, value: false }],
36+
}),
37+
],
38+
}).compileComponents();
39+
40+
fixture = TestBed.createComponent(RenameWikiDialogComponent);
41+
component = fixture.componentInstance;
42+
43+
fixture.detectChanges();
44+
});
45+
46+
it('should create', () => {
47+
expect(component).toBeTruthy();
48+
});
49+
50+
it('should initialize form with current name', () => {
51+
expect(component.renameWikiForm.get('name')?.value).toBe('Wiki Name');
52+
});
53+
54+
it('should have required validation on name field', () => {
55+
const nameControl = component.renameWikiForm.get('name');
56+
nameControl?.setValue('');
57+
58+
expect(nameControl?.hasError('required')).toBe(true);
59+
});
60+
61+
it('should validate name field with valid input', () => {
62+
const nameControl = component.renameWikiForm.get('name');
63+
nameControl?.setValue('Test Wiki Name');
64+
65+
expect(nameControl?.valid).toBe(true);
66+
});
67+
68+
it('should validate name field with whitespace only', () => {
69+
const nameControl = component.renameWikiForm.get('name');
70+
nameControl?.setValue(' ');
71+
72+
expect(nameControl?.hasError('required')).toBe(true);
73+
});
74+
75+
it('should validate name field with max length', () => {
76+
const nameControl = component.renameWikiForm.get('name');
77+
const longName = 'a'.repeat(256);
78+
nameControl?.setValue(longName);
79+
80+
expect(nameControl?.hasError('maxlength')).toBe(true);
81+
});
82+
83+
it('should close dialog on cancel', () => {
84+
const dialogRef = TestBed.inject(DynamicDialogRef);
85+
const closeSpy = jest.spyOn(dialogRef, 'close');
86+
87+
dialogRef.close();
88+
89+
expect(closeSpy).toHaveBeenCalled();
90+
});
91+
92+
it('should not submit form when invalid', () => {
93+
const dialogRef = TestBed.inject(DynamicDialogRef);
94+
const toastService = TestBed.inject(ToastService);
95+
96+
const closeSpy = jest.spyOn(dialogRef, 'close');
97+
const showSuccessSpy = jest.spyOn(toastService, 'showSuccess');
98+
99+
component.renameWikiForm.patchValue({ name: '' });
100+
101+
component.submitForm();
102+
103+
expect(showSuccessSpy).not.toHaveBeenCalled();
104+
expect(closeSpy).not.toHaveBeenCalled();
105+
});
106+
107+
it('should handle form submission with empty name', () => {
108+
const dialogRef = TestBed.inject(DynamicDialogRef);
109+
const toastService = TestBed.inject(ToastService);
110+
111+
const closeSpy = jest.spyOn(dialogRef, 'close');
112+
const showSuccessSpy = jest.spyOn(toastService, 'showSuccess');
113+
114+
component.renameWikiForm.patchValue({ name: ' ' });
115+
116+
component.submitForm();
117+
118+
expect(showSuccessSpy).not.toHaveBeenCalled();
119+
expect(closeSpy).not.toHaveBeenCalled();
120+
});
121+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { createDispatchMap, select } from '@ngxs/store';
2+
3+
import { TranslatePipe } from '@ngx-translate/core';
4+
5+
import { Button } from 'primeng/button';
6+
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
7+
8+
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
9+
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
10+
11+
import { InputLimits } from '@osf/shared/constants/input-limits.const';
12+
import { CustomValidators } from '@osf/shared/helpers/custom-form-validators.helper';
13+
import { ToastService } from '@osf/shared/services/toast.service';
14+
import { RenameWiki, WikiSelectors } from '@osf/shared/stores/wiki';
15+
16+
import { TextInputComponent } from '../../text-input/text-input.component';
17+
18+
@Component({
19+
selector: 'osf-rename-wiki-dialog-component',
20+
imports: [Button, ReactiveFormsModule, TranslatePipe, TextInputComponent],
21+
templateUrl: './rename-wiki-dialog.component.html',
22+
styleUrl: './rename-wiki-dialog.component.scss',
23+
changeDetection: ChangeDetectionStrategy.OnPush,
24+
})
25+
export class RenameWikiDialogComponent {
26+
readonly dialogRef = inject(DynamicDialogRef);
27+
readonly config = inject(DynamicDialogConfig);
28+
private toastService = inject(ToastService);
29+
30+
actions = createDispatchMap({ renameWiki: RenameWiki });
31+
isSubmitting = select(WikiSelectors.getWikiSubmitting);
32+
inputLimits = InputLimits;
33+
34+
renameWikiForm = new FormGroup({
35+
name: new FormControl(this.config.data.wikiName, {
36+
nonNullable: true,
37+
validators: [CustomValidators.requiredTrimmed(), Validators.maxLength(InputLimits.fullName.maxLength)],
38+
}),
39+
});
40+
41+
submitForm(): void {
42+
if (this.renameWikiForm.valid) {
43+
this.actions.renameWiki(this.config.data.wikiId, this.renameWikiForm.value.name ?? '').subscribe({
44+
next: () => {
45+
this.toastService.showSuccess('project.wiki.renameWikiSuccess');
46+
this.dialogRef.close(true);
47+
},
48+
error: (err) => {
49+
if (err?.status === 409) {
50+
this.toastService.showError('project.wiki.renameWikiConflict');
51+
}
52+
},
53+
});
54+
}
55+
}
56+
}

src/app/shared/components/wiki/wiki-list/wiki-list.component.html

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,20 @@ <h4 class="ml-2">{{ item.label | translate }}</h4>
6262
<span class="ml-2">{{ item.label | translate }}</span>
6363
}
6464
@default {
65-
<div>
66-
<i class="far fa-file"></i>
67-
<span class="ml-2">{{ item.label }}</span>
65+
<div class="flex align-items-center justify-content-between w-full">
66+
<div class="flex align-items-center">
67+
<i class="far fa-file"></i>
68+
<span class="ml-2">{{ item.label }}</span>
69+
</div>
70+
71+
<p-button
72+
icon="fas fa-pencil"
73+
[rounded]="true"
74+
variant="text"
75+
osfStopPropagation
76+
(onClick)="openRenameWikiDialog(item.id, item.label)"
77+
>
78+
</p-button>
6879
</div>
6980
}
7081
}

src/app/shared/components/wiki/wiki-list/wiki-list.component.scss

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,4 @@
77
min-width: 300px;
88
width: 300px;
99
}
10-
11-
.active {
12-
background-color: var(--pr-blue-1);
13-
color: var(--white);
14-
}
1510
}

src/app/shared/components/wiki/wiki-list/wiki-list.component.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { WikiItemType } from '@osf/shared/models/wiki/wiki-type.model';
1414
import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service';
1515
import { CustomDialogService } from '@osf/shared/services/custom-dialog.service';
1616
import { ComponentWiki } from '@osf/shared/stores/wiki';
17+
import { RenameWikiDialogComponent } from '@shared/components/wiki/rename-wiki-dialog/rename-wiki-dialog.component';
1718

1819
import { AddWikiDialogComponent } from '../add-wiki-dialog/add-wiki-dialog.component';
1920

@@ -35,6 +36,7 @@ export class WikiListComponent {
3536

3637
readonly deleteWiki = output<void>();
3738
readonly createWiki = output<void>();
39+
readonly renameWiki = output<void>();
3840

3941
private readonly customDialogService = inject(CustomDialogService);
4042
private readonly customConfirmationService = inject(CustomConfirmationService);
@@ -97,6 +99,19 @@ export class WikiListComponent {
9799
.onClose.subscribe(() => this.createWiki.emit());
98100
}
99101

102+
openRenameWikiDialog(wikiId: string, wikiName: string) {
103+
this.customDialogService
104+
.open(RenameWikiDialogComponent, {
105+
header: 'project.wiki.renameWiki',
106+
width: '448px',
107+
data: {
108+
wikiId: wikiId,
109+
wikiName: wikiName,
110+
},
111+
})
112+
.onClose.subscribe(() => this.renameWiki.emit());
113+
}
114+
100115
openDeleteWikiDialog(): void {
101116
this.customConfirmationService.confirmDelete({
102117
headerKey: 'project.wiki.deleteWiki',

src/app/shared/services/wiki.service.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,21 @@ export class WikiService {
6464
.pipe(map((response) => WikiMapper.fromCreateWikiResponse(response.data)));
6565
}
6666

67+
renameWiki(id: string, name: string): Observable<WikiModel> {
68+
const body = {
69+
data: {
70+
type: 'wikis',
71+
attributes: {
72+
id,
73+
name,
74+
},
75+
},
76+
};
77+
return this.jsonApiService
78+
.patch<WikiGetResponse>(`${this.apiUrl}/wikis/${id}/`, body)
79+
.pipe(map((response) => WikiMapper.fromCreateWikiResponse(response)));
80+
}
81+
6782
deleteWiki(wikiId: string): Observable<void> {
6883
return this.jsonApiService.delete(`${this.apiUrl}/wikis/${wikiId}/`);
6984
}

0 commit comments

Comments
 (0)