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 11 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()">
Update
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you used {{ 'appLabels.form.action.cancel' | translate }} line 30, should use the format {{ 'appLabels.form.action.update' | translate }} here as well?! It exists in the en.json file

Copy link
Collaborator Author

@mdelez mdelez Feb 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was André's code that I moved around. I changed the text to use the translate pipe for 'update' in 2c88e7e.

</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,113 @@
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>();

list: ListNodeInfo;

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.list = response.body.nodeinfo;
this.buildForm(response.body.nodeinfo);
},
(error: ApiResponseError) => {
console.error(error);
}
);
}

buildForm(list: ListNodeInfo): void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be possible to add some description for each method, and possibly for tricky code lines?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added in 2c88e7e


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

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

this.loading = false;
}

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;
}
}

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;
}
);
}

}
32 changes: 18 additions & 14 deletions src/app/project/list/list-item-form/list-item-form.component.html
@@ -1,30 +1,34 @@
<!-- add new node item -->
<div class="new-list-item medium-field" *ngIf="parentIri && projectIri">
<dsp-string-literal-input class="list-item-label" [placeholder]="placeholder" [value]="[]"
(dataChanged)="handleData($event)" [language]="language" (enter)="submitData()">
(dataChanged)="handleData($event)" [language]="language" (enter)="createChildNode()">
</dsp-string-literal-input>
<button *ngIf="!loading" mat-icon-button matSuffix [disabled]="!labels && !labels?.length" class="add-node-btn"
(click)="submitData()">
(click)="createChildNode()">
<mat-icon>
add
</mat-icon>
</button>
<dsp-progress-indicator [status]="0" *ngIf="loading" class="progress-indicator"></dsp-progress-indicator>
</div>

<!-- edit node item -->
<div class="list-item medium-field" *ngIf="!(parentIri && projectIri) && labels">
<!-- TODO: at the moment (2019-09-12) we can't modify a node; the api doesn't support it; this is why we set the input to readonly -->
<!-- node item -->
<div class="list-item medium-field"
*ngIf="!(parentIri && projectIri) && labels"
(mouseenter)="mouseEnter()"
(mouseleave)="mouseLeave()">
<dsp-string-literal-input class="list-item-label"
[placeholder]="labels | dspStringifyStringLiteral:'all' | dspTruncate: 128" [value]="labels" [readonly]="true"
(dataChanged)="handleData($event)" [language]="language" (touched)="toggleBtn($event)">
(dataChanged)="handleData($event)" [language]="language">
</dsp-string-literal-input>
<!-- TODO: asap Knora api is ready to update single nodes set [disabled]="labels.length === 0" -->
<button *ngIf="!loading && updateData" type="submit" mat-icon-button matSuffix class="edit-node-btn"
[disabled]="labels.length === 0" (click)="submitData(); updateData = !updateData;">
<mat-icon>
check
</mat-icon>
</button>
<dsp-progress-indicator [status]="0" *ngIf="loading" class="progress-indicator"></dsp-progress-indicator>
<div class="action-bubble" *ngIf="showActionBubble" [@simpleFadeAnimation]="'in'">
<div class="button-container">
<button mat-button
class="edit"
title="edit"
(click)="$event.stopPropagation(); openDialog('editListNode', labels[0].value, iri)">
<mat-icon>edit</mat-icon>
</button>
</div>
</div>
</div>