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(add-link-resource-button): Add Link Resource Button (DEV-404) #645

Merged
merged 16 commits into from Feb 14, 2022
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions src/app/app.module.ts
Expand Up @@ -155,6 +155,7 @@ import { SearchSelectOntologyComponent } from './workspace/search/advanced-searc
import { ExpertSearchComponent } from './workspace/search/expert-search/expert-search.component';
import { FulltextSearchComponent } from './workspace/search/fulltext-search/fulltext-search.component';
import { SearchPanelComponent } from './workspace/search/search-panel/search-panel.component';
import { CreateLinkResourceComponent } from './workspace/resource/operations/create-link-resource/create-link-resource.component';

// translate: AoT requires an exported function for factories
export function httpLoaderFactory(httpClient: HttpClient) {
Expand Down Expand Up @@ -298,6 +299,7 @@ export function httpLoaderFactory(httpClient: HttpClient) {
UsersComponent,
UsersListComponent,
YetAnotherDateValueComponent,
CreateLinkResourceComponent,
],
imports: [
AngularSplitModule.forRoot(),
Expand Down
7 changes: 7 additions & 0 deletions src/app/main/dialog/dialog.component.html
Expand Up @@ -399,6 +399,13 @@
</mat-dialog-content>
</div>

<div *ngSwitchCase="'createLinkResource'">
<app-dialog-header [title]="data.title" [subtitle]="'Create a new linked resource'"></app-dialog-header>
<mat-dialog-content>
<app-create-link-resource [parentResource]="data.parentResource" [propDef]="data.id" [resourceClassDef]="data.resourceClassDefinition" (closeDialog)="dialogRef.close($event)"></app-create-link-resource>
</mat-dialog-content>
</div>

<!-- general error message -->
<div *ngSwitchCase="'error'">
<app-error [status]="data.id"></app-error>
Expand Down
3 changes: 3 additions & 0 deletions src/app/main/dialog/dialog.component.ts
@@ -1,5 +1,6 @@
import { Component, Inject, OnInit, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { ReadResource } from '@dasch-swiss/dsp-js';
import { PropertyInfoObject } from 'src/app/project/ontology/default-data/default-properties';
import { FilteredResources } from 'src/app/workspace/results/list-view/list-view.component';

Expand All @@ -16,8 +17,10 @@ export interface DialogData {
canBeUpdated?: boolean;
position?: number;
parentIri?: string;
parentResource?: ReadResource;
projectCode?: string;
selectedResources?: FilteredResources;
resourceClassDefinition?: string;
}

export interface ConfirmationWithComment {
Expand Down
@@ -0,0 +1,29 @@
<form [formGroup]="propertiesForm" (ngSubmit)="onSubmit()">
<!-- upload file -->
<app-upload *ngIf="hasFileValue"
[parentForm]="propertiesForm"
[representation]="hasFileValue"
(fileInfo)="setFileValue($event)">
</app-upload>

<app-select-properties #selectProps
[ontologyInfo]="ontologyInfo"
[resourceClass]="resourceClass"
[properties]="properties"
[parentForm]="propertiesForm"
class="select-properties">
</app-select-properties>
<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" class="form-submit" [disabled]="!propertiesForm.valid">
{{ 'appLabels.form.action.submit' | translate}}
</button>
</span>
</div>
</form>
Comment on lines +1 to +29
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the create-link-resource.component the same as in the second step of resource-instance-form? Does it make sense to duplicate the code?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@kilchenmann they're quite similar but the resource-instance-form has a back button to go back to the first step and it redirects the user to the created resource after it's been created. resource-instance-form uses an array of ResourceClassDefinition whereas create-link-resource does not use an array. We could try to condense it all into one component but we might have a lot of ugly if statements to hide/show relevant parts of the component depending on what the user is doing. What are your thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for your response. That's true. Sooner or later the resource-instance-form will anyway have only one form (without the first step) and the back button will not be needed anymore. I would appreciate it if we do not have too many of the same forms.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Should I refactor in this PR or another one?

Copy link
Contributor

Choose a reason for hiding this comment

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

What do you prefer? I think it should be resolved in this PR... or what will be the simplest solution?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

after speaking with André, we decided to do the refactor in another PR because the create-resource-instance form will change in the future

@@ -0,0 +1,310 @@
import { Component, DebugElement, Inject, Input, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { By } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {
CreateIntValue,
CreateResource,
CreateTextValueAsString,
CreateValue,
MockOntology,
MockResource,
ReadResource,
ResourceClassAndPropertyDefinitions,
ResourceClassDefinition,
ResourcePropertyDefinition,
ResourcesEndpointV2
} from '@dasch-swiss/dsp-js';
import { OntologyCache } from '@dasch-swiss/dsp-js/src/cache/ontology-cache/OntologyCache';
import { TranslateModule } from '@ngx-translate/core';
import { of } from 'rxjs';
import { DspApiConnectionToken } from 'src/app/main/declarations/dsp-api-tokens';
import { BaseValueDirective } from 'src/app/main/directive/base-value.directive';
import { SwitchPropertiesComponent } from '../../resource-instance-form/select-properties/switch-properties/switch-properties.component';
import { ValueService } from '../../services/value.service';
import { IntValueComponent } from '../../values/int-value/int-value.component';
import { TextValueAsStringComponent } from '../../values/text-value/text-value-as-string/text-value-as-string.component';

import { CreateLinkResourceComponent } from './create-link-resource.component';

/**
* test host component to simulate parent component.
*/
@Component({
template: `
<app-create-link-resource [parentResource]="parentResource" [propDef]="propDef" [resourceClassDef]="resourceClassDef" #createLinkResourceComp></app-create-link-resource>`
})
class TestHostComponent implements OnInit {

@ViewChild('createLinkResourceComp') createLinkResourceComponent: CreateLinkResourceComponent;

parentResource: ReadResource;
propDef: string;
resourceClassDef: string;

ngOnInit() {
MockResource.getTestThing().subscribe(res => {
this.parentResource = res;
this.propDef = 'http://0.0.0.0:3333/ontology/0001/anything/v2#hasOtherThingValue';
this.resourceClassDef = 'http://0.0.0.0:3333/ontology/0001/anything/v2#Thing';
});
}

}

/**
* mock select-properties component to use in tests.
*/
@Component({
selector: 'app-select-properties',
template: `
<app-text-value-as-string #createVal
[mode]="'create'"
[commentDisabled]="true"
[valueRequiredValidator]="true"
[parentForm]="parentForm"
[formName]="'label'">
</app-text-value-as-string>
`
})
class MockSelectPropertiesComponent {
@ViewChildren('switchProp') switchPropertiesComponent: QueryList<SwitchPropertiesComponent>;

// input for resource's label
@ViewChild('createVal') createValueComponent: BaseValueDirective;

@Input() properties: ResourcePropertyDefinition[];

@Input() ontologyInfo: ResourceClassAndPropertyDefinitions;

@Input() resourceClass: ResourceClassDefinition;

@Input() parentForm: FormGroup;

parentResource = new ReadResource();

constructor(private _valueService: ValueService) { }
}

/**
* mock switch-properties component to use in tests.
*/
@Component({
selector: 'app-switch-properties'
})
class MockSwitchPropertiesComponent {
@ViewChild('createVal') createValueComponent: BaseValueDirective;

@Input() property: ResourcePropertyDefinition;

@Input() parentResource: ReadResource;

@Input() parentForm: FormGroup;

@Input() formName: string;
}

/**
* mock value component to use in tests.
*/
@Component({
selector: 'dsp-int-value'
})
class MockCreateIntValueComponent implements OnInit {

@ViewChild('createVal') createValueComponent: IntValueComponent;

@Input() parentForm: FormGroup;

@Input() formName: string;

@Input() mode;

@Input() displayValue;

form: FormGroup;

valueFormControl: FormControl;

constructor(@Inject(FormBuilder) private _fb: FormBuilder) { }

ngOnInit(): void {
this.valueFormControl = new FormControl(null, [Validators.required]);

this.form = this._fb.group({
test: this.valueFormControl
});
}

getNewValue(): CreateValue {
const createIntVal = new CreateIntValue();

createIntVal.int = 123;

return createIntVal;
}

updateCommentVisibility(): void { }
}

/**
* mock value component to use in tests.
*/
@Component({
selector: 'app-text-value-as-string'
})
class MockCreateTextValueComponent implements OnInit {

@ViewChild('createVal') createValueComponent: TextValueAsStringComponent;

@Input() parentForm: FormGroup;

@Input() formName: string;

@Input() mode;

@Input() displayValue;

@Input() commentDisabled?: boolean;

@Input() valueRequiredValidator: boolean;

form: FormGroup;

valueFormControl: FormControl;
constructor(@Inject(FormBuilder) private _fb: FormBuilder) { }
ngOnInit(): void {
this.valueFormControl = new FormControl(null, [Validators.required]);
this.form = this._fb.group({
label: this.valueFormControl
});
}
getNewValue(): CreateValue {
const createTextVal = new CreateTextValueAsString();
createTextVal.text = 'My Label';
return createTextVal;
}
updateCommentVisibility(): void { }
}

describe('CreateLinkResourceComponent', () => {
let testHostComponent: TestHostComponent;
let testHostFixture: ComponentFixture<TestHostComponent>;
let createLinkResourceComponentDe: DebugElement;

beforeEach(waitForAsync(() => {

const dspConnSpy = {
v2: {
ontologyCache: jasmine.createSpyObj('ontologyCache', ['getOntology', 'getResourceClassDefinition']),
res: jasmine.createSpyObj('res', ['createResource'])
}
};

TestBed.configureTestingModule({
declarations: [
CreateLinkResourceComponent,
TestHostComponent,
MockSelectPropertiesComponent,
MockCreateTextValueComponent,
MockSwitchPropertiesComponent,
MockCreateIntValueComponent
],
imports: [
BrowserAnimationsModule,
MatButtonModule,
MatDialogModule,
MatFormFieldModule,
MatSnackBarModule,
ReactiveFormsModule,
TranslateModule.forRoot()
],
providers: [
{
provide: DspApiConnectionToken,
useValue: dspConnSpy
},
ValueService
]
})
.compileComponents();
}));

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

(dspConnSpy.v2.ontologyCache as jasmine.SpyObj<OntologyCache>).getResourceClassDefinition.and.callFake(
(resClassIri: string) => of(MockOntology.mockIResourceClassAndPropertyDefinitions('http://0.0.0.0:3333/ontology/0001/anything/v2#Thing'))
);

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

const hostCompDe = testHostFixture.debugElement;

createLinkResourceComponentDe = hostCompDe.query(By.directive(CreateLinkResourceComponent));

});

it('should initialize the properties array', async () => {
expect(testHostComponent.createLinkResourceComponent.properties.length).toEqual(18);
});

it('should submit the form', () => {
const dspConnSpy = TestBed.inject(DspApiConnectionToken);

(dspConnSpy.v2.res as jasmine.SpyObj<ResourcesEndpointV2>).createResource.and.callFake(
() => {
let resource = new ReadResource();

MockResource.getTestThing().subscribe((res) => {
resource = res;
});

return of(resource);
}
);

const anythingOnto = MockOntology.mockReadOntology('http://0.0.0.0:3333/ontology/0001/anything/v2');

// get resource class definitions
const resourceClasses = anythingOnto.getClassDefinitionsByType(ResourceClassDefinition);

testHostComponent.createLinkResourceComponent.properties = new Array<ResourcePropertyDefinition>();

MockResource.getTestThing().subscribe(res => {
const resourcePropDef = (res.entityInfo as ResourceClassAndPropertyDefinitions).getAllPropertyDefinitions()[9];
testHostComponent.createLinkResourceComponent.properties.push(resourcePropDef as ResourcePropertyDefinition);
});

testHostFixture.detectChanges();

const selectPropertiesComp = createLinkResourceComponentDe.query(By.directive(MockSelectPropertiesComponent));

expect(selectPropertiesComp).toBeTruthy();

const label = new CreateTextValueAsString();
label.text = 'My Label';

const props = {};
const createVal = new CreateIntValue();
createVal.int = 123;
props['http://0.0.0.0:3333/ontology/0001/anything/v2#hasInteger'] = [createVal];

const expectedCreateResource = new CreateResource();
expectedCreateResource.label = 'My Label';
expectedCreateResource.type = 'http://0.0.0.0:3333/ontology/0001/anything/v2#Thing';
expectedCreateResource.properties = props;

testHostComponent.createLinkResourceComponent.onSubmit();

expect(dspConnSpy.v2.res.createResource).toHaveBeenCalledTimes(1);
});
});