Skip to content

Commit

Permalink
feat(list editor): Adds support for editing lists (DSP-741) (#365)
Browse files Browse the repository at this point in the history
* feat(list editor): Adds support for editing lists

* feat(list editor): Adds support for editing lists

* feat(list editor): updates label placeholder when updating labels

* test(edit-list-item): adds unit tests

* test: cleanup and add additional test for form validation

* chore: adds method descriptions and updates a label to use the translation service

* chore: adds more method descriptions
  • Loading branch information
mdelez committed Feb 4, 2021
1 parent 7b795ee commit 5b6ee4b
Show file tree
Hide file tree
Showing 11 changed files with 565 additions and 82 deletions.
4 changes: 3 additions & 1 deletion src/app/app.module.ts
Expand Up @@ -89,6 +89,7 @@ import { AngularSplitModule } from 'angular-split';
import { PersonTemplateComponent } from './project/board/person-template/person-template.component';
import { AddressTemplateComponent } from './project/board/address-template/address-template.component';
import { OrganisationTemplateComponent } from './project/board/organisation-template/organisation-template.component';
import { EditListItemComponent } from './project/list/list-item-form/edit-list-item/edit-list-item.component';

// translate: AoT requires an exported function for factories
export function HttpLoaderFactory(httpClient: HttpClient) {
Expand Down Expand Up @@ -160,7 +161,8 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
ContactsTabViewComponent,
PersonTemplateComponent,
AddressTemplateComponent,
OrganisationTemplateComponent
OrganisationTemplateComponent,
EditListItemComponent
],
imports: [
AppRoutingModule,
Expand Down
7 changes: 7 additions & 0 deletions src/app/main/dialog/dialog.component.html
Expand Up @@ -148,6 +148,13 @@
</app-list-info-form>
</div>

<!-- Update list child node -->
<div *ngSwitchCase="'editListNode'">
<app-dialog-header [title]="data.title" [subtitle]="'Edit child node'">
</app-dialog-header>
<app-edit-list-item [iri]="data.id" [projectIri]="data.project" (closeDialog)="dialogRef.close($event)"></app-edit-list-item>
</div>

<!-- Delete list -->
<div *ngSwitchCase="'deleteList'">
<app-dialog-header [title]="data.title" [subtitle]="'appLabels.form.lists.title.delete' | translate"></app-dialog-header>
Expand Down
@@ -0,0 +1,45 @@
<dsp-progress-indicator *ngIf="loading"></dsp-progress-indicator>

<div *ngIf="!loading" class="form-content list-info">

<!-- list label -->
<dsp-string-literal-input
[placeholder]="'Child node label'"
[value]="labels"
(dataChanged)="handleData($event, 'labels')">
</dsp-string-literal-input>
<span class="invalid-form" *ngIf="formInvalidMessage">{{ formInvalidMessage }}</span>

<br><br>

<!-- list description / comment -->
<dsp-string-literal-input
[textarea]="true"
[placeholder]="'Child node description'"
[value]="comments"
(dataChanged)="handleData($event, 'comments')"
[language]="labels.length ? labels[0].language : 'en'">
</dsp-string-literal-input>

<div class="form-panel form-action">
<span>
<button
mat-button
type="button"
(click)="closeDialog.emit()">
{{ 'appLabels.form.action.cancel' | translate }}
</button>
</span>
<span class="fill-remaining-space"></span>
<span>
<button
mat-raised-button
type="submit"
color="primary"
[disabled]="saveButtonDisabled"
(click)="updateChildNode()">
{{ 'appLabels.form.action.update' | translate }}
</button>
</span>
</div>
</div>
@@ -0,0 +1,4 @@
.invalid-form{
color: red;
font-size: 11px;
}
@@ -0,0 +1,152 @@
import { Component, DebugElement, OnInit, ViewChild } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ApiResponseData, ListNodeInfoResponse, ListsEndpointAdmin, UpdateChildNodeRequest } from '@dasch-swiss/dsp-js';
import { DspActionModule, DspApiConnectionToken, ProgressIndicatorComponent } from '@dasch-swiss/dsp-ui';
import { TranslateModule } from '@ngx-translate/core';
import { of } from 'rxjs';
import { AjaxResponse } from 'rxjs/ajax';
import { EditListItemComponent } from './edit-list-item.component';

/**
* Test host component to simulate parent component.
*/
@Component({
template: `<app-edit-list-item #editListItem [iri]="iri" [projectIri]="projectIri"></app-edit-list-item>`
})
class TestHostComponent implements OnInit {

@ViewChild('editListItem') editListItem: EditListItemComponent;

iri = 'http://rdfh.ch/lists/0001/otherTreeList01';

projectIri = 'http://rdfh.ch/projects/0001';

constructor() {}

ngOnInit() {
}

}

describe('EditListItemComponent', () => {
let testHostComponent: TestHostComponent;
let testHostFixture: ComponentFixture<TestHostComponent>;
let editListItemComponentDe: DebugElement;
let formInvalidMessageDe: DebugElement;

beforeEach(async(() => {

const listsEndpointSpyObj = {
admin: {
listsEndpoint: jasmine.createSpyObj('listsEndpoint', ['getListNodeInfo', 'updateChildNode'])
}
};

TestBed.configureTestingModule({
declarations: [
EditListItemComponent,
TestHostComponent,
ProgressIndicatorComponent,
],
imports: [
BrowserAnimationsModule,
DspActionModule,
TranslateModule.forRoot()
],
providers: [
{
provide: DspApiConnectionToken,
useValue: listsEndpointSpyObj
}
]
})
.compileComponents();
}));

beforeEach(() => {
const dspConnSpy = TestBed.inject(DspApiConnectionToken);

(dspConnSpy.admin.listsEndpoint as jasmine.SpyObj<ListsEndpointAdmin>).getListNodeInfo.and.callFake(
() => {
const response = new ListNodeInfoResponse();
response.nodeinfo.id = 'http://rdfh.ch/lists/0001/otherTreeList01';
response.nodeinfo.labels = [{'value': 'Tree list node 01', 'language': 'en'}];
response.nodeinfo.comments = [{'value': 'My comment', 'language': 'en'}];
return of(ApiResponseData.fromAjaxResponse({response} as AjaxResponse));
}
);

testHostFixture = TestBed.createComponent(TestHostComponent);
testHostComponent = testHostFixture.componentInstance;
testHostFixture.detectChanges();

expect(testHostComponent).toBeTruthy();

const hostCompDe = testHostFixture.debugElement;
editListItemComponentDe = hostCompDe.query(By.directive(EditListItemComponent));
expect(editListItemComponentDe).toBeTruthy();
});

it('should assign labels and comments', () => {
const dspConnSpy = TestBed.inject(DspApiConnectionToken);
expect(testHostComponent.editListItem.labels).toEqual([{'value': 'Tree list node 01', 'language': 'en'}]);
expect(testHostComponent.editListItem.comments).toEqual([{'value': 'My comment', 'language': 'en'}]);
expect(dspConnSpy.admin.listsEndpoint.getListNodeInfo).toHaveBeenCalledTimes(1);
expect(dspConnSpy.admin.listsEndpoint.getListNodeInfo).toHaveBeenCalledWith('http://rdfh.ch/lists/0001/otherTreeList01');

});

it('should update labels when the value changes', () => {
expect(testHostComponent.editListItem.labels).toEqual([{'value': 'Tree list node 01', 'language': 'en'}]);
testHostComponent.editListItem.handleData([{'value': 'Tree list node 01', 'language': 'en'}, {'value': 'Baumlistenknoten 01', 'language': 'de'}], 'labels');
expect(testHostComponent.editListItem.labels).toEqual([{'value': 'Tree list node 01', 'language': 'en'}, {'value': 'Baumlistenknoten 01', 'language': 'de'}]);
expect(testHostComponent.editListItem.saveButtonDisabled).toBeFalsy();
testHostComponent.editListItem.handleData([], 'labels');
expect(testHostComponent.editListItem.saveButtonDisabled).toBeTruthy();
testHostFixture.detectChanges();
formInvalidMessageDe = editListItemComponentDe.query(By.css('span.invalid-form'));
expect(formInvalidMessageDe.nativeElement.innerText).toEqual('A label is required.');
});

it('should update comments when the value changes', () => {
expect(testHostComponent.editListItem.comments).toEqual([{'value': 'My comment', 'language': 'en'}]);
testHostComponent.editListItem.handleData([{'value': 'My comment', 'language': 'en'}, {'value': 'Mein Kommentar', 'language': 'de'}], 'comments');
expect(testHostComponent.editListItem.comments).toEqual([{'value': 'My comment', 'language': 'en'}, {'value': 'Mein Kommentar', 'language': 'de'}]);
expect(testHostComponent.editListItem.saveButtonDisabled).toBeFalsy();
testHostComponent.editListItem.handleData([], 'comments');
expect(testHostComponent.editListItem.saveButtonDisabled).toBeFalsy();
});

it('should update the child node info', () => {
const dspConnSpy = TestBed.inject(DspApiConnectionToken);

testHostComponent.editListItem.handleData([{'value': 'Tree list node 01', 'language': 'en'}, {'value': 'Baumlistenknoten 01', 'language': 'de'}], 'labels');
testHostComponent.editListItem.handleData([{'value': 'My comment', 'language': 'en'}, {'value': 'Mein Kommentar', 'language': 'de'}], 'comments');

(dspConnSpy.admin.listsEndpoint as jasmine.SpyObj<ListsEndpointAdmin>).updateChildNode.and.callFake(
() => {
const response = new ListNodeInfoResponse();
response.nodeinfo.id = 'http://rdfh.ch/lists/0001/otherTreeList01';
response.nodeinfo.labels = [{'value': 'Tree list node 01', 'language': 'en'}, {'value': 'Baumlistenknoten 01', 'language': 'de'}];
response.nodeinfo.comments = [{'value': 'My comment', 'language': 'en'}, {'value': 'Mein Kommentar', 'language': 'de'}];

expect(testHostComponent.editListItem.labels).toEqual(response.nodeinfo.labels);
expect(testHostComponent.editListItem.comments).toEqual(response.nodeinfo.comments);

return of(ApiResponseData.fromAjaxResponse({response} as AjaxResponse));
}
);

const childNodeUpdateData: UpdateChildNodeRequest = new UpdateChildNodeRequest();
childNodeUpdateData.projectIri = testHostComponent.editListItem.projectIri;
childNodeUpdateData.listIri = testHostComponent.editListItem.iri;
childNodeUpdateData.labels = testHostComponent.editListItem.labels;
childNodeUpdateData.comments = testHostComponent.editListItem.comments;

testHostComponent.editListItem.updateChildNode();
expect(dspConnSpy.admin.listsEndpoint.updateChildNode).toHaveBeenCalledTimes(1);
expect(dspConnSpy.admin.listsEndpoint.updateChildNode).toHaveBeenCalledWith(childNodeUpdateData);
});
});
@@ -0,0 +1,130 @@
import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core';
import { ApiResponseData, ApiResponseError, ChildNodeInfoResponse, KnoraApiConnection, List, ListNodeInfo, ListNodeInfoResponse, StringLiteral, UpdateChildNodeRequest } from '@dasch-swiss/dsp-js';
import { DspApiConnectionToken } from '@dasch-swiss/dsp-ui';

@Component({
selector: 'app-edit-list-item',
templateUrl: './edit-list-item.component.html',
styleUrls: ['./edit-list-item.component.scss']
})
export class EditListItemComponent implements OnInit {
loading: boolean;

@Input() iri: string;

@Input() projectIri: string;

@Output() closeDialog: EventEmitter<List | ListNodeInfo> = new EventEmitter<List>();

// the list node being edited
listNode: ListNodeInfo;

// local arrays to use when updating the list node
labels: StringLiteral[];
comments: StringLiteral[];

/**
* error checking on the following fields
*/
formErrors = {
label: {
'required': 'A label is required.'
}
};

/**
* in case of an API error
*/
errorMessage: any;

saveButtonDisabled = false;

formInvalidMessage: string;

constructor(@Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection) { }

ngOnInit(): void {
this.loading = true;

// get list
this._dspApiConnection.admin.listsEndpoint.getListNodeInfo(this.iri).subscribe(
(response: ApiResponseData<ListNodeInfoResponse>) => {
this.listNode = response.body.nodeinfo;
this.buildForm(response.body.nodeinfo);
},
(error: ApiResponseError) => {
console.error(error);
}
);
}

/**
* Separates the labels and comments of a list node into two local arrays.
*
* @param listNode info about a list node
*/
buildForm(listNode: ListNodeInfo): void {

this.labels = [];
this.comments = [];

if (listNode && listNode.id) {
this.labels = listNode.labels;
this.comments = listNode.comments;
}

this.loading = false;
}

/**
* Called from the template any time the labels or comments are changed to update the local arrays.
* At least one label is required. Otherwise, the 'update' button will be disabled.
*
* @param data the data that was changed
* @param type the type of data that was changed
*/
handleData(data: StringLiteral[], type: string) {
switch (type) {
case 'labels':
this.labels = data;
break;

case 'comments':
this.comments = data;
break;
}

if (this.labels.length === 0) {
// invalid form, don't let user submit
this.saveButtonDisabled = true;
this.formInvalidMessage = this.formErrors.label.required;
} else {
this.saveButtonDisabled = false;
this.formInvalidMessage = null;
}
}

/**
* Called from the template when the 'update' button is clicked.
* Sends a request to DSP-API to update the list node with the data inside the two local arrays.
*/
updateChildNode() {
const childNodeUpdateData: UpdateChildNodeRequest = new UpdateChildNodeRequest();
childNodeUpdateData.projectIri = this.projectIri;
childNodeUpdateData.listIri = this.iri;
childNodeUpdateData.labels = this.labels;
childNodeUpdateData.comments = this.comments.length > 0 ? this.comments : [];

this._dspApiConnection.admin.listsEndpoint.updateChildNode(childNodeUpdateData).subscribe(
(response: ApiResponseData<ChildNodeInfoResponse>) => {
this.loading = false;
this.closeDialog.emit(response.body.nodeinfo);
},
(error: ApiResponseError) => {
this.errorMessage = error;
this.loading = false;
}
);
}

}

0 comments on commit 5b6ee4b

Please sign in to comment.