import { computeDifference } from '@meetalva/util' ; import { isEqual , flatMap } from 'lodash' ; import * as Mobx from 'mobx' ; import { Pattern , PatternSlot } from '../pattern' ; import { ElementContent } from '../element' ; import { AnyPatternProperty , PatternEnumProperty , PatternProperty , PatternPropertyBase , EnumValue } from '../pattern-property' ; import { Project } from '../project' ; import * as Types from '@meetalva/types' ; import * as uuid from 'uuid' ; const md5 = require ( 'md5' ) ; export interface PatternLibraryInit { builtin : boolean ; bundleId : string ; bundle : string ; id : string ; installType : Types . PatternLibraryInstallType ; origin : Types . PatternLibraryOrigin ; packageFile : { name : string ; version : string ; [ key ] : unknown ; } ; patternProperties : AnyPatternProperty [ ] ; patterns : Pattern [ ] ; state : Types . PatternLibraryState ; } export interface BuiltInContext { options : PatternLibraryCreateOptions ; patternLibrary : PatternLibrary ; } export interface BuiltInResult { pattern : Pattern ; properties : AnyPatternProperty [ ] ; } export interface PatternLibraryCreateOptions { getGlobalEnumOptionId ( enumId , contextId ) : string ; getGlobalPatternId ( contextId ) : string ; getGlobalPropertyId ( patternId , contextId ) : string ; getGlobalSlotId ( patternId , contextId ) : string ; } export class PatternLibrary { public readonly model = Types . ModelName . PatternLibrary ; public readonly builtin : boolean ; private readonly id : string ; @ Mobx . observable private bundleId : string ; @ Mobx . observable private bundle : string ; @ Mobx . observable private installType : Types . PatternLibraryInstallType ; @ Mobx . observable private origin : Types . PatternLibraryOrigin ; @ Mobx . observable private packageFile : { name : string ; version : string ; [ key ] : unknown ; } ; @ Mobx . observable private patternProperties : Map < string , AnyPatternProperty > = new Map ( ) ; @ Mobx . observable private patterns : Map < string , Pattern > = new Map ( ) ; @ Mobx . observable private state : Types . PatternLibraryState ; @ Mobx . computed private get version ( ) : string { return this . packageFile . version ; } @ Mobx . computed private get name ( ) : string { return this . packageFile . name ; } @ Mobx . computed private get alvaMeta ( ) : { [ key ] : unknown ; } { return typeof this . packageFile . alva === 'object' && this . packageFile . alva !== null ? this . packageFile . alva : { } ; } @ Mobx . computed private get slots ( ) : PatternSlot [ ] { return this . getPatterns ( ) . reduce < PatternSlot [ ] > ( ( acc , pattern ) => [ ... acc , ... pattern . getSlots ( ) ] , [ ] ) ; } @ Mobx . computed public get contextId ( ) : string { return [ this . name , this . version ] . join ( '@' ) ; } public constructor ( init ) { this . builtin = init . builtin ; this . bundleId = init . bundleId ; this . bundle = init . bundle ; this . id = init . id || uuid . v4 ( ) ; this . installType = init . installType ; this . origin = init . origin ; this . packageFile = init . packageFile ; init . patterns . forEach ( pattern => this . patterns . set ( pattern . getId ( ) , pattern ) ) ; init . patternProperties . forEach ( prop => this . patternProperties . set ( prop . getId ( ) , prop ) ) ; this . state = init . state ; } public static Defaults ( ) { return { builtin : false , bundleId : uuid . v4 ( ) , bundle : '' , id : uuid . v4 ( ) , installType : Types . PatternLibraryInstallType . Local , origin : Types . PatternLibraryOrigin . UserProvided , packageFile : { name : 'component-library' , version : '1.0.0' } , patterns : [ ] , patternProperties : [ ] , state : Types . PatternLibraryState . Connected } ; } public static create ( init ) { return new PatternLibrary ( init ) ; } public static fromAnalysis ( analysis , opts ) { const library = PatternLibrary . create ( { builtin : opts . analyzeBuiltins , bundle : analysis . bundle , bundleId : analysis . id , id : uuid . v4 ( ) , installType : opts . installType , origin : opts . analyzeBuiltins ? Types . PatternLibraryOrigin . BuiltIn : Types . PatternLibraryOrigin . UserProvided , packageFile : analysis . packageFile , patternProperties : [ ] , patterns : [ ] , state : Types . PatternLibraryState . Connected } ) ; library . import ( analysis , { project : opts . project } ) ; return library ; } public static from ( serialized ) { const state = deserializeState ( serialized . state ) ; const builtin = typeof serialized . builtin === 'undefined' ? serialized . patterns . some ( p => p . type === Types . PatternType . SyntheticPage ) : serialized . builtin ; const patternLibrary = new PatternLibrary ( { builtin , bundleId : serialized . bundleId , bundle : serialized . bundle , id : serialized . id , installType : serialized . installType , origin : deserializeOrigin ( serialized . origin ) , packageFile : serialized . packageFile , patterns : [ ] , patternProperties : serialized . patternProperties . map ( p => PatternProperty . from ( p ) ) , state } ) ; serialized . patterns . forEach ( pattern => { patternLibrary . addPattern ( Pattern . from ( pattern , { patternLibrary } ) ) ; } ) ; return patternLibrary ; } public static fromDefaults ( ) { return new PatternLibrary ( PatternLibrary . Defaults ( ) ) ; } @ Mobx . action public import ( analysis , { project } ) { this . packageFile = analysis . packageFile ; const patternsBefore = this . getPatterns ( ) ; const patternsAfter = analysis . patterns . map ( item => Pattern . from ( item . pattern , { patternLibrary : this } ) ) ; const patternChanges = computeDifference ( { before : patternsBefore , after : patternsAfter } ) ; patternChanges . removed . map ( change => { this . removePattern ( change . before ) ; } ) ; patternChanges . added . map ( change => { this . addPattern ( change . after ) ; } ) ; patternChanges . changed . map ( change => { change . before . update ( change . after , { patternLibrary : this } ) ; } ) ; const propMap = new Map ( ) ; const props = analysis . patterns . reduce ( ( p , patternAnalysis ) => { const pattern = Pattern . from ( patternAnalysis . pattern , { patternLibrary : this } ) ; patternAnalysis . properties . forEach ( prop => { const patternProperty = PatternProperty . from ( prop ) ; p . push ( patternProperty ) ; propMap . set ( patternProperty . getId ( ) , pattern ) ; } ) ; return p ; } , [ ] ) ; const propChanges = computeDifference ( { before : this . getPatternProperties ( ) , after : props } ) ; propChanges . removed . map ( change => { this . removeProperty ( change . before ) ; } ) ; propChanges . added . map ( change => { const pattern = propMap . get ( change . after . getId ( ) ) ; const p = pattern ? this . getPatternById ( pattern . getId ( ) ) : undefined ; this . addProperty ( change . after ) ; if ( p ) { p . addProperty ( change . after ) ; } } ) ; propChanges . changed . map ( change => change . before . update ( change . after ) ) ; if ( project ) { const touchedPatterns = [ ... patternChanges . added , ... patternChanges . changed ] . map ( change => change . after ) ; touchedPatterns . forEach ( pattern => { project . getElementsByPattern ( pattern ) . forEach ( element => { const contents = element . getContents ( ) ; pattern . getSlots ( ) . filter ( slot => ! contents . some ( content => content . getSlotId ( ) === slot . getId ( ) ) ) . forEach ( slot => { const content = ElementContent . fromSlot ( slot , { project } ) ; content . setParentElement ( element ) ; element . addContent ( content ) ; project . addElementContent ( content ) ; } ) ; } ) ; } ) ; } this . setState ( Types . PatternLibraryState . Connected ) ; this . setBundle ( analysis . bundle ) ; this . setBundleId ( analysis . id ) ; } public equals ( b ) { return isEqual ( this . toJSON ( ) , b . toJSON ( ) ) ; } @ Mobx . action public addPattern ( pattern ) { this . patterns . set ( pattern . getId ( ) , pattern ) ; } @ Mobx . action public addProperty ( property ) { this . patternProperties . set ( property . getId ( ) , property ) ; } public getPatternByProperty ( property ) { return this . getPatterns ( ) . find ( p => p . getProperties ( ) . some ( p => p . getId ( ) === property . getId ( ) ) ) ; } public getPatternBySlot ( slot ) { return this . getPatterns ( ) . find ( p => p . getSlots ( ) . some ( s => s . getId ( ) === slot . getId ( ) ) ) ; } public getDescription ( ) { return typeof this . packageFile . description === 'string' ? this . packageFile . description : undefined ; } public getBundle ( ) { return this . bundle ; } public getBundleId ( ) { return this . bundleId ; } public getBundleHash ( ) { return md5 ( this . bundle ) ; } public getColor ( ) { return typeof this . alvaMeta . color === 'string' ? this . alvaMeta . color : undefined ; } public getCapabilites ( ) { const isUserProvided = this . origin === Types . PatternLibraryOrigin . UserProvided ; if ( ! isUserProvided ) { return [ ] ; } const isConnected = this . state === Types . PatternLibraryState . Connected ; return [ isConnected && Types . LibraryCapability . Disconnect , isConnected && Types . LibraryCapability . Update , isConnected && Types . LibraryCapability . SetPath , Types . LibraryCapability . Reconnect ] . filter ( ( capability ) : capability is Types . LibraryCapability => typeof capability !== 'undefined' ) ; } public getId ( ) { return this . id ; } public getName ( ) { return this . name ; } public getDisplayName ( ) { return typeof this . alvaMeta . name === 'string' ? this . alvaMeta . name : this . name ; } public getPackageName ( ) { return this . name ; } public getVersion ( ) { return this . version ; } public getHomepage ( ) { return typeof this . packageFile . homepage === 'string' ? this . packageFile . homepage : undefined ; } public getOrigin ( ) { return this . origin ; } public getPatternByContextId ( contextId ) { return this . getPatterns ( ) . find ( pattern => pattern . getContextId ( ) === contextId ) ; } public getPatternById ( id ) { return this . patterns . get ( id ) ; } public getPatternByType ( type ) { return this . getPatterns ( ) . find ( pattern => pattern . getType ( ) === type ) ! ; } public getPatternProperties ( ) { return [ ... this . patternProperties . values ( ) ] ; } public getPatternPropertyById ( id ) { return this . patternProperties . get ( id ) ; } public getPatternSlotById ( id ) { return this . getSlots ( ) . find ( slot => slot . getId ( ) === id ) ; } public getPatterns ( ids ? ) { if ( typeof ids === 'undefined' ) { return [ ... this . patterns . values ( ) ] ; } return ids . map ( id => this . getPatternById ( id ) ) . filter ( ( pattern ) : pattern is Pattern => typeof pattern !== 'undefined' ) ; } public getSlots ( ) { return this . slots ; } public getState ( ) { return this . state ; } public getImage ( ) { return typeof this . alvaMeta . image === 'string' ? this . alvaMeta . image : undefined ; } public getInstallType ( ) { if ( this . origin === Types . PatternLibraryOrigin . BuiltIn ) { return Types . PatternLibraryInstallType . Remote ; } return this . installType ; } @ Mobx . action public removePattern ( pattern ) { this . patterns . delete ( pattern . getId ( ) ) ; } @ Mobx . action public removeProperty ( property ) { this . patternProperties . delete ( property . getId ( ) ) ; } @ Mobx . action public setBundle ( bundle ) { this . bundle = bundle ; } @ Mobx . action public setBundleId ( bundleId ) { this . bundleId = bundleId ; } @ Mobx . action public setState ( state ) { this . state = state ; } @ Mobx . action public setInstallType ( installType ) { this . installType = installType ; } public toJSON ( ) { return { bundleId : this . bundleId , bundle : this . bundle , id : this . id , installType : this . installType , model : this . model , origin : serializeOrigin ( this . origin ) , packageFile : this . packageFile , patternProperties : this . getPatternProperties ( ) . map ( p => p . toJSON ( ) ) , patterns : this . getPatterns ( ) . map ( p => p . toJSON ( ) ) , state : this . state } ; } @ Mobx . action public update ( b ) { this . bundleId = b . bundleId ; this . bundle = b . bundle ; this . origin = b . origin ; this . installType = b . installType ; this . packageFile = b . packageFile ; const patternChanges = computeDifference < Pattern > ( { before : this . getPatterns ( ) , after : b . getPatterns ( ) } ) ; patternChanges . added . forEach ( change => this . addPattern ( change . after ) ) ; patternChanges . changed . forEach ( change => change . before . update ( change . after , { patternLibrary : this } ) ) ; patternChanges . removed . forEach ( change => this . removePattern ( change . before ) ) ; const propertyChanges = computeDifference < AnyPatternProperty > ( { before : this . getPatternProperties ( ) , after : b . getPatternProperties ( ) } ) ; propertyChanges . added . forEach ( change => this . addProperty ( change . after ) ) ; propertyChanges . changed . forEach ( change => change . before . update ( change . after ) ) ; propertyChanges . removed . forEach ( change => this . removeProperty ( change . before ) ) ; } } function deserializeState ( input ) { switch ( input ) { case 'pristine' : return Types . PatternLibraryState . Pristine ; case 'connecting' : return Types . PatternLibraryState . Connecting ; case 'connected' : return Types . PatternLibraryState . Connected ; case 'disconnected' : return Types . PatternLibraryState . Disconnected ; } } function deserializeOrigin ( input ) { switch ( input ) { case 'built-in' : return Types . PatternLibraryOrigin . BuiltIn ; case 'user-provided' : return Types . PatternLibraryOrigin . UserProvided ; } throw new Error ( `Unknown pattern library origin: ${ input }` ) ; } function serializeOrigin ( input ) { switch ( input ) { case Types . PatternLibraryOrigin . BuiltIn : return 'built-in' ; case Types . PatternLibraryOrigin . UserProvided : return 'user-provided' ; } throw new Error ( `Unknown pattern library origin: ${ input }` ) ; }