Skip to content
Merged
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
1 change: 1 addition & 0 deletions src/app/features/project/wiki/wiki.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
[canEdit]="hasWriteAccess()"
(createWiki)="onCreateWiki()"
(deleteWiki)="onDeleteWiki()"
(renameWiki)="onRenameWiki()"
></osf-wiki-list>
@if (wikiModes().view) {
<osf-view-section
Expand Down
4 changes: 4 additions & 0 deletions src/app/features/project/wiki/wiki.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ export class WikiComponent {
this.navigateToWiki(this.currentWikiId());
}

onRenameWiki() {
this.navigateToWiki(this.currentWikiId());
}

onDeleteWiki() {
this.actions.deleteWiki(this.currentWikiId()).pipe(tap(() => this.navigateToWiki(this.currentWikiId())));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<form [formGroup]="renameWikiForm" (ngSubmit)="submitForm()">
<osf-text-input
[control]="renameWikiForm.controls['name']"
[placeholder]="'project.wiki.addNewWikiPlaceholder'"
[maxLength]="inputLimits.fullName.maxLength"
>
</osf-text-input>

<div class="flex justify-content-end gap-2 mt-4">
<p-button [label]="'common.buttons.cancel' | translate" severity="info" (onClick)="dialogRef.close()" />
<p-button
[label]="'common.buttons.rename' | translate"
type="submit"
[loading]="isSubmitting()"
[disabled]="renameWikiForm.invalid"
/>
</div>
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { MockComponent, MockProvider } from 'ng-mocks';

import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';

import { ComponentFixture, TestBed } from '@angular/core/testing';

import { ToastService } from '@osf/shared/services/toast.service';
import { WikiSelectors } from '@osf/shared/stores/wiki';

import { TextInputComponent } from '../../text-input/text-input.component';

import { RenameWikiDialogComponent } from './rename-wiki-dialog.component';

import { TranslateServiceMock } from '@testing/mocks/translate.service.mock';
import { provideMockStore } from '@testing/providers/store-provider.mock';

describe('RenameWikiDialogComponent', () => {
let component: RenameWikiDialogComponent;
let fixture: ComponentFixture<RenameWikiDialogComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RenameWikiDialogComponent, MockComponent(TextInputComponent)],
providers: [
TranslateServiceMock,
MockProvider(DynamicDialogRef),
MockProvider(DynamicDialogConfig, {
data: {
resourceId: 'project-123',
wikiName: 'Wiki Name',
},
}),
MockProvider(ToastService),
provideMockStore({
selectors: [{ selector: WikiSelectors.getWikiSubmitting, value: false }],
}),
],
}).compileComponents();

fixture = TestBed.createComponent(RenameWikiDialogComponent);
component = fixture.componentInstance;

fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should initialize form with current name', () => {
expect(component.renameWikiForm.get('name')?.value).toBe('Wiki Name');
});

it('should have required validation on name field', () => {
const nameControl = component.renameWikiForm.get('name');
nameControl?.setValue('');

expect(nameControl?.hasError('required')).toBe(true);
});

it('should validate name field with valid input', () => {
const nameControl = component.renameWikiForm.get('name');
nameControl?.setValue('Test Wiki Name');

expect(nameControl?.valid).toBe(true);
});

it('should validate name field with whitespace only', () => {
const nameControl = component.renameWikiForm.get('name');
nameControl?.setValue(' ');

expect(nameControl?.hasError('required')).toBe(true);
});

it('should validate name field with max length', () => {
const nameControl = component.renameWikiForm.get('name');
const longName = 'a'.repeat(256);
nameControl?.setValue(longName);

expect(nameControl?.hasError('maxlength')).toBe(true);
});

it('should close dialog on cancel', () => {
const dialogRef = TestBed.inject(DynamicDialogRef);
const closeSpy = jest.spyOn(dialogRef, 'close');

dialogRef.close();

expect(closeSpy).toHaveBeenCalled();
});

