diff --git a/.gitignore b/.gitignore index ee5c9d83..2cb66d13 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ *.launch .settings/ *.sublime-workspace +.history # IDE - VSCode .vscode/* diff --git a/cypress/helpers/data.helper.ts b/cypress/helpers/data.helper.ts index 34645121..ea3f690b 100644 --- a/cypress/helpers/data.helper.ts +++ b/cypress/helpers/data.helper.ts @@ -1,8 +1,8 @@ -import { OneListing, ListingType } from '../../src/app/interfaces/listing.interface'; -import { UnreachableCase } from '../../src/app/shared/utils'; +import { CrewMember } from '../../src/app/interfaces/crew-member.interface'; import { DroidType } from '../../src/app/interfaces/droid.interface'; +import { ListingType, OneListing } from '../../src/app/interfaces/listing.interface'; import { VehicleType } from '../../src/app/interfaces/vehicle.interface'; -import { CrewMember } from '../../src/app/interfaces/crew-member.interface'; +import { UnreachableCase } from '../../src/app/shared/utils'; export interface ListElement { readonly title: string; @@ -140,4 +140,6 @@ export const hardcodedElementToTestElement = (item: OneListing): ListElement => export const hardcodedElementsToTestList = (items: OneListing[]): ListElement[] => items.map(item => hardcodedElementToTestElement(item)); -export const extractErrors = (errors: JQuery) => cy.wrap(JSON.parse(errors.text().trim())); +export const extractErrors = (errors: JQuery) => { + return JSON.parse(errors.text().trim()); +}; diff --git a/cypress/helpers/dom.helper.ts b/cypress/helpers/dom.helper.ts index 1945478c..6ac1376e 100644 --- a/cypress/helpers/dom.helper.ts +++ b/cypress/helpers/dom.helper.ts @@ -3,7 +3,6 @@ import { DroidType } from '../../src/app/interfaces/droid.interface'; import { ListingType } from '../../src/app/interfaces/listing.interface'; import { VehicleType } from '../../src/app/interfaces/vehicle.interface'; -import { extractErrors, FormElement, ListElement } from './data.helper'; const getTextFromTag = (element: HTMLElement, tag: string): string => Cypress.$(element) @@ -36,13 +35,6 @@ const getCrewMembers = (element: HTMLElement): { firstName: string; lastName: st })) .get(); -export const expectAll = (selector: string, cb: (el: Cypress.Chainable) => void) => - cy.get(selector).then($elements => { - $elements.each((_, $element) => { - cb(cy.wrap($element)); - }); - }); - export const DOM = { get createNewButton() { return cy.get('*[data-create-new]'); @@ -61,19 +53,6 @@ export const DOM = { }, }; }, - get objList(): Cypress.Chainable { - return DOM.list.elements.cy.then($elements => { - return $elements - .map((_, element) => ({ - title: getTextFromTag(element, 'title'), - type: getTextFromTag(element, 'type'), - price: getTextFromTag(element, 'price'), - subType: getTextFromTag(element, 'sub-type'), - details: getTextFromTag(element, 'details'), - })) - .get(); - }); - }, }; }, get readonlyToggle() { @@ -85,59 +64,11 @@ export const DOM = { return cy.get('app-listing'); }, get errors() { - return { - get cy() { - return cy.get(`*[data-errors]`); - }, - get obj() { - return DOM.form.errors.cy.then(extractErrors); - }, - }; + return cy.get(`*[data-errors]`); }, get noErrors() { return cy.get(`*[data-no-error]`); }, - getObj(type: VehicleType): Cypress.Chainable { - const getVehicleObj = (element: HTMLElement, vehicleType: VehicleType) => - ({ - Spaceship: { - spaceshipForm: { - color: getTextFromInput(element, 'input-color'), - canFire: getToggleValue(element, 'input-can-fire'), - crewMembers: getCrewMembers(element), - wingCount: +getTextFromInput(element, 'input-number-of-wings'), - }, - }, - Speeder: { - speederForm: { - color: getTextFromInput(element, 'input-color'), - canFire: getToggleValue(element, 'input-can-fire'), - crewMembers: getCrewMembers(element), - maximumSpeed: +getTextFromInput(element, 'input-maximum-speed'), - }, - }, - }[vehicleType]); - - return DOM.form.cy.then($element => { - return $element - .map((_, element) => ({ - title: getTextFromTag(element, 'title'), - price: getTextFromTag(element, 'price'), - inputs: { - id: getTextFromInput(element, 'input-id'), - title: getTextFromInput(element, 'input-title'), - imageUrl: getTextFromInput(element, 'input-image-url'), - price: getTextFromInput(element, 'input-price'), - listingType: getSelectedOptionFromSelect(element, 'select-listing-type'), - vehicleForm: { - vehicleType: getSelectedOptionFromSelect(element, 'select-vehicle-type'), - ...getVehicleObj(element, type), - }, - }, - })) - .get()[0]; - }); - }, get elements() { return { get title() { @@ -195,3 +126,54 @@ export const DOM = { }; }, }; + +const getVehicleObj = (element: HTMLElement, vehicleType: VehicleType) => + ({ + Spaceship: { + spaceshipForm: { + color: getTextFromInput(element, 'input-color'), + canFire: getToggleValue(element, 'input-can-fire'), + crewMembers: getCrewMembers(element), + wingCount: +getTextFromInput(element, 'input-number-of-wings'), + }, + }, + Speeder: { + speederForm: { + color: getTextFromInput(element, 'input-color'), + canFire: getToggleValue(element, 'input-can-fire'), + crewMembers: getCrewMembers(element), + maximumSpeed: +getTextFromInput(element, 'input-maximum-speed'), + }, + }, + }[vehicleType]); + +export const getFormValue = (form: JQuery, type: VehicleType) => + form + .map((_, element) => ({ + title: getTextFromTag(element, 'title'), + price: getTextFromTag(element, 'price'), + inputs: { + id: getTextFromInput(element, 'input-id'), + title: getTextFromInput(element, 'input-title'), + imageUrl: getTextFromInput(element, 'input-image-url'), + price: getTextFromInput(element, 'input-price'), + listingType: getSelectedOptionFromSelect(element, 'select-listing-type'), + vehicleForm: { + vehicleType: getSelectedOptionFromSelect(element, 'select-vehicle-type'), + ...getVehicleObj(element, type), + }, + }, + })) + .get()[0]; + +export const getFormList = ($elements: JQuery) => { + return $elements + .map((_, element) => ({ + title: getTextFromTag(element, 'title'), + type: getTextFromTag(element, 'type'), + price: getTextFromTag(element, 'price'), + subType: getTextFromTag(element, 'sub-type'), + details: getTextFromTag(element, 'details'), + })) + .get(); +}; diff --git a/cypress/support/index.js b/cypress/support/index.js index 37a498fb..dce51337 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -18,3 +18,14 @@ import './commands'; // Alternatively you can use CommonJS syntax: // require('./commands') + +Cypress.on('window:before:load', win => { + cy.stub(win.console, 'error', msg => { + cy.now('task', 'error', msg); + throw new Error(msg); // all we needed to add! + }); + + cy.stub(win.console, 'warn', msg => { + cy.now('task', 'warn', msg); + }); +}); diff --git a/package.json b/package.json index c88a83b7..0a78813a 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,9 @@ "test": "yarn lib:test:watch", "commit": "git add . && git-cz", "readme:build": "embedme README.md && yarn run prettier README.md --write", - "readme:check": "yarn readme:build && ! git status | grep README.md || (echo 'You must commit build and commit changes to README.md!' && exit 1)" + "readme:check": "yarn readme:build && ! git status | grep README.md || (echo 'You must commit build and commit changes to README.md!' && exit 1)", + "ngcc": "[ ! -f ./node_modules/.bin/ngcc ] || node --max_old_space_size=8000 ./node_modules/.bin/ngcc", + "postinstall":"yarn run ngcc" }, "private": true, "dependencies": { @@ -50,6 +52,7 @@ "commitizen": "4.0.3", "core-js": "3.6.4", "fast-deep-equal": "3.1.1", + "ngx-observable-lifecycle": "1.0.1", "rxjs": "6.5.4", "tslib": "1.10.0", "uuid": "3.4.0", @@ -66,7 +69,7 @@ "@types/jasminewd2": "2.0.8", "@types/node": "13.7.2", "codelyzer": "5.2.1", - "cypress": "4.0.2", + "cypress": "4.5.0", "cz-conventional-changelog": "3.1.0", "embedme": "1.20.0", "http-server-spa": "1.3.0", @@ -82,6 +85,7 @@ "semantic-release": "17.0.4", "ts-node": "8.6.2", "tsconfig-paths-webpack-plugin": "3.2.0", + "tsdef": "0.0.13", "tslint": "6.0.0", "typescript": "3.7.5" }, diff --git a/projects/ngx-sub-form/src/lib/new/helpers.ts b/projects/ngx-sub-form/src/lib/new/helpers.ts new file mode 100644 index 00000000..0c27f324 --- /dev/null +++ b/projects/ngx-sub-form/src/lib/new/helpers.ts @@ -0,0 +1,187 @@ +import { + AbstractControlOptions, + ControlValueAccessor, + FormArray, + FormControl, + FormGroup, + ValidationErrors, +} from '@angular/forms'; +import { ReplaySubject } from 'rxjs'; +import { Nilable } from 'tsdef'; +import { + ArrayPropertyKey, + ControlsNames, + NewFormErrors, + OneOfControlsTypes, + TypedFormGroup, +} from '../ngx-sub-form-utils'; +import { + ControlValueAccessorComponentInstance, + FormBindings, + NgxSubFormArrayOptions, + NgxSubFormOptions, +} from './ngx-sub-form.types'; + +export const deepCopy = (value: T): T => JSON.parse(JSON.stringify(value)); + +export const patchClassInstance = (componentInstance: any, obj: Object) => { + Object.entries(obj).forEach(([key, newMethod]) => { + componentInstance[key] = newMethod; + }); +}; + +export const getControlValueAccessorBindings = ( + componentInstance: ControlValueAccessorComponentInstance, +): FormBindings => { + const writeValue$$: ReplaySubject> = new ReplaySubject(1); + const registerOnChange$$: ReplaySubject<(formValue: ControlInterface | null) => void> = new ReplaySubject(1); + const registerOnTouched$$: ReplaySubject<(_: any) => void> = new ReplaySubject(1); + const setDisabledState$$: ReplaySubject = new ReplaySubject(1); + + const controlValueAccessorPatch: Required = { + writeValue: (obj: Nilable): void => { + writeValue$$.next(obj); + }, + registerOnChange: (fn: (formValue: ControlInterface | null) => void): void => { + registerOnChange$$.next(fn); + }, + registerOnTouched: (fn: () => void): void => { + registerOnTouched$$.next(fn); + }, + setDisabledState: (shouldDisable: boolean | undefined): void => { + setDisabledState$$.next(shouldDisable); + }, + }; + + patchClassInstance(componentInstance, controlValueAccessorPatch); + + return { + writeValue$: writeValue$$.asObservable(), + registerOnChange$: registerOnChange$$.asObservable(), + registerOnTouched$: registerOnTouched$$.asObservable(), + setDisabledState$: setDisabledState$$.asObservable(), + }; +}; + +export const safelyPatchClassInstance = (componentInstance: any, obj: Object) => { + Object.entries(obj).forEach(([key, newMethod]) => { + const previousMethod = componentInstance[key]; + + componentInstance[key] = (...args: any[]) => { + if (previousMethod) { + previousMethod.apply(componentInstance); + } + + newMethod(args); + }; + }); +}; + +export const getFormGroupErrors = ( + formGroup: TypedFormGroup, +): NewFormErrors => { + const formErrors: NewFormErrors = Object.entries(formGroup.controls).reduce< + Exclude, null> + >((acc, [key, control]) => { + if (control instanceof FormArray) { + // errors within an array are represented as a map + // with the index and the error + // this way, we avoid holding a lot of potential `null` + // values in the array for the valid form controls + const errorsInArray: Record = {}; + + for (let i = 0; i < control.length; i++) { + const controlErrors = control.at(i).errors; + if (controlErrors) { + errorsInArray[i] = controlErrors; + } + } + + if (Object.values(errorsInArray).length > 0) { + const accHoldingArrays = acc as Record>; + accHoldingArrays[key as keyof ControlInterface] = errorsInArray; + } + } else if (control.errors) { + const accHoldingNonArrays = acc as Record; + accHoldingNonArrays[key as keyof ControlInterface] = control.errors; + } + + return acc; + }, {}); + + if (!formGroup.errors && !Object.values(formErrors).length) { + return null; + } + + // todo remove any + return Object.assign({}, formGroup.errors ? { formGroup: formGroup.errors } : {}, formErrors); +}; + +interface FormArrayWrapper { + key: keyof FormInterface; + control: FormArray; +} + +export function createFormDataFromOptions( + options: NgxSubFormOptions, +) { + const formGroup: TypedFormGroup = new FormGroup( + options.formControls, + options.formGroupOptions as AbstractControlOptions, + ) as TypedFormGroup; + const defaultValues: FormInterface = deepCopy(formGroup.value); + const formGroupKeys: (keyof FormInterface)[] = Object.keys(defaultValues) as (keyof FormInterface)[]; + const formControlNames: ControlsNames = formGroupKeys.reduce>( + (acc, curr) => { + acc[curr] = curr; + return acc; + }, + {} as ControlsNames, + ); + + const formArrays: FormArrayWrapper[] = formGroupKeys.reduce[]>( + (acc, key) => { + const control = formGroup.get(key as string); + if (control instanceof FormArray) { + acc.push({ key, control }); + } + return acc; + }, + [], + ); + return { formGroup, defaultValues, formControlNames, formArrays }; +} + +export const handleFArray = ( + formArrayWrappers: FormArrayWrapper[], + obj: FormInterface, + createFormArrayControl: NgxSubFormArrayOptions['createFormArrayControl'] | null, +) => { + if (!formArrayWrappers.length) { + return; + } + + formArrayWrappers.forEach(({ key, control }) => { + const value = obj[key]; + + if (!Array.isArray(value)) { + return; + } + + // instead of creating a new array every time and push a new FormControl + // we just remove or add what is necessary so that: + // - it is as efficient as possible and do not create unnecessary FormControl every time + // - validators are not destroyed/created again and eventually fire again for no reason + while (control.length > value.length) { + control.removeAt(control.length - 1); + } + + for (let i = control.length; i < value.length; i++) { + if (createFormArrayControl) { + control.insert(i, createFormArrayControl(key as ArrayPropertyKey, value[i])); + } else { + control.insert(i, new FormControl(value[i])); + } + } + }); +}; diff --git a/projects/ngx-sub-form/src/lib/new/helpers.types.ts b/projects/ngx-sub-form/src/lib/new/helpers.types.ts new file mode 100644 index 00000000..d77189d4 --- /dev/null +++ b/projects/ngx-sub-form/src/lib/new/helpers.types.ts @@ -0,0 +1 @@ +export type AreTypesSimilar = T extends U ? (U extends T ? true : false) : false; diff --git a/projects/ngx-sub-form/src/lib/new/ngx-sub-form.ts b/projects/ngx-sub-form/src/lib/new/ngx-sub-form.ts new file mode 100644 index 00000000..696e80c7 --- /dev/null +++ b/projects/ngx-sub-form/src/lib/new/ngx-sub-form.ts @@ -0,0 +1,215 @@ +import { ɵmarkDirty as markDirty } from '@angular/core'; +import isEqual from 'fast-deep-equal'; +import { EMPTY, forkJoin, Observable, of } from 'rxjs'; +import { delay, filter, map, mapTo, shareReplay, switchMap, take, takeUntil, tap } from 'rxjs/operators'; +import { isNullOrUndefined } from '../ngx-sub-form-utils'; +import { + createFormDataFromOptions, + getControlValueAccessorBindings, + getFormGroupErrors, + handleFArray as handleFormArrays, + patchClassInstance, +} from './helpers'; +import { + ControlValueAccessorComponentInstance, + FormBindings, + FormType, + NgxFormOptions, + NgxRootForm, + NgxRootFormOptions, + NgxSubForm, + NgxSubFormArrayOptions, + NgxSubFormOptions, + NgxSubFormRemapOptions, +} from './ngx-sub-form.types'; + +const optionsHaveInstructionsToCreateArrays = ( + options: NgxSubFormOptions, +): options is NgxSubFormOptions & NgxSubFormArrayOptions => true; + +// @todo find a better name +const isRemap = ( + options: any, +): options is NgxSubFormRemapOptions => { + const opt = options as NgxSubFormRemapOptions; + return !!opt.fromFormGroup && !!opt.toFormGroup; +}; + +// @todo find a better name +const isRoot = ( + options: any, +): options is NgxRootFormOptions => { + const opt = options as NgxRootFormOptions; + return opt.formType === FormType.ROOT; +}; + +export function createForm( + componentInstance: ControlValueAccessorComponentInstance, + options: NgxRootFormOptions, +): NgxRootForm; +export function createForm( + componentInstance: ControlValueAccessorComponentInstance, + options: NgxSubFormOptions, +): NgxSubForm; +export function createForm( + componentInstance: ControlValueAccessorComponentInstance, + options: NgxFormOptions, +): NgxSubForm { + const { formGroup, defaultValues, formControlNames, formArrays } = createFormDataFromOptions< + ControlInterface, + FormInterface + >(options); + + let isRemoved = false; + + options.componentHooks.onDestroy.pipe(take(1)).subscribe(() => { + isRemoved = true; + }); + + let first = true; + + // define the `validate` method to improve errors + // and support nested errors + patchClassInstance(componentInstance, { + validate: () => { + if (first) { + first = false; + setTimeout(() => { + formGroup.updateValueAndValidity(); + }, 0); + + return null; + } + + if (isRemoved) return null; + + if (formGroup.valid) { + return null; + } + + return getFormGroupErrors(formGroup); + }, + }); + + const componentHooks = getControlValueAccessorBindings(componentInstance); + + const writeValue$: FormBindings['writeValue$'] = isRoot(options) + ? options.input$ + : componentHooks.writeValue$; + + const registerOnChange$: FormBindings['registerOnChange$'] = isRoot< + ControlInterface, + FormInterface + >(options) + ? of(data => { + if (!data) { + return; + } + options.output$.next(data); + }) + : componentHooks.registerOnChange$; + + const setDisabledState$: FormBindings['setDisabledState$'] = isRoot< + ControlInterface, + FormInterface + >(options) + ? options.disabled$ + : componentHooks.setDisabledState$; + + const transformedValue$: Observable = writeValue$.pipe( + map(value => { + if (isNullOrUndefined(value)) { + return defaultValues; + } + + if (isRemap(options)) { + return options.toFormGroup(value); + } + + // if it's not a remap component, the ControlInterface === the FormInterface + return (value as any) as FormInterface; + }), + shareReplay({ refCount: true, bufferSize: 1 }), + ); + + const broadcastValueToParent$: Observable = transformedValue$.pipe( + switchMap(transformedValue => + formGroup.valueChanges.pipe( + delay(0), + filter(formValue => { + if (!isRoot(options)) { + return true; + } + + return !isEqual(transformedValue, formValue); + }), + ), + ), + filter(() => !isRoot(options) || formGroup.valid), + map(value => + isRemap(options) + ? options.fromFormGroup(value) + : // if it's not a remap component, the ControlInterface === the FormInterface + ((value as any) as ControlInterface), + ), + ); + + const emitNullOnDestroy$: Observable = + // emit null when destroyed by default + isNullOrUndefined(options.emitNullOnDestroy) || options.emitNullOnDestroy + ? options.componentHooks.onDestroy.pipe(mapTo(null)) + : EMPTY; + + const sideEffects = { + broadcastValueToParent$: registerOnChange$.pipe( + switchMap(onChange => broadcastValueToParent$.pipe(tap(value => onChange(value)))), + ), + applyUpstreamUpdateOnLocalForm$: transformedValue$.pipe( + tap(value => { + handleFormArrays( + formArrays, + value, + optionsHaveInstructionsToCreateArrays(options) + ? options.createFormArrayControl + : null, + ); + + formGroup.reset(value); + + // support `changeDetection: ChangeDetectionStrategy.OnPush` + // on the component hosting a form + markDirty(componentInstance); + }), + ), + setDisabledState$: setDisabledState$.pipe( + tap((shouldDisable: boolean) => { + shouldDisable ? formGroup.disable() : formGroup.enable(); + }), + ), + }; + + forkJoin(sideEffects) + .pipe(takeUntil(options.componentHooks.onDestroy)) + .subscribe(); + + // following cannot be part of `forkJoin(sideEffects)` + // because it uses `takeUntilDestroyed` which destroys + // the subscription when the component is being destroyed + // and therefore prevents the emit of the null value if needed + registerOnChange$ + .pipe( + switchMap(onChange => emitNullOnDestroy$.pipe(tap(value => onChange(value)))), + takeUntil(options.componentHooks.onDestroy.pipe(delay(0))), + ) + .subscribe(); + + return { + formGroup, + formControlNames, + get formGroupErrors() { + return getFormGroupErrors(formGroup); + }, + // todo + createFormArrayControl: (options as any).createFormArrayControl, + }; +} diff --git a/projects/ngx-sub-form/src/lib/new/ngx-sub-form.types.ts b/projects/ngx-sub-form/src/lib/new/ngx-sub-form.types.ts new file mode 100644 index 00000000..5e753cec --- /dev/null +++ b/projects/ngx-sub-form/src/lib/new/ngx-sub-form.types.ts @@ -0,0 +1,89 @@ +import { ControlValueAccessor, FormControl, Validator } from '@angular/forms'; +import { Observable, Subject } from 'rxjs'; +import { Nilable } from 'tsdef'; +import { + ArrayPropertyKey, + ArrayPropertyValue, + Controls, + ControlsNames, + NewFormErrors, + TypedFormGroup, +} from '../ngx-sub-form-utils'; +import { FormGroupOptions } from '../ngx-sub-form.types'; +import { AreTypesSimilar } from './helpers.types'; + +export interface ComponentHooks { + onDestroy: Observable; +} + +export interface FormBindings { + readonly writeValue$: Observable>; + readonly registerOnChange$: Observable<(formValue: ControlInterface | null) => void>; + readonly registerOnTouched$: Observable<(_: any) => void>; + readonly setDisabledState$: Observable; +} + +export type ControlValueAccessorComponentInstance = Object & + // ControlValueAccessor methods are called + // directly by Angular and expects a value + // so we have to define it within ngx-sub-form + // and this should *never* be overridden by the component + Partial & Record>; + +export interface NgxSubForm { + readonly formGroup: TypedFormGroup; + readonly formControlNames: ControlsNames; + readonly formGroupErrors: NewFormErrors; + readonly createFormArrayControl: any; +} + +export interface NgxRootForm extends NgxSubForm { + // @todo: anything else needed here? +} + +export interface NgxSubFormArrayOptions { + createFormArrayControl?: ( + key: ArrayPropertyKey, + value: ArrayPropertyValue, + ) => FormControl; +} + +export interface NgxSubFormRemapOptions { + toFormGroup: (obj: ControlInterface) => FormInterface; + fromFormGroup: (formValue: FormInterface) => ControlInterface; +} + +type NgxSubFormRemap = AreTypesSimilar extends true + ? {} + : NgxSubFormRemapOptions; + +type NgxSubFormArray = ArrayPropertyKey extends never + ? {} // no point defining `createFormArrayControl` if there's not a single array in the `FormInterface` + : NgxSubFormArrayOptions; + +export type NgxSubFormOptions = { + formType: FormType; + formControls: Controls; + formGroupOptions?: FormGroupOptions; + emitNullOnDestroy?: boolean; + componentHooks: ComponentHooks; +} & NgxSubFormRemap & + NgxSubFormArray; + +export type NgxRootFormOptions = NgxSubFormOptions< + ControlInterface, + FormInterface +> & { + disabled$: Observable; + input$: Observable; + output$: Subject; +}; + +export enum FormType { + SUB, + ROOT, +} + +export type NgxFormOptions = + | NgxSubFormOptions + | NgxRootFormOptions; diff --git a/projects/ngx-sub-form/src/lib/ngx-root-form.component.spec.ts b/projects/ngx-sub-form/src/lib/ngx-root-form.component.spec.ts index fc862779..12cf687e 100644 --- a/projects/ngx-sub-form/src/lib/ngx-root-form.component.spec.ts +++ b/projects/ngx-sub-form/src/lib/ngx-root-form.component.spec.ts @@ -1,10 +1,10 @@ -import { NgxRootFormComponent } from './ngx-root-form.component'; -import { EventEmitter, Input, Component, Output, DebugElement } from '@angular/core'; -import { Controls, ArrayPropertyKey, ArrayPropertyValue } from './ngx-sub-form-utils'; -import { FormControl, Validators, ReactiveFormsModule, FormArray } from '@angular/forms'; -import { BehaviorSubject } from 'rxjs'; -import { TestBed, async, ComponentFixture, fakeAsync, tick } from '@angular/core/testing'; +import { Component, DebugElement, EventEmitter, Input, Output } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormArray, FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; import { By } from '@angular/platform-browser'; +import { BehaviorSubject } from 'rxjs'; +import { NgxRootFormComponent } from './ngx-root-form.component'; +import { ArrayPropertyKey, ArrayPropertyValue, Controls } from './ngx-sub-form-utils'; import { DataInput } from './ngx-sub-form.decorators'; import { NgxFormWithArrayControls } from './ngx-sub-form.types'; diff --git a/projects/ngx-sub-form/src/lib/ngx-sub-form-utils.ts b/projects/ngx-sub-form/src/lib/ngx-sub-form-utils.ts index 8bfb5f4a..8da2d91b 100644 --- a/projects/ngx-sub-form/src/lib/ngx-sub-form-utils.ts +++ b/projects/ngx-sub-form/src/lib/ngx-sub-form-utils.ts @@ -1,16 +1,16 @@ +import { forwardRef, InjectionToken, Type } from '@angular/core'; import { + AbstractControl, ControlValueAccessor, - NG_VALUE_ACCESSOR, - NG_VALIDATORS, - ValidationErrors, - FormControl, FormArray, - AbstractControl, + FormControl, FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, } from '@angular/forms'; -import { InjectionToken, Type, forwardRef, OnDestroy } from '@angular/core'; import { Observable, Subject, timer } from 'rxjs'; -import { takeUntil, debounce } from 'rxjs/operators'; +import { debounce, takeUntil } from 'rxjs/operators'; import { SUB_FORM_COMPONENT_TOKEN } from './ngx-sub-form-tokens'; import { NgxSubFormComponent } from './ngx-sub-form.component'; @@ -24,18 +24,34 @@ export type ControlsType = { [K in keyof T]-?: T[K] extends any[] ? TypedFormArray : TypedFormControl | TypedFormGroup; }; +export type OneOfControlsTypes = ControlsType[keyof ControlsType]; + +// @deprecated export type FormErrorsType = { [K in keyof T]-?: T[K] extends any[] ? (null | ValidationErrors)[] : ValidationErrors; }; export type FormUpdate = { [FormControlInterface in keyof FormInterface]?: true }; +// @deprecated export type FormErrors = null | Partial< FormErrorsType & { formGroup?: ValidationErrors; } >; +// @todo rename to `FormErrorsType` once the deprecated one is removed +export type NewFormErrorsType = { + [K in keyof T]-?: T[K] extends any[] ? Record : ValidationErrors; +}; + +// @todo rename to `FormErrors` once the deprecated one is removed +export type NewFormErrors = null | Partial< + NewFormErrorsType & { + formGroup?: ValidationErrors; + } +>; + // using set/patch value options signature from form controls to allow typing without additional casting export interface TypedAbstractControl extends AbstractControl { value: TValue; @@ -123,8 +139,13 @@ export const NGX_SUB_FORM_HANDLE_VALUE_CHANGES_RATE_STRATEGIES = { * Easily unsubscribe from an observable stream by appending `takeUntilDestroyed(this)` to the observable pipe. * If the component already has a `ngOnDestroy` method defined, it will call this first. * Note that the component *must* implement OnDestroy for this to work (the typings will enforce this anyway) + * --------------- + * following doesn't work anymore with ng9 + * https://github.com/angular/angular/issues/36776 + * there's also a PR that'd fix this here: + * https://github.com/angular/angular/pull/35464 */ -export function takeUntilDestroyed(component: OnDestroy): (source: Observable) => Observable { +export function takeUntilDestroyed(component: any): (source: Observable) => Observable { return (source: Observable): Observable => { const onDestroy = new Subject(); const previousOnDestroy = component.ngOnDestroy; diff --git a/projects/ngx-sub-form/src/lib/ngx-sub-form.component.ts b/projects/ngx-sub-form/src/lib/ngx-sub-form.component.ts index e68f48d1..6b52abf5 100644 --- a/projects/ngx-sub-form/src/lib/ngx-sub-form.component.ts +++ b/projects/ngx-sub-form/src/lib/ngx-sub-form.component.ts @@ -1,26 +1,26 @@ -import { OnDestroy, Directive, Component } from '@angular/core'; +import { Directive, OnDestroy } from '@angular/core'; import { AbstractControl, AbstractControlOptions, ControlValueAccessor, + FormArray, + FormControl, FormGroup, ValidationErrors, Validator, - FormArray, - FormControl, } from '@angular/forms'; import { merge, Observable, Subscription } from 'rxjs'; import { delay, filter, map, startWith, withLatestFrom } from 'rxjs/operators'; import { + ArrayPropertyKey, ControlMap, Controls, ControlsNames, - FormUpdate, - MissingFormControlsError, + ControlsType, FormErrors, + FormUpdate, isNullOrUndefined, - ControlsType, - ArrayPropertyKey, + MissingFormControlsError, TypedAbstractControl, TypedFormGroup, } from './ngx-sub-form-utils'; diff --git a/src/app/app.component.html b/src/app/app.component.html index 276e52c6..0680b43f 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1 +1 @@ - + diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 323284f8..ba8e2e0e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -5,6 +5,4 @@ import { Component } from '@angular/core'; templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], }) -export class AppComponent { - title = 'ngx-sub-form-demo'; -} +export class AppComponent {} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 6bd5dbd4..a1d432d8 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,87 +1,26 @@ -import { LayoutModule } from '@angular/cdk/layout'; -import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; -import { ReactiveFormsModule } from '@angular/forms'; -import { MatButtonModule } from '@angular/material/button'; -import { MatCardModule } from '@angular/material/card'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatIconModule } from '@angular/material/icon'; -import { MatInputModule } from '@angular/material/input'; -import { MatListModule } from '@angular/material/list'; -import { MatSelectModule } from '@angular/material/select'; -import { MatSidenavModule } from '@angular/material/sidenav'; -import { MatSlideToggleModule } from '@angular/material/slide-toggle'; -import { MatToolbarModule } from '@angular/material/toolbar'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouterModule } from '@angular/router'; import { AppComponent } from './app.component'; -import { AssassinDroidComponent } from './main/listing/listing-form/droid-listing/assassin-droid/assassin-droid.component'; -import { AstromechDroidComponent } from './main/listing/listing-form/droid-listing/astromech-droid/astromech-droid.component'; -import { DroidProductComponent } from './main/listing/listing-form/droid-listing/droid-product.component'; -import { MedicalDroidComponent } from './main/listing/listing-form/droid-listing/medical-droid/medical-droid.component'; -import { ProtocolDroidComponent } from './main/listing/listing-form/droid-listing/protocol-droid/protocol-droid.component'; -import { ListingFormComponent } from './main/listing/listing-form/listing-form.component'; -import { SpaceshipComponent } from './main/listing/listing-form/vehicle-listing/spaceship/spaceship.component'; -import { SpeederComponent } from './main/listing/listing-form/vehicle-listing/speeder/speeder.component'; -import { VehicleProductComponent } from './main/listing/listing-form/vehicle-listing/vehicle-product.component'; -import { ListingComponent } from './main/listing/listing.component'; -import { ListingsComponent } from './main/listings/listings.component'; -import { MainComponent } from './main/main.component'; -import { CrewMemberComponent } from './main/listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component'; -import { DisplayCrewMembersPipe } from './main/listings/display-crew-members.pipe'; -import { CrewMembersComponent } from './main/listing/listing-form/vehicle-listing/crew-members/crew-members.component'; - -const MATERIAL_MODULES = [ - LayoutModule, - MatToolbarModule, - MatButtonModule, - MatSidenavModule, - MatIconModule, - MatListModule, - MatFormFieldModule, - MatInputModule, - MatSelectModule, - MatSlideToggleModule, - MatCardModule, -]; +import { SharedModule } from './shared/shared.module'; @NgModule({ - declarations: [ - AppComponent, - MainComponent, - ListingsComponent, - ListingComponent, - VehicleProductComponent, - DroidProductComponent, - SpaceshipComponent, - SpeederComponent, - ProtocolDroidComponent, - MedicalDroidComponent, - AstromechDroidComponent, - AssassinDroidComponent, - ListingFormComponent, - CrewMembersComponent, - CrewMemberComponent, - DisplayCrewMembersPipe, - ], - exports: [DroidProductComponent], + declarations: [AppComponent], imports: [ BrowserModule, BrowserAnimationsModule, - CommonModule, - ReactiveFormsModule, - ...MATERIAL_MODULES, RouterModule.forRoot([ { - path: 'listings', - children: [ - { path: ':listingId', component: ListingComponent }, - { path: 'new', component: ListingComponent, pathMatch: 'full' }, - ], + path: 'rewrite', + loadChildren: () => import('./main-rewrite/main.module').then(x => x.MainModule), + }, + { + path: '', + loadChildren: () => import('./main/main.module').then(x => x.MainModule), }, - { path: '**', pathMatch: 'full', redirectTo: '/' }, ]), + SharedModule, ], providers: [], bootstrap: [AppComponent], diff --git a/src/app/app.spec.e2e.ts b/src/app/app.spec.e2e.ts index 6e6b79f2..ec44617c 100644 --- a/src/app/app.spec.e2e.ts +++ b/src/app/app.spec.e2e.ts @@ -1,282 +1,349 @@ /// -import { DOM, expectAll } from '../../cypress/helpers/dom.helper'; -import { hardCodedListings } from './services/listings.data'; -import { hardcodedElementsToTestList, FormElement } from '../../cypress/helpers/data.helper'; -import { VehicleListing, ListingType } from './interfaces/listing.interface'; -import { Spaceship, Speeder, VehicleType } from './interfaces/vehicle.interface'; +import { extractErrors, FormElement, hardcodedElementsToTestList } from '../../cypress/helpers/data.helper'; +import { DOM, getFormList, getFormValue } from '../../cypress/helpers/dom.helper'; import { DroidType } from './interfaces/droid.interface'; +import { ListingType, VehicleListing } from './interfaces/listing.interface'; +import { Spaceship, Speeder, VehicleType } from './interfaces/vehicle.interface'; +import { hardCodedListings } from './services/listings.data'; context(`EJawa demo`, () => { - beforeEach(() => { - cy.visit(''); - }); + const testContexts = [ + { id: 'old', testName: 'Old implementation', url: '' }, + { id: 'new', testName: 'New implementation', url: '/rewrite' }, + ] as const; - it(`should have a default list displayed`, () => { - DOM.list.objList.should('eql', hardcodedElementsToTestList(hardCodedListings)); - }); + testContexts.forEach(({ id, testName, url }) => { + context(testName, () => { + beforeEach(() => { + cy.visit(url); + }); + + it(`should have a default list displayed`, () => { + DOM.list.elements.cy.should($el => { + expect(getFormList($el)).to.eql(hardcodedElementsToTestList(hardCodedListings)); + }); + }); - it(`should click on the first element and display its data in the form`, () => { - DOM.list.elements.cy.first().click(); - - const x = hardCodedListings[0] as VehicleListing; - const v = x.product as Spaceship; - - const expectedObj: FormElement = { - title: x.title, - price: '£' + x.price.toLocaleString(), - inputs: { - id: x.id, - title: x.title, - imageUrl: x.imageUrl, - price: x.price + '', - listingType: x.listingType, - vehicleForm: { - vehicleType: x.product.vehicleType, - spaceshipForm: { - color: v.color, - canFire: v.canFire, - wingCount: v.wingCount, - crewMembers: v.crewMembers, + it(`should click on the first element and display its data in the form`, () => { + DOM.list.elements.cy.first().click(); + + const x = hardCodedListings[0] as VehicleListing; + const v = x.product as Spaceship; + + const expectedObj: FormElement = { + title: x.title, + price: '£' + x.price.toLocaleString(), + inputs: { + id: x.id, + title: x.title, + imageUrl: x.imageUrl, + price: x.price + '', + listingType: x.listingType, + vehicleForm: { + vehicleType: x.product.vehicleType, + spaceshipForm: { + color: v.color, + canFire: v.canFire, + wingCount: v.wingCount, + crewMembers: v.crewMembers, + }, + }, }, - }, - }, - }; + }; - DOM.form.getObj(VehicleType.SPACESHIP).should('eql', expectedObj); - }); + DOM.form.cy.should($el => { + expect(getFormValue($el, VehicleType.SPACESHIP)).to.eql(expectedObj); + }); + }); - it(`should be able to go from a spaceship to a speeder and update the form`, () => { - DOM.list.elements.cy.eq(0).click(); - DOM.list.elements.cy.eq(1).click(); - - const x = hardCodedListings[1] as VehicleListing; - const v = x.product as Speeder; - - const expectedObj: FormElement = { - title: x.title, - price: '£' + x.price.toLocaleString(), - inputs: { - id: x.id, - title: x.title, - imageUrl: x.imageUrl, - price: x.price + '', - listingType: x.listingType, - vehicleForm: { - vehicleType: x.product.vehicleType, - speederForm: { - color: v.color, - canFire: v.canFire, - crewMembers: v.crewMembers, - maximumSpeed: v.maximumSpeed, + it(`should be able to go from a spaceship to a speeder and update the form`, () => { + DOM.list.elements.cy.eq(0).click(); + DOM.list.elements.cy.eq(1).click(); + + const x = hardCodedListings[1] as VehicleListing; + const v = x.product as Speeder; + + const expectedObj: FormElement = { + title: x.title, + price: '£' + x.price.toLocaleString(), + inputs: { + id: x.id, + title: x.title, + imageUrl: x.imageUrl, + price: x.price + '', + listingType: x.listingType, + vehicleForm: { + vehicleType: x.product.vehicleType, + speederForm: { + color: v.color, + canFire: v.canFire, + crewMembers: v.crewMembers, + maximumSpeed: v.maximumSpeed, + }, + }, }, - }, - }, - }; + }; - DOM.form.getObj(VehicleType.SPEEDER).should('eql', expectedObj); - }); + DOM.form.cy.should($el => { + expect(getFormValue($el, VehicleType.SPEEDER)).to.eql(expectedObj); + }); + }); - it(`should display the (nested) errors from the form`, () => { - DOM.createNewButton.click(); - - DOM.form.errors.obj.should('eql', { - listingType: { - required: true, - }, - title: { - required: true, - }, - imageUrl: { - required: true, - }, - price: { - required: true, - }, - }); + it(`should display the (nested) errors from the form`, () => { + DOM.createNewButton.click(); - DOM.form.elements.selectListingTypeByType(ListingType.VEHICLE); - - DOM.form.errors.obj.should('eql', { - vehicleProduct: { - vehicleType: { - required: true, - }, - }, - title: { - required: true, - }, - imageUrl: { - required: true, - }, - price: { - required: true, - }, - }); + DOM.form.errors.should($el => { + expect(extractErrors($el)).to.eql({ + listingType: { + required: true, + }, + title: { + required: true, + }, + imageUrl: { + required: true, + }, + price: { + required: true, + }, + }); + }); - DOM.form.elements.vehicleForm.selectVehicleTypeByType(VehicleType.SPACESHIP); + DOM.form.elements.selectListingTypeByType(ListingType.VEHICLE); - DOM.form.errors.obj.should('eql', { - vehicleProduct: { - spaceship: { - color: { - required: true, - }, - crewMembers: { - required: true, - }, - wingCount: { - required: true, - }, - }, - }, - title: { - required: true, - }, - imageUrl: { - required: true, - }, - price: { - required: true, - }, - }); + DOM.form.errors.should($el => { + expect(extractErrors($el)).to.eql({ + vehicleProduct: { + vehicleType: { + required: true, + }, + }, + title: { + required: true, + }, + imageUrl: { + required: true, + }, + price: { + required: true, + }, + }); + }); - DOM.form.elements.vehicleForm.addCrewMemberButton.click(); + DOM.form.elements.vehicleForm.selectVehicleTypeByType(VehicleType.SPACESHIP); - DOM.form.errors.obj.should('eql', { - vehicleProduct: { - spaceship: { - color: { - required: true, - }, - crewMembers: { - crewMembers: [ - { - firstName: { + DOM.form.errors.should($el => { + expect(extractErrors($el)).to.eql({ + vehicleProduct: { + spaceship: { + color: { required: true, }, - lastName: { + crewMembers: { + required: true, + }, + wingCount: { required: true, }, }, - ], - }, - wingCount: { - required: true, - }, - }, - }, - title: { - required: true, - }, - imageUrl: { - required: true, - }, - price: { - required: true, - }, - }); + }, + title: { + required: true, + }, + imageUrl: { + required: true, + }, + price: { + required: true, + }, + }); + }); - DOM.form.elements.selectListingTypeByType(ListingType.DROID); - - DOM.form.errors.obj.should('eql', { - droidProduct: { - droidType: { - required: true, - }, - }, - title: { - required: true, - }, - imageUrl: { - required: true, - }, - price: { - required: true, - }, - }); + DOM.form.elements.vehicleForm.addCrewMemberButton.click(); + + if (id === 'old') { + DOM.form.errors.should($el => { + expect(extractErrors($el)).to.eql({ + vehicleProduct: { + spaceship: { + color: { + required: true, + }, + crewMembers: { + crewMembers: [ + { + firstName: { + required: true, + }, + lastName: { + required: true, + }, + }, + ], + }, + wingCount: { + required: true, + }, + }, + }, + title: { + required: true, + }, + imageUrl: { + required: true, + }, + price: { + required: true, + }, + }); + }); + } else { + DOM.form.errors.should($el => { + expect(extractErrors($el)).to.eql({ + vehicleProduct: { + spaceship: { + color: { + required: true, + }, + crewMembers: { + crewMembers: { + 0: { + firstName: { + required: true, + }, + lastName: { + required: true, + }, + }, + }, + }, + wingCount: { + required: true, + }, + }, + }, + title: { + required: true, + }, + imageUrl: { + required: true, + }, + price: { + required: true, + }, + }); + }); + } - DOM.form.elements.droidForm.selectDroidTypeByType(DroidType.ASSASSIN); + DOM.form.elements.selectListingTypeByType(ListingType.DROID); - DOM.form.errors.obj.should('eql', { - droidProduct: { - assassinDroid: { - color: { - required: true, - }, - name: { - required: true, - }, - weapons: { - required: true, - }, - }, - }, - title: { - required: true, - }, - imageUrl: { - required: true, - }, - price: { - required: true, - }, - }); + DOM.form.errors.should($el => { + expect(extractErrors($el)).to.eql({ + droidProduct: { + droidType: { + required: true, + }, + }, + title: { + required: true, + }, + imageUrl: { + required: true, + }, + price: { + required: true, + }, + }); + }); - DOM.form.elements.droidForm.name.type(`IG-86 sentinel`); + DOM.form.elements.droidForm.selectDroidTypeByType(DroidType.ASSASSIN); - DOM.form.errors.obj.should('eql', { - droidProduct: { - assassinDroid: { - color: { - required: true, - }, - weapons: { - required: true, - }, - }, - }, - title: { - required: true, - }, - imageUrl: { - required: true, - }, - price: { - required: true, - }, - }); - }); + DOM.form.errors.should($el => { + expect(extractErrors($el)).to.eql({ + droidProduct: { + assassinDroid: { + color: { + required: true, + }, + name: { + required: true, + }, + weapons: { + required: true, + }, + }, + }, + title: { + required: true, + }, + imageUrl: { + required: true, + }, + price: { + required: true, + }, + }); + }); - it(`should display no error when form is valid`, () => { - // we want to make sure that it's easy to detect from the template that there's no error - // previously we returned an empty object which made that check way harder in the template - DOM.list.elements.cy.eq(0).click(); + DOM.form.elements.droidForm.name.type(`IG-86 sentinel`); - DOM.form.errors.cy.should('not.exist'); - DOM.form.noErrors.should('exist'); - }); + DOM.form.errors.should($el => { + expect(extractErrors($el)).to.eql({ + droidProduct: { + assassinDroid: { + color: { + required: true, + }, + weapons: { + required: true, + }, + }, + }, + title: { + required: true, + }, + imageUrl: { + required: true, + }, + price: { + required: true, + }, + }); + }); + }); - it(`should recursively disable the form when disabling the top formGroup`, () => { - DOM.list.elements.cy.eq(0).click(); + it(`should display no error when form is valid`, () => { + // we want to make sure that it's easy to detect from the template that there's no error + // previously we returned an empty object which made that check way harder in the template + DOM.list.elements.cy.eq(0).click(); - DOM.form.cy.within(() => { - cy.get(`mat-card`).within(() => { - expectAll(`input`, el => el.should('be.enabled')); - expectAll(`mat-select`, el => el.should('not.have.class', 'mat-select-disabled')); - expectAll(`mat-slide-toggle`, el => el.should('not.have.class', 'mat-disabled')); - expectAll(`button`, el => el.should('be.enabled')); + DOM.form.errors.should('not.exist'); + DOM.form.noErrors.should('exist'); }); - }); - DOM.readonlyToggle.click(); + it(`should recursively disable the form when disabling the top formGroup`, () => { + DOM.list.elements.cy.eq(0).click(); + + DOM.form.cy.within(() => { + cy.get(`mat-card`).within(() => { + cy.get(`input`).should('be.enabled'); + cy.get(`mat-select`).should('not.have.class', 'mat-select-disabled'); + cy.get(`mat-slide-toggle`).should('not.have.class', 'mat-disabled'); + cy.get(`button`).should('be.enabled'); + }); + }); + + DOM.readonlyToggle.click(); - DOM.form.cy.within(() => { - cy.get(`mat-card`).within(() => { - expectAll(`input`, el => el.should('be.disabled')); - expectAll(`mat-select`, el => el.should('have.class', 'mat-select-disabled')); - expectAll(`mat-slide-toggle`, el => el.should('have.class', 'mat-disabled')); - expectAll(`button`, el => el.should('be.disabled')); + DOM.form.cy.within(() => { + cy.get(`mat-card`).within(() => { + cy.get(`input`).should('be.disabled'); + cy.get(`mat-select`).should('have.class', 'mat-select-disabled'); + cy.get(`mat-slide-toggle`).should('have.class', 'mat-disabled'); + cy.get(`button`).should('be.disabled'); + }); + }); }); }); }); diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/assassin-droid/assassin-droid.component.html b/src/app/main-rewrite/listing/listing-form/droid-listing/assassin-droid/assassin-droid.component.html new file mode 100644 index 00000000..f9aa5ac7 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/assassin-droid/assassin-droid.component.html @@ -0,0 +1,42 @@ +
+ Assassin Droid form + + + + + + + + + + + + + {{ assassinDroidWeaponText[weapon.value] }} + + + +
diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/assassin-droid/assassin-droid.component.scss b/src/app/main-rewrite/listing/listing-form/droid-listing/assassin-droid/assassin-droid.component.scss new file mode 100644 index 00000000..abde53a3 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/assassin-droid/assassin-droid.component.scss @@ -0,0 +1,3 @@ +.container { + display: flex; +} diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/assassin-droid/assassin-droid.component.ts b/src/app/main-rewrite/listing/listing-form/droid-listing/assassin-droid/assassin-droid.component.ts new file mode 100644 index 00000000..a0781d52 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/assassin-droid/assassin-droid.component.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { getObservableLifecycle, ObservableLifecycle } from 'ngx-observable-lifecycle'; +import { subformComponentProviders } from 'ngx-sub-form'; +import { AssassinDroid, AssassinDroidWeapon, DroidType } from 'src/app/interfaces/droid.interface'; +import { createForm } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form'; +import { FormType } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form.types'; + +export const ASSASSIN_DROID_WEAPON_TEXT: { [K in AssassinDroidWeapon]: string } = { + [AssassinDroidWeapon.SABER]: 'Saber', + [AssassinDroidWeapon.FLAME_THROWER]: 'Flame thrower', + [AssassinDroidWeapon.GUN]: 'Gun', + [AssassinDroidWeapon.AXE]: 'Axe', +}; + +@ObservableLifecycle() +@Component({ + selector: 'app-assassin-droid', + templateUrl: './assassin-droid.component.html', + styleUrls: ['./assassin-droid.component.scss'], + providers: subformComponentProviders(AssassinDroidComponent), +}) +export class AssassinDroidComponent { + public AssassinDroidWeapon = AssassinDroidWeapon; + + public assassinDroidWeaponText = ASSASSIN_DROID_WEAPON_TEXT; + + public form = createForm(this, { + formType: FormType.SUB, + formControls: { + color: new FormControl(null, { validators: [Validators.required] }), + name: new FormControl(null, { validators: [Validators.required] }), + droidType: new FormControl(DroidType.ASSASSIN, { validators: [Validators.required] }), + weapons: new FormControl([], { validators: [Validators.required] }), + }, + componentHooks: { + onDestroy: getObservableLifecycle(this).onDestroy, + }, + }); +} diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/astromech-droid/astromech-droid.component.html b/src/app/main-rewrite/listing/listing-form/droid-listing/astromech-droid/astromech-droid.component.html new file mode 100644 index 00000000..0a62f73b --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/astromech-droid/astromech-droid.component.html @@ -0,0 +1,35 @@ +
+ Astromech Droid form + + + + + + + + + + + + + + + + + {{ shape.value }} + + + +
diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/astromech-droid/astromech-droid.component.scss b/src/app/main-rewrite/listing/listing-form/droid-listing/astromech-droid/astromech-droid.component.scss new file mode 100644 index 00000000..abde53a3 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/astromech-droid/astromech-droid.component.scss @@ -0,0 +1,3 @@ +.container { + display: flex; +} diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/astromech-droid/astromech-droid.component.ts b/src/app/main-rewrite/listing/listing-form/droid-listing/astromech-droid/astromech-droid.component.ts new file mode 100644 index 00000000..934fc078 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/astromech-droid/astromech-droid.component.ts @@ -0,0 +1,32 @@ +import { Component } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { getObservableLifecycle, ObservableLifecycle } from 'ngx-observable-lifecycle'; +import { subformComponentProviders } from 'ngx-sub-form'; +import { createForm } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form'; +import { FormType } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form.types'; +import { AstromechDroid, AstromechDroidShape, DroidType } from '../../../../../interfaces/droid.interface'; + +@ObservableLifecycle() +@Component({ + selector: 'app-astromech-droid', + templateUrl: './astromech-droid.component.html', + styleUrls: ['./astromech-droid.component.scss'], + providers: subformComponentProviders(AstromechDroidComponent), +}) +export class AstromechDroidComponent { + public AstromechDroidShape = AstromechDroidShape; + + public form = createForm(this, { + formType: FormType.SUB, + formControls: { + color: new FormControl(null, { validators: [Validators.required] }), + name: new FormControl(null, { validators: [Validators.required] }), + droidType: new FormControl(DroidType.ASTROMECH, { validators: [Validators.required] }), + toolCount: new FormControl(null, { validators: [Validators.required] }), + shape: new FormControl(null, { validators: [Validators.required] }), + }, + componentHooks: { + onDestroy: getObservableLifecycle(this).onDestroy, + }, + }); +} diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/droid-product.component.html b/src/app/main-rewrite/listing/listing-form/droid-listing/droid-product.component.html new file mode 100644 index 00000000..cf21d149 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/droid-product.component.html @@ -0,0 +1,38 @@ +
+ Droid form + + + + + {{ droidType.value }} + + + + +
+ + + + +
+
diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/droid-product.component.scss b/src/app/main-rewrite/listing/listing-form/droid-listing/droid-product.component.scss new file mode 100644 index 00000000..abde53a3 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/droid-product.component.scss @@ -0,0 +1,3 @@ +.container { + display: flex; +} diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/droid-product.component.ts b/src/app/main-rewrite/listing/listing-form/droid-listing/droid-product.component.ts new file mode 100644 index 00000000..aae60ffe --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/droid-product.component.ts @@ -0,0 +1,73 @@ +import { Component } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { getObservableLifecycle, ObservableLifecycle } from 'ngx-observable-lifecycle'; +import { subformComponentProviders } from 'ngx-sub-form'; +import { + AssassinDroid, + AstromechDroid, + DroidType, + MedicalDroid, + OneDroid, + ProtocolDroid, +} from 'src/app/interfaces/droid.interface'; +import { createForm } from '../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form'; +import { FormType } from '../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form.types'; +import { UnreachableCase } from '../../../../shared/utils'; + +interface OneDroidForm { + protocolDroid: ProtocolDroid | null; + medicalDroid: MedicalDroid | null; + astromechDroid: AstromechDroid | null; + assassinDroid: AssassinDroid | null; + droidType: DroidType | null; +} + +@ObservableLifecycle() +@Component({ + selector: 'app-droid-product', + templateUrl: './droid-product.component.html', + styleUrls: ['./droid-product.component.scss'], + providers: subformComponentProviders(DroidProductComponent), +}) +export class DroidProductComponent { + public DroidType = DroidType; + + public form = createForm(this, { + formType: FormType.SUB, + formControls: { + protocolDroid: new FormControl(null), + medicalDroid: new FormControl(null), + astromechDroid: new FormControl(null), + assassinDroid: new FormControl(null), + droidType: new FormControl(null, { validators: [Validators.required] }), + }, + toFormGroup: (obj: OneDroid): OneDroidForm => { + return { + protocolDroid: obj.droidType === DroidType.PROTOCOL ? obj : null, + medicalDroid: obj.droidType === DroidType.MEDICAL ? obj : null, + astromechDroid: obj.droidType === DroidType.ASTROMECH ? obj : null, + assassinDroid: obj.droidType === DroidType.ASSASSIN ? obj : null, + droidType: obj.droidType, + }; + }, + fromFormGroup: (formValue: OneDroidForm): OneDroid => { + switch (formValue.droidType) { + case DroidType.PROTOCOL: + return formValue.protocolDroid as any; // todo + case DroidType.MEDICAL: + return formValue.medicalDroid as any; // todo + case DroidType.ASTROMECH: + return formValue.astromechDroid as any; // todo + case DroidType.ASSASSIN: + return formValue.assassinDroid as any; // todo + case null: + return null as any; // todo + default: + throw new UnreachableCase(formValue.droidType); + } + }, + componentHooks: { + onDestroy: getObservableLifecycle(this).onDestroy, + }, + }); +} diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/medical-droid/medical-droid.component.html b/src/app/main-rewrite/listing/listing-form/droid-listing/medical-droid/medical-droid.component.html new file mode 100644 index 00000000..915f6bc5 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/medical-droid/medical-droid.component.html @@ -0,0 +1,21 @@ +
+ Medical Droid form + + + + + + + + + + Can heal humans + + Can fix robots +
diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/medical-droid/medical-droid.component.scss b/src/app/main-rewrite/listing/listing-form/droid-listing/medical-droid/medical-droid.component.scss new file mode 100644 index 00000000..abde53a3 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/medical-droid/medical-droid.component.scss @@ -0,0 +1,3 @@ +.container { + display: flex; +} diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/medical-droid/medical-droid.component.ts b/src/app/main-rewrite/listing/listing-form/droid-listing/medical-droid/medical-droid.component.ts new file mode 100644 index 00000000..ce4062ba --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/medical-droid/medical-droid.component.ts @@ -0,0 +1,30 @@ +import { Component } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { getObservableLifecycle, ObservableLifecycle } from 'ngx-observable-lifecycle'; +import { subformComponentProviders } from 'ngx-sub-form'; +import { DroidType, MedicalDroid } from 'src/app/interfaces/droid.interface'; +import { createForm } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form'; +import { FormType } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form.types'; + +@ObservableLifecycle() +@Component({ + selector: 'app-medical-droid', + templateUrl: './medical-droid.component.html', + styleUrls: ['./medical-droid.component.scss'], + providers: subformComponentProviders(MedicalDroidComponent), +}) +export class MedicalDroidComponent { + public form = createForm(this, { + formType: FormType.SUB, + formControls: { + color: new FormControl(null, { validators: [Validators.required] }), + name: new FormControl(null, { validators: [Validators.required] }), + droidType: new FormControl(DroidType.MEDICAL, { validators: [Validators.required] }), + canHealHumans: new FormControl(false, { validators: [Validators.required] }), + canFixRobots: new FormControl(false, { validators: [Validators.required] }), + }, + componentHooks: { + onDestroy: getObservableLifecycle(this).onDestroy, + }, + }); +} diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/protocol-droid/protocol-droid.component.html b/src/app/main-rewrite/listing/listing-form/droid-listing/protocol-droid/protocol-droid.component.html new file mode 100644 index 00000000..0bfc5e33 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/protocol-droid/protocol-droid.component.html @@ -0,0 +1,29 @@ +
+ Protocol Droid form + + + + + + + + + + + + + {{ language.value }} + + + +
diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/protocol-droid/protocol-droid.component.scss b/src/app/main-rewrite/listing/listing-form/droid-listing/protocol-droid/protocol-droid.component.scss new file mode 100644 index 00000000..abde53a3 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/protocol-droid/protocol-droid.component.scss @@ -0,0 +1,3 @@ +.container { + display: flex; +} diff --git a/src/app/main-rewrite/listing/listing-form/droid-listing/protocol-droid/protocol-droid.component.ts b/src/app/main-rewrite/listing/listing-form/droid-listing/protocol-droid/protocol-droid.component.ts new file mode 100644 index 00000000..23806f84 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/droid-listing/protocol-droid/protocol-droid.component.ts @@ -0,0 +1,31 @@ +import { Component } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { getObservableLifecycle, ObservableLifecycle } from 'ngx-observable-lifecycle'; +import { subformComponentProviders } from 'ngx-sub-form'; +import { createForm } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form'; +import { FormType } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form.types'; +import { DroidType, Languages, ProtocolDroid } from '../../../../../interfaces/droid.interface'; + +@ObservableLifecycle() +@Component({ + selector: 'app-protocol-droid', + templateUrl: './protocol-droid.component.html', + styleUrls: ['./protocol-droid.component.scss'], + providers: subformComponentProviders(ProtocolDroidComponent), +}) +export class ProtocolDroidComponent { + public Languages = Languages; + + public form = createForm(this, { + formType: FormType.SUB, + formControls: { + color: new FormControl(null, { validators: [Validators.required] }), + name: new FormControl(null, { validators: [Validators.required] }), + droidType: new FormControl(DroidType.PROTOCOL, { validators: [Validators.required] }), + spokenLanguages: new FormControl(null, { validators: [Validators.required] }), + }, + componentHooks: { + onDestroy: getObservableLifecycle(this).onDestroy, + }, + }); +} diff --git a/src/app/main-rewrite/listing/listing-form/listing-form.component.html b/src/app/main-rewrite/listing/listing-form/listing-form.component.html new file mode 100644 index 00000000..1c45aad6 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/listing-form.component.html @@ -0,0 +1,168 @@ + + + + {{ form.formGroup.value.title }} + + + £{{ form.formGroup.value.price | number }} + + +
+ Photo of {{ form.formGroup.value.title }} +
+ +
+ + + + + + + + ID is required + + + + + + + + Title is required + + + + + + + + Image url is required + + + + + + + + Price is required + + + + + + {{ listingType.value }} + + + + +
+ + + +
+
+
+ + +
+ + + +
+ Form is invalid +
+
+
+
+ + + Form errors + + + +
{{ errors | json }}
+ + + + Form is valid, no error! + + +
+
+ + + Form values + + +
{{ form.formGroup.value | json }}
+
+
diff --git a/src/app/main-rewrite/listing/listing-form/listing-form.component.scss b/src/app/main-rewrite/listing/listing-form/listing-form.component.scss new file mode 100644 index 00000000..1841500f --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/listing-form.component.scss @@ -0,0 +1,35 @@ +img { + max-width: 300px; + max-height: 500px; + object-fit: contain; +} + +.img-container { + width: 100%; + text-align: center; +} + +mat-card { + margin-bottom: 15px; + max-width: 500px; + + mat-card-title, + mat-card-subtitle { + min-height: 25px; + } + + &.errors, + &.values { + mat-card-content { + overflow: auto; + } + } +} + +mat-form-field { + width: 100%; +} + +.invalid-form { + padding: 15px 0; +} diff --git a/src/app/main-rewrite/listing/listing-form/listing-form.component.ts b/src/app/main-rewrite/listing/listing-form/listing-form.component.ts new file mode 100644 index 00000000..ee2acd09 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/listing-form.component.ts @@ -0,0 +1,85 @@ +import { Component, Input, Output } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { getObservableLifecycle, ObservableLifecycle } from 'ngx-observable-lifecycle'; +import { Subject } from 'rxjs'; +import { ListingType, OneListing } from 'src/app/interfaces/listing.interface'; +import { createForm } from '../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form'; +import { FormType } from '../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form.types'; +import { OneDroid } from '../../../interfaces/droid.interface'; +import { OneVehicle } from '../../../interfaces/vehicle.interface'; +import { UnreachableCase } from '../../../shared/utils'; + +interface OneListingForm { + vehicleProduct: OneVehicle | null; + droidProduct: OneDroid | null; + listingType: ListingType | null; + id: string; + title: string; + imageUrl: string; + price: number; +} + +@ObservableLifecycle() +@Component({ + selector: 'app-listing-form', + templateUrl: './listing-form.component.html', + styleUrls: ['./listing-form.component.scss'], +}) +export class ListingFormComponent { + public ListingType: typeof ListingType = ListingType; + + private input$: Subject = new Subject(); + @Input() set listing(value: OneListing | undefined) { + this.input$.next(value); + } + + private disabled$: Subject = new Subject(); + @Input() set disabled(value: boolean | undefined) { + this.disabled$.next(!value ? false : value); + } + + @Output() listingUpdated: Subject = new Subject(); + + public form = createForm(this, { + formType: FormType.ROOT, + disabled$: this.disabled$, + input$: this.input$, + output$: this.listingUpdated, + formControls: { + vehicleProduct: new FormControl(null), + droidProduct: new FormControl(null), + listingType: new FormControl(null, Validators.required), + id: new FormControl(null, Validators.required), + title: new FormControl(null, Validators.required), + imageUrl: new FormControl(null, Validators.required), + price: new FormControl(null, Validators.required), + }, + toFormGroup: (obj: OneListing): OneListingForm => { + const { listingType, product, ...commonValues } = obj; + + return { + vehicleProduct: obj.listingType === ListingType.VEHICLE ? obj.product : null, + droidProduct: obj.listingType === ListingType.DROID ? obj.product : null, + listingType: obj.listingType, + ...commonValues, + }; + }, + fromFormGroup: (formValue: OneListingForm): OneListing => { + const { vehicleProduct, droidProduct, listingType, ...commonValues } = formValue; + + switch (listingType) { + case ListingType.DROID: + return droidProduct ? { product: droidProduct, listingType, ...commonValues } : (null as any); //todo; + case ListingType.VEHICLE: + return vehicleProduct ? { product: vehicleProduct, listingType, ...commonValues } : (null as any); //todo; + case null: + return null as any; // todo; + default: + throw new UnreachableCase(listingType); + } + }, + componentHooks: { + onDestroy: getObservableLifecycle(this).onDestroy, + }, + }); +} diff --git a/src/app/main-rewrite/listing/listing-form/test-types.ts b/src/app/main-rewrite/listing/listing-form/test-types.ts new file mode 100644 index 00000000..0c9d7601 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/test-types.ts @@ -0,0 +1,32 @@ +// export type Controls = { [K in keyof T]-?: AbstractControl }; + +type Hello = { a: number | null; b: number }; + +type AAAA = number | null; +type BBBB = Extract; +type CCCC = BBBB extends never ? never : BBBB; + +type H = { [key in keyof T]: Extract extends never ? key : never }; +type DDDD = H; +interface Person { + id: string; + name?: string; + age: number | null; +} + +type AAA = Exclude>; +type NoUndefinedField = { [P in keyof T]-?: NoUndefinedField> }; + +type D = NoUndefinedField; + +type zzz = H; +type AAAAAA = Pick>>; +type KKKKKK = {} extends AAAAAA ? true : false; + +type B = Person['age'] extends null ? never : Person['age']; +type RequiredKeys = { + [K in keyof T]-?: {} extends H> ? never : K; +}[keyof T]; +type A = RequiredKeys; + +new FormGroup({}); diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component.html b/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component.html new file mode 100644 index 00000000..457a5e47 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component.html @@ -0,0 +1,25 @@ +
+ Crew member form + + + + + + + + +
diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component.scss b/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component.ts b/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component.ts new file mode 100644 index 00000000..ff8771e8 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { getObservableLifecycle, ObservableLifecycle } from 'ngx-observable-lifecycle'; +import { subformComponentProviders } from 'ngx-sub-form'; +import { createForm } from '../../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form'; +import { FormType } from '../../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form.types'; +import { CrewMember } from '../../../../../../interfaces/crew-member.interface'; + +@ObservableLifecycle() +@Component({ + selector: 'app-crew-member', + templateUrl: './crew-member.component.html', + styleUrls: ['./crew-member.component.scss'], + providers: subformComponentProviders(CrewMemberComponent), +}) +export class CrewMemberComponent { + public form = createForm(this, { + formType: FormType.SUB, + formControls: { + firstName: new FormControl(null, [Validators.required]), + lastName: new FormControl(null, [Validators.required]), + }, + componentHooks: { + onDestroy: getObservableLifecycle(this).onDestroy, + }, + }); +} diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-members.component.html b/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-members.component.html new file mode 100644 index 00000000..a4b024c6 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-members.component.html @@ -0,0 +1,26 @@ +
+ Crew members form + +
+ + + +
+ + +
diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-members.component.scss b/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-members.component.scss new file mode 100644 index 00000000..93ccc271 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-members.component.scss @@ -0,0 +1,12 @@ +.crew-member { + display: flex; + align-items: center; +} + +app-crew-member { + margin-bottom: 15px; +} + +.add-crew-member { + margin-top: 15px; +} diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-members.component.ts b/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-members.component.ts new file mode 100644 index 00000000..1c101321 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/crew-members/crew-members.component.ts @@ -0,0 +1,62 @@ +import { Component } from '@angular/core'; +import { FormArray, FormControl, Validators } from '@angular/forms'; +import { getObservableLifecycle, ObservableLifecycle } from 'ngx-observable-lifecycle'; +import { ArrayPropertyKey, ArrayPropertyValue, subformComponentProviders } from 'ngx-sub-form'; +import { createForm } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form'; +import { FormType } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form.types'; +import { CrewMember } from '../../../../../interfaces/crew-member.interface'; + +interface CrewMembersForm { + crewMembers: CrewMember[]; +} + +@ObservableLifecycle() +@Component({ + selector: 'app-crew-members', + templateUrl: './crew-members.component.html', + styleUrls: ['./crew-members.component.scss'], + providers: subformComponentProviders(CrewMembersComponent), +}) +export class CrewMembersComponent { + public form = createForm(this, { + formType: FormType.SUB, + formControls: { + crewMembers: new FormArray([]), + }, + toFormGroup: (obj: CrewMember[]): CrewMembersForm => { + return { + crewMembers: !obj ? [] : obj, + }; + }, + fromFormGroup: (formValue: CrewMembersForm): CrewMember[] => { + return formValue.crewMembers; + }, + createFormArrayControl: ( + key: ArrayPropertyKey | undefined, + value: ArrayPropertyValue, + ) => { + switch (key) { + case 'crewMembers': + return new FormControl(value, [Validators.required]); + default: + return new FormControl(value); + } + }, + componentHooks: { + onDestroy: getObservableLifecycle(this).onDestroy, + }, + }); + + public removeCrewMember(index: number): void { + this.form.formGroup.controls.crewMembers.removeAt(index); + } + + public addCrewMember(): void { + this.form.formGroup.controls.crewMembers.push( + this.form.createFormArrayControl('crewMembers', { + firstName: '', + lastName: '', + }), + ); + } +} diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/spaceship/spaceship.component.html b/src/app/main-rewrite/listing/listing-form/vehicle-listing/spaceship/spaceship.component.html new file mode 100644 index 00000000..65bc3c09 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/spaceship/spaceship.component.html @@ -0,0 +1,29 @@ +
+ Spaceship form + + + + + + Can fire + + + + + + +
diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/spaceship/spaceship.component.scss b/src/app/main-rewrite/listing/listing-form/vehicle-listing/spaceship/spaceship.component.scss new file mode 100644 index 00000000..abde53a3 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/spaceship/spaceship.component.scss @@ -0,0 +1,3 @@ +.container { + display: flex; +} diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/spaceship/spaceship.component.ts b/src/app/main-rewrite/listing/listing-form/vehicle-listing/spaceship/spaceship.component.ts new file mode 100644 index 00000000..3170ee75 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/spaceship/spaceship.component.ts @@ -0,0 +1,30 @@ +import { Component } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { getObservableLifecycle, ObservableLifecycle } from 'ngx-observable-lifecycle'; +import { subformComponentProviders } from 'ngx-sub-form'; +import { Spaceship, VehicleType } from 'src/app/interfaces/vehicle.interface'; +import { createForm } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form'; +import { FormType } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form.types'; + +@ObservableLifecycle() +@Component({ + selector: 'app-spaceship', + templateUrl: './spaceship.component.html', + styleUrls: ['./spaceship.component.scss'], + providers: subformComponentProviders(SpaceshipComponent), +}) +export class SpaceshipComponent { + public form = createForm(this, { + formType: FormType.SUB, + formControls: { + color: new FormControl(null, { validators: [Validators.required] }), + canFire: new FormControl(false, { validators: [Validators.required] }), + crewMembers: new FormControl(null, { validators: [Validators.required] }), + wingCount: new FormControl(null, { validators: [Validators.required] }), + vehicleType: new FormControl(VehicleType.SPACESHIP, { validators: [Validators.required] }), + }, + componentHooks: { + onDestroy: getObservableLifecycle(this).onDestroy, + }, + }); +} diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/speeder/speeder.component.html b/src/app/main-rewrite/listing/listing-form/vehicle-listing/speeder/speeder.component.html new file mode 100644 index 00000000..c248bd6b --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/speeder/speeder.component.html @@ -0,0 +1,29 @@ +
+ Speeder form + + + + + + Can fire + + + + + + +
diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/speeder/speeder.component.scss b/src/app/main-rewrite/listing/listing-form/vehicle-listing/speeder/speeder.component.scss new file mode 100644 index 00000000..abde53a3 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/speeder/speeder.component.scss @@ -0,0 +1,3 @@ +.container { + display: flex; +} diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/speeder/speeder.component.ts b/src/app/main-rewrite/listing/listing-form/vehicle-listing/speeder/speeder.component.ts new file mode 100644 index 00000000..243456fa --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/speeder/speeder.component.ts @@ -0,0 +1,30 @@ +import { Component } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { getObservableLifecycle, ObservableLifecycle } from 'ngx-observable-lifecycle'; +import { subformComponentProviders } from 'ngx-sub-form'; +import { Speeder, VehicleType } from 'src/app/interfaces/vehicle.interface'; +import { createForm } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form'; +import { FormType } from '../../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form.types'; + +@ObservableLifecycle() +@Component({ + selector: 'app-speeder', + templateUrl: './speeder.component.html', + styleUrls: ['./speeder.component.scss'], + providers: subformComponentProviders(SpeederComponent), +}) +export class SpeederComponent { + public form = createForm(this, { + formType: FormType.SUB, + formControls: { + color: new FormControl(null, { validators: [Validators.required] }), + canFire: new FormControl(false, { validators: [Validators.required] }), + crewMembers: new FormControl(null, { validators: [Validators.required] }), + vehicleType: new FormControl(VehicleType.SPEEDER, { validators: [Validators.required] }), + maximumSpeed: new FormControl(null, { validators: [Validators.required] }), + }, + componentHooks: { + onDestroy: getObservableLifecycle(this).onDestroy, + }, + }); +} diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/vehicle-product.component.html b/src/app/main-rewrite/listing/listing-form/vehicle-listing/vehicle-product.component.html new file mode 100644 index 00000000..89f651b0 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/vehicle-product.component.html @@ -0,0 +1,28 @@ +
+ Vehicle form + + + + + {{ vehicleType.value }} + + + + +
+ + + +
+
diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/vehicle-product.component.scss b/src/app/main-rewrite/listing/listing-form/vehicle-listing/vehicle-product.component.scss new file mode 100644 index 00000000..abde53a3 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/vehicle-product.component.scss @@ -0,0 +1,3 @@ +.container { + display: flex; +} diff --git a/src/app/main-rewrite/listing/listing-form/vehicle-listing/vehicle-product.component.ts b/src/app/main-rewrite/listing/listing-form/vehicle-listing/vehicle-product.component.ts new file mode 100644 index 00000000..144a5de8 --- /dev/null +++ b/src/app/main-rewrite/listing/listing-form/vehicle-listing/vehicle-product.component.ts @@ -0,0 +1,56 @@ +import { Component } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { getObservableLifecycle, ObservableLifecycle } from 'ngx-observable-lifecycle'; +import { subformComponentProviders } from 'ngx-sub-form'; +import { OneVehicle, Spaceship, Speeder, VehicleType } from 'src/app/interfaces/vehicle.interface'; +import { UnreachableCase } from 'src/app/shared/utils'; +import { createForm } from '../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form'; +import { FormType } from '../../../../../../projects/ngx-sub-form/src/lib/new/ngx-sub-form.types'; + +export interface OneVehicleForm { + speeder: Speeder | null; + spaceship: Spaceship | null; + vehicleType: VehicleType | null; +} + +@ObservableLifecycle() +@Component({ + selector: 'app-vehicle-product', + templateUrl: './vehicle-product.component.html', + styleUrls: ['./vehicle-product.component.scss'], + providers: subformComponentProviders(VehicleProductComponent), +}) +export class VehicleProductComponent { + public VehicleType = VehicleType; + + public form = createForm(this, { + formType: FormType.SUB, + formControls: { + speeder: new FormControl(null), + spaceship: new FormControl(null), + vehicleType: new FormControl(null, { validators: [Validators.required] }), + }, + toFormGroup: (obj: OneVehicle): OneVehicleForm => { + return { + speeder: obj.vehicleType === VehicleType.SPEEDER ? obj : null, + spaceship: obj.vehicleType === VehicleType.SPACESHIP ? obj : null, + vehicleType: obj.vehicleType, + }; + }, + fromFormGroup: (formValue: OneVehicleForm): OneVehicle => { + switch (formValue.vehicleType) { + case VehicleType.SPEEDER: + return formValue.speeder as any; // todo + case VehicleType.SPACESHIP: + return formValue.spaceship as any; // todo + case null: + return null as any; //todo + default: + throw new UnreachableCase(formValue.vehicleType); + } + }, + componentHooks: { + onDestroy: getObservableLifecycle(this).onDestroy, + }, + }); +} diff --git a/src/app/main-rewrite/listing/listing.component.html b/src/app/main-rewrite/listing/listing.component.html new file mode 100644 index 00000000..286594d2 --- /dev/null +++ b/src/app/main-rewrite/listing/listing.component.html @@ -0,0 +1,7 @@ +Readonly + + diff --git a/src/app/main-rewrite/listing/listing.component.scss b/src/app/main-rewrite/listing/listing.component.scss new file mode 100644 index 00000000..81fb7772 --- /dev/null +++ b/src/app/main-rewrite/listing/listing.component.scss @@ -0,0 +1,3 @@ +.readonly { + padding: 15px 0; +} diff --git a/src/app/main-rewrite/listing/listing.component.ts b/src/app/main-rewrite/listing/listing.component.ts new file mode 100644 index 00000000..6a782e32 --- /dev/null +++ b/src/app/main-rewrite/listing/listing.component.ts @@ -0,0 +1,50 @@ +import { Component } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { FormControl } from '@angular/forms'; +import { NullableObject } from 'ngx-sub-form'; +import { Observable, of } from 'rxjs'; +import { map, switchMap } from 'rxjs/operators'; +import { OneListing } from 'src/app/interfaces/listing.interface'; +import { ListingService } from 'src/app/services/listing.service'; +import { UuidService } from '../../services/uuid.service'; + +@Component({ + selector: 'app-listing', + templateUrl: './listing.component.html', + styleUrls: ['./listing.component.scss'], +}) +export class ListingComponent { + public readonlyFormControl: FormControl = new FormControl(false); + + constructor( + private route: ActivatedRoute, + private listingService: ListingService, + private uuidService: UuidService, + ) {} + + public listing$: Observable> = this.route.paramMap.pipe( + map(params => params.get('listingId')), + switchMap(listingId => { + if (listingId === 'new' || !listingId) { + return of(null); + } + return this.listingService.getOneListing(listingId); + }), + map(listing => (listing ? listing : this.emptyListing())), + ); + + private emptyListing(): NullableObject { + return { + id: this.uuidService.generate(), + listingType: null, + title: null, + imageUrl: null, + price: null, + product: null, + }; + } + + public upsertListing(listing: OneListing): void { + this.listingService.upsertListing(listing); + } +} diff --git a/src/app/main-rewrite/listings/display-crew-members.pipe.ts b/src/app/main-rewrite/listings/display-crew-members.pipe.ts new file mode 100644 index 00000000..994d9138 --- /dev/null +++ b/src/app/main-rewrite/listings/display-crew-members.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { CrewMember } from 'src/app/interfaces/crew-member.interface'; + +@Pipe({ + name: 'displayCrewMembers', +}) +export class DisplayCrewMembersPipe implements PipeTransform { + transform(crewMembers: CrewMember[]): string { + return crewMembers.map(crewMember => `${crewMember.firstName} ${crewMember.lastName}`).join(', '); + } +} diff --git a/src/app/main-rewrite/listings/listings.component.html b/src/app/main-rewrite/listings/listings.component.html new file mode 100644 index 00000000..c8760aa5 --- /dev/null +++ b/src/app/main-rewrite/listings/listings.component.html @@ -0,0 +1,48 @@ + + +

+ {{ listing.title }} + ( + {{ listing.listingType }} + ) £ + {{ listing.price | number }} +

+

+ + {{ listing.product.droidType }} + - + + + Weapons: {{ listing.product.weapons.join(', ') }} + + Number of tools: {{ listing.product.toolCount }} + + + {{ listing.product.canHealHumans ? 'Can' : "Can't" }} heal humans, + {{ listing.product.canFixRobots ? 'can' : "can't" }} fix robots + + + + Spoken languages: {{ listing.product.spokenLanguages.join(', ') }} + + + + + + {{ listing.product.vehicleType }} + - + + + Crew members: {{ listing.product.crewMembers | displayCrewMembers }}, + {{ listing.product.canFire ? 'can' : "can't" }} fire, + + maximum speed: {{ listing.product.maximumSpeed }}kph + + number of wings: {{ listing.product.wingCount }} + + +

+
+
+ +Create new diff --git a/src/app/main-rewrite/listings/listings.component.scss b/src/app/main-rewrite/listings/listings.component.scss new file mode 100644 index 00000000..6d64ef00 --- /dev/null +++ b/src/app/main-rewrite/listings/listings.component.scss @@ -0,0 +1,3 @@ +mat-nav-list { + padding: 0; +} diff --git a/src/app/main-rewrite/listings/listings.component.ts b/src/app/main-rewrite/listings/listings.component.ts new file mode 100644 index 00000000..49eb971d --- /dev/null +++ b/src/app/main-rewrite/listings/listings.component.ts @@ -0,0 +1,19 @@ +import { Component, Input } from '@angular/core'; +import { OneListing, ListingType } from '../../interfaces/listing.interface'; +import { DroidType } from 'src/app/interfaces/droid.interface'; +import { VehicleType } from 'src/app/interfaces/vehicle.interface'; + +@Component({ + selector: 'app-listings', + templateUrl: './listings.component.html', + styleUrls: ['./listings.component.scss'], +}) +export class ListingsComponent { + @Input() listings: OneListing[] = []; + + public ListingType = ListingType; + + public DroidType = DroidType; + + public VehicleType = VehicleType; +} diff --git a/src/app/main-rewrite/main.component.html b/src/app/main-rewrite/main.component.html new file mode 100644 index 00000000..5463cedc --- /dev/null +++ b/src/app/main-rewrite/main.component.html @@ -0,0 +1,11 @@ + + + + +
+ + +
+ +
+
diff --git a/src/app/main-rewrite/main.component.scss b/src/app/main-rewrite/main.component.scss new file mode 100644 index 00000000..ad048ce0 --- /dev/null +++ b/src/app/main-rewrite/main.component.scss @@ -0,0 +1,14 @@ +.container { + height: calc(100% - 64px); + display: flex; + + .left-part, + .right-part { + flex-grow: 1; + height: 100%; + } +} + +.logo { + max-width: 200px; +} diff --git a/src/app/main-rewrite/main.component.ts b/src/app/main-rewrite/main.component.ts new file mode 100644 index 00000000..1c42a292 --- /dev/null +++ b/src/app/main-rewrite/main.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { ListingService } from '../services/listing.service'; + +@Component({ + selector: 'app-main', + templateUrl: './main.component.html', + styleUrls: ['./main.component.scss'], +}) +export class MainComponent { + public listings$ = this.listingService.getListings(); + + constructor(private listingService: ListingService) {} +} diff --git a/src/app/main-rewrite/main.module.ts b/src/app/main-rewrite/main.module.ts new file mode 100644 index 00000000..8861b3f7 --- /dev/null +++ b/src/app/main-rewrite/main.module.ts @@ -0,0 +1,60 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { SharedModule } from '../shared/shared.module'; +import { AssassinDroidComponent } from './listing/listing-form/droid-listing/assassin-droid/assassin-droid.component'; +import { AstromechDroidComponent } from './listing/listing-form/droid-listing/astromech-droid/astromech-droid.component'; +import { DroidProductComponent } from './listing/listing-form/droid-listing/droid-product.component'; +import { MedicalDroidComponent } from './listing/listing-form/droid-listing/medical-droid/medical-droid.component'; +import { ProtocolDroidComponent } from './listing/listing-form/droid-listing/protocol-droid/protocol-droid.component'; +import { ListingFormComponent } from './listing/listing-form/listing-form.component'; +import { CrewMemberComponent } from './listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component'; +import { CrewMembersComponent } from './listing/listing-form/vehicle-listing/crew-members/crew-members.component'; +import { SpaceshipComponent } from './listing/listing-form/vehicle-listing/spaceship/spaceship.component'; +import { SpeederComponent } from './listing/listing-form/vehicle-listing/speeder/speeder.component'; +import { VehicleProductComponent } from './listing/listing-form/vehicle-listing/vehicle-product.component'; +import { ListingComponent } from './listing/listing.component'; +import { DisplayCrewMembersPipe } from './listings/display-crew-members.pipe'; +import { ListingsComponent } from './listings/listings.component'; +import { MainComponent } from './main.component'; + +@NgModule({ + declarations: [ + MainComponent, + ListingsComponent, + ListingComponent, + VehicleProductComponent, + DroidProductComponent, + SpaceshipComponent, + SpeederComponent, + ProtocolDroidComponent, + MedicalDroidComponent, + AstromechDroidComponent, + AssassinDroidComponent, + ListingFormComponent, + CrewMembersComponent, + CrewMemberComponent, + DisplayCrewMembersPipe, + ], + imports: [ + CommonModule, + SharedModule, + RouterModule.forChild([ + { + path: '', + component: MainComponent, + children: [ + { + path: 'listings', + children: [ + { path: ':listingId', component: ListingComponent }, + { path: 'new', component: ListingComponent, pathMatch: 'full' }, + ], + }, + ], + }, + { path: '**', pathMatch: 'full', redirectTo: '/' }, + ]), + ], +}) +export class MainModule {} diff --git a/src/app/main/listing/listing-form/listing-form.component.ts b/src/app/main/listing/listing-form/listing-form.component.ts index 75fecc91..c6049180 100644 --- a/src/app/main/listing/listing-form/listing-form.component.ts +++ b/src/app/main/listing/listing-form/listing-form.component.ts @@ -2,13 +2,11 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { FormControl, Validators } from '@angular/forms'; import { Controls, - takeUntilDestroyed, // NgxAutomaticRootFormComponent, // NGX_SUB_FORM_HANDLE_VALUE_CHANGES_RATE_STRATEGIES, DataInput, NgxRootFormComponent, } from 'ngx-sub-form'; -import { tap } from 'rxjs/operators'; import { ListingType, OneListing } from 'src/app/interfaces/listing.interface'; import { OneDroid } from '../../../interfaces/droid.interface'; import { OneVehicle } from '../../../interfaces/vehicle.interface'; diff --git a/src/app/main/listing/listing-form/test-types.ts b/src/app/main/listing/listing-form/test-types.ts new file mode 100644 index 00000000..0c9d7601 --- /dev/null +++ b/src/app/main/listing/listing-form/test-types.ts @@ -0,0 +1,32 @@ +// export type Controls = { [K in keyof T]-?: AbstractControl }; + +type Hello = { a: number | null; b: number }; + +type AAAA = number | null; +type BBBB = Extract; +type CCCC = BBBB extends never ? never : BBBB; + +type H = { [key in keyof T]: Extract extends never ? key : never }; +type DDDD = H; +interface Person { + id: string; + name?: string; + age: number | null; +} + +type AAA = Exclude>; +type NoUndefinedField = { [P in keyof T]-?: NoUndefinedField> }; + +type D = NoUndefinedField; + +type zzz = H; +type AAAAAA = Pick>>; +type KKKKKK = {} extends AAAAAA ? true : false; + +type B = Person['age'] extends null ? never : Person['age']; +type RequiredKeys = { + [K in keyof T]-?: {} extends H> ? never : K; +}[keyof T]; +type A = RequiredKeys; + +new FormGroup({}); diff --git a/src/app/main/listings/listings.component.html b/src/app/main/listings/listings.component.html index c8760aa5..e6482340 100644 --- a/src/app/main/listings/listings.component.html +++ b/src/app/main/listings/listings.component.html @@ -1,5 +1,5 @@ - +

{{ listing.title }} ( diff --git a/src/app/main/main.module.ts b/src/app/main/main.module.ts new file mode 100644 index 00000000..8861b3f7 --- /dev/null +++ b/src/app/main/main.module.ts @@ -0,0 +1,60 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { SharedModule } from '../shared/shared.module'; +import { AssassinDroidComponent } from './listing/listing-form/droid-listing/assassin-droid/assassin-droid.component'; +import { AstromechDroidComponent } from './listing/listing-form/droid-listing/astromech-droid/astromech-droid.component'; +import { DroidProductComponent } from './listing/listing-form/droid-listing/droid-product.component'; +import { MedicalDroidComponent } from './listing/listing-form/droid-listing/medical-droid/medical-droid.component'; +import { ProtocolDroidComponent } from './listing/listing-form/droid-listing/protocol-droid/protocol-droid.component'; +import { ListingFormComponent } from './listing/listing-form/listing-form.component'; +import { CrewMemberComponent } from './listing/listing-form/vehicle-listing/crew-members/crew-member/crew-member.component'; +import { CrewMembersComponent } from './listing/listing-form/vehicle-listing/crew-members/crew-members.component'; +import { SpaceshipComponent } from './listing/listing-form/vehicle-listing/spaceship/spaceship.component'; +import { SpeederComponent } from './listing/listing-form/vehicle-listing/speeder/speeder.component'; +import { VehicleProductComponent } from './listing/listing-form/vehicle-listing/vehicle-product.component'; +import { ListingComponent } from './listing/listing.component'; +import { DisplayCrewMembersPipe } from './listings/display-crew-members.pipe'; +import { ListingsComponent } from './listings/listings.component'; +import { MainComponent } from './main.component'; + +@NgModule({ + declarations: [ + MainComponent, + ListingsComponent, + ListingComponent, + VehicleProductComponent, + DroidProductComponent, + SpaceshipComponent, + SpeederComponent, + ProtocolDroidComponent, + MedicalDroidComponent, + AstromechDroidComponent, + AssassinDroidComponent, + ListingFormComponent, + CrewMembersComponent, + CrewMemberComponent, + DisplayCrewMembersPipe, + ], + imports: [ + CommonModule, + SharedModule, + RouterModule.forChild([ + { + path: '', + component: MainComponent, + children: [ + { + path: 'listings', + children: [ + { path: ':listingId', component: ListingComponent }, + { path: 'new', component: ListingComponent, pathMatch: 'full' }, + ], + }, + ], + }, + { path: '**', pathMatch: 'full', redirectTo: '/' }, + ]), + ], +}) +export class MainModule {} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts new file mode 100644 index 00000000..eee8e0c1 --- /dev/null +++ b/src/app/shared/shared.module.ts @@ -0,0 +1,34 @@ +import { LayoutModule } from '@angular/cdk/layout'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatListModule } from '@angular/material/list'; +import { MatSelectModule } from '@angular/material/select'; +import { MatSidenavModule } from '@angular/material/sidenav'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; +import { MatToolbarModule } from '@angular/material/toolbar'; + +const MATERIAL_MODULES = [ + LayoutModule, + MatToolbarModule, + MatButtonModule, + MatSidenavModule, + MatIconModule, + MatListModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule, + MatSlideToggleModule, + MatCardModule, +]; + +@NgModule({ + imports: [CommonModule, ReactiveFormsModule, ...MATERIAL_MODULES], + exports: [CommonModule, ReactiveFormsModule, ...MATERIAL_MODULES], +}) +export class SharedModule {} diff --git a/yarn.lock b/yarn.lock index 56ce21b8..4c8910ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1083,6 +1083,32 @@ date-fns "^1.27.2" figures "^1.7.0" +"@cypress/request@2.88.5": + version "2.88.5" + resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.5.tgz#8d7ecd17b53a849cfd5ab06d5abe7d84976375d7" + integrity sha512-TzEC1XMi1hJkywWpRfD2clreTa/Z+lOrXDCxxBTBPEcY5azdPi56A6Xw+O4tWJnaJH3iIE7G5aDXZC6JgRZLcA== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + "@cypress/webpack-preprocessor@4.1.0": version "4.1.0" resolved "https://registry.yarnpkg.com/@cypress/webpack-preprocessor/-/webpack-preprocessor-4.1.0.tgz#8c4debc0b1abf045b62524d1996dd9aeaf7e86a8" @@ -1394,6 +1420,34 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.0.0.tgz#9c13c2574c92d4503b005feca8f2e16cc1611506" integrity sha512-KYyTT/T6ALPkIRd2Ge080X/BsXvy9O0hcWTtMWkPvwAwF99+vn6Dv4GzrFT/Nn1LePr+FFDbRXXlqmsy9lw2zA== +"@types/blob-util@1.3.3": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@types/blob-util/-/blob-util-1.3.3.tgz#adba644ae34f88e1dd9a5864c66ad651caaf628a" + integrity sha512-4ahcL/QDnpjWA2Qs16ZMQif7HjGP2cw3AGjHabybjw7Vm1EKu+cfQN1D78BaZbS1WJNa1opSMF5HNMztx7lR0w== + +"@types/bluebird@3.5.29": + version "3.5.29" + resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.29.tgz#7cd933c902c4fc83046517a1bef973886d00bdb6" + integrity sha512-kmVtnxTuUuhCET669irqQmPAez4KFnFVKvpleVRyfC3g+SHD1hIkFZcWLim9BVcwUBLO59o8VZE4yGCmTif8Yw== + +"@types/chai-jquery@1.1.40": + version "1.1.40" + resolved "https://registry.yarnpkg.com/@types/chai-jquery/-/chai-jquery-1.1.40.tgz#445bedcbbb2ae4e3027f46fa2c1733c43481ffa1" + integrity sha512-mCNEZ3GKP7T7kftKeIs7QmfZZQM7hslGSpYzKbOlR2a2HCFf9ph4nlMRA9UnuOETeOQYJVhJQK7MwGqNZVyUtQ== + dependencies: + "@types/chai" "*" + "@types/jquery" "*" + +"@types/chai@*": + version "4.2.11" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.11.tgz#d3614d6c5f500142358e6ed24e1bf16657536c50" + integrity sha512-t7uW6eFafjO+qJ3BIV2gGUyZs27egcNRkUdalkud+Qa3+kg//f129iuOFivHDXQ+vnU3fDXuwgv0cqMCbcE8sw== + +"@types/chai@4.2.7": + version "4.2.7" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.7.tgz#1c8c25cbf6e59ffa7d6b9652c78e547d9a41692d" + integrity sha512-luq8meHGYwvky0O7u0eQZdA7B4Wd9owUCqvbw2m3XCrCU8mplYOujMBbvyS547AxJkC+pGnd0Cm15eNxEUNU8g== + "@types/color-name@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" @@ -1435,16 +1489,40 @@ dependencies: "@types/jasmine" "*" +"@types/jquery@*": + version "3.3.38" + resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.38.tgz#6385f1e1b30bd2bff55ae8ee75ea42a999cc3608" + integrity sha512-nkDvmx7x/6kDM5guu/YpXkGZ/Xj/IwGiLDdKM99YA5Vag7pjGyTJ8BNUh/6hxEn/sEu5DKtyRgnONJ7EmOoKrA== + dependencies: + "@types/sizzle" "*" + +"@types/jquery@3.3.31": + version "3.3.31" + resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.31.tgz#27c706e4bf488474e1cb54a71d8303f37c93451b" + integrity sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg== + dependencies: + "@types/sizzle" "*" + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= -"@types/minimatch@*": +"@types/lodash@4.14.149": + version "4.14.149" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440" + integrity sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ== + +"@types/minimatch@*", "@types/minimatch@3.0.3": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== +"@types/mocha@5.2.7": + version "5.2.7" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea" + integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ== + "@types/node@*", "@types/node@13.7.2", "@types/node@>= 8": version "13.7.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-13.7.2.tgz#50375b95b5845a34efda2ffb3a087c7becbc46c6" @@ -1477,7 +1555,32 @@ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== -"@types/sizzle@2.3.2": +"@types/sinon-chai@3.2.3": + version "3.2.3" + resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.3.tgz#afe392303dda95cc8069685d1e537ff434fa506e" + integrity sha512-TOUFS6vqS0PVL1I8NGVSNcFaNJtFoyZPXZ5zur+qlhDfOmQECZZM4H4kKgca6O8L+QceX/ymODZASfUfn+y4yQ== + dependencies: + "@types/chai" "*" + "@types/sinon" "*" + +"@types/sinon@*": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.1.tgz#463da26696a3d142a336a5dcbefc99006a6d6f38" + integrity sha512-vqWk3K1HYJExooYgORUdiGX1EdCWQxPi7P/OEIetdaJn4jNvEYoRRGLG/HwomtbzZ4IP9Syz2k4N50CItv6w6g== + dependencies: + "@types/sinonjs__fake-timers" "*" + +"@types/sinon@7.5.1": + version "7.5.1" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.5.1.tgz#d27b81af0d1cfe1f9b24eebe7a24f74ae40f5b7c" + integrity sha512-EZQUP3hSZQyTQRfiLqelC9NMWd1kqLcmQE0dMiklxBkgi84T+cHOhnKpgk4NnOWpGX863yE6+IaGnOXUNFqDnQ== + +"@types/sinonjs__fake-timers@*": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e" + integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA== + +"@types/sizzle@*", "@types/sizzle@2.3.2": version "2.3.2" resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== @@ -2912,7 +3015,7 @@ cli-spinners@^2.2.0: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.2.0.tgz#e8b988d9206c692302d8ee834e7a85c0144d8f77" integrity sha512-tgU3fKwzYjiLEQgPMD9Jt+JjHVL9kW93FiIMX/l7rivvOD4/LL0Mf7gda3+4U2KJBloybwgj5KEoQgGRioMiKQ== -cli-table3@^0.5.0, cli-table3@^0.5.1: +cli-table3@0.5.1, cli-table3@^0.5.0, cli-table3@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202" integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw== @@ -3219,7 +3322,7 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@1.6.2, concat-stream@^1.5.0: +concat-stream@^1.5.0, concat-stream@^1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== @@ -3750,26 +3853,38 @@ cyclist@^1.0.1: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= -cypress@4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.0.2.tgz#ede194d7bc73fb449f8de553c9e1db4ca15309ef" - integrity sha512-WRzxOoSd+TxyXKa7Zi9orz3ii5VW7yhhVYstCU+EpOKfPan9x5Ww2Clucmy4H/W0GHUYAo7GYFZRD33ZCSNBQA== +cypress@4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.5.0.tgz#01940d085f6429cec3c87d290daa47bb976a7c7b" + integrity sha512-2A4g5FW5d2fHzq8HKUGAMVTnW6P8nlWYQALiCoGN4bqBLvgwhYM/oG9oKc2CS6LnvgHFiKivKzpm9sfk3uU3zQ== dependencies: "@cypress/listr-verbose-renderer" "0.4.1" + "@cypress/request" "2.88.5" "@cypress/xvfb" "1.2.4" + "@types/blob-util" "1.3.3" + "@types/bluebird" "3.5.29" + "@types/chai" "4.2.7" + "@types/chai-jquery" "1.1.40" + "@types/jquery" "3.3.31" + "@types/lodash" "4.14.149" + "@types/minimatch" "3.0.3" + "@types/mocha" "5.2.7" + "@types/sinon" "7.5.1" + "@types/sinon-chai" "3.2.3" "@types/sizzle" "2.3.2" arch "2.1.1" bluebird "3.7.2" cachedir "2.3.0" - chalk "3.0.0" + chalk "2.4.2" check-more-types "2.24.0" + cli-table3 "0.5.1" commander "4.1.0" common-tags "1.8.0" debug "4.1.1" eventemitter2 "4.1.2" - execa "3.3.0" + execa "1.0.0" executable "4.1.1" - extract-zip "1.6.7" + extract-zip "1.7.0" fs-extra "8.1.0" getos "3.1.4" is-ci "2.0.0" @@ -3778,10 +3893,11 @@ cypress@4.0.2: listr "0.14.3" lodash "4.17.15" log-symbols "3.0.0" - minimist "1.2.0" + minimist "1.2.5" moment "2.24.0" + ospath "1.2.2" + pretty-bytes "5.3.0" ramda "0.26.1" - request "2.88.0" request-progress "3.0.0" supports-color "7.1.0" tmp "0.1.0" @@ -3845,7 +3961,7 @@ dateformat@^3.0.0: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== -debug@2.6.9, debug@^2.2.0, debug@^2.3.3: +debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -4506,21 +4622,18 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: md5.js "^1.3.4" safe-buffer "^5.1.1" -execa@3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-3.3.0.tgz#7e348eef129a1937f21ecbbd53390942653522c1" - integrity sha512-j5Vit5WZR/cbHlqU97+qcnw9WHRCIL4V1SVe75VcHcD1JRBdt8fv0zw89b7CQHQdUHTt2VjuhcF5ibAgVOxqpg== +execa@1.0.0, execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== dependencies: - cross-spawn "^7.0.0" - get-stream "^5.0.0" - human-signals "^1.1.1" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.0" - onetime "^5.1.0" - p-finally "^2.0.0" - signal-exit "^3.0.2" - strip-final-newline "^2.0.0" + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" execa@^0.7.0: version "0.7.0" @@ -4535,19 +4648,6 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== - dependencies: - cross-spawn "^6.0.0" - get-stream "^4.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - execa@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.0.tgz#7f37d6ec17f09e6b8fc53288611695b6d12b9daf" @@ -4674,15 +4774,15 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" -extract-zip@1.6.7: - version "1.6.7" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9" - integrity sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k= +extract-zip@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" + integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== dependencies: - concat-stream "1.6.2" - debug "2.6.9" - mkdirp "0.5.1" - yauzl "2.4.1" + concat-stream "^1.6.2" + debug "^2.6.9" + mkdirp "^0.5.4" + yauzl "^2.10.0" extsprintf@1.3.0: version "1.3.0" @@ -4751,13 +4851,6 @@ faye-websocket@~0.11.1: dependencies: websocket-driver ">=0.5.1" -fd-slicer@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" - integrity sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU= - dependencies: - pend "~1.2.0" - fd-slicer@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" @@ -5378,7 +5471,7 @@ har-schema@^2.0.0: resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= -har-validator@~5.1.0, har-validator@~5.1.3: +har-validator@~5.1.3: version "5.1.3" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== @@ -7617,6 +7710,11 @@ minimist@1.2.0, minimist@^1.2.0: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= +minimist@1.2.5, minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + minimist@~0.0.1: version "0.0.10" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" @@ -7689,13 +7787,20 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1, mkdirp@~0.5.x: +mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1, mkdirp@~0.5.x: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= dependencies: minimist "0.0.8" +mkdirp@^0.5.4: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + modify-values@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" @@ -7825,6 +7930,13 @@ ng-packagr@9.0.1: terser "^4.3.8" update-notifier "^4.0.0" +ngx-observable-lifecycle@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ngx-observable-lifecycle/-/ngx-observable-lifecycle-1.0.1.tgz#13a19debadd5e9dba38cfd7fbf17ae1f5f2f121a" + integrity sha512-TT/yNKKTn4JMibej+5Xjv1eo1WMqvDoHq5+ZoVclrMLnJWPBq36MUtviu8FkEshWpQDR8H2hvHOKC6bRPtY6dg== + dependencies: + tslib "^1.10.0" + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -8479,6 +8591,11 @@ osenv@^0.1.4, osenv@^0.1.5: os-homedir "^1.0.0" os-tmpdir "^1.0.0" +ospath@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" + integrity sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs= + p-cancelable@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" @@ -8506,11 +8623,6 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= -p-finally@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-2.0.1.tgz#bd6fcaa9c559a096b680806f4d657b3f0f240561" - integrity sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw== - p-is-promise@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" @@ -9291,6 +9403,11 @@ prettier@1.19.1: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== +pretty-bytes@5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.3.0.tgz#f2849e27db79fb4d6cfe24764fc4134f165989f2" + integrity sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg== + private@^0.1.6: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" @@ -9363,7 +9480,7 @@ pseudomap@^1.0.2: resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= -psl@^1.1.24, psl@^1.1.28: +psl@^1.1.28: version "1.7.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c" integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ== @@ -9410,7 +9527,7 @@ punycode@1.3.2: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= -punycode@^1.2.4, punycode@^1.4.1: +punycode@^1.2.4: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= @@ -9890,32 +10007,6 @@ request-progress@3.0.0: dependencies: throttleit "^1.0.0" -request@2.88.0: - version "2.88.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" - integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.0" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.4.3" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - request@^2.83.0, request@^2.88.0: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" @@ -11401,14 +11492,6 @@ toidentifier@1.0.0: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== -tough-cookie@~2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" - integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== - dependencies: - psl "^1.1.24" - punycode "^1.4.1" - tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" @@ -11478,6 +11561,11 @@ tsconfig-paths@^3.4.0: minimist "^1.2.0" strip-bom "^3.0.0" +tsdef@0.0.13: + version "0.0.13" + resolved "https://registry.yarnpkg.com/tsdef/-/tsdef-0.0.13.tgz#c71c2bd756c84887386ac8539ace63a38bc114e1" + integrity sha512-Twcdol23BQ+J+WD3NYhqusB7vvCDdK2bvcXnivgHu4xjrxnngUshgB+SWs2FN+I6BxY6BRkaE2KllO403GwbKA== + tslib@1.10.0, tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0: version "1.10.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" @@ -12389,7 +12477,7 @@ yargs@^8.0.2: y18n "^3.2.1" yargs-parser "^7.0.0" -yauzl@2.10.0: +yauzl@2.10.0, yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= @@ -12397,13 +12485,6 @@ yauzl@2.10.0: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" -yauzl@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" - integrity sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU= - dependencies: - fd-slicer "~1.0.1" - yeast@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"