diff --git a/ui/src/app/metadata/manager/container/manager.component.spec.ts b/ui/src/app/metadata/manager/container/manager.component.spec.ts new file mode 100644 index 000000000..be02fd913 --- /dev/null +++ b/ui/src/app/metadata/manager/container/manager.component.spec.ts @@ -0,0 +1,35 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { ManagerComponent } from './manager.component'; +import { RouterModule, Router } from '@angular/router'; +import { RouterStub, RouterLinkStubDirective, RouterOutletStubComponent } from '../../../../testing/router.stub'; + +describe('Metadata Manager Parent Page', () => { + let fixture: ComponentFixture; + let instance: ManagerComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: Router, useClass: RouterStub } + ], + imports: [ + NoopAnimationsModule, + ], + declarations: [ + ManagerComponent, + RouterLinkStubDirective, + RouterOutletStubComponent + ], + }); + + fixture = TestBed.createComponent(ManagerComponent); + instance = fixture.componentInstance; + }); + + it('should compile', () => { + fixture.detectChanges(); + + expect(fixture).toBeDefined(); + }); +}); diff --git a/ui/src/app/metadata/manager/manager.module.ts b/ui/src/app/metadata/manager/manager.module.ts index ed29e36c9..0f00569d1 100644 --- a/ui/src/app/metadata/manager/manager.module.ts +++ b/ui/src/app/metadata/manager/manager.module.ts @@ -9,6 +9,8 @@ import { EffectsModule } from '@ngrx/effects'; import { ManagerComponent } from './container/manager.component'; import { EntityItemComponent } from './component/entity-item.component'; +import { ProviderItemComponent } from './component/provider-item.component'; +import { ResolverItemComponent } from './component/resolver-item.component'; import { ProviderSearchComponent } from './component/provider-search.component'; import { DashboardResolversListComponent } from './container/dashboard-resolvers-list.component'; import { DashboardProvidersListComponent } from './container/dashboard-providers-list.component'; @@ -22,6 +24,8 @@ import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; declarations: [ ManagerComponent, EntityItemComponent, + ResolverItemComponent, + ProviderItemComponent, ProviderSearchComponent, DeleteDialogComponent, DashboardResolversListComponent, diff --git a/ui/src/app/metadata/manager/reducer/index.ts b/ui/src/app/metadata/manager/reducer/index.ts index d35d2b66e..fae0c5418 100644 --- a/ui/src/app/metadata/manager/reducer/index.ts +++ b/ui/src/app/metadata/manager/reducer/index.ts @@ -2,6 +2,8 @@ import { createSelector, createFeatureSelector } from '@ngrx/store'; import * as fromRoot from '../../../core/reducer'; import * as fromDashboard from './manager.reducer'; import * as fromSearch from './search.reducer'; +import { MetadataEntity } from '../../domain/model'; +import { Metadata } from '../../domain/domain.type'; export interface DashboardState { manager: fromDashboard.State; @@ -29,6 +31,7 @@ export const getSearchResults = createSelector( getSearchState, fromSearch.getEntities ); + export const getSearchQuery = createSelector( getSearchState, fromSearch.getQuery diff --git a/ui/src/app/metadata/provider/action/editor.action.ts b/ui/src/app/metadata/provider/action/editor.action.ts index 85ea00edd..3c82c0c19 100644 --- a/ui/src/app/metadata/provider/action/editor.action.ts +++ b/ui/src/app/metadata/provider/action/editor.action.ts @@ -8,7 +8,10 @@ export enum EditorActionTypes { SELECT_PROVIDER_TYPE = '[Provider Editor] Select Provider Type', - CLEAR = '[Provider Editor] Clear' + CLEAR = '[Provider Editor] Clear', + + LOCK = '[Provider Editor] Lock', + UNLOCK = '[Provider Editor] Unlock' } export class UpdateStatus implements Action { @@ -45,10 +48,20 @@ export class ClearEditor implements Action { readonly type = EditorActionTypes.CLEAR; } +export class LockEditor implements Action { + readonly type = EditorActionTypes.LOCK; +} + +export class UnlockEditor implements Action { + readonly type = EditorActionTypes.UNLOCK; +} + export type EditorActionUnion = | UpdateStatus | LoadSchemaRequest | LoadSchemaSuccess | LoadSchemaFail | SelectProviderType - | ClearEditor; + | ClearEditor + | LockEditor + | UnlockEditor; diff --git a/ui/src/app/metadata/provider/component/summary-property.component.html b/ui/src/app/metadata/provider/component/summary-property.component.html index 8f4781e47..664dcb82a 100644 --- a/ui/src/app/metadata/provider/component/summary-property.component.html +++ b/ui/src/app/metadata/provider/component/summary-property.component.html @@ -3,7 +3,7 @@ {{ property.name }} - {{ property.value || '-' }} + {{ property.value || property.value === false ? property.value : '-' }} diff --git a/ui/src/app/metadata/provider/container/provider-edit-step.component.html b/ui/src/app/metadata/provider/container/provider-edit-step.component.html index 5d07730fd..bab3e5b90 100644 --- a/ui/src/app/metadata/provider/container/provider-edit-step.component.html +++ b/ui/src/app/metadata/provider/container/provider-edit-step.component.html @@ -1,4 +1,11 @@ +
+ + + {{ lock.value ? 'Locked' : 'Unlocked' }} + + For Advanced Knowledge Only +
{ app.currentPage = 'common'; app.updateStatus({value: 'common'}); app.updateStatus({value: 'foo'}); - expect(store.dispatch).toHaveBeenCalledTimes(2); + expect(store.dispatch).toHaveBeenCalledTimes(3); }); }); diff --git a/ui/src/app/metadata/provider/container/provider-edit-step.component.ts b/ui/src/app/metadata/provider/container/provider-edit-step.component.ts index 63256e8a6..6ffc7e13c 100644 --- a/ui/src/app/metadata/provider/container/provider-edit-step.component.ts +++ b/ui/src/app/metadata/provider/container/provider-edit-step.component.ts @@ -3,20 +3,20 @@ import { Observable, Subject } from 'rxjs'; import { Store } from '@ngrx/store'; import * as fromProvider from '../reducer'; -import { UpdateStatus } from '../action/editor.action'; -import { Wizard } from '../../../wizard/model'; +import { UpdateStatus, LockEditor, UnlockEditor } from '../action/editor.action'; +import { Wizard, WizardStep } from '../../../wizard/model'; import { MetadataProvider } from '../../domain/model'; import * as fromWizard from '../../../wizard/reducer'; -import { withLatestFrom, map, skipWhile, distinctUntilChanged } from 'rxjs/operators'; +import { withLatestFrom, map, skipWhile, distinctUntilChanged, startWith, combineLatest } from 'rxjs/operators'; import { UpdateProvider } from '../action/entity.action'; +import { FormControl } from '@angular/forms'; @Component({ selector: 'provider-edit-step', templateUrl: './provider-edit-step.component.html', styleUrls: [] }) - export class ProviderEditStepComponent implements OnDestroy { valueChangeSubject = new Subject>(); private valueChangeEmitted$ = this.valueChangeSubject.asObservable(); @@ -33,20 +33,41 @@ export class ProviderEditStepComponent implements OnDestroy { model$: Observable; definition$: Observable>; changes$: Observable; + step$: Observable; validators$: Observable<{ [key: string]: any }>; + lock: FormControl = new FormControl(true); + constructor( private store: Store ) { - this.schema$ = this.store.select(fromProvider.getSchema); this.definition$ = this.store.select(fromWizard.getWizardDefinition); this.changes$ = this.store.select(fromProvider.getEntityChanges); this.provider$ = this.store.select(fromProvider.getSelectedProvider); + this.step$ = this.store.select(fromWizard.getCurrent); + this.schema$ = this.store.select(fromProvider.getSchema); - this.validators$ = this.store.select(fromProvider.getProviderNames).pipe( - withLatestFrom(this.definition$, this.provider$), - map(([names, def, provider]) => def.getValidators(names.filter(n => n !== provider.name))) + this.step$.subscribe(s => { + if (s && s.locked) { + this.store.dispatch(new LockEditor()); + } else { + this.store.dispatch(new UnlockEditor()); + } + }); + + this.lock.valueChanges.subscribe(locked => this.store.dispatch(locked ? new LockEditor() : new UnlockEditor())); + + this.validators$ = this.definition$.pipe( + withLatestFrom( + this.store.select(fromProvider.getProviderNames), + this.store.select(fromProvider.getProviderXmlIds), + this.provider$ + ), + map(([def, names, ids, provider]) => def.getValidators( + names.filter(n => n !== provider.name), + ids.filter(id => id !== provider.xmlId) + )) ); this.model$ = this.schema$.pipe( @@ -76,7 +97,11 @@ export class ProviderEditStepComponent implements OnDestroy { ) .subscribe(changes => this.store.dispatch(new UpdateProvider(changes))); - this.statusChangeEmitted$.pipe(distinctUntilChanged()).subscribe(errors => this.updateStatus(errors)); + this.statusChangeEmitted$ + .pipe(distinctUntilChanged()) + .subscribe(errors => { + this.updateStatus(errors); + }); this.store.select(fromWizard.getWizardIndex).subscribe(i => this.currentPage = i); } diff --git a/ui/src/app/metadata/provider/container/provider-wizard.component.ts b/ui/src/app/metadata/provider/container/provider-wizard.component.ts index 014061305..f7f958db8 100644 --- a/ui/src/app/metadata/provider/container/provider-wizard.component.ts +++ b/ui/src/app/metadata/provider/container/provider-wizard.component.ts @@ -10,7 +10,6 @@ import { startWith } from 'rxjs/operators'; import { Wizard, WizardStep } from '../../../wizard/model'; import { MetadataProvider } from '../../domain/model'; import { ClearProvider } from '../action/entity.action'; -import { Router, ActivatedRoute } from '@angular/router'; import { map } from 'rxjs/operators'; import { AddProviderRequest } from '../action/collection.action'; import { MetadataProviderWizard } from '../model'; @@ -36,9 +35,7 @@ export class ProviderWizardComponent implements OnDestroy { provider: MetadataProvider; constructor( - private store: Store, - private router: Router, - private route: ActivatedRoute + private store: Store ) { this.store .select(fromWizard.getCurrentWizardSchema) diff --git a/ui/src/app/metadata/provider/effect/collection.effect.ts b/ui/src/app/metadata/provider/effect/collection.effect.ts index d4eb5ff0f..a3f8aa723 100644 --- a/ui/src/app/metadata/provider/effect/collection.effect.ts +++ b/ui/src/app/metadata/provider/effect/collection.effect.ts @@ -108,7 +108,7 @@ export class CollectionEffects { ); @Effect() - addResolverSuccessReload$ = this.actions$.pipe( + addProviderSuccessReload$ = this.actions$.pipe( ofType(ProviderCollectionActionTypes.ADD_PROVIDER_SUCCESS), map(action => action.payload), map(provider => new LoadProviderRequest()) diff --git a/ui/src/app/metadata/provider/model/file-backed-http.provider.form.ts b/ui/src/app/metadata/provider/model/file-backed-http.provider.form.ts index 5af8642bb..8d091c463 100644 --- a/ui/src/app/metadata/provider/model/file-backed-http.provider.form.ts +++ b/ui/src/app/metadata/provider/model/file-backed-http.provider.form.ts @@ -6,6 +6,19 @@ export const FileBackedHttpMetadataProviderWizard: Wizard { + const err = xmlIdList.indexOf(value) > -1 ? { + code: 'INVALID_ID', + path: `#${property.path}`, + message: 'ID must be unique.', + params: [value] + } : null; + return err; + }; + return validators; + }, steps: [ { id: 'common', @@ -77,6 +90,7 @@ export const FileBackedHttpMetadataProviderEditor: Wizard { it('should add the loaded providers to the collection', () => { spyOn(fromProvider.adapter, 'addAll').and.callThrough(); const providers = [ - { resourceId: 'foo', name: 'foo', '@type': 'foo', enabled: true, createdDate: new Date().toLocaleDateString() }, - { resourceId: 'bar', name: 'bar', '@type': 'bar', enabled: false, createdDate: new Date().toLocaleDateString() } + { + resourceId: 'foo', + name: 'name', + '@type': 'foo', + enabled: true, + createdDate: new Date().toLocaleDateString(), + sortKey: 1, + xmlId: 'foo', + metadataFilters: [] + }, + { + resourceId: 'bar', + name: 'bar', + '@type': 'bar', + enabled: false, + createdDate: new Date().toLocaleDateString(), + sortKey: 2, + xmlId: 'bar', + metadataFilters: [] + } ]; const action = new LoadProviderSuccess(providers); const result = reducer(snapshot, action); diff --git a/ui/src/app/metadata/provider/reducer/editor.reducer.spec.ts b/ui/src/app/metadata/provider/reducer/editor.reducer.spec.ts index d1b58da34..9ab661f87 100644 --- a/ui/src/app/metadata/provider/reducer/editor.reducer.spec.ts +++ b/ui/src/app/metadata/provider/reducer/editor.reducer.spec.ts @@ -1,5 +1,14 @@ import { reducer, initialState as snapshot } from './editor.reducer'; -import { EditorActionTypes, ClearEditor } from '../action/editor.action'; +import { + EditorActionTypes, + ClearEditor, + LockEditor, + LoadSchemaRequest, + LoadSchemaFail, + LoadSchemaSuccess, + UnlockEditor, + SelectProviderType +} from '../action/editor.action'; describe('Provider Editor Reducer', () => { describe('undefined action', () => { @@ -15,4 +24,40 @@ describe('Provider Editor Reducer', () => { expect(reducer(snapshot, new ClearEditor())).toEqual(snapshot); }); }); + + describe(`${EditorActionTypes.LOCK}`, () => { + it('should reset to initial state', () => { + expect(reducer(snapshot, new LockEditor())).toEqual({ ...snapshot, locked: true }); + }); + }); + + describe(`${EditorActionTypes.UNLOCK}`, () => { + it('should reset to initial state', () => { + expect(reducer(snapshot, new UnlockEditor())).toEqual({ ...snapshot, locked: false }); + }); + }); + + describe(`${EditorActionTypes.LOAD_SCHEMA_REQUEST}`, () => { + it('should reset to initial state', () => { + expect(reducer(snapshot, new LoadSchemaRequest('foo'))).toEqual({ ...snapshot, schemaPath: 'foo', loading: true }); + }); + }); + + describe(`${EditorActionTypes.LOAD_SCHEMA_FAIL}`, () => { + it('should reset to initial state', () => { + expect(reducer(snapshot, new LoadSchemaFail(new Error('fail')))).toEqual({ ...snapshot }); + }); + }); + + describe(`${EditorActionTypes.LOAD_SCHEMA_REQUEST}`, () => { + it('should reset to initial state', () => { + expect(reducer(snapshot, new LoadSchemaSuccess({}))).toEqual({ ...snapshot, schema: {} }); + }); + }); + + describe(`${EditorActionTypes.SELECT_PROVIDER_TYPE}`, () => { + it('should reset to initial state', () => { + expect(reducer(snapshot, new SelectProviderType('foo'))).toEqual({ ...snapshot, type: 'foo' }); + }); + }); }); diff --git a/ui/src/app/metadata/provider/reducer/editor.reducer.ts b/ui/src/app/metadata/provider/reducer/editor.reducer.ts index 609f707f0..32c96c86c 100644 --- a/ui/src/app/metadata/provider/reducer/editor.reducer.ts +++ b/ui/src/app/metadata/provider/reducer/editor.reducer.ts @@ -6,6 +6,7 @@ export interface EditorState { loading: boolean; schema: any; type: string; + locked: boolean; } export const initialState: EditorState = { @@ -13,7 +14,8 @@ export const initialState: EditorState = { schemaPath: null, loading: false, schema: null, - type: null + type: null, + locked: false }; export function reducer(state = initialState, action: EditorActionUnion): EditorState { @@ -59,6 +61,20 @@ export function reducer(state = initialState, action: EditorActionUnion): Editor schema: initialState.schema }; } + + case EditorActionTypes.LOCK: { + return { + ...state, + locked: true + }; + } + + case EditorActionTypes.UNLOCK: { + return { + ...state, + locked: false + }; + } default: { return state; } @@ -66,6 +82,7 @@ export function reducer(state = initialState, action: EditorActionUnion): Editor } export const getSchema = (state: EditorState) => state.schema; +export const getLocked = (state: EditorState) => state.locked; export const isEditorValid = (state: EditorState) => !Object.keys(state.status).some(key => state.status[key] === ('INVALID')); diff --git a/ui/src/app/metadata/provider/reducer/index.ts b/ui/src/app/metadata/provider/reducer/index.ts index ee98fec25..7655e4314 100644 --- a/ui/src/app/metadata/provider/reducer/index.ts +++ b/ui/src/app/metadata/provider/reducer/index.ts @@ -4,7 +4,11 @@ import * as fromEditor from './editor.reducer'; import * as fromEntity from './entity.reducer'; import * as fromCollection from './collection.reducer'; import * as utils from '../../domain/domain.util'; + +import * as fromWizard from '../../../wizard/reducer'; + import { MetadataProvider } from '../../domain/model'; +import { WizardStep } from '../../../wizard/model'; export interface ProviderState { editor: fromEditor.EditorState; @@ -36,13 +40,41 @@ export const getCollectionState = createSelector(getProviderState, getCollection Editor State */ -export const getSchema = createSelector(getEditorState, fromEditor.getSchema); +export function getSchemaParseFn(schema, locked): any { + if (!schema) { + return null; + } + return { + ...schema, + properties: Object.keys(schema.properties).reduce((prev, current) => { + return { + ...prev, + [current]: { + ...schema.properties[current], + readOnly: locked, + ...(schema.properties[current].hasOwnProperty('properties') ? + getSchemaParseFn(schema.properties[current], locked) : + {} + ) + } + }; + }, {}) + }; +} + +export const getSchemaLockedFn = (step, locked) => step ? step.locked ? locked : false : false; +export const getLockedStatus = createSelector(getEditorState, fromEditor.getLocked); +export const getLocked = createSelector(fromWizard.getCurrent, getLockedStatus, getSchemaLockedFn); + +export const getSchemaObject = createSelector(getEditorState, fromEditor.getSchema); +export const getSchema = createSelector(getSchemaObject, getLocked, getSchemaParseFn); export const getEditorIsValid = createSelector(getEditorState, fromEditor.isEditorValid); export const getFormStatus = createSelector(getEditorState, fromEditor.getFormStatus); export const getInvalidEditorForms = createSelector(getEditorState, fromEditor.getInvalidForms); + /* Entity State */ @@ -63,3 +95,4 @@ export const getProviderIds = createSelector(getCollectionState, fromCollection. export const getProviderCollectionIsLoaded = createSelector(getCollectionState, fromCollection.getIsLoaded); export const getProviderNames = createSelector(getAllProviders, (providers: MetadataProvider[]) => providers.map(p => p.name)); +export const getProviderXmlIds = createSelector(getAllProviders, (providers: MetadataProvider[]) => providers.map(p => p.xmlId)); diff --git a/ui/src/app/schema-form/registry.ts b/ui/src/app/schema-form/registry.ts index f99f9eece..44a20841a 100644 --- a/ui/src/app/schema-form/registry.ts +++ b/ui/src/app/schema-form/registry.ts @@ -36,6 +36,8 @@ export class CustomWidgetRegistry extends WidgetRegistry { this.register('boolean-radio', BooleanRadioComponent); this.register('fieldset', FieldsetComponent); + this.register('object', FieldsetComponent); + this.register('array', CustomArrayComponent); this.register('select', CustomSelectComponent); @@ -50,7 +52,6 @@ export class CustomWidgetRegistry extends WidgetRegistry { this.register('datalist', DatalistComponent); /* NGX-Form */ - this.register('object', ObjectWidget); this.register('range', RangeWidget); this.register('file', FileWidget); diff --git a/ui/src/app/schema-form/widget/boolean-radio/boolean-radio.component.ts b/ui/src/app/schema-form/widget/boolean-radio/boolean-radio.component.ts index bd5ffa7df..d4ad09d6f 100644 --- a/ui/src/app/schema-form/widget/boolean-radio/boolean-radio.component.ts +++ b/ui/src/app/schema-form/widget/boolean-radio/boolean-radio.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, AfterViewInit } from '@angular/core'; import { ControlWidget } from 'ngx-schema-form'; @Component({ @@ -6,6 +6,13 @@ import { ControlWidget } from 'ngx-schema-form'; templateUrl: './boolean-radio.component.html', styleUrls: ['./boolean-radio.component.scss'] }) -export class BooleanRadioComponent extends ControlWidget { - +export class BooleanRadioComponent extends ControlWidget implements AfterViewInit { + ngAfterViewInit(): void { + super.ngAfterViewInit(); + if (this.schema.readOnly) { + this.control.disable(); + } else { + this.control.enable(); + } + } } diff --git a/ui/src/app/schema-form/widget/check/checkbox.component.html b/ui/src/app/schema-form/widget/check/checkbox.component.html index d47aa2d4c..db1385630 100644 --- a/ui/src/app/schema-form/widget/check/checkbox.component.html +++ b/ui/src/app/schema-form/widget/check/checkbox.component.html @@ -7,7 +7,7 @@ [attr.name]="name" [indeterminate]="control.value !== false && control.value !== true ? true :null" type="checkbox" - [attr.disabled]="schema.readOnly" + [attr.disabled]="schema.readOnly?true:null" id="{{ name }}" [disableValidation]="true">