it('should not submit form when invalid', () => {
const dialogRef = TestBed.inject(DynamicDialogRef);
const toastService = TestBed.inject(ToastService);

const closeSpy = jest.spyOn(dialogRef, 'close');
const showSuccessSpy = jest.spyOn(toastService, 'showSuccess');

component.renameWikiForm.patchValue({ name: '' });

component.submitForm();

expect(showSuccessSpy).not.toHaveBeenCalled();
expect(closeSpy).not.toHaveBeenCalled();
});

it('should handle form submission with empty name', () => {
const dialogRef = TestBed.inject(DynamicDialogRef);
const toastService = TestBed.inject(ToastService);

const closeSpy = jest.spyOn(dialogRef, 'close');
const showSuccessSpy = jest.spyOn(toastService, 'showSuccess');

component.renameWikiForm.patchValue({ name: ' ' });

component.submitForm();

expect(showSuccessSpy).not.toHaveBeenCalled();
expect(closeSpy).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { createDispatchMap, select } from '@ngxs/store';

import { TranslatePipe } from '@ngx-translate/core';

import { Button } from 'primeng/button';
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';

import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';

import { InputLimits } from '@osf/shared/constants/input-limits.const';
import { CustomValidators } from '@osf/shared/helpers/custom-form-validators.helper';
import { ToastService } from '@osf/shared/services/toast.service';
import { RenameWiki, WikiSelectors } from '@osf/shared/stores/wiki';

import { TextInputComponent } from '../../text-input/text-input.component';

@Component({
selector: 'osf-rename-wiki-dialog-component',
imports: [Button, ReactiveFormsModule, TranslatePipe, TextInputComponent],
templateUrl: './rename-wiki-dialog.component.html',
styleUrl: './rename-wiki-dialog.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RenameWikiDialogComponent {
readonly dialogRef = inject(DynamicDialogRef);
readonly config = inject(DynamicDialogConfig);
private toastService = inject(ToastService);

actions = createDispatchMap({ renameWiki: RenameWiki });
isSubmitting = select(WikiSelectors.getWikiSubmitting);
inputLimits = InputLimits;

renameWikiForm = new FormGroup({
name: new FormControl(this.config.data.wikiName, {
nonNullable: true,
validators: [CustomValidators.requiredTrimmed(), Validators.maxLength(InputLimits.fullName.maxLength)],
}),
});

submitForm(): void {
if (this.renameWikiForm.valid) {
this.actions.renameWiki(this.config.data.wikiId, this.renameWikiForm.value.name ?? '').subscribe({
next: () => {
this.toastService.showSuccess('project.wiki.renameWikiSuccess');
this.dialogRef.close(true);
},
error: (err) => {
if (err?.status === 409) {
this.toastService.showError('project.wiki.renameWikiConflict');
}
},
});
}
}
}
17 changes: 14 additions & 3 deletions src/app/shared/components/wiki/wiki-list/wiki-list.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,20 @@ <h4 class="ml-2">{{ item.label | translate }}</h4>
<span class="ml-2">{{ item.label | translate }}</span>
}
@default {
<div>
<i class="far fa-file"></i>
<span class="ml-2">{{ item.label }}</span>
<div class="flex align-items-center justify-content-between w-full">
<div class="flex align-items-center">
<i class="far fa-file"></i>
<span class="ml-2">{{ item.label }}</span>
</div>

<p-button
icon="fas fa-pencil"
[rounded]="true"
variant="text"
osfStopPropagation
(onClick)="openRenameWikiDialog(item.id, item.label)"
>
</p-button>
</div>
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,4 @@
min-width: 300px;
width: 300px;
}

.active {
background-color: var(--pr-blue-1);
color: var(--white);
}
}
15 changes: 15 additions & 0 deletions src/app/shared/components/wiki/wiki-list/wiki-list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { WikiItemType } from '@osf/shared/models/wiki/wiki-type.model';
import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service';
import { CustomDialogService } from '@osf/shared/services/custom-dialog.service';
import { ComponentWiki } from '@osf/shared/stores/wiki';
import { RenameWikiDialogComponent } from '@shared/components/wiki/rename-wiki-dialog/rename-wiki-dialog.component';

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

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

readonly deleteWiki = output<void>();
readonly createWiki = output<void>();
readonly renameWiki = output<void>();

private readonly customDialogService = inject(CustomDialogService);
private readonly customConfirmationService = inject(CustomConfirmationService);
Expand Down Expand Up @@ -97,6 +99,19 @@ export class WikiListComponent {
.onClose.subscribe(() => this.createWiki.emit());
}

openRenameWikiDialog(wikiId: string, wikiName: string) {
this.customDialogService
.open(RenameWikiDialogComponent, {
header: 'project.wiki.renameWiki',
width: '448px',
data: {
wikiId: wikiId,
wikiName: wikiName,
},
})
.onClose.subscribe(() => this.renameWiki.emit());
}

openDeleteWikiDialog(): void {
this.customConfirmationService.confirmDelete({
headerKey: 'project.wiki.deleteWiki',
Expand Down
15 changes: 15 additions & 0 deletions src/app/shared/services/wiki.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,21 @@ export class WikiService {
.pipe(map((response) => WikiMapper.fromCreateWikiResponse(response.data)));
}

renameWiki(id: string, name: string): Observable<WikiModel> {
const body = {
data: {
type: 'wikis',
attributes: {
id,
name,
},
},
};
return this.jsonApiService
.patch<WikiGetResponse>(`${this.apiUrl}/wikis/${id}/`, body)
.pipe(map((response) => WikiMapper.fromCreateWikiResponse(response)));
}

deleteWiki(wikiId: string): Observable<void> {
return this.jsonApiService.delete(`${this.apiUrl}/wikis/${wikiId}/`);
}
Expand Down
9 changes: 9 additions & 0 deletions src/app/shared/stores/wiki/wiki.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ export class CreateWiki {
) {}
}

export class RenameWiki {
static readonly type = '[Wiki] Rename Wiki';

constructor(
public wikiId: string,
public name: string
) {}
}

export class DeleteWiki {
static readonly type = '[Wiki] Delete Wiki';
constructor(public wikiId: string) {}
Expand Down
27 changes: 27 additions & 0 deletions src/app/shared/stores/wiki/wiki.state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
GetWikiList,
GetWikiVersionContent,
GetWikiVersions,
RenameWiki,
SetCurrentWiki,
ToggleMode,
UpdateWikiPreviewContent,
Expand Down Expand Up @@ -55,6 +56,32 @@ export class WikiState {
);
}

@Action(RenameWiki)
renameWiki(ctx: StateContext<WikiStateModel>, action: RenameWiki) {
const state = ctx.getState();
ctx.patchState({
wikiList: {
...state.wikiList,
isSubmitting: true,
},
});
return this.wikiService.renameWiki(action.wikiId, action.name).pipe(
tap((wiki) => {
const updatedWiki = wiki.id === action.wikiId ? { ...wiki, name: action.name } : wiki;
const updatedList = state.wikiList.data.map((w) => (w.id === updatedWiki.id ? updatedWiki : w));
ctx.patchState({
wikiList: {
...state.wikiList,
data: [...updatedList],
isSubmitting: false,
},
currentWikiId: updatedWiki.id,
});
}),
catchError((error) => this.handleError(ctx, error))
);
}

@Action(DeleteWiki)
deleteWiki(ctx: StateContext<WikiStateModel>, action: DeleteWiki) {
const state = ctx.getState();
Expand Down
6 changes: 4 additions & 2 deletions src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -899,10 +899,13 @@
},
"wiki": {
"addNewWiki": "Add new wiki page",
"renameWiki": "Rename wiki page",
"deleteWiki": "Delete wiki page",
"deleteWikiMessage": "Are you sure you want to delete this wiki page?",
"addNewWikiPlaceholder": "New wiki name",
"addWikiSuccess": "Wiki page has been added successfully",
"renameWikiSuccess": "Wiki page has been renamed successfully",
"renameWikiConflict": "That wiki name already exists.",
"view": "View",
"edit": "Edit",
"compare": "Compare",
Expand Down Expand Up @@ -1906,8 +1909,7 @@
"emailPlaceholder": "email@example.com"
},
"buttons": {
"cancel": "Cancel",
"add": "Add"
"cancel": "Cancel"
},
"messages": {
"success": "Alternative email added successfully",
Expand Down