diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/support/RestControllersSupport.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/support/RestControllersSupport.java index 1605b86dd..7dc1a9abe 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/support/RestControllersSupport.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/support/RestControllersSupport.java @@ -1,9 +1,11 @@ package edu.internet2.tier.shibboleth.admin.ui.controller.support; import com.google.common.collect.ImmutableMap; +import edu.internet2.tier.shibboleth.admin.ui.controller.ErrorResponse; import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -30,12 +32,18 @@ public MetadataResolver findResolverOrThrowHttp404(String resolverResourceId) { return resolver; } - - @ExceptionHandler + //TODO: Review this handler and update accordingly. Do we still need it? + @ExceptionHandler(HttpClientErrorException.class) public ResponseEntity notFoundHandler(HttpClientErrorException ex) { if(ex.getStatusCode() == NOT_FOUND) { return ResponseEntity.status(NOT_FOUND).body(ex.getStatusText()); } throw ex; } + + @ExceptionHandler(Exception.class) + public final ResponseEntity handleAllOtherExceptions(Exception ex) { + ErrorResponse errorResponse = new ErrorResponse("400", ex.getLocalizedMessage()); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } } diff --git a/backend/src/main/resources/dynamic-http-metadata-provider.schema.json b/backend/src/main/resources/dynamic-http-metadata-provider.schema.json index 7362b40d7..f9dec02b8 100644 --- a/backend/src/main/resources/dynamic-http-metadata-provider.schema.json +++ b/backend/src/main/resources/dynamic-http-metadata-provider.schema.json @@ -2,7 +2,8 @@ "type": "object", "required": [ "xmlId", - "metadataURL" + "metadataURL", + "metadataRequestURLConstructionScheme" ], "properties": { "enabled": { @@ -20,6 +21,39 @@ }, "metadataRequestURLConstructionScheme": { "type": "object", + "required": [ + "@type", + "content" + ], + "anyOf": [ + { + "properties": { + "@type": { + "enum": [ + "Regex" + ] + } + }, + "required": [ + "@type", + "content", + "match" + ] + }, + { + "properties": { + "@type": { + "enum": [ + "MetadataQueryProtocol" + ] + } + }, + "required": [ + "@type", + "content" + ] + } + ], "properties": { "@type": { "title": "label.md-request-type", @@ -35,12 +69,6 @@ ], "description": "value.md-query-protocol" }, - { - "enum": [ - "Template" - ], - "description": "value.template" - }, { "enum": [ "Regex" @@ -54,67 +82,6 @@ "description": "tooltip.md-request-value", "type": "string" }, - "transformRef": { - "title": "label.transform-ref", - "description": "tooltip.transform-ref", - "type": "string", - "visibleIf": { - "content": [ - "" - ] - } - }, - "encodingStyle": { - "title": "label.encoding-style", - "description": "tooltip.encoding-style", - "type": "string", - "widget": { - "id": "select" - }, - "default": "FORM", - "oneOf": [ - { - "enum": [ - "NONE" - ], - "description": "None" - }, - { - "enum": [ - "FORM" - ], - "description": "Form" - }, - { - "enum": [ - "PATH" - ], - "description": "Path" - }, - { - "enum": [ - "FRAGMENT" - ], - "description": "Fragment" - } - ], - "visibleIf": { - "@type": [ - "Template" - ] - } - }, - "velocityEngine": { - "title": "label.velocity-engine", - "description": "tooltip.velocity-engine", - "type": "string", - "default": "shibboleth.VelocityEngine", - "visibleIf": { - "@type": [ - "Template" - ] - } - }, "match": { "title": "label.match", "description": "tooltip.match", @@ -638,7 +605,6 @@ "title": "label.certificate-file", "description": "tooltip.certificate-file", "type": "string", - "widget": "textarea", "default": "" } }, diff --git a/backend/src/main/resources/i18n/messages_en.properties b/backend/src/main/resources/i18n/messages_en.properties index 3d44ca8eb..01b5c18e2 100644 --- a/backend/src/main/resources/i18n/messages_en.properties +++ b/backend/src/main/resources/i18n/messages_en.properties @@ -371,6 +371,11 @@ message.uri-valid-format=URI must be valid format. message.id-unique=ID must be unique. message.array-items-must-be-unique=Items in list must be unique. +message.org-name-required=Organization Name is required. +message.org-displayName-required=Organization Name is required. +message.org-url-required=Organization Name is required. +message.org-incomplete=These three fields must all be entered if any single field has a value. + message.conflict=Conflict message.data-version-contention=Data Version Contention message.contention-new-version=A newer version of this metadata source has been saved. Below are a list of changes. You can use your changes or their changes. diff --git a/backend/src/main/resources/metadata-sources-ui-schema.json b/backend/src/main/resources/metadata-sources-ui-schema.json index d1353043e..3f002253a 100644 --- a/backend/src/main/resources/metadata-sources-ui-schema.json +++ b/backend/src/main/resources/metadata-sources-ui-schema.json @@ -45,18 +45,24 @@ } }, "dependencies": { - "name": [ - "displayName", - "url" - ], - "displayName": [ - "name", - "url" - ], - "url": [ - "name", - "displayName" - ] + "name": { + "required": [ + "displayName", + "url" + ] + }, + "displayName": { + "required": [ + "name", + "url" + ] + }, + "url": { + "required": [ + "name", + "displayName" + ] + } } }, "contacts": { diff --git a/ui/src/app/metadata/domain/model/wizards/metadata-source-base.spec.ts b/ui/src/app/metadata/domain/model/wizards/metadata-source-base.spec.ts index 3386d65e4..0cf3344ad 100644 --- a/ui/src/app/metadata/domain/model/wizards/metadata-source-base.spec.ts +++ b/ui/src/app/metadata/domain/model/wizards/metadata-source-base.spec.ts @@ -40,7 +40,10 @@ describe('Metadata Source Base class', () => { it('should return a list of validators for the ngx-schema-form', () => { expect(Object.keys(getValidators([]))).toEqual([ '/', - '/entityId' + '/entityId', + '/organization/name', + '/organization/displayName', + '/organization/url' ]); }); }); diff --git a/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts b/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts index 7d29d34c5..ebc8921d8 100644 --- a/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts +++ b/ui/src/app/metadata/domain/model/wizards/metadata-source-base.ts @@ -60,6 +60,17 @@ export class MetadataSourceBase implements Wizard { } getValidators(entityIdList: string[]): { [key: string]: any } { + const checkOrg = (value, property, form) => { + const org = property.parent; + const orgValue = org.value || {}; + const err = Object.keys(orgValue) && !value ? { + code: 'ORG_INCOMPLETE', + path: `#${property.path}`, + message: `message.org-incomplete`, + params: [value] + } : null; + return err; + }; const validators = { '/': (value, property, form_current) => { let errors; @@ -68,7 +79,7 @@ export class MetadataSourceBase implements Wizard { const item = value[key]; const validatorKey = `/${key}`; const validator = validators.hasOwnProperty(validatorKey) ? validators[validatorKey] : null; - const error = validator ? validator(item, { path: `/${key}` }, form_current) : null; + const error = validator ? validator(item, form_current.getProperty(key), form_current) : null; if (error) { errors = errors || []; errors.push(error); @@ -84,7 +95,10 @@ export class MetadataSourceBase implements Wizard { params: [value] } : null; return err; - } + }, + '/organization/name': checkOrg, + '/organization/displayName': checkOrg, + '/organization/url': checkOrg }; return validators; } diff --git a/ui/src/app/metadata/domain/service/draft.service.ts b/ui/src/app/metadata/domain/service/draft.service.ts index b43713359..824e8ec64 100644 --- a/ui/src/app/metadata/domain/service/draft.service.ts +++ b/ui/src/app/metadata/domain/service/draft.service.ts @@ -29,6 +29,10 @@ export class EntityDraftService { ); } + exists(id: string, attr: string = 'id'): boolean { + return this.storage.query().some(entity => entity[attr] === id); + } + save(provider: MetadataResolver): Observable { this.storage.add(provider); return of(provider); diff --git a/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.spec.ts b/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.spec.ts index a7e0dd900..0469b62ca 100644 --- a/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.spec.ts +++ b/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.spec.ts @@ -127,7 +127,7 @@ describe('Dashboard Resolvers List Page', () => { it('should route to the wizard page', () => { spyOn(router, 'navigate'); instance.edit(draft); - expect(router.navigate).toHaveBeenCalledWith(['metadata', 'resolver', 'new', 'blank', 'org-info'], { + expect(router.navigate).toHaveBeenCalledWith(['metadata', 'resolver', 'new', 'blank', 'common'], { queryParams: { id: '1' } }); }); diff --git a/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.ts b/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.ts index e6577886d..537cf198c 100644 --- a/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.ts +++ b/ui/src/app/metadata/manager/container/dashboard-resolvers-list.component.ts @@ -72,7 +72,7 @@ export class DashboardResolversListComponent implements OnInit { edit(entity: MetadataEntity): void { if (entity.isDraft()) { - this.router.navigate(['metadata', 'resolver', 'new', 'blank', 'org-info'], { + this.router.navigate(['metadata', 'resolver', 'new', 'blank', 'common'], { queryParams: { id: entity.getId() } diff --git a/ui/src/app/metadata/provider/model/dynamic-http.provider.form.ts b/ui/src/app/metadata/provider/model/dynamic-http.provider.form.ts index 1692d8f36..7f38d02e3 100644 --- a/ui/src/app/metadata/provider/model/dynamic-http.provider.form.ts +++ b/ui/src/app/metadata/provider/model/dynamic-http.provider.form.ts @@ -11,21 +11,7 @@ export const DynamicHttpMetadataProviderWizard: Wizard { - let transform = property.parent.getProperty('transformRef'); - let content = property.parent.getProperty('content'); - if (!content.value && property.value !== 'Regex') { - transform.setVisible(true); - } else { - transform.setVisible(false); - } - } - } - ] - }, + bindings: {}, getValidators(namesList: string[] = [], xmlIdList: string[] = []): any { const validators = BaseMetadataProviderEditor.getValidators(namesList); validators['/xmlId'] = (value, property, form) => { diff --git a/ui/src/app/metadata/resolver/container/new-resolver.component.html b/ui/src/app/metadata/resolver/container/new-resolver.component.html index 924321a93..efaea7239 100644 --- a/ui/src/app/metadata/resolver/container/new-resolver.component.html +++ b/ui/src/app/metadata/resolver/container/new-resolver.component.html @@ -19,7 +19,8 @@

