Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(list editor): Adds support for editing lists (DSP-741) #365

Merged
merged 13 commits into from Feb 4, 2021
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
}
);
}

}