How are you ad class="btn btn-lg btn-block btn-secondary" aria-label="Upload local metadata file or use a metadata URL" role="button" - routerLink="upload" + [routerLink]="['./', 'upload']" + queryParamsHandling="merge" routerLinkActive="btn-success"> Upload/URL @@ -33,7 +34,8 @@

How are you ad class="btn btn-lg btn-block btn-secondary" aria-label="Create metadata source using the wizard" role="button" - [routerLink]="['./']" + [routerLink]="['./', 'blank', 'common']" + queryParamsHandling="merge" routerLinkActive="btn-info"> Create @@ -47,7 +49,8 @@

How are you ad class="btn btn-lg btn-block btn-secondary" aria-label="Copy a metadata source" role="button" - routerLink="copy" + [routerLink]="['./', 'copy']" + queryParamsHandling="merge" routerLinkActive="btn-warning"> Copy diff --git a/ui/src/app/metadata/resolver/container/new-resolver.component.spec.ts b/ui/src/app/metadata/resolver/container/new-resolver.component.spec.ts index bf0f91506..ca735a319 100644 --- a/ui/src/app/metadata/resolver/container/new-resolver.component.spec.ts +++ b/ui/src/app/metadata/resolver/container/new-resolver.component.spec.ts @@ -23,6 +23,7 @@ describe('New Resolver Page', () => { let instance: NewResolverComponent; let activatedRoute: ActivatedRouteStub = new ActivatedRouteStub(); activatedRoute.testParamMap = { id: 'foo', events: of({}) }; + activatedRoute.data = of('foo'); beforeEach(() => { TestBed.configureTestingModule({ diff --git a/ui/src/app/metadata/resolver/container/new-resolver.component.ts b/ui/src/app/metadata/resolver/container/new-resolver.component.ts index ab4f09103..fa51a426d 100644 --- a/ui/src/app/metadata/resolver/container/new-resolver.component.ts +++ b/ui/src/app/metadata/resolver/container/new-resolver.component.ts @@ -27,13 +27,13 @@ export class NewResolverComponent { debounceTime(10), map(url => { let child = this.route.snapshot.firstChild; - return child.routeConfig.path.match('blank').length === 0 || child.params.index === 'common'; + return !child.routeConfig.path.match('blank') || child.params.index === 'common'; }) ); - this.actionsSubscription = this.route.queryParams.pipe( + this.actionsSubscription = this.route.data.pipe( distinctUntilChanged(), - map(params => new SelectDraftRequest(params.id)) + map(data => new SelectDraftRequest(data.draft)) ).subscribe(this.store); } } diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts b/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts index 566291404..bd5dfced5 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts +++ b/ui/src/app/metadata/resolver/container/resolver-wizard-step.component.ts @@ -77,14 +77,10 @@ export class ResolverWizardStepComponent implements OnDestroy { this.valueChangeEmitted$.pipe( withLatestFrom(this.definition$), filter(([ changes, definition ]) => (!!definition && !!changes)), - map(([ changes, definition ]) => definition.parser(changes.value)), - withLatestFrom(this.store.select(fromResolver.getSelectedDraft)), - map(([changes, original]) => ({ ...original, ...changes })) + map(([ changes, definition ]) => definition.parser(changes.value)) ) .subscribe(changes => { - if (changes.id) { - this.store.dispatch(new UpdateChanges(changes)); - } + this.store.dispatch(new UpdateChanges(changes)); }); this.statusChangeEmitted$.pipe(distinctUntilChanged()).subscribe(errors => this.updateStatus(errors)); diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard.component.spec.ts b/ui/src/app/metadata/resolver/container/resolver-wizard.component.spec.ts index fdd16fd5f..877a7298c 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard.component.spec.ts +++ b/ui/src/app/metadata/resolver/container/resolver-wizard.component.spec.ts @@ -2,24 +2,22 @@ import { Component, ViewChild } from '@angular/core'; import { TestBed, async, ComponentFixture } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { StoreModule, Store, combineReducers } from '@ngrx/store'; - -import { NgbDropdownModule, NgbPopoverModule, NgbModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; +import { RouterStateSnapshot } from '@angular/router'; +import { NgbDropdownModule, NgbPopoverModule, NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { of } from 'rxjs'; import { ResolverWizardComponent } from './resolver-wizard.component'; import * as fromRoot from '../reducer'; -import { WizardModule } from '../../../wizard/wizard.module'; -import { WizardSummaryComponent } from '../../domain/component/wizard-summary.component'; -import { SummaryPropertyComponent } from '../../domain/component/summary-property.component'; import * as fromWizard from '../../../wizard/reducer'; import { MockI18nModule } from '../../../../testing/i18n.stub'; import { METADATA_SOURCE_WIZARD } from '../wizard-definition'; import { MetadataSourceWizard } from '../../domain/model/wizards/metadata-source-wizard'; import { initialState } from '../reducer/entity.reducer'; import { MockWizardModule } from '../../../../testing/wizard.stub'; -import { RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router'; + import { NgbModalStub } from '../../../../testing/modal.stub'; -import { of } from 'rxjs'; import { MetadataResolver } from '../../domain/model'; +import { DifferentialService } from '../../../core/service/differential.service'; @Component({ template: ` @@ -94,6 +92,7 @@ describe('Resolver Wizard Component', () => { TestHostComponent ], providers: [ + DifferentialService, { provide: NgbModal, useClass: NgbModalStub }, { provide: METADATA_SOURCE_WIZARD, useValue: MetadataSourceWizard } ] diff --git a/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts b/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts index d6cac6053..c3c7881f1 100644 --- a/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts +++ b/ui/src/app/metadata/resolver/container/resolver-wizard.component.ts @@ -12,6 +12,7 @@ import { import { Observable, Subject, of, combineLatest as combine } from 'rxjs'; import { skipWhile, startWith, distinctUntilChanged, map, takeUntil, combineLatest } from 'rxjs/operators'; import { Store } from '@ngrx/store'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { MetadataResolver } from '../../domain/model/metadata-resolver'; import * as fromCollections from '../reducer'; @@ -26,8 +27,8 @@ import { SetDefinition, SetIndex, SetDisabled, ClearWizard } from '../../../wiza import * as fromWizard from '../../../wizard/reducer'; import { LoadSchemaRequest } from '../../../wizard/action/wizard.action'; import { UnsavedEntityComponent } from '../../domain/component/unsaved-entity.dialog'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { Clear } from '../action/entity.action'; +import { DifferentialService } from '../../../core/service/differential.service'; @Component({ selector: 'resolver-wizard-page', @@ -66,6 +67,7 @@ export class ResolverWizardComponent implements OnDestroy, CanComponentDeactivat private route: ActivatedRoute, private router: Router, private modalService: NgbModal, + private diffService: DifferentialService, @Inject(METADATA_SOURCE_WIZARD) private sourceWizard: Wizard ) { this.store @@ -113,6 +115,8 @@ export class ResolverWizardComponent implements OnDestroy, CanComponentDeactivat combineLatest(this.resolver$, (changes, base) => ({ ...base, ...changes })) ).subscribe(latest => this.latest = latest); + // this.changes$.subscribe(c => console.log(c)); + this.summary$ = combine( this.store.select(fromWizard.getWizardDefinition), this.store.select(fromWizard.getSchemaCollection), @@ -128,6 +132,7 @@ export class ResolverWizardComponent implements OnDestroy, CanComponentDeactivat ); this.changes$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(c => this.changes = c); + this.resolver$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(r => this.resolver = r); } next(): void { @@ -160,6 +165,18 @@ export class ResolverWizardComponent implements OnDestroy, CanComponentDeactivat this.store.dispatch(new SetIndex(page)); } + hasChanges(changes: MetadataResolver): boolean { + // const updated = this.diffService.updatedDiff(this.resolver, changes); + // const deleted = this.diffService.deletedDiff(this.resolver, changes); + let blacklist = ['id', 'resourceId']; + return Object.keys(changes).filter(key => !(blacklist.indexOf(key) > -1)).length > 0; + } + + isNew(changes: MetadataResolver): boolean { + let blacklist = ['id', 'resourceId']; + return Object.keys(changes).filter(key => !(blacklist.indexOf(key) > -1)).length === 0; + } + ngOnDestroy(): void { this.ngUnsubscribe.next(); this.ngUnsubscribe.complete(); @@ -171,18 +188,23 @@ export class ResolverWizardComponent implements OnDestroy, CanComponentDeactivat currentState: RouterStateSnapshot, nextState: RouterStateSnapshot ): Observable { - if (nextState.url.match('blank') && !!nextState.root.queryParams.id) { return of(true); } - if (Object.keys(this.changes).length > 0) { + if (nextState.url.match('blank') && !!nextState.root.queryParams.id) { + return of(true); + } + if (this.hasChanges(this.changes)) { let modal = this.modalService.open(UnsavedEntityComponent); modal.componentInstance.message = 'resolver'; modal.result.then( () => { this.store.dispatch(new Clear()); - this.router.navigate([nextState.url]); + this.router.navigateByUrl(nextState.url); }, () => console.warn('denied') ); } + if (this.isNew(this.latest)) { + return of(true); + } return this.store.select(fromResolver.getEntityIsSaved); } } diff --git a/ui/src/app/metadata/resolver/effect/collection.effects.ts b/ui/src/app/metadata/resolver/effect/collection.effects.ts index 8824a02b3..d56110a7e 100644 --- a/ui/src/app/metadata/resolver/effect/collection.effects.ts +++ b/ui/src/app/metadata/resolver/effect/collection.effects.ts @@ -111,7 +111,10 @@ export class ResolverCollectionEffects { addResolverSuccessRemoveDraft$ = this.actions$.pipe( ofType(ResolverCollectionActionTypes.ADD_RESOLVER_SUCCESS), map(action => action.payload), - map(provider => new draftActions.RemoveDraftRequest(provider)) + map(provider => { + console.log(provider); + return new draftActions.RemoveDraftRequest(provider); + }) ); @Effect() diff --git a/ui/src/app/metadata/resolver/effect/draft-collection.effects.ts b/ui/src/app/metadata/resolver/effect/draft-collection.effects.ts index f6e203381..ac94d2410 100644 --- a/ui/src/app/metadata/resolver/effect/draft-collection.effects.ts +++ b/ui/src/app/metadata/resolver/effect/draft-collection.effects.ts @@ -63,13 +63,6 @@ export class DraftCollectionEffects { ) ); - @Effect({ dispatch: false }) - addDraftSuccessRedirect$ = this.actions$.pipe( - ofType(DraftActionTypes.ADD_DRAFT_SUCCESS), - map(getPayload), - tap(provider => this.router.navigate(['metadata', 'resolver', provider.entityId, 'wizard'])) - ); - @Effect() updateDraft$ = this.actions$.pipe( ofType(DraftActionTypes.UPDATE_DRAFT_REQUEST), @@ -107,24 +100,6 @@ export class DraftCollectionEffects { map(id => new actions.LoadDraftRequest()) ); - @Effect() - selectDraftError$ = this.actions$.pipe( - ofType(DraftActionTypes.SELECT_ERROR), - map(getPayload), - switchMap(id => - this.draftService - .save({ id: `r-${ Date.now() }`, serviceProviderName: '' }) - .pipe( - map(p => new SelectDraftRequest(p.id)), - catchError(e => of(new SelectDraftError())) - ) - ), - tap(() => { - // this.store.dispatch(new ClearWizard()); - this.store.dispatch(new Clear()); - }) - ); - @Effect() removeDraft$ = this.actions$.pipe( ofType(DraftActionTypes.REMOVE_DRAFT), diff --git a/ui/src/app/metadata/resolver/effect/wizard.effect.ts b/ui/src/app/metadata/resolver/effect/wizard.effect.ts index 69d24f732..1632fb441 100644 --- a/ui/src/app/metadata/resolver/effect/wizard.effect.ts +++ b/ui/src/app/metadata/resolver/effect/wizard.effect.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { Effect, Actions, ofType } from '@ngrx/effects'; import { ActivatedRoute, Router } from '@angular/router'; -import { map, filter, tap } from 'rxjs/operators'; +import { map, filter, tap, withLatestFrom } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { @@ -29,7 +29,8 @@ export class WizardEffects { ofType(ResolverEntityActionTypes.UPDATE_CHANGES), map(action => action.payload), filter(provider => !provider.createdDate), - map(provider => new UpdateDraftRequest(provider)) + withLatestFrom(this.store.select(fromResolver.getSelectedDraft)), + map(([provider, draft]) => new UpdateDraftRequest({ ...draft, ...provider })) ); @Effect() diff --git a/ui/src/app/metadata/resolver/resolver.module.ts b/ui/src/app/metadata/resolver/resolver.module.ts index 39f4b67f1..c5290431f 100644 --- a/ui/src/app/metadata/resolver/resolver.module.ts +++ b/ui/src/app/metadata/resolver/resolver.module.ts @@ -35,6 +35,7 @@ import { ResolverSelectComponent } from './container/resolver-select.component'; import { MetadataSourceEditor } from '../domain/model/wizards/metadata-source-editor'; import { FinishFormComponent } from './component/finish-form.component'; import { ProviderFormFragmentComponent } from './component/provider-form-fragment.component'; +import { CreateDraftResolverService } from './service/create-draft.resolver'; @NgModule({ declarations: [ @@ -75,7 +76,8 @@ export class ResolverModule { return { ngModule: RootResolverModule, providers: [ - CopyIsSetGuard + CopyIsSetGuard, + CreateDraftResolverService ] }; } diff --git a/ui/src/app/metadata/resolver/resolver.routing.ts b/ui/src/app/metadata/resolver/resolver.routing.ts index 1d080bd5f..9916e2206 100644 --- a/ui/src/app/metadata/resolver/resolver.routing.ts +++ b/ui/src/app/metadata/resolver/resolver.routing.ts @@ -13,6 +13,7 @@ import { ResolverWizardStepComponent } from './container/resolver-wizard-step.co import { ResolverEditComponent } from './container/resolver-edit.component'; import { ResolverEditStepComponent } from './container/resolver-edit-step.component'; import { ResolverSelectComponent } from './container/resolver-select.component'; +import { CreateDraftResolverService } from './service/create-draft.resolver'; export const ResolverRoutes: Routes = [ { @@ -21,6 +22,9 @@ export const ResolverRoutes: Routes = [ { path: 'new', component: NewResolverComponent, + resolve: { + draft: CreateDraftResolverService + }, children: [ { path: '', redirectTo: 'blank/common', pathMatch: 'prefix' }, { diff --git a/ui/src/app/metadata/resolver/service/create-draft.resolver.ts b/ui/src/app/metadata/resolver/service/create-draft.resolver.ts new file mode 100644 index 000000000..1c6cb84a0 --- /dev/null +++ b/ui/src/app/metadata/resolver/service/create-draft.resolver.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { Observable, of } from 'rxjs'; +import { Store } from '@ngrx/store'; +import { MetadataResolver } from '../../domain/model'; +import { AddDraftRequest } from '../action/draft.action'; +import * as fromResolver from '../reducer'; +import { EntityDraftService } from '../../domain/service/draft.service'; + +@Injectable() +export class CreateDraftResolverService { + constructor( + private store: Store, + private draftService: EntityDraftService + ) { } + + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Observable { + let id = route.queryParamMap.get('id'); + if (id) { + let exists = this.draftService.exists(id); + if (!exists) { + let resolver = {id}; + this.store.dispatch(new AddDraftRequest(resolver)); + } + } + if (!id) { + let resolver = { + id: `r-${Date.now()}` + }; + id = resolver.id; + this.store.dispatch(new AddDraftRequest(resolver)); + } + return of(id); + } +} diff --git a/ui/src/app/schema-form/service/schema.service.spec.ts b/ui/src/app/schema-form/service/schema.service.spec.ts index 0dad960fe..eac84cd95 100644 --- a/ui/src/app/schema-form/service/schema.service.spec.ts +++ b/ui/src/app/schema-form/service/schema.service.spec.ts @@ -1,6 +1,6 @@ -import { TestBed, async, inject } from '@angular/core/testing'; -import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing'; -import { HttpClientModule, HttpRequest } from '@angular/common/http'; +import { TestBed, inject } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { HttpClientModule } from '@angular/common/http'; import { SchemaService } from './schema.service'; describe(`Schema Service`, () => { @@ -171,5 +171,91 @@ describe(`Schema Service`, () => { })).toBe(true); }) ); + + it(`should return true if dependency is active`, + inject([SchemaService], (service: SchemaService) => { + expect(service.isRequired({ + parent: { + schema: { + properties: { + foo: { type: 'string' }, + bar: { type: 'string' }, + baz: { type: 'string' } + }, + dependencies: { + foo: { required: ['bar', 'baz'] }, + bar: { required: ['foo', 'baz'] }, + baz: { required: ['foo', 'bar'] } + } + }, + value: { + foo: 'abcdef' + } + }, + path: '/bar' + })).toBe(true); + }) + ); + + it(`should return true if the property has an active dependency`, + inject([SchemaService], (service: SchemaService) => { + expect(service.isRequired({ + parent: { + schema: { + properties: { + foo: { type: 'string' }, + bar: { type: 'string' }, + baz: { type: 'string' } + }, + dependencies: { + foo: { required: ['bar', 'baz'] }, + bar: { required: ['foo', 'baz'] }, + baz: { required: ['foo', 'bar'] } + } + }, + value: { + foo: 'abc', + bar: '123' + } + }, + path: '/foo' + })).toBe(true); + }) + ); + + it(`should return false if no dependencies are defined`, + inject([SchemaService], (service: SchemaService) => { + expect(service.isRequired({ + parent: { + schema: { + properties: { + foo: { type: 'string' }, + bar: { type: 'string' }, + baz: { type: 'string' } + } + }, + value: { + foo: true, + baz: true + } + }, + path: '/bar' + })).toBe(false); + }) + ); + }); + + describe('getRequiredDependencies method', () => { + it('should return the provided result if an array', inject([SchemaService], (service: SchemaService) => { + expect(service.getRequiredDependencies(['foo', 'bar'])).toEqual(['foo', 'bar']); + })); + + it('should return the content of the required attribute if provided', inject([SchemaService], (service: SchemaService) => { + expect(service.getRequiredDependencies({required: ['foo', 'bar'] })).toEqual(['foo', 'bar']); + })); + + it('should return an empty array if not provided with required property', inject([SchemaService], (service: SchemaService) => { + expect(service.getRequiredDependencies({ foo: 'bar' })).toEqual([]); + })); }); }); diff --git a/ui/src/app/schema-form/service/schema.service.ts b/ui/src/app/schema-form/service/schema.service.ts index 2114ff283..9cd7346de 100644 --- a/ui/src/app/schema-form/service/schema.service.ts +++ b/ui/src/app/schema-form/service/schema.service.ts @@ -18,6 +18,7 @@ export class SchemaService { if (!formProperty || !formProperty.parent) { return false; } + let requiredFields = formProperty.parent.schema.required || []; let fieldPath = formProperty.path; let controlName = fieldPath.substr(fieldPath.lastIndexOf('/') + 1); @@ -38,6 +39,28 @@ export class SchemaService { required = !required ? requiredFields.indexOf(controlName) > -1 : required; }); } + + if (!required && formProperty.parent instanceof Object) { + const parent = formProperty.parent; + const dependencies = parent.schema.dependencies; + if (dependencies) { + const isDependencyOf = Object.keys(dependencies).filter(d => { + let dep = dependencies[d]; + return this.getRequiredDependencies(dep); + }); + const hasActiveDependencies = dependencies.hasOwnProperty(controlName) && + this.getRequiredDependencies(dependencies[controlName]).filter( + d => parent.value.hasOwnProperty(d) + ); + const isRequired = isDependencyOf.some(d => parent.value.hasOwnProperty(d) && !!parent.value[d]); + required = isRequired || !!hasActiveDependencies.length; + } + } + return required; } + + getRequiredDependencies(dep: any): string[] { + return (dep instanceof Array) ? dep : dep.hasOwnProperty('required') ? dep.required : []; + } } diff --git a/ui/src/app/schema-form/widget/array/array.component.ts b/ui/src/app/schema-form/widget/array/array.component.ts index f47beeca5..520915baa 100644 --- a/ui/src/app/schema-form/widget/array/array.component.ts +++ b/ui/src/app/schema-form/widget/array/array.component.ts @@ -28,7 +28,7 @@ export class CustomArrayComponent extends ArrayWidget implements AfterViewInit, ngAfterViewInit(): void { this.errors$ = this.formProperty.errorsChanges.pipe( - map(errors => errors ? errors.reduce((coll, err) => { + map(errors => errors ? errors.filter(err => err.code !== 'UNRESOLVABLE_REFERENCE').reduce((coll, err) => { coll[err.code] = err; return coll; }, {}) : {}), diff --git a/ui/src/app/schema-form/widget/object/object.component.ts b/ui/src/app/schema-form/widget/object/object.component.ts index 8850e7a74..54445c30e 100644 --- a/ui/src/app/schema-form/widget/object/object.component.ts +++ b/ui/src/app/schema-form/widget/object/object.component.ts @@ -7,4 +7,4 @@ import { ObjectWidget } from 'ngx-schema-form'; selector: 'custom-object', templateUrl: `./object.component.html` }) -export class CustomObjectWidget extends ObjectWidget { } +export class CustomObjectWidget extends ObjectWidget {} diff --git a/ui/src/app/schema-form/widget/string/string.component.html b/ui/src/app/schema-form/widget/string/string.component.html index e8dd0001a..26964a59f 100644 --- a/ui/src/app/schema-form/widget/string/string.component.html +++ b/ui/src/app/schema-form/widget/string/string.component.html @@ -1,7 +1,9 @@