diff --git a/src/Category.ts b/src/Category.ts index d9ef59e4..617bdc51 100644 --- a/src/Category.ts +++ b/src/Category.ts @@ -62,7 +62,8 @@ export namespace Category { canHaveApplicationArguments: boolean, canHaveConstructParameters: boolean, icon: string, - color: string + color: string, + radius: number, }; // TODO: add to CategoryData somehow? use in Node.isData() etc? diff --git a/src/CategoryData.ts b/src/CategoryData.ts index bc3f8bd3..3a40f76d 100644 --- a/src/CategoryData.ts +++ b/src/CategoryData.ts @@ -1,58 +1,59 @@ import { Category } from './Category'; +import { EagleConfig } from './EagleConfig'; export class CategoryData { static readonly cData : {[category:string] : Category.CategoryData} = { - Branch : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 2, maxOutputs: 2, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-branch", color: Category.Color.Application}, - ExclusiveForceNode : {categoryType: Category.Type.Construct, isGroup: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveComponentParameters: false, canHaveApplicationArguments: false, canHaveConstructParameters: false, icon: "icon-force_node", color: Category.Color.Construct}, + Branch : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 2, maxOutputs: 2, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-branch", color: Category.Color.Application, radius: EagleConfig.BRANCH_NODE_RADIUS}, + ExclusiveForceNode : {categoryType: Category.Type.Construct, isGroup: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveComponentParameters: false, canHaveApplicationArguments: false, canHaveConstructParameters: false, icon: "icon-force_node", color: Category.Color.Construct, radius: EagleConfig.NORMAL_NODE_RADIUS}, - Comment : {categoryType: Category.Type.Other, isGroup: false, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveComponentParameters: false, canHaveApplicationArguments: false, canHaveConstructParameters: false, icon: "icon-comment", color: Category.Color.Description}, + Comment : {categoryType: Category.Type.Other, isGroup: false, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveComponentParameters: false, canHaveApplicationArguments: false, canHaveConstructParameters: false, icon: "icon-comment", color: Category.Color.Description, radius: EagleConfig.COMMENT_NODE_WIDTH}, - Scatter : {categoryType: Category.Type.Construct, isGroup: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-scatter", color: Category.Color.Construct}, - Gather : {categoryType: Category.Type.Construct, isGroup: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-gather", color: Category.Color.Construct}, - MKN : {categoryType: Category.Type.Construct, isGroup: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-many_to_many", color: Category.Color.Construct}, - GroupBy : {categoryType: Category.Type.Construct, isGroup: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-group", color: Category.Color.Construct}, - Loop : {categoryType: Category.Type.Construct, isGroup: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-loop", color: Category.Color.Construct}, - SubGraph : {categoryType: Category.Type.Construct, isGroup: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-sub_graph", color: Category.Color.Construct}, + Scatter : {categoryType: Category.Type.Construct, isGroup: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-scatter", color: Category.Color.Construct, radius: EagleConfig.NORMAL_NODE_RADIUS}, + Gather : {categoryType: Category.Type.Construct, isGroup: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-gather", color: Category.Color.Construct, radius: EagleConfig.NORMAL_NODE_RADIUS}, + MKN : {categoryType: Category.Type.Construct, isGroup: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-many_to_many", color: Category.Color.Construct, radius: EagleConfig.NORMAL_NODE_RADIUS}, + GroupBy : {categoryType: Category.Type.Construct, isGroup: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-group", color: Category.Color.Construct, radius: EagleConfig.NORMAL_NODE_RADIUS}, + Loop : {categoryType: Category.Type.Construct, isGroup: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-loop", color: Category.Color.Construct, radius: EagleConfig.NORMAL_NODE_RADIUS}, + SubGraph : {categoryType: Category.Type.Construct, isGroup: true, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-sub_graph", color: Category.Color.Construct, radius: EagleConfig.NORMAL_NODE_RADIUS}, - DALiuGEApp : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-daliugeApp", color: Category.Color.Application}, - PyFuncApp : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-python_function", color: Category.Color.Application}, - BashShellApp : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-bash", color: Category.Color.Application}, - DynlibApp : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-dynamic_library", color: Category.Color.Application}, - DynlibProcApp : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-dynamic_library", color: Category.Color.Application}, - Mpi : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-mpi", color: Category.Color.Application}, - Docker : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-docker", color: Category.Color.Application}, - Singularity : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-singularity", color: Category.Color.Application}, - UnknownApplication : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-question_mark", color: Category.Color.Error}, + DALiuGEApp : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-daliugeApp", color: Category.Color.Application, radius: EagleConfig.NORMAL_NODE_RADIUS}, + PyFuncApp : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-python_function", color: Category.Color.Application, radius: EagleConfig.NORMAL_NODE_RADIUS}, + BashShellApp : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-bash", color: Category.Color.Application, radius: EagleConfig.NORMAL_NODE_RADIUS}, + DynlibApp : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-dynamic_library", color: Category.Color.Application, radius: EagleConfig.NORMAL_NODE_RADIUS}, + DynlibProcApp : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-dynamic_library", color: Category.Color.Application, radius: EagleConfig.NORMAL_NODE_RADIUS}, + Mpi : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-mpi", color: Category.Color.Application, radius: EagleConfig.NORMAL_NODE_RADIUS}, + Docker : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-docker", color: Category.Color.Application, radius: EagleConfig.NORMAL_NODE_RADIUS}, + Singularity : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-singularity", color: Category.Color.Application, radius: EagleConfig.NORMAL_NODE_RADIUS}, + UnknownApplication : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-question_mark", color: Category.Color.Error, radius: EagleConfig.NORMAL_NODE_RADIUS}, - PythonMemberFunction : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-python_member_function", color: Category.Color.Object}, - PythonObject : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-python_object", color: Category.Color.Object}, - DynlibMemberFunction : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-dynamic_library_member_function", color: Category.Color.Object}, - DynlibObject : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-dynamic_library_object", color: Category.Color.Object}, + PythonMemberFunction : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-python_member_function", color: Category.Color.Object, radius: EagleConfig.NORMAL_NODE_RADIUS}, + PythonObject : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-python_object", color: Category.Color.Object, radius: EagleConfig.NORMAL_NODE_RADIUS}, + DynlibMemberFunction : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-dynamic_library_member_function", color: Category.Color.Object, radius: EagleConfig.NORMAL_NODE_RADIUS}, + DynlibObject : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-dynamic_library_object", color: Category.Color.Object, radius: EagleConfig.NORMAL_NODE_RADIUS}, - File : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-description", color: Category.Color.Data}, - Directory : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-folder", color: Category.Color.Data}, - Memory : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 1, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-memory", color: Category.Color.Data}, - SharedMemory : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 1, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-shared_memory", color: Category.Color.Data}, - NGAS : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-ngas", color: Category.Color.Data}, - S3 : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-s3_bucket", color: Category.Color.Data}, - ParameterSet : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-tune", color: Category.Color.Data}, - Data : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-hard_drive", color: Category.Color.Data}, + File : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-description", color: Category.Color.Data, radius: EagleConfig.DATA_NODE_RADIUS}, + Directory : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-folder", color: Category.Color.Data, radius: EagleConfig.DATA_NODE_RADIUS}, + Memory : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 1, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-memory", color: Category.Color.Data, radius: EagleConfig.DATA_NODE_RADIUS}, + SharedMemory : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 1, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-shared_memory", color: Category.Color.Data, radius: EagleConfig.DATA_NODE_RADIUS}, + NGAS : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-ngas", color: Category.Color.Data, radius: EagleConfig.DATA_NODE_RADIUS}, + S3 : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-s3_bucket", color: Category.Color.Data, radius: EagleConfig.DATA_NODE_RADIUS}, + ParameterSet : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-tune", color: Category.Color.Data, radius: EagleConfig.DATA_NODE_RADIUS}, + Data : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-hard_drive", color: Category.Color.Data, radius: EagleConfig.DATA_NODE_RADIUS}, - Plasma : {categoryType: Category.Type.Service, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-plasma", color: Category.Color.Service}, - PlasmaFlight : {categoryType: Category.Type.Service, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-plasma_flight", color: Category.Color.Service}, - RDBMS : {categoryType: Category.Type.Service, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-database", color: Category.Color.Service}, - Service : {categoryType: Category.Type.Service, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: 0, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-database", color: Category.Color.Service}, + Plasma : {categoryType: Category.Type.Service, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-plasma", color: Category.Color.Service, radius: EagleConfig.NORMAL_NODE_RADIUS}, + PlasmaFlight : {categoryType: Category.Type.Service, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-plasma_flight", color: Category.Color.Service, radius: EagleConfig.NORMAL_NODE_RADIUS}, + RDBMS : {categoryType: Category.Type.Service, isGroup: false, minInputs: 0, maxInputs: 1, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-database", color: Category.Color.Service, radius: EagleConfig.NORMAL_NODE_RADIUS}, + Service : {categoryType: Category.Type.Service, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: 0, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-database", color: Category.Color.Service, radius: EagleConfig.NORMAL_NODE_RADIUS}, - GlobalVariables : {categoryType: Category.Type.Global, isGroup: false, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveComponentParameters: true, canHaveApplicationArguments: false, canHaveConstructParameters: false, icon: "icon-tune", color: Category.Color.Global}, + GlobalVariables : {categoryType: Category.Type.Global, isGroup: false, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveComponentParameters: true, canHaveApplicationArguments: false, canHaveConstructParameters: false, icon: "icon-tune", color: Category.Color.Global, radius: EagleConfig.DATA_NODE_RADIUS}, - Unknown : {categoryType: Category.Type.Unknown, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-question_mark", color: Category.Color.Error}, - None : {categoryType: Category.Type.Unknown, isGroup: false, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveComponentParameters: false, canHaveApplicationArguments: false, canHaveConstructParameters: false, icon: "icon-none", color: Category.Color.Error}, + Unknown : {categoryType: Category.Type.Unknown, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-question_mark", color: Category.Color.Error, radius: EagleConfig.NORMAL_NODE_RADIUS}, + None : {categoryType: Category.Type.Unknown, isGroup: false, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveComponentParameters: false, canHaveApplicationArguments: false, canHaveConstructParameters: false, icon: "icon-none", color: Category.Color.Error, radius: EagleConfig.NORMAL_NODE_RADIUS}, // legacy - make sure to add here AND to the LEGACY_CATEGORIES_UP array below - Component : {categoryType: Category.Type.Unknown, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-none", color: Category.Color.Legacy}, - Description : {categoryType: Category.Type.Other, isGroup: false, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveComponentParameters: false, canHaveApplicationArguments: false, canHaveConstructParameters: false, icon: "icon-none", color: Category.Color.Legacy}, - PythonApp : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-python", color: Category.Color.Legacy}, - EnvironmentVariables : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-tune", color: Category.Color.Legacy}, + Component : {categoryType: Category.Type.Unknown, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: true, icon: "icon-none", color: Category.Color.Legacy, radius: EagleConfig.NORMAL_NODE_RADIUS}, + Description : {categoryType: Category.Type.Other, isGroup: false, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: 0, canHaveComponentParameters: false, canHaveApplicationArguments: false, canHaveConstructParameters: false, icon: "icon-none", color: Category.Color.Legacy, radius: EagleConfig.NORMAL_NODE_RADIUS}, + PythonApp : {categoryType: Category.Type.Application, isGroup: false, minInputs: 0, maxInputs: Number.MAX_SAFE_INTEGER, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-python", color: Category.Color.Legacy, radius: EagleConfig.NORMAL_NODE_RADIUS}, + EnvironmentVariables : {categoryType: Category.Type.Data, isGroup: false, minInputs: 0, maxInputs: 0, minOutputs: 0, maxOutputs: Number.MAX_SAFE_INTEGER, canHaveComponentParameters: true, canHaveApplicationArguments: true, canHaveConstructParameters: false, icon: "icon-tune", color: Category.Color.Legacy, radius: EagleConfig.DATA_NODE_RADIUS}, }; static readonly LEGACY_CATEGORIES_UPGRADES: Map = new Map([ @@ -77,6 +78,7 @@ export class CategoryData { canHaveConstructParameters: false, icon: "icon-none", color: "pink", + radius: EagleConfig.NORMAL_NODE_RADIUS, }; } diff --git a/src/ComponentUpdater.ts b/src/ComponentUpdater.ts index 5cfb551e..80803280 100644 --- a/src/ComponentUpdater.ts +++ b/src/ComponentUpdater.ts @@ -23,7 +23,7 @@ export class ComponentUpdater { } static updateNode(palettes: Palette[], node: Node, errorsWarnings: Errors.ErrorsWarnings): Node | null { - let newVersion : Node = null; + let newVersion : Node | null = null; for (const palette of palettes){ for (const paletteNode of palette.getNodes()){ @@ -48,7 +48,7 @@ export class ComponentUpdater { static _replaceNode(dest: Node, src: Node){ for (const srcField of src.getFields()){ // try to find a field with the same name in the destination - let destField: Field = dest.getFieldById(srcField.getId()); + let destField: Field | undefined = dest.getFieldById(srcField.getId()); // if dest field not found, try to find something that matches by displayText if (typeof destField === 'undefined'){ @@ -56,7 +56,7 @@ export class ComponentUpdater { } // if dest field could not be found, then go ahead and add a NEW field to the dest node - if (destField === null){ + if (typeof destField === 'undefined'){ destField = srcField.clone(); dest.addField(destField); } diff --git a/src/Daliuge.ts b/src/Daliuge.ts index 030a3884..bd6084b2 100644 --- a/src/Daliuge.ts +++ b/src/Daliuge.ts @@ -167,32 +167,33 @@ export namespace Daliuge { Unknown = "Unknown" } - // These are the canonical example definition of each field - export const groupStartField = new Field(null, FieldName.GROUP_START, "true", "true", "Is this node the start of a group?", false, DataType.Boolean, false, [], false, FieldType.Component, FieldUsage.NoPort); - export const groupEndField = new Field(null, FieldName.GROUP_END, "true", "true", "Is this node the end of a group?", false, DataType.Boolean, false, [], false, FieldType.Component, FieldUsage.NoPort); - - export const branchYesField = new Field(null, FieldName.TRUE, "", "", "The affirmative output from a branch node", false, DataType.Object, false, [], false, FieldType.Component, FieldUsage.OutputPort); - export const branchNoField = new Field(null, FieldName.FALSE, "", "", "he negative output from a branch node", false, DataType.Object, false, [], false, FieldType.Component, FieldUsage.OutputPort); + // dummy node for use in field definitions + const dummyNode = new Node("", "", "", Category.Unknown); - export const dropClassField = new Field(null, FieldName.DROP_CLASS, "", "", "", false, DataType.String, false, [], false, FieldType.Component, FieldUsage.NoPort); + // These are the canonical example definition of each field + export const groupStartField = new Field(dummyNode, FieldName.GROUP_START as FieldId, FieldName.GROUP_START, "true", "true", "Is this node the start of a group?", false, DataType.Boolean, false, [], false, FieldType.Component, FieldUsage.NoPort); + export const groupEndField = new Field(dummyNode, FieldName.GROUP_END as FieldId, FieldName.GROUP_END, "true", "true", "Is this node the end of a group?", false, DataType.Boolean, false, [], false, FieldType.Component, FieldUsage.NoPort); - export const executionTimeField = new Field(null, FieldName.EXECUTION_TIME, "5", "5", "", false, DataType.Float, false, [], false, FieldType.Constraint, FieldUsage.NoPort); - export const numCpusField = new Field(null, FieldName.NUM_OF_CPUS, "1", "1", "", false, DataType.Integer, false, [], false, FieldType.Constraint, FieldUsage.NoPort); - export const dataVolumeField = new Field(null, FieldName.DATA_VOLUME, "5", "5", "", false, DataType.Float, false, [], false, FieldType.Constraint, FieldUsage.NoPort); + export const branchYesField = new Field(dummyNode, FieldName.TRUE as FieldId, FieldName.TRUE, "", "", "The affirmative output from a branch node", false, DataType.Object, false, [], false, FieldType.Component, FieldUsage.OutputPort); + export const branchNoField = new Field(dummyNode, FieldName.FALSE as FieldId, FieldName.FALSE, "", "", "he negative output from a branch node", false, DataType.Object, false, [], false, FieldType.Component, FieldUsage.OutputPort); + export const dropClassField = new Field(dummyNode, FieldName.DROP_CLASS as FieldId, FieldName.DROP_CLASS, "", "", "", false, DataType.String, false, [], false, FieldType.Component, FieldUsage.NoPort); - export const kField = new Field(null, FieldName.K, "1", "1", "", false, DataType.Integer, false, [], false, FieldType.Construct, FieldUsage.NoPort); - export const numCopiesField = new Field(null, FieldName.NUM_OF_COPIES, "1", "1", "", false, DataType.Integer, false, [], false, FieldType.Construct, FieldUsage.NoPort); - export const numInputsField = new Field(null, FieldName.NUM_OF_INPUTS, "1", "1", "", false, DataType.Integer, false, [], false, FieldType.Construct, FieldUsage.NoPort); - export const numIterationsField = new Field(null, FieldName.NUM_OF_ITERATIONS, "1", "1", "", false, DataType.Integer, false, [], false, FieldType.Construct, FieldUsage.NoPort); + export const executionTimeField = new Field(dummyNode, FieldName.EXECUTION_TIME as FieldId, FieldName.EXECUTION_TIME, "5", "5", "", false, DataType.Float, false, [], false, FieldType.Constraint, FieldUsage.NoPort); + export const numCpusField = new Field(dummyNode, FieldName.NUM_OF_CPUS as FieldId, FieldName.NUM_OF_CPUS, "1", "1", "", false, DataType.Integer, false, [], false, FieldType.Constraint, FieldUsage.NoPort); + export const dataVolumeField = new Field(dummyNode, FieldName.DATA_VOLUME as FieldId, FieldName.DATA_VOLUME, "5", "5", "", false, DataType.Float, false, [], false, FieldType.Constraint, FieldUsage.NoPort); - export const baseNameField = new Field(null, FieldName.BASE_NAME, "", "", "The base name of the class of this Member function", false, DataType.String, false, [], false, FieldType.Component, FieldUsage.NoPort); - export const selfField = new Field(null, FieldName.SELF, "", "", "", false, DataType.Object, false, [], false, FieldType.Component, FieldUsage.InputOutput); + export const kField = new Field(dummyNode, FieldName.K as FieldId, FieldName.K, "1", "1", "", false, DataType.Integer, false, [], false, FieldType.Construct, FieldUsage.NoPort); + export const numCopiesField = new Field(dummyNode, FieldName.NUM_OF_COPIES as FieldId, FieldName.NUM_OF_COPIES, "1", "1", "", false, DataType.Integer, false, [], false, FieldType.Construct, FieldUsage.NoPort); + export const numInputsField = new Field(dummyNode, FieldName.NUM_OF_INPUTS as FieldId, FieldName.NUM_OF_INPUTS, "1", "1", "", false, DataType.Integer, false, [], false, FieldType.Construct, FieldUsage.NoPort); + export const numIterationsField = new Field(dummyNode, FieldName.NUM_OF_ITERATIONS as FieldId, FieldName.NUM_OF_ITERATIONS, "1", "1", "", false, DataType.Integer, false, [], false, FieldType.Construct, FieldUsage.NoPort); - export const funcCodeField = new Field(null, FieldName.FUNC_CODE, "", "def func_name(args): return args", "Python function code", false, Daliuge.DataType.Python, false, [], false, Daliuge.FieldType.Component, FieldUsage.NoPort); - export const funcNameField = new Field(null, FieldName.FUNC_NAME, "", "func_name", "Python function name", false, Daliuge.DataType.Python, false, [], false, Daliuge.FieldType.Component, FieldUsage.NoPort); + export const baseNameField = new Field(dummyNode, FieldName.BASE_NAME as FieldId, FieldName.BASE_NAME, "", "", "The base name of the class of this Member function", false, DataType.String, false, [], false, FieldType.Component, FieldUsage.NoPort); + export const selfField = new Field(dummyNode, FieldName.SELF as FieldId, FieldName.SELF, "", "", "", false, DataType.Object, false, [], false, FieldType.Component, FieldUsage.InputOutput); + export const funcCodeField = new Field(dummyNode, FieldName.FUNC_CODE as FieldId, FieldName.FUNC_CODE, "", "def func_name(args): return args", "Python function code", false, Daliuge.DataType.Python, false, [], false, Daliuge.FieldType.Component, FieldUsage.NoPort); + export const funcNameField = new Field(dummyNode, FieldName.FUNC_NAME as FieldId, FieldName.FUNC_NAME, "", "func_name", "Python function name", false, Daliuge.DataType.Python, false, [], false, Daliuge.FieldType.Component, FieldUsage.NoPort); - export const persistField = new Field(null, FieldName.PERSIST, "false", "false", "Specifies whether this data component contains data that should not be deleted after execution", false, Daliuge.DataType.Boolean, false, [], false, Daliuge.FieldType.Component, Daliuge.FieldUsage.NoPort); - export const streamingField = new Field(null, FieldName.STREAMING, "false", "false", "Specifies whether this data component streams input and output data", false, Daliuge.DataType.Boolean, false, [], false, Daliuge.FieldType.Component, Daliuge.FieldUsage.NoPort); + export const persistField = new Field(dummyNode, FieldName.PERSIST as FieldId, FieldName.PERSIST, "false", "false", "Specifies whether this data component contains data that should not be deleted after execution", false, Daliuge.DataType.Boolean, false, [], false, Daliuge.FieldType.Component, Daliuge.FieldUsage.NoPort); + export const streamingField = new Field(dummyNode, FieldName.STREAMING as FieldId, FieldName.STREAMING, "false", "false", "Specifies whether this data component streams input and output data", false, Daliuge.DataType.Boolean, false, [], false, Daliuge.FieldType.Component, Daliuge.FieldUsage.NoPort); // This list defines the fields required for ALL nodes belonging to a given Category.Type export const categoryTypeFieldsRequired = [ diff --git a/src/DockerHubBrowser.ts b/src/DockerHubBrowser.ts index 1c5aa511..3c43e15e 100644 --- a/src/DockerHubBrowser.ts +++ b/src/DockerHubBrowser.ts @@ -45,7 +45,7 @@ export class DockerHubBrowser { isValid: ko.Observable; // true iff a valid selection has been made in the docker hub browser UI constructor(){ - this.username = ko.observable(Setting.findValue(Setting.DOCKER_HUB_USERNAME)); + this.username = ko.observable(Setting.findValue(Setting.DOCKER_HUB_USERNAME, "")); this.images = ko.observableArray([]); this.tags = ko.observableArray([]); this.digests = ko.observableArray([]); @@ -63,7 +63,7 @@ export class DockerHubBrowser { } clear = () : void => { - this.username(Setting.findValue(Setting.DOCKER_HUB_USERNAME)); + this.username(Setting.findValue(Setting.DOCKER_HUB_USERNAME, "")); this.images([]); this.tags([]); this.digests([]); @@ -86,7 +86,7 @@ export class DockerHubBrowser { this.fetchImages(image, tag); } - fetchImages = async (selectedImage: string, selectedTag: string): Promise => { + fetchImages = async (selectedImage: string | null, selectedTag: string | null): Promise => { // if already fetched, abort if (this.hasFetchedImages()){ console.warn("Already fetched images"); @@ -135,7 +135,7 @@ export class DockerHubBrowser { this.fetchTags(selectedTag); } - fetchTags = async (selectedTag: string): Promise => { + fetchTags = async (selectedTag: string | null): Promise => { // if already fetched, abort if (this.hasFetchedTags()){ console.warn("Already fetched tags"); diff --git a/src/Eagle.ts b/src/Eagle.ts index 470478d2..c2da6109 100644 --- a/src/Eagle.ts +++ b/src/Eagle.ts @@ -78,11 +78,11 @@ export class Eagle { rightWindow : ko.Observable; bottomWindow : ko.Observable; - selectedObjects : ko.ObservableArray; + selectedObjects : ko.ObservableArray; static selectedLocation : ko.Observable; - currentField :ko.Observable; + currentField :ko.Observable; - static selectedRightClickObject : ko.Observable; + static selectedRightClickObject : ko.Observable; static selectedRightClickLocation : ko.Observable; static selectedRightClickPosition : {x: number, y: number} = {x:0, y:0} @@ -101,12 +101,12 @@ export class Eagle { graphErrors : ko.ObservableArray; loadingWarnings : ko.ObservableArray; loadingErrors : ko.ObservableArray; - currentFileInfo : ko.Observable; + currentFileInfo : ko.Observable; currentFileInfoTitle : ko.Observable; showDataNodes : ko.Observable; snapToGrid : ko.Observable; - dropdownMenuHoverTimeout : number = 0; + dropdownMenuHoverTimeout : number | undefined = undefined; static paletteComponentSearchString : ko.Observable; static componentParamsSearchString : ko.Observable; @@ -121,8 +121,8 @@ export class Eagle { static lastClickTime : number = 0; static nodeDropLocation : {x: number, y: number} = {x:0, y:0}; // if this remains x=0,y=0, the button has been pressed and the getNodePosition function will be used to determine a location on the canvas. if not x:0, y:0, it has been over written by the nodeDrop function as the node has been dragged into the canvas. The node will then be placed into the canvas using these co-ordinates. - static nodeDragPaletteIndex : number; - static nodeDragComponentId : NodeId; + static nodeDragPaletteIndex : number | null; + static nodeDragComponentId : NodeId | null; static shortcutModalCooldown : number; constructor(){ @@ -131,7 +131,7 @@ export class Eagle { UiModeSystem.initialise() this.palettes = ko.observableArray(); - this.logicalGraph = ko.observable(null); + this.logicalGraph = ko.observable(new LogicalGraph()); this.eagleIsReady = ko.observable(false); this.leftWindow = ko.observable(new SideWindow(Utils.getLeftWindowWidth())); @@ -142,7 +142,7 @@ export class Eagle { Eagle.selectedLocation = ko.observable(Eagle.FileType.Unknown); this.currentField = ko.observable(null); - Eagle.selectedRightClickObject = ko.observable(); + Eagle.selectedRightClickObject = ko.observable(null); Eagle.selectedRightClickLocation = ko.observable(Eagle.FileType.Unknown); this.repositories = ko.observable(new Repositories()); @@ -182,11 +182,17 @@ export class Eagle { this.showDataNodes = ko.observable(true); this.snapToGrid = ko.observable(false); - this.dropdownMenuHoverTimeout = null; + this.dropdownMenuHoverTimeout = undefined; this.selectedObjects.subscribe(function(){ + // abort if logicalGraph is null + const lg = this.logicalGraph(); + if (lg === null){ + return; + } + //TODO check if the selectedObjects array has changed, if not, abort - GraphRenderer.nodeData = GraphRenderer.depthFirstTraversalOfNodes(this.logicalGraph(), this.showDataNodes()); + GraphRenderer.nodeData = GraphRenderer.depthFirstTraversalOfNodes(lg, this.showDataNodes()); Hierarchy.updateDisplay() Hierarchy.scrollToNode() }, this) @@ -211,12 +217,17 @@ export class Eagle { return false; } - static selectedNodePalette() : Palette { + static selectedNodePalette() : Palette | null { const eagle : Eagle = Eagle.getInstance(); + const selectedNode = eagle.selectedNode(); + + if (selectedNode === null){ + return null; + } for (const palette of eagle.palettes()){ for (const node of palette.getNodes()){ - if (Node.match(node, eagle.selectedNode())){ + if (Node.match(node, selectedNode)){ return palette; } } @@ -241,10 +252,12 @@ export class Eagle { // add additional custom types switch (Eagle.selectedLocation()){ case Eagle.FileType.Palette: + const selectedNode = this.selectedNode(); + // build a list from the selected component in the palettes - if(this.selectedNode() !== null){ + if(selectedNode !== null){ - for (const field of this.selectedNode().getFields()) { + for (const field of selectedNode.getFields()) { Utils.addTypeIfUnique(result, field.getType()); } }else{ @@ -259,16 +272,19 @@ export class Eagle { Utils.addTypeIfUnique(result, field.getType()); } + const inputApplication = node.getInputApplication(); + const outputApplication = node.getOutputApplication(); + // also check for fields that belong to the inputApplication - if (node.hasInputApplication()){ - for (const field of node.getInputApplication().getFields()){ + if (inputApplication !== null){ + for (const field of inputApplication.getFields()){ Utils.addTypeIfUnique(result, field.getType()); } } // also check for fields that belong to the outputApplication - if (node.hasOutputApplication()){ - for (const field of node.getOutputApplication().getFields()){ + if (outputApplication !== null){ + for (const field of outputApplication.getFields()){ Utils.addTypeIfUnique(result, field.getType()); } } @@ -298,7 +314,8 @@ export class Eagle { } deployDefaultTranslationAlgorithm = async () => { - const defaultTranslatorAlgorithmMethod : string = $('#'+Setting.findValue(Setting.TRANSLATOR_ALGORITHM_DEFAULT)+ ' .generatePgt').val().toString() + const translatorAlgorithmDefault = Setting.findValue(Setting.TRANSLATOR_ALGORITHM_DEFAULT, Translator.DEFAULT_TRANSLATION_ALGORITHM); + const defaultTranslatorAlgorithmMethod : string = Utils.getUIValue('#'+ translatorAlgorithmDefault + ' .generatePgt', Translator.DEFAULT_TRANSLATION_ALGORITHM); try { await this.translator().genPGT(defaultTranslatorAlgorithmMethod, false); } catch (error){ @@ -367,7 +384,7 @@ export class Eagle { } isTranslationDefault = (algorithmName:string) : boolean => { - return algorithmName === Setting.findValue(Setting.TRANSLATOR_ALGORITHM_DEFAULT) + return algorithmName === Setting.findValue(Setting.TRANSLATOR_ALGORITHM_DEFAULT, Translator.DEFAULT_TRANSLATION_ALGORITHM); } repositoryFileName : ko.PureComputed = ko.pureComputed(() => { @@ -386,19 +403,27 @@ export class Eagle { }, this); activeConfigHtml : ko.PureComputed = ko.pureComputed(() => { - if (typeof this.logicalGraph().getActiveGraphConfig() === 'undefined'){ + const activeGraphConfig = this.logicalGraph().getActiveGraphConfig(); + + if (typeof activeGraphConfig === 'undefined'){ return ""; } - return "Config: " + this.logicalGraph().getActiveGraphConfig().fileInfo().name; + return "Config: " + activeGraphConfig.fileInfo().name; }, this); // TODO: move to SideWindow.ts? toggleWindows = () : void => { - const setOpen = !Setting.findValue(Setting.LEFT_WINDOW_VISIBLE) || !Setting.findValue(Setting.RIGHT_WINDOW_VISIBLE) || !Setting.findValue(Setting.BOTTOM_WINDOW_VISIBLE) + const leftWindowVisible : boolean = Setting.findValue(Setting.LEFT_WINDOW_VISIBLE, false); + const rightWindowVisible : boolean = Setting.findValue(Setting.RIGHT_WINDOW_VISIBLE, false); + const bottomWindowVisible : boolean = Setting.findValue(Setting.BOTTOM_WINDOW_VISIBLE, false); + + const setOpen : boolean = !leftWindowVisible || !rightWindowVisible || !bottomWindowVisible; // don't allow open if palette and graph editing are disabled - const editingAllowed: boolean = Setting.findValue(Setting.ALLOW_PALETTE_EDITING) || Setting.findValue(Setting.ALLOW_GRAPH_EDITING); + const allowPaletteEditing: boolean = Setting.findValue(Setting.ALLOW_PALETTE_EDITING, false); + const allowGraphEditing: boolean = Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false); + const editingAllowed: boolean = allowPaletteEditing || allowGraphEditing; if (setOpen && !editingAllowed){ Utils.notifyUserOfEditingIssue(Eagle.FileType.Unknown, "Toggle Windows"); return; @@ -409,12 +434,21 @@ export class Eagle { SideWindow.setShown('bottom', setOpen); } - emptySearchBar = (target : ko.Observable,data:string, event : Event) => { + emptySearchBar = (target: ko.Observable, data: string, event : Event) => { target("") + if (event.target === null){ + console.warn("emptySearchBar: event.target is null"); + return; + } $(event.target).parent().hide() } - setSearchBarClearBtnState = (data:string, event : Event) => { + setSearchBarClearBtnState = (data: string, event: Event) => { + if (event.target === null){ + console.warn("setSearchBarClearBtnState: event.target is null"); + return; + } + if($(event.target).val() === ""){ $(event.target).parent().find('a').hide() }else{ @@ -493,12 +527,17 @@ export class Eagle { //because the saved bottom window height is a percentage, its easier to grab the height using jquery than to convert the percentage into pixels let bottomWindow = 0 - if(Setting.findValue(Setting.BOTTOM_WINDOW_VISIBLE)){ - bottomWindow = $('#bottomWindow').height() + if(Setting.findValue(Setting.BOTTOM_WINDOW_VISIBLE, false)){ + const bottomWindowHeight = $('#bottomWindow').height() + bottomWindow = bottomWindowHeight !== undefined ? bottomWindowHeight : 0 } + // get width and height of the logical graph parent container + const logicalGraphParentWidth = $('#logicalGraphParent').width() || 0; + const logicalGraphParentHeight = $('#logicalGraphParent').height() || 0; + //calculating scale multipliers needed for each, height and width in order to fit the graph - const containerHeight = $('#logicalGraphParent').height() - bottomWindow + const containerHeight = logicalGraphParentHeight - bottomWindow const graphHeight = maxY-minY+200 const graphYScale = containerHeight/graphHeight @@ -507,7 +546,7 @@ export class Eagle { const leftWindow = Utils.getLeftWindowWidth() const rightWindow = Utils.getRightWindowWidth() - const containerWidth = $('#logicalGraphParent').width() - leftWindow - rightWindow + const containerWidth = logicalGraphParentWidth - leftWindow - rightWindow const graphWidth = maxX-minX+200 const graphXScale = containerWidth/graphWidth @@ -606,7 +645,7 @@ export class Eagle { } // if selectedObjects contains nothing but one node, return the node, else null - selectedNode : ko.PureComputed = ko.pureComputed(() : Node => { + selectedNode : ko.PureComputed = ko.pureComputed(() : Node | null => { if (this.selectedObjects().length !== 1){ return null; } @@ -621,7 +660,7 @@ export class Eagle { }, this); // if selectedObjects contains nothing but one edge, return the edge, else null - selectedEdge : ko.PureComputed = ko.pureComputed(() : Edge => { + selectedEdge : ko.PureComputed = ko.pureComputed(() : Edge | null => { if (this.selectedObjects().length !== 1){ return null; } @@ -641,7 +680,7 @@ export class Eagle { if(!serviceIsGit){ return 'dodgerblue' - }else if(Setting.findValue(Setting.TEST_TRANSLATE_MODE)){ + }else if(Setting.findValue(Setting.TEST_TRANSLATE_MODE, false)){ return 'orange' }else if (this.logicalGraph().fileInfo().modified){ return 'red' @@ -650,7 +689,7 @@ export class Eagle { } }, this); - setSelection = (selection : Node | Edge, selectedLocation: Eagle.FileType) : void => { + setSelection = (selection : Node | Edge | null, selectedLocation: Eagle.FileType) : void => { Eagle.selectedLocation(selectedLocation); GraphRenderer.clearPortPeek() @@ -713,11 +752,11 @@ export class Eagle { $('#inspector').removeClass('inspectorTransition') },100) - return Setting.findValue(Setting.INSPECTOR_COLLAPSED_STATE) + return Setting.findValue(Setting.INSPECTOR_COLLAPSED_STATE, false) }, this); toggleInspectorCollapsedState = () : void => { - Setting.find(Setting.INSPECTOR_COLLAPSED_STATE).toggle() + Setting.toggle(Setting.INSPECTOR_COLLAPSED_STATE); }; getGraphModifiedDateText = () : string => { @@ -729,8 +768,10 @@ export class Eagle { SideWindow.setShown('right', true) + const rightWindowMode = Setting.findValue(Setting.RIGHT_WINDOW_MODE, Eagle.RightWindowMode.None); + //trigger a re-render of the hierarchy - if (Setting.findValue(Setting.RIGHT_WINDOW_MODE) === Eagle.RightWindowMode.Hierarchy){ + if (rightWindowMode === Eagle.RightWindowMode.Hierarchy){ window.setTimeout(function(){ Hierarchy.updateDisplay() }, 100) @@ -792,7 +833,7 @@ export class Eagle { let thisParentIsSelected = true let thisObject = object while (thisParentIsSelected){ - const thisParent: Node = thisObject.getParent(); + const thisParent = thisObject.getParent(); if(thisParent != null){ thisParentIsSelected = eagle.objectIsSelectedById(thisParent.getId()) if(thisParentIsSelected){ @@ -834,6 +875,12 @@ export class Eagle { return; } + // abort if input element has no files + if (!graphFileToLoadInputElement.files){ + console.error("loadLocalGraphFile: no files found in input element"); + return; + } + // get reference to file from the html element const file = graphFileToLoadInputElement.files[0]; @@ -842,7 +889,7 @@ export class Eagle { const reader = new FileReader(); reader.readAsText(file, "UTF-8"); reader.onload = function (evt) { - const data: string = evt.target.result.toString(); + const data: string = evt.target?.result?.toString() || ""; eagle._loadGraphJSON(data, fileFullPath, (lg: LogicalGraph) : void => { eagle.logicalGraph(lg); @@ -873,6 +920,12 @@ export class Eagle { return; } + // abort if input element has no files + if (!graphFileToInsertInputElement.files){ + console.error("insertLocalGraphFile: no files found in input element"); + return; + } + // get reference to file from the html element const file = graphFileToInsertInputElement.files[0]; @@ -881,7 +934,7 @@ export class Eagle { const reader = new FileReader(); reader.readAsText(file, "UTF-8"); reader.onload = function (evt) { - const data: string = evt.target.result.toString(); + const data: string = evt.target?.result?.toString() || ""; eagle._loadGraphJSON(data, fileFullPath, (lg: LogicalGraph) : void => { const parentNode: Node = new Node(lg.fileInfo().name, lg.fileInfo().location.getText(), "", Category.SubGraph); @@ -905,7 +958,7 @@ export class Eagle { } private _handleLoadingErrors = (errorsWarnings: Errors.ErrorsWarnings, fileName: string, service: Repository.Service) : void => { - const showErrors: boolean = Setting.findValue(Setting.SHOW_FILE_LOADING_ERRORS); + const showErrors: boolean = Setting.findValue(Setting.SHOW_FILE_LOADING_ERRORS, false); this.hideEagleIsLoading() // show errors (if found) @@ -980,7 +1033,7 @@ export class Eagle { return } - if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING)){ + if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false)){ Utils.notifyUserOfEditingIssue(Eagle.FileType.Graph, "Create Subgraph From Selection"); return; } @@ -1006,7 +1059,8 @@ export class Eagle { } // if already parented to a node in this selection, skip - if (node.getParent() !== null && this.objectIsSelected(node.getParent())){ + const nodeParent = node.getParent(); + if (nodeParent !== null && this.objectIsSelected(nodeParent)){ continue; } @@ -1035,7 +1089,7 @@ export class Eagle { return } - if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING)){ + if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false)){ Utils.notifyUserOfEditingIssue(Eagle.FileType.Graph, "Create Construct From Selection"); return; } @@ -1081,7 +1135,7 @@ export class Eagle { } // NOTE: parentNode would be null if we are duplicating a selection of objects - insertGraph = async (nodes: Node[], edges: Edge[], parentNode: Node, errorsWarnings: Errors.ErrorsWarnings) => { + insertGraph = async (nodes: Node[], edges: Edge[], parentNode: Node | null, errorsWarnings: Errors.ErrorsWarnings) => { const DUPLICATE_OFFSET: number = 20; // amount (in x and y) by which duplicated nodes will be positioned away from the originals // create map of inserted graph keys to final graph nodes, and of inserted port ids to final graph ports @@ -1135,12 +1189,21 @@ export class Eagle { // copy embedded applications for (const node of nodes){ - const insertedNode: Node = nodeMap.get(node.getId()); + const insertedNode = nodeMap.get(node.getId()); + + // abort if the inserted node was not found in the node map + if (typeof insertedNode === "undefined"){ + console.error("Error: could not find mapping for node " + node.getName() + " " + node.getId()); + continue; + } + + const oldInputApplication = node.getInputApplication(); + const oldOutputApplication = node.getOutputApplication(); // copy embedded input application - if (node.hasInputApplication()){ - const oldInputApplication : Node = node.getInputApplication(); - const newInputApplication : Node = nodeMap.get(oldInputApplication.getId()); + if (oldInputApplication !== null){ + + const newInputApplication = nodeMap.get(oldInputApplication.getId()); if (typeof newInputApplication === "undefined"){ console.error("Error: could not find mapping for input application " + oldInputApplication.getName() + " " + oldInputApplication.getId()); @@ -1168,10 +1231,11 @@ export class Eagle { } } + + // copy embedded output application - if (node.hasOutputApplication()){ - const oldOutputApplication : Node = node.getOutputApplication(); - const newOutputApplication : Node = nodeMap.get(oldOutputApplication.getId()); + if (oldOutputApplication !== null){ + const newOutputApplication = nodeMap.get(oldOutputApplication.getId()); if (typeof newOutputApplication === "undefined"){ console.error("Error: could not find mapping for output application " + oldOutputApplication.getName() + " " + oldOutputApplication.getId()); @@ -1201,12 +1265,19 @@ export class Eagle { // update some other details of the nodes are updated correctly for (const node of nodes){ - const insertedNode: Node = nodeMap.get(node.getId()); + const insertedNode = nodeMap.get(node.getId()); + + // abort if the inserted node was not found in the node map + if (typeof insertedNode === "undefined"){ + console.error("Error: could not find mapping for node " + node.getName() + " " + node.getId()); + continue; + } // if original node has a parent, set the parent of the inserted node to the inserted parent - if (node.getParent() !== null){ + const nodeParent = node.getParent(); + if (nodeParent !== null){ // check if parent of original node was also mapped to a new node - const insertedParent: Node = nodeMap.get(node.getParent().getId()); + const insertedParent = nodeMap.get(nodeParent.getId()); // make sure parent is set correctly // if no mapping is available for the parent, then set parent to the new parentNode, or if no parentNode exists, just set parent to null @@ -1232,7 +1303,7 @@ export class Eagle { const loopAware = edge.isLoopAware(); const closesLoop = edge.isClosesLoop(); - if (typeof srcNode === "undefined" || typeof destNode === "undefined"){ + if (typeof srcNode === "undefined" || typeof srcPort === "undefined" || typeof destNode === "undefined" || typeof destPort === "undefined"){ errorsWarnings.warnings.push(Errors.Message("Unable to insert edge " + edge.getId() + " source node or destination node could not be found.")); continue; } @@ -1269,6 +1340,12 @@ export class Eagle { return; } + // abort if input element has no files + if (!paletteFileInputElement.files){ + console.error("loadLocalPaletteFile: no files found in input element"); + return; + } + // get a reference to the file in the html element const file = paletteFileInputElement.files[0]; @@ -1277,7 +1354,7 @@ export class Eagle { const reader = new FileReader(); reader.readAsText(file, "UTF-8"); reader.onload = function (evt) { - const data: string = evt.target.result.toString(); + const data: string = evt.target?.result?.toString() || ""; eagle._loadPaletteJSON(data, fileFullPath); @@ -1339,6 +1416,12 @@ export class Eagle { return; } + // abort if input element has no files + if (!graphConfigFileInputElement.files){ + console.error("loadLocalGraphConfigFile: no files found in input element"); + return; + } + // get a reference to the file in the html element const file = graphConfigFileInputElement.files[0]; @@ -1347,7 +1430,7 @@ export class Eagle { const reader = new FileReader(); reader.readAsText(file, "UTF-8"); reader.onload = function (evt) { - const data: string = evt.target.result.toString(); + const data: string = evt.target?.result?.toString() || ""; let dataObject; try { @@ -1373,48 +1456,78 @@ export class Eagle { * The following two functions allows the file selectors to be hidden and let tags 'click' them */ getGraphFileToLoad = () : void => { - if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING)){ + const allowGraphEditing = Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false); + + if (!allowGraphEditing){ Utils.notifyUserOfEditingIssue(Eagle.FileType.Graph, "Load Graph"); return; } - document.getElementById("graphFileToLoad").click(); + const element = document.getElementById("graphFileToLoad"); + if (!element) { + console.error("Could not find 'graph file to load' element"); + return; + } + element.click(); this.resetEditor() } getGraphFileToInsert = () : void => { - if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING)){ + const allowGraphEditing = Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false); + + if (!allowGraphEditing){ Utils.notifyUserOfEditingIssue(Eagle.FileType.Graph, "Insert Graph"); return; } - document.getElementById("graphFileToInsert").click(); + const element = document.getElementById("graphFileToInsert"); + if (!element) { + console.error("Could not find 'graph file to insert' element"); + return; + } + element.click(); } getPaletteFileToLoad = () : void => { - if (!Setting.findValue(Setting.ALLOW_PALETTE_EDITING)){ + const allowPaletteEditing = Setting.findValue(Setting.ALLOW_PALETTE_EDITING, false); + + if (allowPaletteEditing){ Utils.notifyUserOfEditingIssue(Eagle.FileType.Palette, "Load Palette"); return; } - document.getElementById("paletteFileToLoad").click(); + const element = document.getElementById("paletteFileToLoad"); + if (!element) { + console.error("Could not find 'palette file to load' element"); + return; + } + element.click(); } getGraphConfigFileToLoad = () : void => { - if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING)){ + const allowGraphEditing = Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false); + + if (!allowGraphEditing){ Utils.notifyUserOfEditingIssue(Eagle.FileType.Graph, "Load Graph Config"); return; } - document.getElementById("graphConfigFileToLoad").click(); + const element = document.getElementById("graphConfigFileToLoad"); + if (!element) { + console.error("Could not find 'graph config file to load' element"); + return; + } + element.click(); } /** * Creates a new logical graph for editing. */ newLogicalGraph = async(): Promise => { + const allowGraphEditing = Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false); + // check that graph editing is permitted - if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING)){ + if (!allowGraphEditing){ Utils.notifyUserOfEditingIssue(Eagle.FileType.Graph, "New Logical Graph"); return; } @@ -1436,8 +1549,10 @@ export class Eagle { * Presents the user with a textarea in which to paste JSON. Reads the JSON and parses it into a logical graph for editing. */ newLogicalGraphFromJson = async (): Promise => { + const allowGraphEditing = Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false); + // check that graph editing is permitted - if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING)){ + if (!allowGraphEditing){ Utils.notifyUserOfEditingIssue(Eagle.FileType.Graph, "New Logical Graph From JSON") return; } @@ -1458,8 +1573,10 @@ export class Eagle { } insertGraphFromJson = async (): Promise => { + const allowGraphEditing = Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false); + // check that graph editing is permitted - if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING)){ + if (!allowGraphEditing){ Utils.notifyUserOfEditingIssue(Eagle.FileType.Graph, "Insert Graph from JSON"); return; } @@ -1507,7 +1624,7 @@ export class Eagle { displayObjectAsJson = (fileType: Eagle.FileType, object: LogicalGraph | Palette | GraphConfig) : void => { let jsonString: string; - const version: Setting.SchemaVersion = Setting.findValue(Setting.DALIUGE_SCHEMA_VERSION); + const version: Setting.SchemaVersion = Setting.findValue(Setting.DALIUGE_SCHEMA_VERSION, Setting.SchemaVersion.Unknown); switch(fileType){ case Eagle.FileType.Graph: @@ -1529,7 +1646,7 @@ export class Eagle { displayNodeAsJson = (node: Node) : void => { let jsonString: string; - const version: Setting.SchemaVersion = Setting.findValue(Setting.DALIUGE_SCHEMA_VERSION); + const version: Setting.SchemaVersion = Setting.findValue(Setting.DALIUGE_SCHEMA_VERSION, Setting.SchemaVersion.Unknown); switch(version){ case Setting.SchemaVersion.OJS: @@ -1551,8 +1668,10 @@ export class Eagle { * Creates a new palette for editing. */ newPalette = async () : Promise => { + const allowPaletteEditing = Setting.findValue(Setting.ALLOW_PALETTE_EDITING, false); + // check that palette editing is permitted - if (!Setting.findValue(Setting.ALLOW_PALETTE_EDITING)){ + if (!allowPaletteEditing){ Utils.notifyUserOfEditingIssue(Eagle.FileType.Palette, "New Palette"); return; } @@ -1582,8 +1701,10 @@ export class Eagle { * Presents the user with a textarea in which to paste JSON. Reads the JSON and parses it into a palette. */ newPaletteFromJson = async (): Promise => { + const allowPaletteEditing = Setting.findValue(Setting.ALLOW_PALETTE_EDITING, false); + // check that palette editing is permitted - if (!Setting.findValue(Setting.ALLOW_PALETTE_EDITING)){ + if (!allowPaletteEditing){ Utils.notifyUserOfEditingIssue(Eagle.FileType.Palette, "New Palette from JSON"); return; } @@ -1638,8 +1759,10 @@ export class Eagle { */ newConfig = () : void => { + const allowGraphEditing = Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false); + // check that editing graphs is permitted - if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING)){ + if (!allowGraphEditing){ Utils.notifyUserOfEditingIssue(Eagle.FileType.Graph, "New Config"); return; } @@ -1813,6 +1936,12 @@ export class Eagle { } break; case Eagle.FileType.GraphConfig: + if (graphConfig === null){ + Utils.showUserMessage("Error", "No graph config provided to save."); + reject(new Error("No graph config provided to save.")); + return; + } + try { await this.saveGraphConfigToDisk(graphConfig, graphConfig.fileInfo().name); } catch(error) { @@ -1831,7 +1960,7 @@ export class Eagle { const destinationPalette = this.findPalette(paletteName, false); // check that a palette was found - if (destinationPalette === null){ + if (typeof destinationPalette === 'undefined'){ return; } @@ -1863,6 +1992,12 @@ export class Eagle { } break; case Eagle.FileType.GraphConfig: + if (graphConfig === null){ + Utils.showUserMessage("Error", "No graph config provided to save."); + reject(new Error("No graph config provided to save.")); + return; + } + try { await this.saveAsFileToDisk(graphConfig); } catch(error) { @@ -1881,7 +2016,7 @@ export class Eagle { const destinationPalette = this.findPalette(paletteName, false); // check that a palette was found - if (destinationPalette === null){ + if (typeof destinationPalette === 'undefined'){ return; } @@ -1973,6 +2108,12 @@ export class Eagle { obj = this.logicalGraph(); break; case Eagle.FileType.GraphConfig: + if (graphConfig === null){ + Utils.showUserMessage("Error", "No graph config provided to commit."); + reject("No graph config provided to commit."); + return; + } + fileInfo = graphConfig.fileInfo; obj = graphConfig; break; @@ -1980,7 +2121,7 @@ export class Eagle { const paletteNames: string[] = this.buildReadablePaletteNamesList(); const paletteName = await Utils.userChoosePalette(paletteNames); const palette = this.findPalette(paletteName, false); - if (palette === null){ + if (typeof palette === 'undefined'){ reject("Chosen palette not found in open palettes"); return; } @@ -1996,7 +2137,7 @@ export class Eagle { // create default repository to supply to modal so that the modal is populated with useful defaults - let defaultRepository: Repository; + let defaultRepository: Repository = Repository.placeholder(); if (this.logicalGraph()){ // if the repository service is unknown (or file), probably because the graph hasn't been saved before, then @@ -2026,6 +2167,11 @@ export class Eagle { // determine a default filename let defaultFilename: string = fileInfo().location.repositoryFileName(); if (fileType === Eagle.FileType.GraphConfig){ + // abort if graphConfig is null + if (graphConfig === null){ + reject("No graph config provided to commit."); + return; + } defaultFilename = Utils.generateFilenameForGraphConfig(this.logicalGraph(), graphConfig); } @@ -2038,7 +2184,13 @@ export class Eagle { } // check repository name - const repository : Repository = Repositories.get(commit.location.repositoryService(), commit.location.repositoryName(), commit.location.repositoryBranch()); + const repository : Repository | null = Repositories.get(commit.location.repositoryService(), commit.location.repositoryName(), commit.location.repositoryBranch()); + + // abort if respository could not be found + if (repository === null){ + reject("Repository not found: " + commit.location.repositoryName()); + return; + } // TODO: a bit of a kludge here to have to create a new RepositoryFile object just to pass to _commit() const file: RepositoryFile = new RepositoryFile(repository, commit.location.repositoryPath(), commit.location.repositoryFileName()); @@ -2054,8 +2206,8 @@ export class Eagle { */ commitToGit = async (fileType : Eagle.FileType) : Promise => { return new Promise(async(resolve, reject) => { - let fileInfo : ko.Observable; - let obj : LogicalGraph | Palette; + let fileInfo : ko.Observable | undefined; + let obj : LogicalGraph | Palette | GraphConfig | undefined; // determine which object of the given filetype we are committing switch (fileType){ @@ -2068,7 +2220,7 @@ export class Eagle { const paletteNames: string[] = this.buildReadablePaletteNamesList(); const paletteName = await Utils.userChoosePalette(paletteNames); const palette = this.findPalette(paletteName, false); - if (palette === null){ + if (typeof palette === 'undefined'){ return; } @@ -2081,6 +2233,18 @@ export class Eagle { break; } + // abort if a fileInfo could not be found + if (typeof fileInfo === 'undefined'){ + reject("No fileInfo found for fileType " + fileType); + return; + } + + // abort if object could not be found + if (typeof obj === 'undefined'){ + reject("No object found for fileType " + fileType); + return; + } + console.log("fileInfo().repositoryService", fileInfo().location.repositoryService()); console.log("fileInfo().repositoryName", fileInfo().location.repositoryName()); @@ -2112,6 +2276,11 @@ export class Eagle { fileInfo().updateEagleInfo(); const repository = Repositories.getByLocation(fileInfo().location); + // abort if repository could not be found + if (repository === null){ + reject("Repository not found: " + fileInfo().location.repositoryName()); + return; + } try { // TODO: a bit of a kludge here to have to create a new RepositoryFile object just to pass to _commit() @@ -2161,7 +2330,7 @@ export class Eagle { const clone: LogicalGraph | Palette | GraphConfig = obj.clone(); clone.fileInfo().updateEagleInfo(); - const version: Setting.SchemaVersion = Setting.findValue(Setting.DALIUGE_SCHEMA_VERSION); + const version: Setting.SchemaVersion = Setting.findValue(Setting.DALIUGE_SCHEMA_VERSION, Setting.SchemaVersion.Unknown); let jsonString: string = ""; switch (file.type){ @@ -2197,10 +2366,10 @@ export class Eagle { switch (file.repository.service){ case Repository.Service.GitHub: - token = Setting.findValue(Setting.GITHUB_ACCESS_TOKEN_KEY); + token = Setting.findValue(Setting.GITHUB_ACCESS_TOKEN_KEY, ""); break; case Repository.Service.GitLab: - token = Setting.findValue(Setting.GITLAB_ACCESS_TOKEN_KEY); + token = Setting.findValue(Setting.GITLAB_ACCESS_TOKEN_KEY, ""); break; default: reject("Unknown repository service. Not GitHub or GitLab!"); @@ -2231,8 +2400,8 @@ export class Eagle { loadDefaultPalettes = async (): Promise => { // get collapsed/expanded state of palettes from html local storage - let templatePaletteExpanded: boolean = Setting.findValue(Setting.OPEN_TEMPLATE_PALETTE); - let builtinPaletteExpanded: boolean = Setting.findValue(Setting.OPEN_BUILTIN_PALETTE); + let templatePaletteExpanded: boolean = Setting.findValue(Setting.OPEN_TEMPLATE_PALETTE, false); + let builtinPaletteExpanded: boolean = Setting.findValue(Setting.OPEN_BUILTIN_PALETTE, false); templatePaletteExpanded = templatePaletteExpanded === null ? false : templatePaletteExpanded; builtinPaletteExpanded = builtinPaletteExpanded === null ? false : builtinPaletteExpanded; @@ -2241,7 +2410,7 @@ export class Eagle { {name:Palette.BUILTIN_PALETTE_NAME, filename:Daliuge.PALETTE_URL, readonly:true, expanded: builtinPaletteExpanded} ]); - const showErrors: boolean = Setting.findValue(Setting.SHOW_FILE_LOADING_ERRORS); + const showErrors: boolean = Setting.findValue(Setting.SHOW_FILE_LOADING_ERRORS, false); // display of errors if setting is true if (showErrors && (Errors.hasErrors(errorsWarnings) || Errors.hasWarnings(errorsWarnings))){ @@ -2354,13 +2523,13 @@ export class Eagle { file.isFetching(true); // check palette is not already loaded - const alreadyLoadedPalette : Palette = this.findPaletteByFile(file); - if (alreadyLoadedPalette !== null){ + const alreadyLoadedPalette = this.findPaletteByFile(file); + if (typeof alreadyLoadedPalette !== 'undefined'){ this.closePalette(alreadyLoadedPalette); } // if this is a palette, create the destination palette and add to list of palettes so that it shows in the UI - let destinationPalette: Palette = null; + let destinationPalette: Palette | null = null; if (file.type === Eagle.FileType.Palette){ destinationPalette = new Palette(); destinationPalette.isFetching(true); @@ -2383,7 +2552,7 @@ export class Eagle { break; default: console.warn("Unsure how to fetch file with unknown service ", file.repository.service); - break; + return; } // load file from github or gitlab @@ -2431,7 +2600,7 @@ export class Eagle { // warn user if file newer than EAGLE if (Utils.newerEagleVersion(eagleVersion, (window).version)){ - const confirmed = await Utils.requestUserConfirm("Newer EAGLE Version", "File " + file.name + " was written with EAGLE version " + eagleVersion + ", whereas the current EAGLE version is " + (window).version + ". Do you wish to load the file anyway?", "Yes", "No", null); + const confirmed = await Utils.requestUserConfirm("Newer EAGLE Version", "File " + file.name + " was written with EAGLE version " + eagleVersion + ", whereas the current EAGLE version is " + (window).version + ". Do you wish to load the file anyway?", "Yes", "No", undefined); if (confirmed){ this._loadGraph(data, file); } @@ -2441,6 +2610,11 @@ export class Eagle { break; } case Eagle.FileType.Palette: + // abort if destination palette is null + if (destinationPalette === null){ + Utils.showUserMessage("Error", "Destination palette is null when loading remote palette."); + return; + } this._remotePaletteLoaded(file, data, destinationPalette); break; @@ -2497,7 +2671,7 @@ export class Eagle { // check if LogicalGraph is modified, if so warn user that loading a GraphConfig may overwrite unsaved changes if (graphModified && !configMatch){ - const confirmed = await Utils.requestUserConfirm("Graph Modified", "The current graph has unsaved changes. Loading a GraphConfig may overwrite some of these changes. Do you wish to continue?", "Yes", "No", null); + const confirmed = await Utils.requestUserConfirm("Graph Modified", "The current graph has unsaved changes. Loading a GraphConfig may overwrite some of these changes. Do you wish to continue?", "Yes", "No", undefined); if (!confirmed) { return; @@ -2564,7 +2738,7 @@ export class Eagle { break; default: console.warn("Unsure how to fetch file with unknown service ", file.repository.service); - break; + return; } // load file from github or gitlab @@ -2663,7 +2837,7 @@ export class Eagle { break; default: console.warn("Unsure how to delete file with unknown service ", file.repository.service); - break; + return; } // run the delete file function @@ -2727,14 +2901,14 @@ export class Eagle { this._handleLoadingErrors(errorsWarnings, file.name, file.repository.service); } - findPaletteByFile = (file : RepositoryFile) : Palette => { + findPaletteByFile = (file : RepositoryFile) : Palette | undefined => { for (const palette of this.palettes()){ if (palette.fileInfo().name === file.name){ return palette; } } - return null; + return undefined; } closePalette = async (palette : Palette): Promise => { @@ -2745,8 +2919,8 @@ export class Eagle { if (p.fileInfo().name === palette.fileInfo().name){ // check if the palette is modified, and if so, ask the user to confirm they wish to close - if (p.fileInfo().modified && Setting.findValue(Setting.CONFIRM_DISCARD_CHANGES)){ - const confirmed = await Utils.requestUserConfirm("Close Modified Palette", "Are you sure you wish to close this modified palette?", "Close", "Cancel", null); + if (p.fileInfo().modified && Setting.findValue(Setting.CONFIRM_DISCARD_CHANGES, false)){ + const confirmed = await Utils.requestUserConfirm("Close Modified Palette", "Are you sure you wish to close this modified palette?", "Close", "Cancel", undefined); if (confirmed){ this.palettes.splice(i, 1); } @@ -2811,7 +2985,7 @@ export class Eagle { p_clone.fileInfo().updateEagleInfo(); // get version - const version: Setting.SchemaVersion = Setting.findValue(Setting.DALIUGE_SCHEMA_VERSION); + const version: Setting.SchemaVersion = Setting.findValue(Setting.DALIUGE_SCHEMA_VERSION, Setting.SchemaVersion.Unknown); // convert to json const jsonString: string = Palette.toJsonString(p_clone, version); @@ -2870,7 +3044,7 @@ export class Eagle { lg_clone.fileInfo().updateEagleInfo(); // get version - const version: Setting.SchemaVersion = Setting.findValue(Setting.DALIUGE_SCHEMA_VERSION); + const version: Setting.SchemaVersion = Setting.findValue(Setting.DALIUGE_SCHEMA_VERSION, Setting.SchemaVersion.Unknown); // convert to json const jsonString: string = LogicalGraph.toJsonString(lg_clone, false, version); @@ -2912,7 +3086,7 @@ export class Eagle { console.log("saveGraphConfigToDisk()", fileName); // get version - const version: Setting.SchemaVersion = Setting.findValue(Setting.DALIUGE_SCHEMA_VERSION); + const version: Setting.SchemaVersion = Setting.findValue(Setting.DALIUGE_SCHEMA_VERSION, Setting.SchemaVersion.Unknown); // convert to json const jsonString: string = GraphConfig.toJsonString(graphConfig); @@ -2991,7 +3165,7 @@ export class Eagle { } // check repository name - const repository : Repository = Repositories.get(commit.location.repositoryService(), commit.location.repositoryName(), commit.location.repositoryBranch()); + const repository : Repository | null = Repositories.get(commit.location.repositoryService(), commit.location.repositoryName(), commit.location.repositoryBranch()); if (repository === null){ console.log("Abort commit"); return; @@ -3002,10 +3176,10 @@ export class Eagle { switch (commit.location.repositoryService()){ case Repository.Service.GitHub: - token = Setting.findValue(Setting.GITHUB_ACCESS_TOKEN_KEY); + token = Setting.findValue(Setting.GITHUB_ACCESS_TOKEN_KEY, ""); break; case Repository.Service.GitLab: - token = Setting.findValue(Setting.GITLAB_ACCESS_TOKEN_KEY); + token = Setting.findValue(Setting.GITLAB_ACCESS_TOKEN_KEY, ""); break; default: Utils.showUserMessage("Error", "Unknown repository service. Not GitHub or GitLab!"); @@ -3023,7 +3197,7 @@ export class Eagle { p_clone.fileInfo().updateEagleInfo(); // get version - const version: Setting.SchemaVersion = Setting.findValue(Setting.DALIUGE_SCHEMA_VERSION); + const version: Setting.SchemaVersion = Setting.findValue(Setting.DALIUGE_SCHEMA_VERSION, Setting.SchemaVersion.Unknown); // convert to json const jsonString: string = Palette.toJsonString(p_clone, version); @@ -3045,7 +3219,7 @@ export class Eagle { const lg: LogicalGraph = Eagle.getInstance().logicalGraph(); // get schema version - const version: Setting.SchemaVersion = Setting.findValue(Setting.DALIUGE_SCHEMA_VERSION); + const version: Setting.SchemaVersion = Setting.findValue(Setting.DALIUGE_SCHEMA_VERSION, Setting.SchemaVersion.Unknown); // get json for logical graph const jsonString: string = LogicalGraph.toJsonString(lg, true, version); @@ -3083,12 +3257,29 @@ export class Eagle { //prepare the graph for a screenshot eagle.centerGraph() eagle.setSelection(null,Eagle.FileType.Graph) - document.querySelector('body').style.cursor = 'none';//temporarily disabling the cursor so it doesn't appear in the screenshot + + // get reference to the body element + const bodyElement = document.querySelector('body'); + + // abort if the body element is not found + if (!bodyElement) { + Utils.showNotification("Error", "Unable to access body element for screenshot.", "danger"); + return; + } + + // temporarily disabling the cursor so it doesn't appear in the screenshot + bodyElement.style.cursor = 'none'; try { const width = stream.getVideoTracks()[0].getSettings().width const height = stream.getVideoTracks()[0].getSettings().height + // abort if width or height is undefined + if (width === undefined || height === undefined) { + Utils.showNotification("Error", "Unable to determine screen width and height for screenshot.", "danger"); + return; + } + const video = document.createElement("video") video.srcObject = stream video.autoplay = true @@ -3099,22 +3290,28 @@ export class Eagle { }) setTimeout(() => { - const canvas = document.createElement("canvas") + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext('2d'); + + // abort if ctx is null + if (ctx === null) { + Utils.showNotification("Error", "Unable to get canvas context for screenshot.", "danger"); + return; + } + canvas.width = width canvas.height = height //cropping the ui, so the screenshot only includes the graph - const ctx = canvas.getContext('2d'); + const realWidth = window.innerWidth const divisor = realWidth/width - const lx = (eagle.leftWindow().size()+50)/divisor const rx = (eagle.rightWindow().size()+50)/divisor const ly = 90/divisor canvas.width=width-rx-lx//trimming the right window ctx.translate(-lx,-ly) - - canvas.getContext("2d").drawImage(video, 0, 0) + ctx.drawImage(video, 0, 0) const png = canvas.toDataURL() // Element that will be used for downloading. @@ -3129,7 +3326,7 @@ export class Eagle { a.click(); window.URL.revokeObjectURL(a.href); document.body.removeChild(a); - document.querySelector('body').style.cursor = 'auto'; + bodyElement.style.cursor = 'auto'; }, 400); } finally { setTimeout(() => { @@ -3139,19 +3336,25 @@ export class Eagle { } toggleEdgeClosesLoop = () : void => { - this.selectedEdge().toggleClosesLoop(); + const selectedEdge = this.selectedEdge(); - // get nodes from edge - const sourceNode = this.selectedEdge().getSrcNode(); - const destNode = this.selectedEdge().getDestNode(); + if (selectedEdge === null){ + Utils.showNotification("Error", "No edge selected", "danger"); + return; + } + + selectedEdge.toggleClosesLoop(); - sourceNode.setGroupEnd(this.selectedEdge().isClosesLoop()); - destNode.setGroupStart(this.selectedEdge().isClosesLoop()); + // get nodes from edge + const sourceNode = selectedEdge.getSrcNode(); + const destNode = selectedEdge.getDestNode(); + sourceNode.setGroupEnd(selectedEdge.isClosesLoop()); + destNode.setGroupStart(selectedEdge.isClosesLoop()); this.checkGraph(); - const groupStartValue = destNode.getFieldByDisplayText(Daliuge.FieldName.GROUP_START).getValue(); - const groupEndValue = sourceNode.getFieldByDisplayText(Daliuge.FieldName.GROUP_END).getValue(); + const groupStartValue = destNode.findFieldByDisplayText(Daliuge.FieldName.GROUP_START)?.getValue(); + const groupEndValue = sourceNode.findFieldByDisplayText(Daliuge.FieldName.GROUP_END)?.getValue(); Utils.showNotification( "Toggle edge closes loop", "Node " + sourceNode.getName() + " component parameter '" + Daliuge.FieldName.GROUP_END + "' set to " + groupEndValue + ". Node " + destNode.getName() + " component parameter '" + Daliuge.FieldName.GROUP_START + "' set to " + groupStartValue + ".", "success" @@ -3242,14 +3445,21 @@ export class Eagle { incomingNodes = this.selectedObjects() }else{ location = Eagle.selectedRightClickLocation() - incomingNodes.push(Eagle.selectedRightClickObject()) + + const selectedRightClickObject = Eagle.selectedRightClickObject() + + if(selectedRightClickObject === null){ + Utils.showNotification('Unable to duplicate selection','No node or edge was right-clicked','warning') + return + } + incomingNodes.push(selectedRightClickObject) } switch(location){ case Eagle.FileType.Graph: { // check that graph editing is allowed - if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING)){ + if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false)){ Utils.notifyUserOfEditingIssue(Eagle.FileType.Graph, "Duplicate Selection"); return; } @@ -3285,7 +3495,7 @@ export class Eagle { case Eagle.FileType.Palette: { // check that palette editing is allowed - if (!Setting.findValue(Setting.ALLOW_PALETTE_EDITING)){ + if (!Setting.findValue(Setting.ALLOW_PALETTE_EDITING, false)){ Utils.showNotification("Unable to Duplicate Selection", "Palette Editing is disabled", "danger"); return; } @@ -3377,7 +3587,9 @@ export class Eagle { _removeAlreadySelectedEmbeddedNodes = (nodes: Node[]) : Node[] => { const newNodes: Node[] = []; for (const node of nodes){ - if (node.isEmbedded() && this.objectIsSelected(node.getEmbed())){ + const nodeEmbed = node.getEmbed(); + + if (nodeEmbed !== null && this.objectIsSelected(nodeEmbed)){ continue; // skip this node, as it is already selected } newNodes.push(node); @@ -3425,7 +3637,7 @@ export class Eagle { console.log("pasteFromClipboard()"); // check that graph editing is allowed - if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING)){ + if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false)){ Utils.notifyUserOfEditingIssue(Eagle.FileType.Graph, "Paste from Clipboard"); return; } @@ -3458,7 +3670,9 @@ export class Eagle { for (const e of clipboard.edges){ const edge = Edge.fromOJSJson(e, nodes, errorsWarnings); - edges.push(edge); + if (edge !== null){ + edges.push(edge); + } } // set parent links @@ -3548,7 +3762,7 @@ export class Eagle { const destinationPalette = this.findPalette(userString, true); // check that a palette was found - if (destinationPalette === null){ + if (typeof destinationPalette === "undefined"){ Utils.showUserMessage("Error", "Unable to find selected palette!"); return; } @@ -3568,13 +3782,16 @@ export class Eagle { const nodes: Node[] = Array.from(destinationPalette.getNodes()); const embedNode: Node = nodes[destinationPalette.getNumNodes() - 1]; + const inputApplication = node.getInputApplication(); + const outputApplication = node.getOutputApplication(); + // check if clone has embedded applications, if so, add them to destination palette and remove - if (node.hasInputApplication()){ - destinationPalette.addNode(node.getInputApplication(), true); + if (inputApplication !== null){ + destinationPalette.addNode(inputApplication, true); nodes[destinationPalette.getNumNodes() - 1].setEmbed(embedNode); } - if (node.hasOutputApplication()){ - destinationPalette.addNode(node.getOutputApplication(), true); + if (outputApplication !== null){ + destinationPalette.addNode(outputApplication, true); nodes[destinationPalette.getNumNodes() - 1].setEmbed(embedNode); } @@ -3616,7 +3833,12 @@ export class Eagle { GraphRenderer.clearPortPeek() if (rightClick){ - data.push(Eagle.selectedRightClickObject()) + const selectedRightClickObject = Eagle.selectedRightClickObject(); + if(selectedRightClickObject === null){ + console.error("deleteSelection(): right click object is null"); + return; + } + data.push(selectedRightClickObject) location = Eagle.selectedRightClickLocation(); }else{ data = this.selectedObjects() @@ -3624,7 +3846,7 @@ export class Eagle { } // check that graph editing is allowed - if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING)){ + if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false)){ Utils.notifyUserOfEditingIssue(Eagle.FileType.Graph, "Delete Selection"); return; } @@ -3642,7 +3864,7 @@ export class Eagle { } // skip confirmation if setting dictates - if (!Setting.find(Setting.CONFIRM_DELETE_OBJECTS).value() || suppressUserConfirmationRequest){ + if (!Setting.findValue(Setting.CONFIRM_DELETE_OBJECTS, true) || suppressUserConfirmationRequest){ this._deleteSelection(deleteChildren, data, location); // if we're NOT in rightClick mode, empty the selected objects, should have all been deleted @@ -3810,7 +4032,9 @@ export class Eagle { for (const object of this.selectedObjects()){ if (object instanceof Node){ for (const node of this.logicalGraph().getNodes()){ - if (node.getParent() !== null && node.getParent().getId() === object.getId()){ + const nodeParent = node.getParent(); + + if (nodeParent !== null && nodeParent.getId() === object.getId()){ node.setParent(object.getParent()); } } @@ -3819,18 +4043,28 @@ export class Eagle { } addNodeToLogicalGraphAndConnect = async (newNodeId: NodeId) => { - const nodes: Node[] = await this.addNodeToLogicalGraph(null, newNodeId, Eagle.AddNodeMode.ContextMenu); + const nodes: Node[] = await this.addNodeToLogicalGraph(undefined, newNodeId, Eagle.AddNodeMode.ContextMenu); - const realSourceNode: Node = RightClick.edgeDropSrcNode; - const realSourcePort: Field = RightClick.edgeDropSrcPort; + const realSourceNode: Node | null = RightClick.edgeDropSrcNode; + const realSourcePort: Field | null = RightClick.edgeDropSrcPort; const realDestNode: Node = nodes[0]; - let realDestPort = realDestNode.findPortByMatchingType(realSourcePort.getType(), !RightClick.edgeDropSrcIsInput); + let realDestPort: Field | null = null; + + if (realSourcePort !== null){ + realDestPort = realDestNode.findPortByMatchingType(realSourcePort.getType(), !RightClick.edgeDropSrcIsInput); + } // if no dest port was found, just use first input port on dest node if (realDestPort === null){ realDestPort = realDestNode.findPortOfAnyType(true); } + // abort if we don't have sourceNode, destNode, sourcePort and destPort + if (realSourceNode === null || realSourcePort === null || realDestNode === null || realDestPort === null){ + Utils.showNotification("Error", "Unable to create edge: missing source or destination node or port", "danger"); + return; + } + // create edge (in correct direction) let edge: Edge; if (!RightClick.edgeDropSrcIsInput){ @@ -3847,7 +4081,7 @@ export class Eagle { this.logicalGraph.valueHasMutated(); } - addNodeToLogicalGraph = (node: Node, nodeId: NodeId, mode: Eagle.AddNodeMode): Promise => { + addNodeToLogicalGraph = (node: Node | undefined, nodeId: NodeId | null, mode: Eagle.AddNodeMode): Promise => { return new Promise(async(resolve, reject) => { const result: Node[] = []; let pos : {x:number, y:number}; @@ -3855,15 +4089,21 @@ export class Eagle { let searchAreaExtended = false; //used if we cant find space on the canvas, we then extend the search area for space and center the graph after adding to bring new nodes into view // check that graph editing is allowed - if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING)){ + if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false)){ reject("Unable to Add Component. Graph Editing is disabled"); return; } if(mode === Eagle.AddNodeMode.ContextMenu){ - // we addNodeToLogicalGraph is called from the ContextMenu, we expect node to be null. The node is specified by the nodeId instead + // when addNodeToLogicalGraph is called from the ContextMenu, we expect node to be null. The node is specified by the nodeId instead console.assert(node === null); + // check that nodeId is not null + if (nodeId === null){ + reject(new Error("nodeId is null")); + return; + } + // try to find the node (by nodeId) in the palettes node = Utils.getPaletteComponentById(nodeId); @@ -3885,6 +4125,12 @@ export class Eagle { RightClick.closeCustomContextMenu(true); } + // abort if node is still undefined + if (typeof node === 'undefined'){ + reject(new Error("Node is undefined")); + return; + } + // if node is a construct, set width and height a little larger if (node.isGroup()){ node.setRadius(EagleConfig.MINIMUM_CONSTRUCT_RADIUS); @@ -3903,7 +4149,7 @@ export class Eagle { } // check for parent before adding the node - const parent : Node = this.logicalGraph().checkForNodeAt(pos.x, pos.y, EagleConfig.MINIMUM_CONSTRUCT_RADIUS, true); + const parent : Node | null = this.logicalGraph().checkForNodeAt(pos.x, pos.y, EagleConfig.MINIMUM_CONSTRUCT_RADIUS, true); // add the node const newNode: Node = await this.addNode(node, pos.x, pos.y); @@ -3914,18 +4160,21 @@ export class Eagle { // if the node is a construct, add the input and output applications, if they exist if (node.isGroup()){ + const inputApplication = node.getInputApplication(); + const outputApplication = node.getOutputApplication(); + // check if the node has an input application, if so, add it - if (node.hasInputApplication()){ + if (inputApplication !== null){ // add the input application to the logical graph - const inputApp: Node = await this.addNode(node.getInputApplication(), 0, 0); + const inputApp: Node = await this.addNode(inputApplication, 0, 0); newNode.setInputApplication(inputApp); result.push(inputApp); } // check if the node has an output application, if so, add it - if (node.hasOutputApplication()){ - // add the input application to the logical graph - const outputApp: Node = await this.addNode(node.getOutputApplication(), 0, 0); - newNode.setInputApplication(outputApp); + if (outputApplication !== null){ + // add the output application to the logical graph + const outputApp: Node = await this.addNode(outputApplication, 0, 0); + newNode.setInputApplication(outputApp); // TODO: bug? should this be setOutputApplication?` result.push(outputApp); } } @@ -3939,15 +4188,18 @@ export class Eagle { let poName: string = Daliuge.FieldName.SELF; // use this as a fall-back default // use the dataType of the self field - const selfField = newNode.getFieldByDisplayText(Daliuge.FieldName.SELF); - if (selfField !== null){ + const selfField = newNode.findFieldByDisplayText(Daliuge.FieldName.SELF); + if (typeof selfField !== 'undefined'){ poName = selfField.getType(); } // get name of the "base" class from the PythonMemberFunction node, - const baseNameField = newNode.getFieldByDisplayText(Daliuge.FieldName.BASE_NAME); - if (baseNameField !== null){ - poName = baseNameField.getValue(); + const baseNameField = newNode.findFieldByDisplayText(Daliuge.FieldName.BASE_NAME); + if (typeof baseNameField !== 'undefined'){ + const value = baseNameField.getValue(); + if (value !== null){ + poName = value; + } } // create node @@ -3967,10 +4219,10 @@ export class Eagle { Utils.copyFieldsFromPrototype(pythonObjectNode, Palette.BUILTIN_PALETTE_NAME, Category.PythonObject); // find the "object" port on the PythonMemberFunction - let sourcePort: Field = newNode.findPortByDisplayText(Daliuge.FieldName.SELF, false, false); + let sourcePort = newNode.findPortByDisplayText(Daliuge.FieldName.SELF, false, false); // make sure we can find a port on the PythonMemberFunction - if (sourcePort === null){ + if (typeof sourcePort === 'undefined'){ sourcePort = Daliuge.selfField.clone().setId(Utils.generateFieldId()); newNode.addField(sourcePort); Utils.showNotification("Component Warning", "The PythonMemberFunction does not have a '" + Daliuge.FieldName.SELF + "' port. Added this port to enable connection.", "warning"); @@ -4003,7 +4255,7 @@ export class Eagle { addGraphNodesToPalette = async () => { // check that palette editing is permitted - if (!Setting.findValue(Setting.ALLOW_PALETTE_EDITING)){ + if (!Setting.findValue(Setting.ALLOW_PALETTE_EDITING, false)){ Utils.notifyUserOfEditingIssue(Eagle.FileType.Palette, "Add Graph Nodes to Palette"); return; } @@ -4041,19 +4293,22 @@ export class Eagle { const destinationPalette = this.findPalette(userString, true); // check that a palette was found - if (destinationPalette === null){ + if (typeof destinationPalette === "undefined"){ Utils.showUserMessage("Error", "Unable to find selected palette!"); return; } // copy nodes to palette for (const node of this.logicalGraph().getNodes()){ + const inputApplication = node.getInputApplication(); + const outputApplication = node.getOutputApplication(); + // check if clone has embedded applications, if so, add them to destination palette and remove - if (node.hasInputApplication()){ - destinationPalette.addNode(node.getInputApplication(), false); + if (inputApplication !== null){ + destinationPalette.addNode(inputApplication, false); } - if (node.hasOutputApplication()){ - destinationPalette.addNode(node.getOutputApplication(), false); + if (outputApplication){ + destinationPalette.addNode(outputApplication, false); } destinationPalette.addNode(node, false); @@ -4094,8 +4349,8 @@ export class Eagle { return paletteNames; } - findPalette = (name: string, createIfNotFound: boolean) : Palette => { - let p: Palette = null; + findPalette = (name: string, createIfNotFound: boolean) : Palette | undefined => { + let p: Palette | undefined = undefined; // look for palette in open palettes for (const palette of this.palettes()){ @@ -4127,20 +4382,22 @@ export class Eagle { } // get imageName, tag, digest values in currently selected node - const imageField: Field = selectedNode.getFieldByDisplayText(Daliuge.FieldName.IMAGE); - const tagField: Field = selectedNode.getFieldByDisplayText(Daliuge.FieldName.TAG); - const digestField: Field = selectedNode.getFieldByDisplayText(Daliuge.FieldName.DIGEST); - let image, tag, digest: string = ""; + const imageField = selectedNode.findFieldByDisplayText(Daliuge.FieldName.IMAGE); + const tagField = selectedNode.findFieldByDisplayText(Daliuge.FieldName.TAG); + const digestField = selectedNode.findFieldByDisplayText(Daliuge.FieldName.DIGEST); + let image: string = ""; + let tag: string = ""; + let digest: string = ""; // set values for the fields - if (imageField !== null){ - image = imageField.getValue(); + if (typeof imageField !== 'undefined'){ + image = imageField.getValue() || ""; } - if (tagField !== null){ - tag = tagField.getValue(); + if (typeof tagField !== 'undefined'){ + tag = tagField.getValue() || ""; } - if (digestField !== null){ - digest = digestField.getValue(); + if (typeof digestField !== 'undefined'){ + digest = digestField.getValue() || ""; } Modals.showBrowseDockerHub(image, tag, (completed: boolean) => { @@ -4153,13 +4410,13 @@ export class Eagle { const digest = this.dockerHubBrowser().digest(); // set values for the fields - if (imageField !== null){ + if (typeof imageField !== 'undefined'){ imageField.setValue(imageName); } - if (tagField !== null){ + if (typeof tagField !== 'undefined'){ tagField.setValue(tag); } - if (digestField !== null){ + if (typeof digestField !== 'undefined'){ digestField.setValue(digest); } @@ -4172,8 +4429,15 @@ export class Eagle { // therefore, we add at least one option, so the value remains well defined if (newType === Daliuge.DataType.Select){ if (field.getOptions().length === 0){ - field.addOption(field.getValue()); - field.addOption(field.getDefaultValue()); + const value = field.getValue(); + const defaultValue = field.getDefaultValue(); + + if (value !== null){ + field.addOption(value); + } + if (defaultValue !== null){ + field.addOption(defaultValue); + } } } @@ -4198,7 +4462,7 @@ export class Eagle { changeNodeParent = async () => { // build list of node name + ids (exclude self) - const selectedNode: Node = this.selectedNode(); + const selectedNode = this.selectedNode(); if (selectedNode === null){ Utils.showNotification("Unable to Change Node Parent", "Attempt to change parent node when no node selected", "warning"); @@ -4206,7 +4470,7 @@ export class Eagle { } // check that graph editing is allowed - if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING)){ + if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false)){ Utils.notifyUserOfEditingIssue(Eagle.FileType.Graph, "Change Node Parent") return; } @@ -4232,8 +4496,14 @@ export class Eagle { // this index only counts up if the above doesn't filter out the choice validChoiceIndex++ + const selectedNodeParent = selectedNode.getParent(); + // if the selected node has no parent, then we can't preselect anything + if (selectedNodeParent === null){ + continue; + } + // if this node is already the parent, note its index, so that we can preselect this parent node in the modal dialog - if (node.getId() === selectedNode.getParent().getId()){ + if (node.getId() === selectedNodeParent.getId()){ selectedChoiceIndex = validChoiceIndex; } @@ -4255,7 +4525,7 @@ export class Eagle { // change the parent // key '0' is a special case const newParentId: NodeId = choice.substring(choice.lastIndexOf(" ") + 1).toString() as NodeId - const newParent: Node = this.logicalGraph().getNodeById(newParentId); + const newParent = this.logicalGraph().getNodeById(newParentId); // abort if specified new parent can not be found in the graph if (typeof newParent === 'undefined'){ @@ -4289,7 +4559,11 @@ export class Eagle { // if some node in the graph is selected, ignore it and used the node that was dragged from the palette if (Eagle.selectedLocation() === Eagle.FileType.Graph || Eagle.selectedLocation() === Eagle.FileType.Unknown){ - const component: Node = this.palettes()[Eagle.nodeDragPaletteIndex].getNodeById(Eagle.nodeDragComponentId); + const component = this.palettes()[Eagle.nodeDragPaletteIndex].getNodeById(Eagle.nodeDragComponentId); + if (typeof component === 'undefined'){ + console.error("Unable to find dragged component in palette"); + return; + } sourceComponents.push(component); } @@ -4325,7 +4599,13 @@ export class Eagle { // if some node in the graph is selected, ignore it and used the node that was dragged from the palette if (Eagle.selectedLocation() === Eagle.FileType.Graph || Eagle.selectedLocation() === Eagle.FileType.Unknown){ - const component: Node = this.palettes()[Eagle.nodeDragPaletteIndex].getNodeById(Eagle.nodeDragComponentId); + const component = this.palettes()[Eagle.nodeDragPaletteIndex].getNodeById(Eagle.nodeDragComponentId); + + if (typeof component === 'undefined'){ + console.error("Unable to find dragged component in palette"); + return; + } + sourceComponents.push(component); } @@ -4339,10 +4619,16 @@ export class Eagle { } // determine destination palette - const destinationPaletteIndex : number = parseInt((e.currentTarget as HTMLElement).getAttribute('data-palette-index'), 10); + const target = e.currentTarget as HTMLElement; + const targetPaletteIndexData = target.getAttribute('data-palette-index'); + if (targetPaletteIndexData === null){ + console.error("Unable to determine destination palette index from drop target"); + return; + } + const destinationPaletteIndex : number = parseInt(targetPaletteIndexData, 10); const destinationPalette: Palette = this.palettes()[destinationPaletteIndex]; - const allowReadonlyPaletteEditing = Setting.findValue(Setting.ALLOW_READONLY_PALETTE_EDITING); + const allowReadonlyPaletteEditing = Setting.findValue(Setting.ALLOW_READONLY_PALETTE_EDITING, false); // check user can write to destination palette if (destinationPalette.fileInfo().readonly && !allowReadonlyPaletteEditing){ @@ -4375,11 +4661,21 @@ export class Eagle { } selectInputApplicationNode = () : void => { - this.setSelection(this.selectedNode().getInputApplication(), Eagle.FileType.Graph); + const selectedNode = this.selectedNode(); + if (selectedNode === null){ + Utils.showNotification("No node selected", "Please select a node before trying to select its input application", "warning"); + return; + } + this.setSelection(selectedNode.getInputApplication(), Eagle.FileType.Graph); } selectOutputApplicationNode = () : void => { - this.setSelection(this.selectedNode().getOutputApplication(), Eagle.FileType.Graph); + const selectedNode = this.selectedNode(); + if (selectedNode === null){ + Utils.showNotification("No node selected", "Please select a node before trying to select its output application", "warning"); + return; + } + this.setSelection(selectedNode.getOutputApplication(), Eagle.FileType.Graph); } editField = async (field: Field): Promise => { @@ -4399,8 +4695,15 @@ export class Eagle { allFieldNames.push(field.getDisplayText() + " (" + field.getType() + ")"); } + // check that there is a node selected + const selectedNode = this.selectedNode(); + if (selectedNode === null){ + console.error("No node selected while trying to edit field"); + return; + } + // build modal header text - const title = this.selectedNode().getName() + " - " + field.getDisplayText() + " : " + Field.getHtmlTitleText(field.getParameterType(), field.getUsage()); + const title = selectedNode.getName() + " - " + field.getDisplayText() + " : " + Field.getHtmlTitleText(field.getParameterType(), field.getUsage()); try { await Utils.requestUserEditField(this, field, title, allFieldNames); @@ -4423,19 +4726,28 @@ export class Eagle { let numIterations = 0; let increaseSearchArea = false const MAX_ITERATIONS = 150; - let x; - let y; + let x = 0; + let y = 0; while (!suitablePositionFound && numIterations <= MAX_ITERATIONS){ + const leftWindowVisible = Setting.findValue(Setting.LEFT_WINDOW_VISIBLE, false); + const rightWindowVisible = Setting.findValue(Setting.RIGHT_WINDOW_VISIBLE, false); + const bottomWindowVisible = Setting.findValue(Setting.BOTTOM_WINDOW_VISIBLE, false); + + // get logical graph display area dimensions + const logicalGraphParentWidth = $('#logicalGraphParent').width() || 0; + const logicalGraphParentHeight = $('#logicalGraphParent').height() || 0; + const bottomWindowHeight = $('#bottomWindow').height() || 0; + // get visible screen size - let minX = Setting.findValue(Setting.LEFT_WINDOW_VISIBLE) ? this.leftWindow().size()+MARGIN: 0+MARGIN; - let maxX = Setting.findValue(Setting.RIGHT_WINDOW_VISIBLE) ? $('#logicalGraphParent').width() - this.rightWindow().size() - MARGIN : $('#logicalGraphParent').width() - MARGIN; + let minX = leftWindowVisible ? this.leftWindow().size()+MARGIN: 0+MARGIN; + let maxX = rightWindowVisible ? logicalGraphParentWidth - this.rightWindow().size() - MARGIN : logicalGraphParentWidth - MARGIN; let minY = 0 + navBarHeight + MARGIN; //using jquery here to get the bottom window height because it is internally saved in VH (percentage screen height). Doing it this way means we don't have to convert it to pixels - let maxY = $('#logicalGraphParent').height() - MARGIN + navBarHeight + let maxY = logicalGraphParentHeight - MARGIN + navBarHeight; - if(Setting.findValue(Setting.BOTTOM_WINDOW_VISIBLE)){ - maxY = $('#logicalGraphParent').height() - $('#bottomWindow').height() - MARGIN + navBarHeight; + if(bottomWindowVisible){ + maxY = logicalGraphParentHeight - bottomWindowHeight - MARGIN + navBarHeight; } if(increaseSearchArea){ @@ -4523,7 +4835,8 @@ export class Eagle { this.errorsMode(Errors.Mode.Graph); //switch bottom window mode - Setting.find(Setting.BOTTOM_WINDOW_MODE).setValue(Eagle.BottomWindowMode.GraphErrors) + Setting.setValue(Setting.BOTTOM_WINDOW_MODE, Eagle.BottomWindowMode.GraphErrors); + //show bottom window SideWindow.setShown('bottom',true) } else { @@ -4552,7 +4865,7 @@ export class Eagle { } // check that graph editing is allowed - if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING)){ + if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false)){ reject("Unable to Add Edge: Graph Editing is disabled"); return; } @@ -4564,7 +4877,8 @@ export class Eagle { const twoEventPorts : boolean = srcPort.getIsEvent() && destPort.getIsEvent(); // consult the DEFAULT_DATA_NODE setting to determine which category of intermediate data node to use - let intermediaryComponent = Utils.getPaletteComponentByName(Setting.findValue(Setting.DEFAULT_DATA_NODE)); + const defaultData = Setting.findValue(Setting.DEFAULT_DATA_NODE, Category.Memory); + let intermediaryComponent = Utils.getPaletteComponentByName(defaultData || ""); // if intermediaryComponent is undefined (not found), then choose something guaranteed to be available // if intermediaryComponent is defined (found), then duplicate the node so that we don't modify the original in the palette @@ -4583,7 +4897,7 @@ export class Eagle { // re-name node and port according to the port name of the Application node //force auto rename use used when we are adding in a new node. When dragging an edge to empty space or connecting two application nodes. - if (!Setting.findValue(Setting.DISABLE_RENAME_ON_EDGE_CONNECT) || forceAutoRename){ + if (!Setting.findValue(Setting.DISABLE_RENAME_ON_EDGE_CONNECT, false) || forceAutoRename){ if (srcNode.isApplication()){ const newName = srcPort.getDisplayText(); const newDescription = srcPort.getDescription(); @@ -4611,11 +4925,13 @@ export class Eagle { let destNodePosition = destNode.getPosition(); // if source or destination node is an embedded application, use position of parent construct node - if (srcNode.isEmbedded()){ - srcNodePosition = srcNode.getEmbed().getPosition(); + const srcNodeEmbed = srcNode.getEmbed(); + const destNodeEmbed = destNode.getEmbed(); + if (srcNodeEmbed !== null){ + srcNodePosition = srcNodeEmbed.getPosition(); } - if (destNode.isEmbedded()){ - destNodePosition = destNode.getEmbed().getPosition(); + if (destNodeEmbed !== null){ + destNodePosition = destNodeEmbed.getPosition(); } // count number of edges between source and destination @@ -4639,20 +4955,24 @@ export class Eagle { newNode.removeAllOutputPorts(); // add InputOutput port for dataType - const newInputOutputPort = new Field(Utils.generateFieldId(), srcPort.getDisplayText(), "", "", srcPort.getDescription(), false, srcPort.getType(), false, [], false, Daliuge.FieldType.Application, Daliuge.FieldUsage.InputOutput); + const newInputOutputPort = new Field(newNode, Utils.generateFieldId(), srcPort.getDisplayText(), "", "", srcPort.getDescription(), false, srcPort.getType(), false, [], false, Daliuge.FieldType.Application, Daliuge.FieldUsage.InputOutput); + // TODO: is this required, or will the Field constructor already have set this correctly? newNode.addField(newInputOutputPort); + const srcNodeParent = srcNode.getParent(); + const destNodeParent = destNode.getParent(); + // set the parent of the new node // by default, set parent to parent of dest node, - newNode.setParent(destNode.getParent()); + newNode.setParent(destNodeParent); // if source node is a child of dest node, make the new node a child too - if (srcNode.getParent() !== null && srcNode.getParent().getId() === destNode.getId()){ + if (srcNodeParent !== null && srcNodeParent.getId() === destNode.getId()){ newNode.setParent(destNode); } // if dest node is a child of source node, make the new node a child too - if (destNode.getParent() !== null && destNode.getParent().getId() === srcNode.getId()){ + if (destNodeParent !== null && destNodeParent.getId() === srcNode.getId()){ newNode.setParent(srcNode); } @@ -4669,7 +4989,7 @@ export class Eagle { } editShortDescription = async(fileInfo: FileInfo): Promise => { - const markdownEditingEnabled: boolean = Setting.findValue(Setting.MARKDOWN_EDITING_ENABLED); + const markdownEditingEnabled: boolean = Setting.findValue(Setting.MARKDOWN_EDITING_ENABLED, false); let description: string; try { @@ -4687,7 +5007,7 @@ export class Eagle { } editDetailedDescription = async(fileInfo: FileInfo): Promise => { - const markdownEditingEnabled: boolean = Setting.findValue(Setting.MARKDOWN_EDITING_ENABLED); + const markdownEditingEnabled: boolean = Setting.findValue(Setting.MARKDOWN_EDITING_ENABLED, false); let description: string; try { @@ -4705,8 +5025,15 @@ export class Eagle { } editNodeDescription = async (node?: Node): Promise => { - const markdownEditingEnabled: boolean = Setting.findValue(Setting.MARKDOWN_EDITING_ENABLED); + const markdownEditingEnabled: boolean = Setting.findValue(Setting.MARKDOWN_EDITING_ENABLED, false); const targetNode = node || this.selectedNode(); + + // abort if no node is selected AND no node was passed in + if (targetNode === null) { + console.warn("No node selected"); + return; + } + let nodeDescription: string; try { nodeDescription = await Utils.requestUserMarkdown(targetNode.getDisplayName() + " - Description", targetNode.getDescription(), markdownEditingEnabled); @@ -4719,7 +5046,7 @@ export class Eagle { } editNodeComment = async (): Promise => { - const markdownEditingEnabled: boolean = Setting.findValue(Setting.MARKDOWN_EDITING_ENABLED); + const markdownEditingEnabled: boolean = Setting.findValue(Setting.MARKDOWN_EDITING_ENABLED, false); const node = this.selectedNode() // abort if no node is selected @@ -4740,7 +5067,7 @@ export class Eagle { } editEdgeComment = async (): Promise => { - const markdownEditingEnabled: boolean = Setting.findValue(Setting.MARKDOWN_EDITING_ENABLED); + const markdownEditingEnabled: boolean = Setting.findValue(Setting.MARKDOWN_EDITING_ENABLED, false); const edge = this.selectedEdge() // abort if no edge is selected @@ -4764,9 +5091,11 @@ export class Eagle { let category : Category = Category.Unknown; let categoryType: Category.Type = Category.Type.Unknown; - if (this.selectedNode() !== null){ - category = this.selectedNode().getCategory(); - categoryType = this.selectedNode().getCategoryType(); + const selectedNode = this.selectedNode(); + + if (selectedNode !== null){ + category = selectedNode.getCategory(); + categoryType = selectedNode.getCategoryType(); } // if selectedNode categoryType is Unknown, return list of all categories @@ -4779,8 +5108,8 @@ export class Eagle { }, this) inspectorChangeNodeCategoryRequest = async (event: Event): Promise => { - const confirmNodeCategoryChanges = Setting.findValue(Setting.CONFIRM_NODE_CATEGORY_CHANGES); - const keepOldFields = Setting.findValue(Setting.KEEP_OLD_FIELDS_DURING_CATEGORY_CHANGE); + const confirmNodeCategoryChanges = Setting.findValue(Setting.CONFIRM_NODE_CATEGORY_CHANGES, false); + const keepOldFields = Setting.findValue(Setting.KEEP_OLD_FIELDS_DURING_CATEGORY_CHANGE, false); // request confirmation from user // old request if 'confirm' setting is true AND we're not going to keep the old fields @@ -4789,8 +5118,13 @@ export class Eagle { if (confirmed){ this.inspectorChangeNodeCategory(event) } else { + const selectedNode = this.selectedNode(); + if (selectedNode === null){ + console.error("No selected node to reset category selection for"); + return; + } // reset the category selection in the inspector to match the node's actual category - $('#objectInspectorCategorySelect').val(this.selectedNode().getCategory()); + $('#objectInspectorCategorySelect').val(selectedNode.getCategory()); } }else{ this.inspectorChangeNodeCategory(event) @@ -4798,13 +5132,24 @@ export class Eagle { } inspectorChangeNodeCategory = (event: Event) : void => { + if (event.target === null){ + console.error("No event target for inspectorChangeNodeCategory"); + return; + } + const newNodeCategory: Category = $(event.target).val() as Category; const newNodeCategoryType: Category.Type = CategoryData.getCategoryData(newNodeCategory).categoryType; const oldNode = this.selectedNode(); + // abort if no node selected + if (oldNode === null){ + console.error("No selected node to change category for"); + return; + } + // try to find new node category in palettes - let oldCategoryTemplate: Node = Utils.getPaletteComponentByName(oldNode.getCategory(), true); - const newCategoryTemplate: Node = Utils.getPaletteComponentByName(newNodeCategory, true); + let oldCategoryTemplate = Utils.getPaletteComponentByName(oldNode.getCategory(), true); + const newCategoryTemplate = Utils.getPaletteComponentByName(newNodeCategory, true); // check that new category prototype was found, if not, skip transform node if (typeof newCategoryTemplate === "undefined"){ @@ -4817,7 +5162,7 @@ export class Eagle { } // consult user setting - whether they want to remove old fields - const keepOldFields: boolean = Setting.findValue(Setting.KEEP_OLD_FIELDS_DURING_CATEGORY_CHANGE); + const keepOldFields: boolean = Setting.findValue(Setting.KEEP_OLD_FIELDS_DURING_CATEGORY_CHANGE, false); Utils.transformNodeFromTemplates(oldNode, oldCategoryTemplate, newCategoryTemplate, keepOldFields); } @@ -4877,7 +5222,7 @@ export class Eagle { } // check if graph editing is allowed - if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING)){ + if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false)){ Utils.notifyUserOfEditingIssue(Eagle.FileType.Graph, "Check for Component Updates"); return; } @@ -4911,7 +5256,7 @@ export class Eagle { updateSelection = (): void => { // check if graph editing is allowed - if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING)){ + if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false)){ Utils.notifyUserOfEditingIssue(Eagle.FileType.Graph, "Update Selection"); return; } @@ -4963,7 +5308,7 @@ export class Eagle { fixSelection = (): void => { // check if graph editing is allowed - if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING)){ + if (!Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false)){ Utils.notifyUserOfEditingIssue(Eagle.FileType.Graph, "Fix Selection"); return; } @@ -5025,7 +5370,7 @@ export class Eagle { this.undo().pushSnapshot(this, "Fix Component(s): " + updatedNodeNames); } - findPaletteContainingNode = (nodeId: NodeId): Palette => { + findPaletteContainingNode = (nodeId: NodeId): Palette | undefined => { for (const palette of this.palettes()){ for (const node of palette.getNodes()){ if (node.getId() === nodeId){ @@ -5034,7 +5379,7 @@ export class Eagle { } } - return null; + return undefined; } toggleAllPalettes = (): void => { @@ -5042,6 +5387,11 @@ export class Eagle { let anyExpanded: boolean = false; for (let i = 0 ; i < this.palettes().length; i++){ const element = document.querySelector('#collapse'+i); + if (element === null){ + console.error("Palette accordion element not found: " + '#collapse'+i); + continue; + } + if ($(element).hasClass('show')){ anyExpanded = true; break; @@ -5050,6 +5400,11 @@ export class Eagle { for (let i = 0 ; i < this.palettes().length; i++){ const element = document.querySelector('#collapse'+i); + if (element === null){ + console.error("Palette accordion element not found: " + '#collapse'+i); + continue; + } + if (anyExpanded){ bootstrap.Collapse.getOrCreateInstance(element).hide(); } else { @@ -5084,8 +5439,14 @@ export class Eagle { slowScroll = (data:any, event: JQuery.TriggeredEvent) : void => { const target = event.currentTarget;//gets the element that has the event binding + const scrollTop = $(target).scrollTop(); + + if (scrollTop === undefined) { + console.error("Unable to get scrollTop for slowScroll"); + return; + } - $(target).scrollTop($(target).scrollTop() + (event.originalEvent as WheelEvent).deltaY * 0.5); + $(target).scrollTop(scrollTop + (event.originalEvent as WheelEvent).deltaY * 0.5); } } @@ -5210,7 +5571,13 @@ $( document ).ready(function() { } }) - //toggle method on + // abort if e.target is null + if (e.target === null){ + console.error("No event target for translationDefault click"); + return; + } + + // toggle method on const element = $(e.target) if(element.val() === "true"){ element.val('false') @@ -5220,7 +5587,9 @@ $( document ).ready(function() { //saving the new translation default into the settings system const translationId = element.closest('.accordion-item').attr('id') - Setting.find(Setting.TRANSLATOR_ALGORITHM_DEFAULT).setValue(translationId) + if (typeof translationId !== 'undefined'){ + Setting.setValue(Setting.TRANSLATOR_ALGORITHM_DEFAULT, translationId); + } $(this).prop('checked',true).trigger("change"); }) @@ -5241,8 +5610,14 @@ $( document ).ready(function() { $(document).on('click', '.hierarchyEdgeExtra', function(event: JQuery.TriggeredEvent){ const e: MouseEvent = event.originalEvent as MouseEvent; + const target = e.target as HTMLElement; + if (target === null){ + console.error("No event target for hierarchyEdgeExtra click"); + return; + } + const selectedEdgeId: EdgeId = $(target).attr("id") as EdgeId; + const eagle: Eagle = Eagle.getInstance(); - const selectedEdgeId: EdgeId = $(e.target).attr("id") as EdgeId; const selectEdge = eagle.logicalGraph().getEdgeById(selectedEdgeId); if(typeof selectEdge === 'undefined'){ diff --git a/src/EagleConfig.ts b/src/EagleConfig.ts index 30926b4b..8ed9abc0 100644 --- a/src/EagleConfig.ts +++ b/src/EagleConfig.ts @@ -97,7 +97,12 @@ export class EagleConfig { } static initCSS(){ - const style: CSSStyleDeclaration = $("#logicalGraphParent").get(0).style; + const logicalGraphParent = $("#logicalGraphParent").get(0); + if (typeof logicalGraphParent === 'undefined'){ + console.error("EagleConfig.initCSS(): could not find logicalGraphParent element!"); + return; + } + const style: CSSStyleDeclaration = logicalGraphParent.style; //overwriting css variables using colors from EagleConfig. I am using this for simple styling to avoid excessive css data binds in the node html files style.setProperty("--selectedBg", EagleConfig.getColor('selectBackground')); @@ -115,6 +120,12 @@ export class EagleConfig { style.setProperty("--nodeInputColor", EagleConfig.getColor('nodeInputPort')); style.setProperty("--edgeSVGSize", EagleConfig.EDGE_SVG_SIZE+'px'); style.setProperty("--edgeThickness", EagleConfig.EDGE_THICKNESS+'px'); - $("html").get(0).style.setProperty("--hoverHighlight", EagleConfig.getColor('hoverHighlight')); + + const htmlElement = $("html").get(0); + if (typeof htmlElement === 'undefined'){ + console.error("EagleConfig.initCSS(): could not find html element!"); + return; + } + htmlElement.style.setProperty("--hoverHighlight", EagleConfig.getColor('hoverHighlight')); } } \ No newline at end of file diff --git a/src/Edge.ts b/src/Edge.ts index 4b715257..7555bb3e 100644 --- a/src/Edge.ts +++ b/src/Edge.ts @@ -175,20 +175,6 @@ export class Edge { return false } - clear = () : void => { - this.comment(''); - this.id = null; - this.srcNode = null; - this.srcPort = null; - this.destNode = null; - this.destPort = null; - this.loopAware(false); - this.closesLoop(false); - this.selectionRelative = false; - this.isShortEdge(false); - this.issues([]); - } - clone = () : Edge => { const result : Edge = new Edge(this.comment(), this.srcNode, this.srcPort, this.destNode, this.destPort, this.loopAware(), this.closesLoop(), this.selectionRelative); @@ -264,7 +250,7 @@ export class Edge { } } - static fromOJSJson(linkData: any, nodes: Node[], errorsWarnings: Errors.ErrorsWarnings) : Edge { + static fromOJSJson(linkData: any, nodes: Node[], errorsWarnings: Errors.ErrorsWarnings) : Edge | null{ let comment = '' // get comment (if exists) if (typeof linkData.comment !== 'undefined'){ @@ -272,10 +258,10 @@ export class Edge { } // try to read source and destination nodes and ports - let srcNodeId: NodeId = null; - let srcPortId: FieldId = null; - let destNodeId: NodeId = null; - let destPortId: FieldId = null; + let srcNodeId: NodeId | null = null; + let srcPortId: FieldId | null = null; + let destNodeId: NodeId | null = null; + let destPortId: FieldId | null = null; if (typeof linkData.from === 'undefined'){ errorsWarnings.warnings.push(Errors.Message("Edge is missing a 'from' attribute")); @@ -298,6 +284,11 @@ export class Edge { destPortId = linkData.toPort; } + if (srcNodeId === null || srcPortId === null || destNodeId === null || destPortId === null){ + console.warn("Edge is missing required attributes, cannot create edge. srcNodeId:", srcNodeId, "srcPortId:", srcPortId, "destNodeId:", destNodeId, "destPortId:", destPortId, "linkData:", linkData); + return null; + } + // try to read loop_aware attribute let loopAware: boolean = false; if (typeof linkData.loop_aware !== 'undefined'){ @@ -313,10 +304,10 @@ export class Edge { closesLoop = linkData.closesLoop; } - let srcNode: Node; - let destNode: Node; - let srcPort: Field; - let destPort: Field; + let srcNode: Node | undefined; + let destNode: Node | undefined; + let srcPort: Field | undefined; + let destPort: Field | undefined; for (const node of nodes){ if (node.getId() === srcNodeId){ @@ -324,13 +315,12 @@ export class Edge { // check input and output applications for srcPort const result = node.findPortInApplicationsById(srcPortId); - if (result.node !== null){ + if (typeof result.node !== 'undefined'){ srcNode = result.node; srcPort = result.port; - continue; + } else { + srcPort = node.getFieldById(srcPortId); } - - srcPort = node.getFieldById(srcPortId); } if (node.getId() === destNodeId){ @@ -338,72 +328,69 @@ export class Edge { // check input and output applications for destPort const result = node.findPortInApplicationsById(destPortId); - if (result.node !== null){ + if (typeof result.node !== 'undefined'){ destNode = result.node; destPort = result.port; - continue; + } else { + destPort = node.getFieldById(destPortId); } - - destPort = node.getFieldById(destPortId); } } // check if source and destination nodes and ports were found if (typeof srcNode === 'undefined'){ errorsWarnings.warnings.push(Errors.Message("Could not find source node for edge")); + console.warn("Could not find source node for edge. srcNodeId:", srcNodeId, "linkData:", linkData); return null; } if (typeof destNode === 'undefined'){ errorsWarnings.warnings.push(Errors.Message("Could not find destination node for edge")); + console.warn("Could not find destination node for edge. destNodeId:", destNodeId, "linkData:", linkData); return null; } if (typeof srcPort === 'undefined'){ errorsWarnings.warnings.push(Errors.Message("Could not find source port for edge")); + console.warn("Could not find source port for edge. srcPortId:", srcPortId, "linkData:", linkData); return null; } if (typeof destPort === 'undefined'){ errorsWarnings.warnings.push(Errors.Message("Could not find destination port for edge")); + console.warn("Could not find destination port for edge. destPortId:", destPortId, "linkData:", linkData); return null; } return new Edge(comment, srcNode, srcPort, destNode, destPort, loopAware, closesLoop, false); } - static fromV4Json(edgeData: any, lg: LogicalGraph, errorsWarnings: Errors.ErrorsWarnings) : Edge { + static fromV4Json(edgeData: any, lg: LogicalGraph, errorsWarnings: Errors.ErrorsWarnings) : Edge | null { const comment: string = edgeData.comment || ''; const loopAware: boolean = edgeData.loopAware; const closesLoop: boolean = edgeData.closesLoop; const edgeId: EdgeId = edgeData.id; - const srcNode: Node = lg.getNodeById(edgeData.srcNodeId); - const destNode: Node = lg.getNodeById(edgeData.destNodeId); - - let errorFound: boolean = false; + const srcNode: Node | undefined = lg.getNodeById(edgeData.srcNodeId); + const destNode: Node | undefined = lg.getNodeById(edgeData.destNodeId); if (typeof srcNode === 'undefined'){ errorsWarnings.warnings.push(Errors.Message("edge (" + edgeData.id + ") source node (" + edgeData.srcNodeId + ") could not be found, skipping")); - errorFound = true; } if (typeof destNode === 'undefined'){ errorsWarnings.warnings.push(Errors.Message("edge (" + edgeData.id + ") destination node (" + edgeData.destNodeId + ") could not be found, skipping")); - errorFound = true; } - if (errorFound){ + if (typeof srcNode === 'undefined' || typeof destNode === 'undefined'){ return null; } - const srcPort: Field = srcNode.getFieldById(edgeData.srcPortId); - const destPort: Field = destNode.getFieldById(edgeData.destPortId); + const srcPort: Field | undefined = srcNode.getFieldById(edgeData.srcPortId); + const destPort: Field | undefined = destNode.getFieldById(edgeData.destPortId); if (typeof srcPort === 'undefined'){ errorsWarnings.warnings.push(Errors.Message("edge (" + edgeData.id + ") source port (" + edgeData.srcPortId + ") could not be found, skipping")); - errorFound = true; } if (typeof destPort === 'undefined'){ errorsWarnings.warnings.push(Errors.Message("edge (" + edgeData.id + ") destination port (" + edgeData.destPortId + ") could not be found, skipping")); - errorFound = true; } - if (errorFound){ + if (typeof srcPort === 'undefined' || typeof destPort === 'undefined'){ return null; } @@ -413,11 +400,14 @@ export class Edge { return e; } - static isValid(eagle: Eagle, draggingPortMode: boolean, edgeId: EdgeId, sourceNodeId: NodeId, sourcePortId: FieldId, destinationNodeId: NodeId, destinationPortId: FieldId, loopAware: boolean, closesLoop: boolean, showNotification: boolean, showConsole: boolean, errorsWarnings: Errors.ErrorsWarnings) : Errors.Validity { + static isValid(eagle: Eagle, draggingPortMode: boolean, edgeId: EdgeId | null, sourceNodeId: NodeId, sourcePortId: FieldId, destinationNodeId: NodeId, destinationPortId: FieldId, loopAware: boolean, closesLoop: boolean, showNotification: boolean, showConsole: boolean, errorsWarnings: Errors.ErrorsWarnings) : Errors.Validity { let impossibleEdge : boolean = false; let draggingEdgeFixable : boolean = false; + let edge: Edge | undefined = undefined; - const edge = eagle.logicalGraph().getEdgeById(edgeId); + if (edgeId !== null){ + edge = eagle.logicalGraph().getEdgeById(edgeId); + } // if this is a real edge, then clear its issues, otherwise, if this is just a temp test edge, don't worry if(typeof edge !== 'undefined'){ @@ -448,8 +438,8 @@ export class Edge { } // get references to actual source and destination nodes (from the ids) - const sourceNode : Node = eagle.logicalGraph().getNodeById(sourceNodeId); - const destinationNode : Node = eagle.logicalGraph().getNodeById(destinationNodeId); + const sourceNode = eagle.logicalGraph().getNodeById(sourceNodeId); + const destinationNode = eagle.logicalGraph().getNodeById(destinationNodeId); if (typeof sourceNode === "undefined" || typeof destinationNode === "undefined"){ return Errors.Validity.Unknown; @@ -479,8 +469,8 @@ export class Edge { } } - const sourcePort : Field = sourceNode.getFieldById(sourcePortId); - const destinationPort : Field = destinationNode.getFieldById(destinationPortId); + const sourcePort = sourceNode.getFieldById(sourcePortId); + const destinationPort = destinationNode.getFieldById(destinationPortId); // check if source port was found if (typeof sourcePort === 'undefined') { @@ -533,23 +523,23 @@ export class Edge { } // check relationship of destination Node in relation to source node - const sourceHasParent = sourceNode.getParent() !== null; - const sourceHasEmbed = sourceNode.getEmbed() !== null; - const destinationHasParent = destinationNode.getParent() !== null; - const destinationHasEmbed = destinationNode.getEmbed() !== null; - const isParentOfConstruct : boolean = sourceHasParent && destinationHasEmbed && sourceNode.getParent().getId() === destinationNode.getEmbed().getId(); // is the connection from a child of a construct to an embedded app of the same construct - const isChildOfConstruct : boolean = destinationHasParent && sourceHasEmbed && destinationNode.getParent().getId() === sourceNode.getEmbed().getId(); //is the connections from an embedded app of a construct to a child of that same construct - const isSibling : boolean = (sourceHasParent && destinationHasParent && sourceNode.getParent().getId() === destinationNode.getParent().getId()) || (!sourceHasParent && !destinationHasParent); // do the two nodes have the same parent - let associatedConstructType : Category = null; //the category type of the parent construct of the source or destination node + const sourceParent = sourceNode.getParent(); + const destinationParent = destinationNode.getParent(); + const sourceEmbed = sourceNode.getEmbed(); + const destinationEmbed = destinationNode.getEmbed(); + const isParentOfConstruct : boolean = sourceParent !== null && destinationEmbed !== null && sourceParent.getId() === destinationEmbed.getId(); // is the connection from a child of a construct to an embedded app of the same construct + const isChildOfConstruct : boolean = destinationParent !== null && sourceEmbed !== null && destinationParent.getId() === sourceEmbed.getId(); //is the connections from an embedded app of a construct to a child of that same construct + const isSibling : boolean = (sourceParent !== null && destinationParent !== null && sourceParent.getId() === destinationParent.getId()) || (sourceParent === null && destinationParent === null); // do the two nodes have the same parent + let associatedConstructType : Category = Category.Unknown; //the category type of the parent construct of the source or destination node //these checks are to see if the source or destination node are embedded apps whose parent is a sibling of the other source or destination node - const destPortIsEmbeddedAppOfSibling : boolean = sourceHasParent && destinationHasEmbed && sourceNode.getParent().getId() === destinationNode.getEmbed()?.getParent()?.getId(); - const srcPortIsEmbeddedAppOfSibling : boolean = destinationHasParent && sourceHasEmbed && destinationNode.getParent().getId() === sourceNode.getEmbed()?.getParent()?.getId(); + const destPortIsEmbeddedAppOfSibling : boolean = sourceParent !== null && destinationEmbed !== null && sourceParent.getId() === destinationEmbed?.getParent()?.getId(); + const srcPortIsEmbeddedAppOfSibling : boolean = destinationParent !== null && sourceEmbed !== null && destinationParent.getId() === sourceEmbed?.getParent()?.getId(); //checking the type of the parent nodes if(!isSibling){ - const srcNodeParent: Node = sourceNode.getParent() - const destNodeParent: Node = destinationNode.getParent() + const srcNodeParent = sourceNode.getParent() + const destNodeParent = destinationNode.getParent() if(destNodeParent !== null && destNodeParent.getCategory() === Category.Loop || srcNodeParent !== null && srcNodeParent.getCategory() === Category.Loop){ associatedConstructType = Category.Loop @@ -570,11 +560,11 @@ export class Edge { if( isSibling && loopAware || destPortIsEmbeddedAppOfSibling && loopAware || srcPortIsEmbeddedAppOfSibling && loopAware - || sourceNode.isEmbedded() && destinationNode.hasParent() && sourceNode.getEmbed().getId() === destinationNode.getParent().getId() && loopAware - || destinationNode.isEmbedded() && sourceNode.hasParent() && destinationNode.getEmbed().getId() === sourceNode.getParent().getId() && loopAware + || sourceNode.isEmbedded() && destinationNode.hasParent() && sourceEmbed !== null && destinationParent !== null &&sourceEmbed.getId() === destinationParent.getId() && loopAware + || destinationNode.isEmbedded() && sourceNode.hasParent() && destinationEmbed !== null && sourceParent !== null && destinationEmbed.getId() === sourceParent.getId() && loopAware || associatedConstructType !== Category.Loop && loopAware ){ - const x = Errors.ShowFix("Edge between two siblings should not be loop aware", function(){Utils.showEdge(eagle, edge);}, function(){Utils.fixDisableEdgeLoopAware(eagle, edgeId);}, "Disable loop aware on the edge."); + const x = Errors.ShowFix("Edge between two siblings should not be loop aware", function(){Utils.showEdge(eagle, edge);}, function(){if (edgeId !== null) {Utils.fixDisableEdgeLoopAware(eagle, edgeId);}}, "Disable loop aware on the edge."); Edge.isValidLog(edge, draggingPortMode, Errors.Validity.Warning, x, showNotification, showConsole, errorsWarnings); } @@ -589,7 +579,7 @@ export class Edge { const isDestMatch = edge.getDestNode().getId() === destinationNodeId && edge.getDestPort().getId() === destinationPortId; if ( isSrcMatch && isDestMatch && edge.getId() !== edgeId){ - const x = Errors.ShowFix("Edge is a duplicate. Another edge with the same source port and destination port already exists", function(){Utils.showEdge(eagle, edge);}, function(){Utils.fixDeleteEdge(eagle, edgeId);}, "Delete edge"); + const x = Errors.ShowFix("Edge is a duplicate. Another edge with the same source port and destination port already exists", function(){Utils.showEdge(eagle, edge);}, function(){if (edgeId !== null) {Utils.fixDeleteEdge(eagle, edgeId);}}, "Delete edge"); Edge.isValidLog(edge, draggingPortMode, Errors.Validity.Error, x, showNotification, showConsole, errorsWarnings); } } @@ -598,7 +588,7 @@ export class Edge { // - begin from a Data component // - end with a App component // - sourceNode has a 'group_end' field set to true - // - destNode has a 'group_start' field set to true + // - destNode has a 'group_start' field set to true if (closesLoop){ if (!sourceNode.isData()){ const x = Errors.Show("Closes Loop Edge does not start from a Data component.", function(){Utils.showEdge(eagle, edge);}); @@ -610,12 +600,15 @@ export class Edge { Edge.isValidLog(edge, draggingPortMode, Errors.Validity.Error, x, showNotification, showConsole, errorsWarnings); } - if (!sourceNode.hasFieldWithDisplayText(Daliuge.FieldName.GROUP_END) || !Utils.asBool(sourceNode.getFieldByDisplayText(Daliuge.FieldName.GROUP_END).getValue())){ + const groupStartField = destinationNode.findFieldByDisplayText(Daliuge.FieldName.GROUP_START); + const groupEndField = sourceNode.findFieldByDisplayText(Daliuge.FieldName.GROUP_END); + + if (typeof groupEndField === 'undefined' || !Utils.asBool(groupEndField.getValue())){ const x = Errors.ShowFix("'Closes Loop' Edge start node (" + sourceNode.getName() + ") does not have 'group_end' set to true.", function(){Utils.showEdge(eagle, edge);}, function(){Utils.fixFieldValue(eagle, sourceNode, Daliuge.groupEndField, "true")}, "Set 'group_end' to true"); Edge.isValidLog(edge, draggingPortMode, Errors.Validity.Error, x, showNotification, showConsole, errorsWarnings); } - if (!destinationNode.hasFieldWithDisplayText(Daliuge.FieldName.GROUP_START) || !Utils.asBool(destinationNode.getFieldByDisplayText(Daliuge.FieldName.GROUP_START).getValue())){ + if (typeof groupStartField === 'undefined' || !Utils.asBool(groupStartField.getValue())){ const x = Errors.ShowFix("'Closes Loop' Edge end node (" + destinationNode.getName() + ") does not have 'group_start' set to true.", function(){Utils.showEdge(eagle, edge);}, function(){Utils.fixFieldValue(eagle, destinationNode, Daliuge.groupStartField, "true")}, "Set 'group_start' to true"); Edge.isValidLog(edge, draggingPortMode, Errors.Validity.Error, x, showNotification, showConsole, errorsWarnings); } @@ -639,7 +632,7 @@ export class Edge { } } - private static isValidLog(edge: Edge, draggingPortMode: boolean, linkValid: Errors.Validity, issue: Errors.Issue, showNotification: boolean, showConsole: boolean, errorsWarnings: Errors.ErrorsWarnings): void { + private static isValidLog(edge: Edge | undefined, draggingPortMode: boolean, linkValid: Errors.Validity, issue: Errors.Issue, showNotification: boolean, showConsole: boolean, errorsWarnings: Errors.ErrorsWarnings): void { // determine correct title let title = "Edge Valid"; let type : "success" | "info" | "warning" | "danger" = "success"; diff --git a/src/Errors.ts b/src/Errors.ts index 06a2921f..c3c2d40f 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -117,7 +117,7 @@ export class Errors { export namespace Errors { - export type Issue = {message: string, show: () => void, fix: () => void, fixDescription: string}; + export type Issue = {message: string, show: (() => void) | null, fix: (() => void) | null, fixDescription: string}; export type ErrorsWarnings = {warnings: Issue[], errors: Issue[]}; export enum Validity { diff --git a/src/Field.ts b/src/Field.ts index acdd4e59..26dfffa9 100644 --- a/src/Field.ts +++ b/src/Field.ts @@ -14,8 +14,8 @@ import { Utils } from './Utils'; export class Field { private displayText : ko.Observable; // user-facing name - private value : ko.Observable; // the current value - private defaultValue : ko.Observable; // default value + private value : ko.Observable; // the current value + private defaultValue : ko.Observable; // default value private description : ko.Observable; private readonly : ko.Observable; private type : ko.Observable; // NOTE: this is a little unusual (type can have more values than just the enum) @@ -46,7 +46,7 @@ export class Field { private issues : ko.ObservableArray<{issue:Errors.Issue, validity:Errors.Validity}>//keeps track of issues on the field - constructor(id: FieldId, displayText: string, value: string, defaultValue: string, description: string, readonly: boolean, type: Daliuge.DataType, precious: boolean, options: string[], positional: boolean, parameterType: Daliuge.FieldType, usage: Daliuge.FieldUsage){ + constructor(node: Node, id: FieldId, displayText: string, value: string | null, defaultValue: string | null, description: string, readonly: boolean, type: Daliuge.DataType, precious: boolean, options: string[], positional: boolean, parameterType: Daliuge.FieldType, usage: Daliuge.FieldUsage){ this.displayText = ko.observable(displayText); this.value = ko.observable(value); this.defaultValue = ko.observable(defaultValue); @@ -62,7 +62,7 @@ export class Field { this.parameterType = ko.observable(parameterType); this.usage = ko.observable(usage); this.isEvent = ko.observable(false); - this.node = ko.observable(null); + this.node = ko.observable(node); this.edges = ko.observable(new Map()); //graph related things @@ -98,20 +98,20 @@ export class Field { return this; } - getValue = () : string => { + getValue = () : string | null => { return this.value(); } - setValue = (value: string): Field => { + setValue = (value: string | null): Field => { this.value(value); return this; } - getDefaultValue = () : string => { + getDefaultValue = () : string | null => { return this.defaultValue(); } - setDefaultValue = (value: string): Field => { + setDefaultValue = (value: string | null): Field => { this.defaultValue(value); return this; } @@ -207,17 +207,27 @@ export class Field { return this.encoding(); } - valIsTrue = (val:string) : boolean => { + valIsTrue = (val:string | null) : boolean => { return Utils.asBool(val); } toggle = (): Field => { - this.value((!Utils.asBool(this.value())).toString()); + const oldValue = this.value(); + if (oldValue === null) { + this.value("true"); + } else { + this.value((!Utils.asBool(oldValue)).toString()); + } return this; } toggleDefault = (): Field => { - this.defaultValue((!Utils.asBool(this.defaultValue())).toString()); + const oldValue = this.defaultValue(); + if (oldValue === null) { + this.defaultValue("true"); + } else { + this.defaultValue((!Utils.asBool(oldValue)).toString()); + } return this; } @@ -406,10 +416,11 @@ export class Field { // TODO: these colors could be added to EagleConfig.ts getBackgroundColor : ko.PureComputed = ko.pureComputed(() => { const errorsWarnings = this.getErrorsWarnings() + const showGraphWarnings = Setting.findValue(Setting.SHOW_GRAPH_WARNINGS, Setting.ShowErrorsMode.None); - if(errorsWarnings.errors.length>0 && Setting.findValue(Setting.SHOW_GRAPH_WARNINGS) != Setting.ShowErrorsMode.None){ + if(errorsWarnings.errors.length>0 && showGraphWarnings != Setting.ShowErrorsMode.None){ return EagleConfig.getColor('graphError') - }else if(errorsWarnings.warnings.length>0 && Setting.findValue(Setting.SHOW_GRAPH_WARNINGS) === Setting.ShowErrorsMode.Warnings){ + }else if(errorsWarnings.warnings.length>0 && showGraphWarnings === Setting.ShowErrorsMode.Warnings){ return EagleConfig.getColor('graphWarning') }else{ return '' @@ -443,39 +454,17 @@ export class Field { return this; } - getGraphConfigField : ko.PureComputed = ko.pureComputed(() => { + getGraphConfigField : ko.PureComputed = ko.pureComputed(() => { return Eagle.getInstance().logicalGraph().getActiveGraphConfig()?.getNodeById(this.node().getId())?.getFieldById(this.id()); }, this); - clear = () : Field => { - this.displayText(""); - this.value(""); - this.defaultValue(""); - this.description(""); - this.readonly(false); - this.type(Daliuge.DataType.Unknown); - this.precious(false); - this.options([]); - this.positional(false); - this.parameterType(Daliuge.FieldType.Unknown); - this.usage(Daliuge.FieldUsage.NoPort); - this.encoding(Daliuge.Encoding.Pickle); - - this.id(null); - this.isEvent(false); - this.node(null); - this.edges().clear(); - - return this; - } - clone = () : Field => { const options : string[] = [] for (const option of this.options()){ options.push(option); } - const f = new Field(this.id(), this.displayText(), this.value(), this.defaultValue(), this.description(), this.readonly(), this.type(), this.precious(), options, this.positional(), this.parameterType(), this.usage()); + const f = new Field(this.node(), this.id(), this.displayText(), this.value(), this.defaultValue(), this.description(), this.readonly(), this.type(), this.precious(), options, this.positional(), this.parameterType(), this.usage()); f.encoding(this.encoding()); f.isEvent(this.isEvent()); f.node(this.node()); @@ -487,7 +476,7 @@ export class Field { } shallowCopy = () : Field => { - const f = new Field(this.id(), this.displayText(), this.value(), this.defaultValue(), this.description(), this.readonly(), this.type(), this.precious(), this.options(), this.positional(), this.parameterType(), this.usage()); + const f = new Field(this.node(), this.id(), this.displayText(), this.value(), this.defaultValue(), this.description(), this.readonly(), this.type(), this.precious(), this.options(), this.positional(), this.parameterType(), this.usage()); f.id = this.id; f.displayText = this.displayText; @@ -599,6 +588,7 @@ export class Field { let searchTermNo : number = 0 let searchTermTrueNo : number = 0 const that = this + const bottomWindowMode = Setting.findValue(Setting.BOTTOM_WINDOW_MODE, Eagle.BottomWindowMode.None); Eagle.tableSearchString().toLocaleLowerCase().split(',').forEach(function(term){ term = term.trim() @@ -611,7 +601,7 @@ export class Field { } //check if the node name matches, but only if using the key parameter table modal - if(Setting.findValue(Setting.BOTTOM_WINDOW_MODE) === Eagle.BottomWindowMode.ConfigParameterTable){ + if(bottomWindowMode === Eagle.BottomWindowMode.ConfigParameterTable){ if(that.node().getName().toLowerCase().indexOf(term) >= 0){ result = true } @@ -736,7 +726,11 @@ export class Field { // used to transform the value attribute of a field into a variable with the correct type // the value attribute is always stored as a string internally - static stringAsType(value: string, type: Daliuge.DataType) : boolean | number | string { + static stringAsType(value: string | null, type: Daliuge.DataType) : boolean | number | string | null { + if (value === null){ + return null; + } + switch (type){ case Daliuge.DataType.Boolean: return Utils.asBool(value); @@ -791,7 +785,7 @@ export class Field { }; } - static fromOJSJson(data : any) : Field { + static fromOJSJson(data : any, node: Node) : Field { let id: FieldId = Utils.generateFieldId(); let name: string = ""; let description: string = ""; @@ -876,13 +870,13 @@ export class Field { isEvent = data.event; if (typeof data.encoding !== 'undefined') encoding = data.encoding; - const result = new Field(id, name, value, defaultValue, description, readonly, type, precious, options, positional, parameterType, usage); + const result = new Field(node, id, name, value, defaultValue, description, readonly, type, precious, options, positional, parameterType, usage); result.isEvent(isEvent); result.encoding(encoding); return result; } - static fromOJSJsonPort(data : any) : Field { + static fromOJSJsonPort(data : any, node: Node) : Field { let name: string = ""; let event: boolean = false; let type: Daliuge.DataType = Daliuge.DataType.Unknown; @@ -905,28 +899,28 @@ export class Field { name = data.IdText; } - const f = new Field(data.Id, name, "", "", description, false, type, false, [], false, Daliuge.FieldType.Unknown, Daliuge.FieldUsage.NoPort); + const f = new Field(node, data.Id, name, "", "", description, false, type, false, [], false, Daliuge.FieldType.Unknown, Daliuge.FieldUsage.NoPort); f.isEvent(event); f.encoding(encoding); return f; } - static fromV4Json(data: any): Field { - let id: FieldId; - let name: string; - let value: string; - let defaultValue: string; - let description: string; - let readonly: boolean; - let type: Daliuge.DataType; - let precious: boolean; - let options: string[]; - let positional: boolean; - let parameterType: Daliuge.FieldType; - let usage: Daliuge.FieldUsage; + static fromV4Json(data: any, node: Node): Field { + let id: FieldId = Utils.generateFieldId(); + let name: string = ""; + let value: string = ""; + let defaultValue: string = ""; + let description: string = ""; + let readonly: boolean = false; + let type: Daliuge.DataType = Daliuge.DataType.Unknown; + let precious: boolean = false; + let options: string[] = []; + let positional: boolean = false; + let parameterType: Daliuge.FieldType = Daliuge.FieldType.Unknown; + let usage: Daliuge.FieldUsage = Daliuge.FieldUsage.NoPort; - let event: boolean; - let encoding: Daliuge.Encoding; + let event: boolean = false; + let encoding: Daliuge.Encoding = Daliuge.Encoding.Pickle; if (typeof data.id !== 'undefined') id = data.id; @@ -958,7 +952,7 @@ export class Field { if (typeof data.encoding !== 'undefined') encoding = data.encoding; - const f = new Field(id, name, value, defaultValue, description, readonly, type, precious, options, positional, parameterType, usage); + const f = new Field(node, id, name, value, defaultValue, description, readonly, type, precious, options, positional, parameterType, usage); f.isEvent(event); f.encoding(encoding); return f; @@ -974,14 +968,13 @@ export class Field { //check the data type is known (except in the case of event ports, they can be unknown) if (!field.isEvent() && field.isType(Daliuge.DataType.Unknown)){ let issue: Errors.Issue + const constructNode = node.getEmbed(); // for normal nodes - if(!node.isEmbedded()){ + if(constructNode === null){ issue = Errors.ShowFix("Node (" + node.getName() + ") has input port (" + field.getDisplayText() + ") whose type is not specified", function(){Utils.showField(eagle, location, node, field);}, function(){Utils.fixFieldType(eagle, field)}, ""); }else{ // for embedded nodes - const constructNode = node.getEmbed(); - if(constructNode.getInputApplication() === node){ //if node is input application issue = Errors.ShowFix("Node (" + constructNode.getName() + ") has input application (" + node.getName() + ") with input port (" + field.getDisplayText() + ") whose type is not specified", function(){Utils.showField(eagle, location, node, field);}, function(){Utils.fixFieldType(eagle, field)}, ""); @@ -1001,14 +994,13 @@ export class Field { //check the data type is known (except in the case of event ports, they can be unknown) if (!field.isEvent() && field.isType(Daliuge.DataType.Unknown)){ let issue: Errors.Issue + const constructNode = node.getEmbed(); //for normal nodes - if(!node.isEmbedded()){ + if(constructNode === null){ issue = Errors.ShowFix("Node (" + node.getName() + ") has output port (" + field.getDisplayText() + ") whose type is not specified", function(){Utils.showField(eagle, location, node, field);}, function(){Utils.fixFieldType(eagle, field)}, ""); }else{ // for embedded nodes - const constructNode = node.getEmbed(); - if(constructNode.getInputApplication() === node){ //if node is input application issue = Errors.ShowFix("Node (" + constructNode.getName() + ") has input application (" + node.getName() + ") with output port (" + field.getDisplayText() + ") whose type is not specified", function(){Utils.showField(eagle, location, node, field);}, function(){Utils.fixFieldType(eagle, field)}, ""); diff --git a/src/GitHub.ts b/src/GitHub.ts index a719968f..b3b7a895 100644 --- a/src/GitHub.ts +++ b/src/GitHub.ts @@ -41,7 +41,7 @@ export class GitHub { let data; try { - data = await Utils.httpGetJSON("/getGitHubRepositoryList", null) as {repository: string, branch: string}[]; + data = await Utils.httpGetJSON("/getGitHubRepositoryList", {}) as {repository: string, branch: string}[]; } catch (error){ console.error(error); reject(error); @@ -63,7 +63,7 @@ export class GitHub { static async loadStudentRepoList(){ let data; try { - data = await Utils.httpGetJSON("/getStudentRepositoryList", null) as {repository: string, branch: string}[]; + data = await Utils.httpGetJSON("/getStudentRepositoryList", {}) as {repository: string, branch: string}[]; } catch (error){ console.error(error); return; @@ -90,10 +90,15 @@ export class GitHub { */ static async loadRepoContent(repository : Repository, path: string): Promise { return new Promise(async(resolve, reject) => { - const token = Setting.findValue(Setting.GITHUB_ACCESS_TOKEN_KEY); + const token = Setting.findValue(Setting.GITHUB_ACCESS_TOKEN_KEY, ""); // get location - const location: Repository | RepositoryFolder = repository.findPath(path); + const location: Repository | RepositoryFolder | null = repository.findPath(path); + + if (location === null) { + reject(new Error("Location not found for path: " + path)); + return; + } // flag the location as being fetched location.isFetching(true); @@ -200,7 +205,7 @@ export class GitHub { */ static async openRemoteFile(repositoryService : Repository.Service, repositoryName : string, repositoryBranch : string, filePath : string, fileName : string): Promise { return new Promise(async(resolve, reject) => { - const token = Setting.findValue(Setting.GITHUB_ACCESS_TOKEN_KEY); + const token = Setting.findValue(Setting.GITHUB_ACCESS_TOKEN_KEY, ""); const fullFileName : string = Utils.joinPath(filePath, fileName); // Add parameters in json data. @@ -225,7 +230,7 @@ export class GitHub { static async deleteRemoteFile(repositoryService : Repository.Service, repositoryName : string, repositoryBranch : string, filePath : string, fileName : string){ return new Promise(async(resolve, reject) => { - const token = Setting.findValue(Setting.GITHUB_ACCESS_TOKEN_KEY); + const token = Setting.findValue(Setting.GITHUB_ACCESS_TOKEN_KEY, ""); if (token === null || token === "") { Utils.showUserMessage("Access Token", "The GitHub access token is not set! To open GitHub repositories, set the token via settings."); diff --git a/src/GitLab.ts b/src/GitLab.ts index e2944a51..da04b136 100644 --- a/src/GitLab.ts +++ b/src/GitLab.ts @@ -41,7 +41,7 @@ export class GitLab { // fetch additional gitlab repositories from the server let data; try { - data = await Utils.httpGetJSON("/getGitLabRepositoryList", null) as {repository: string, branch: string}[]; + data = await Utils.httpGetJSON("/getGitLabRepositoryList", {}) as {repository: string, branch: string}[]; } catch (error) { console.error(error); resolve(repositories); @@ -62,10 +62,15 @@ export class GitLab { */ static async loadRepoContent(repository : Repository, path: string): Promise { return new Promise(async(resolve, reject) => { - const token = Setting.findValue(Setting.GITLAB_ACCESS_TOKEN_KEY); + const token = Setting.findValue(Setting.GITLAB_ACCESS_TOKEN_KEY, ""); // get location - const location: Repository | RepositoryFolder = repository.findPath(path); + const location: Repository | RepositoryFolder | null = repository.findPath(path); + + if (location === null) { + reject(new Error("Location not found for path: " + path)); + return; + } // flag the location as being fetched location.isFetching(true); @@ -169,7 +174,7 @@ export class GitLab { */ static async openRemoteFile(repositoryService : Repository.Service, repositoryName : string, repositoryBranch : string, filePath : string, fileName : string): Promise { return new Promise(async(resolve, reject) => { - const token = Setting.findValue(Setting.GITLAB_ACCESS_TOKEN_KEY); + const token = Setting.findValue(Setting.GITLAB_ACCESS_TOKEN_KEY, ""); const fullFileName : string = Utils.joinPath(filePath, fileName); @@ -195,7 +200,7 @@ export class GitLab { static deleteRemoteFile(repositoryService : Repository.Service, repositoryName : string, repositoryBranch : string, filePath : string, fileName : string){ return new Promise(async(resolve, reject) => { - const token = Setting.findValue(Setting.GITLAB_ACCESS_TOKEN_KEY); + const token = Setting.findValue(Setting.GITLAB_ACCESS_TOKEN_KEY, ""); if (token === null || token === "") { Utils.showUserMessage("Access Token", "The GitLab access token is not set! To open GitLab repositories, set the token via settings."); diff --git a/src/GraphConfig.ts b/src/GraphConfig.ts index b8ae092b..c6578594 100644 --- a/src/GraphConfig.ts +++ b/src/GraphConfig.ts @@ -56,14 +56,15 @@ export class GraphConfig { addNode = (node: Node): GraphConfigNode => { // check to see if node already exists - const graphConfigNode: GraphConfigNode = this.nodes().get(node.getId()); + const graphConfigNode = this.nodes().get(node.getId()); if (typeof graphConfigNode !== 'undefined'){ return graphConfigNode; } // otherwise add new node - const newNode: GraphConfigNode = new GraphConfigNode(); + const newNode: GraphConfigNode = new GraphConfigNode(node); + // TODO: required? probably better to set this in GraphConfigNode constructor newNode.setNode(node); this.nodes().set(node.getId(), newNode); this.nodes.valueHasMutated(); @@ -75,7 +76,7 @@ export class GraphConfig { this.addNode(node).addField(field); } - getNodeById = (id: NodeId): GraphConfigNode => { + getNodeById = (id: NodeId): GraphConfigNode | undefined => { return this.nodes().get(id); } @@ -91,7 +92,12 @@ export class GraphConfig { removeField = (field: Field): void => { // get reference to the GraphConfigNode containing the field - const graphConfigNode: GraphConfigNode = this.getNodeById(field.getNode().getId()); + const graphConfigNode = this.getNodeById(field.getNode().getId()); + + if (typeof graphConfigNode === 'undefined'){ + console.warn("GraphConfig.removeField(): Could not find GraphConfigNode for field", field.getId()); + return; + } // remove the field graphConfigNode.removeFieldById(field.getId()); @@ -105,7 +111,7 @@ export class GraphConfig { Eagle.getInstance().checkGraph(); } - addValue = (node: Node, field: Field, value: string) => { + addValue = (node: Node, field: Field, value: string | null) => { this.addNode(node).addField(field).setValue(value); } @@ -123,7 +129,7 @@ export class GraphConfig { // get the Node for this field const node: Node = field.getNode(); - const f: GraphConfigField = this.nodes().get(node.getId())?.getFieldById(field.getId()); + const f: GraphConfigField | undefined = this.nodes().get(node.getId())?.getFieldById(field.getId()); return typeof f !== 'undefined'; } @@ -165,7 +171,7 @@ export class GraphConfig { if (typeof data.nodes !== 'undefined'){ for (const nodeId in data.nodes){ const nodeData = data.nodes[nodeId]; - const lgNode: Node = lg.getNodeById(nodeId as NodeId); + const lgNode = lg.getNodeById(nodeId as NodeId); if (typeof lgNode === 'undefined'){ console.warn("GraphConfig.fromJson(): Could not find node", nodeId); errorsWarnings.errors.push(Errors.Message("GraphConfig.fromJson(): Could not find node " + nodeId)); @@ -222,8 +228,8 @@ export class GraphConfigNode { private node: ko.Observable; private fields: ko.Observable>; - constructor(){ - this.node = ko.observable(null); + constructor(node: Node){ + this.node = ko.observable(node); this.fields = ko.observable(new Map()); } @@ -232,9 +238,7 @@ export class GraphConfigNode { } clone = () : GraphConfigNode => { - const result: GraphConfigNode = new GraphConfigNode(); - - result.node(this.node()); + const result: GraphConfigNode = new GraphConfigNode(this.node()); for (const [id, field] of this.fields()){ result.fields().set(id, field.clone()); @@ -261,14 +265,15 @@ export class GraphConfigNode { } // otherwise add new field - const newField: GraphConfigField = new GraphConfigField(); + const newField: GraphConfigField = new GraphConfigField(field); + // TODO: required? probably better to set this in GraphConfigField constructor newField.setField(field); this.fields().set(field.getId(), newField); this.fields.valueHasMutated(); return newField; } - getFieldById = (id: FieldId): GraphConfigField => { + getFieldById = (id: FieldId): GraphConfigField | undefined => { return this.fields().get(id); } @@ -283,14 +288,21 @@ export class GraphConfigNode { } static fromJson(data: any, node: Node, errorsWarnings: Errors.ErrorsWarnings): GraphConfigNode { - const result = new GraphConfigNode(); + const result = new GraphConfigNode(node); if (data.fields !== undefined){ for (const fieldId in data.fields){ const fieldData = data.fields[fieldId]; - const newField: GraphConfigField = GraphConfigField.fromJson(fieldData, errorsWarnings); const lgField = node.getFieldById(fieldId as FieldId); + if (typeof lgField === 'undefined'){ + console.warn("GraphConfigNode.fromJson(): Could not find field", fieldId, "in node", node.getName()); + errorsWarnings.errors.push(Errors.Message("GraphConfigNode.fromJson(): Could not find field " + fieldId + " in node " + node.getName())); + continue; + } + + const newField: GraphConfigField = GraphConfigField.fromJson(fieldData, lgField, errorsWarnings); + // TODO: required? probably better to set this in GraphConfigField.fromJson() newField.setField(lgField); result.fields().set(lgField.getId(), newField); result.fields.valueHasMutated(); @@ -306,7 +318,7 @@ export class GraphConfigNode { // add fields result.fields = {}; for (const [id, field] of node.fields()){ - const graphField: Field = graphNode.getFieldById(id); + const graphField = graphNode.getFieldById(id); if (typeof graphField === 'undefined'){ continue; @@ -321,19 +333,19 @@ export class GraphConfigNode { export class GraphConfigField { private field: ko.Observable; - private value: ko.Observable; + private value: ko.Observable; private comment: ko.Observable; - constructor(){ - this.field = ko.observable(null); + constructor(field: Field){ + this.field = ko.observable(field); this.value = ko.observable(""); this.comment = ko.observable(""); } clone = (): GraphConfigField => { - const result = new GraphConfigField(); + const result = new GraphConfigField(this.field()); - result.field(this.field()); + //result.field(this.field()); result.value(this.value()); result.comment(this.comment()); @@ -349,17 +361,22 @@ export class GraphConfigField { return this.field(); } - setValue = (value: string): GraphConfigField => { + setValue = (value: string | null): GraphConfigField => { this.value(value); return this; } - getValue = (): string => { + getValue = (): string | null => { return this.value(); } - toggle = () : GraphConfigField => { - this.value((!Utils.asBool(this.value())).toString()); + toggle = () : GraphConfigField => { + let oldValue = this.value(); + if (oldValue === null){ + oldValue = "false"; + } + + this.value((!Utils.asBool(oldValue)).toString()); return this; } @@ -372,8 +389,8 @@ export class GraphConfigField { return this.comment(); } - static fromJson(data: any, errorsWarnings: Errors.ErrorsWarnings): GraphConfigField { - const result = new GraphConfigField(); + static fromJson(data: any, field: Field, errorsWarnings: Errors.ErrorsWarnings): GraphConfigField { + const result = new GraphConfigField(field); if (typeof data.value !== 'undefined'){ if (data.value === null){ diff --git a/src/GraphConfigurationsTable.ts b/src/GraphConfigurationsTable.ts index a5176bfc..d788eb32 100644 --- a/src/GraphConfigurationsTable.ts +++ b/src/GraphConfigurationsTable.ts @@ -8,7 +8,7 @@ export class GraphConfigurationsTable { static toggleTable = () : void => { //if we are already in the requested mode, we can toggle the bottom window - if(Setting.findValue(Setting.BOTTOM_WINDOW_MODE) === Eagle.BottomWindowMode.GraphConfigsTable){ + if(Setting.findValue(Setting.BOTTOM_WINDOW_MODE, Eagle.BottomWindowMode.None) === Eagle.BottomWindowMode.GraphConfigsTable){ SideWindow.toggleShown('bottom') }else{ this.openTable() @@ -21,8 +21,8 @@ export class GraphConfigurationsTable { if($('.modal.show').length>0){ $('.modal.show').modal('hide') } - - Setting.find(Setting.BOTTOM_WINDOW_MODE).setValue(Eagle.BottomWindowMode.GraphConfigsTable) + + Setting.setValue(Setting.BOTTOM_WINDOW_MODE, Eagle.BottomWindowMode.GraphConfigsTable) SideWindow.setShown('bottom',true) } } diff --git a/src/GraphRenderer.ts b/src/GraphRenderer.ts index 426616cf..83a45fc4 100644 --- a/src/GraphRenderer.ts +++ b/src/GraphRenderer.ts @@ -47,7 +47,7 @@ ko.bindingHandlers.nodeRenderHandler = { } }, update: function (element:any, valueAccessor) { - let node: Node = ko.unwrap(valueAccessor()); + const node: Node = ko.unwrap(valueAccessor()); // set size $(element).css({'height':node.getRadius()*2+'px','width':node.getRadius()*2+'px'}); @@ -66,23 +66,30 @@ ko.bindingHandlers.nodeRenderHandler = { } const pos = node.getPosition() // this line is needed because referencing position here causes this update function to run when the node position gets updated aka. when we are dragging a node on the graph - if(node.isConstruct() || node.getParent() !== null ){ - if(!node.isConstruct()){ - node = node.getParent(); + const nodeParent = node.getParent(); + const isConstruct = node.isConstruct(); + + if (isConstruct){ + GraphRenderer.resizeConstruct(node); + } else { + if (nodeParent !== null){ + GraphRenderer.resizeConstruct(nodeParent); } - GraphRenderer.resizeConstruct(node) } }, }; ko.bindingHandlers.embeddedAppPosition = { update: function (element:any, valueAccessor) { - const eagle : Eagle = Eagle.getInstance(); const applicationNode: Node = ko.utils.unwrapObservable(valueAccessor()).applicationNode; const input: boolean = ko.utils.unwrapObservable(valueAccessor()).input; // find the node in which the applicationNode has been embedded - const parentNode: Node = applicationNode.getEmbed(); + const parentNode = applicationNode.getEmbed(); + if (parentNode === null){ + console.warn("embeddedAppPosition binding: application node " + applicationNode.getId() + " has no parent node"); + return; + } // determine all the adjacent nodes // TODO: earlier abort if field is null @@ -144,18 +151,10 @@ ko.bindingHandlers.graphRendererPortPosition = { const eagle : Eagle = Eagle.getInstance(); const n: Node = ko.utils.unwrapObservable(valueAccessor()).n; const f: Field = ko.utils.unwrapObservable(valueAccessor()).f; - const dataType: string = ko.utils.unwrapObservable(valueAccessor()).type; + const dataType: "inputPort" | "outputPort" = ko.utils.unwrapObservable(valueAccessor()).type; // determine the 'node' and 'field' attributes (for this way of using this binding) - let node : Node - let field : Field - - switch(dataType){ - case 'inputPort': - case 'outputPort': - node = f.getNode() - field = f - break; - } + const node : Node = f.getNode(); + const field : Field = f; // determine all the adjacent nodes const adjacentNodes: Node[] = []; @@ -235,6 +234,7 @@ ko.bindingHandlers.graphRendererPortPosition = { break; default: console.warn("disconnected field with dataType:", dataType); + averageAngle = 0 break; } } @@ -269,36 +269,38 @@ ko.bindingHandlers.graphRendererPortPosition = { } }; +type position2d = {x:number, y:number} | null; + export class GraphRenderer { - static nodeData : Node[] = null + static nodeData : Node[] | null = null // TODO: group all the dragging variables. move into a structure? static isDragging : ko.Observable = ko.observable(false); - static draggingNode : ko.Observable = ko.observable(null); + static draggingNode : ko.Observable = ko.observable(null); static draggingPaletteNode : boolean = false; //port drag handler globals static draggingPort : boolean = false; static isDraggingPortValid: ko.Observable = ko.observable(Errors.Validity.Unknown); - static destinationNode : Node = null; - static destinationPort : Field = null; + static destinationNode : Node | null = null; + static destinationPort : Field | null = null; - static portDragSourceNode : ko.Observable = ko.observable(null); - static portDragSourcePort : ko.Observable = ko.observable(null); + static portDragSourceNode : ko.Observable = ko.observable(null); + static portDragSourcePort : ko.Observable = ko.observable(null); static portDragSourcePortIsInput: boolean = false; - static portDragSuggestedNode : ko.Observable = ko.observable(null); - static portDragSuggestedField : ko.Observable = ko.observable(null); + static portDragSuggestedNode : ko.Observable = ko.observable(null); + static portDragSuggestedField : ko.Observable = ko.observable(null); static portDragSuggestionValidity : ko.Observable = ko.observable(Errors.Validity.Unknown) // this is necessary because we cannot keep the validity on the ege as it does not exist static createEdgeSuggestedPorts : {field:Field,node:Node,validity: Errors.Validity}[] = [] static portMatchCloseEnough :ko.Observable = ko.observable(false); //node drag handler globals - static nodeParentRadiusPreDrag : number = null; + static nodeParentRadiusPreDrag : number | null = null; static nodeDragElement : any = null - static nodeDragNode : Node = null - static dragStartPosition : any = null - static dragCurrentPosition : any = null + static nodeDragNode : Node | null = null + static dragStartPosition: position2d = null; + static dragCurrentPosition : position2d = null; static dragSelectionHandled : any = ko.observable(true) static dragSelectionDoubleClick :boolean = false; @@ -309,11 +311,11 @@ export class GraphRenderer { static altSelect : boolean = false; static shiftSelect : boolean = false; static isDraggingSelectionRegion :boolean = false; - static selectionRegionStart = {x:0, y:0}; - static selectionRegionEnd = {x:0, y:0}; - static ctrlDrag:boolean = null; + static selectionRegionStart: position2d = null; + static selectionRegionEnd: position2d = null; + static ctrlDrag:boolean | null = null; static editNodeName:boolean = false; - static portDragStartPos = {x:0, y:0}; + static portDragStartPos: position2d = null; static simpleSelect : boolean = true; // used for node dragging/selecting. if the cursor position hasn't moved far when click/dragging a node. we wont update the node's position and handle it as a simple select action static mousePosX : ko.Observable = ko.observable(-1); @@ -476,18 +478,18 @@ export class GraphRenderer { } } - static findClosestMatchingAngle (node:Node, angle:number, minPortDistance:number,field:Field,mode: "input" | "output") : number { - let result = 0 - let minAngle - let maxAngle + static findClosestMatchingAngle (node: Node, angle: number, minPortDistance: number, field: Field, mode: "input" | "output") : number { + let result: number = 0 + let minAngle: number = 0 + let maxAngle: number = 0 - let currentAngle = angle - let noMatch = true - let circles = 0 + let currentAngle: number = angle + let noMatch: boolean = true + let circles: number = 0 //checking max angle while(noMatch && circles<10){ - const collidingPortAngle:number = GraphRenderer.checkForPortUsingAngle(node,currentAngle,minPortDistance, field,mode) + const collidingPortAngle = GraphRenderer.checkForPortUsingAngle(node,currentAngle,minPortDistance, field,mode) if(collidingPortAngle === null){ maxAngle = currentAngle // we've found our closest gap when adding to our angle noMatch = false @@ -511,7 +513,7 @@ export class GraphRenderer { //checking min angle while(noMatch && circles<10){ - const collidingPortAngle:number = GraphRenderer.checkForPortUsingAngle(node,currentAngle,minPortDistance, field,mode) + const collidingPortAngle = GraphRenderer.checkForPortUsingAngle(node,currentAngle,minPortDistance, field,mode) if(collidingPortAngle === null){ minAngle = currentAngle // we've found our closest gap when adding to our angle noMatch = false @@ -554,9 +556,9 @@ export class GraphRenderer { return result } - static checkForPortUsingAngle (node:Node, angle:number, minPortDistance:number, activeField:Field, mode: "input" | "output") : number { + static checkForPortUsingAngle (node:Node, angle:number, minPortDistance:number, activeField:Field, mode: "input" | "output") : number | null { //we check if there are any ports within range of the desired angle. if there are we will return the angle of the port we collided with - let result:number = null + let result: number | null = null //dangling ports will collide with all other ports including other dandling ports, connected ports take priority and will push dangling ones out of the way let danglingActivePort = false @@ -636,7 +638,7 @@ export class GraphRenderer { // find a single port of the correct type to consider when looking for adjacentNodes // TODO: why do we select a single port here, why not consider all ports (if multiple exist)? - let field : Field; + let field: Field | undefined = undefined; for(const port of node.getFields()){ if (input && port.isInputPort()){ field = port; @@ -743,7 +745,7 @@ export class GraphRenderer { return interpolatedAngle; } - static createBezier(straightEdgeForce:boolean,addArrowForce:boolean, edge:Edge, srcNodeRadius:number, destNodeRadius:number, srcNodePosition: {x: number, y: number}, destNodePosition: {x: number, y: number}, srcField: Field, destField: Field, sourcePortIsInput: boolean) : string { + static createBezier(straightEdgeForce:boolean, addArrowForce:boolean, edge:Edge | null, srcNodeRadius:number, destNodeRadius:number, srcNodePosition: {x: number, y: number}, destNodePosition: {x: number, y: number}, srcField: Field | null, destField: Field | null, sourcePortIsInput: boolean) : string { //since the svg parent is translated -50% to center our working area, we need to add half of its size to correct the positions const svgTranslationCorrection = EagleConfig.EDGE_SVG_SIZE/2 @@ -817,13 +819,14 @@ export class GraphRenderer { const c2y = destNodePosition.y + destCPOffset.y; //the edge parameter is null if we are rendering a comment edge and this is not needed - if(edge != null || addArrowForce){ - let arrowContainer + if(edge !== null || addArrowForce){ + let arrowContainer = $('#draggingEdge polygon') - if(addArrowForce){ - arrowContainer = $('#draggingEdge polygon') - }else{ - arrowContainer = $('#'+edge.getId() +" polygon") + // TODO: not sure about this change, it think the structure of branches here is a little hard to understand + if(!addArrowForce){ + if (edge !== null) { + arrowContainer = $('#' + edge.getId() + " polygon") + } } //we are hiding the arrows if the edge is too short @@ -899,19 +902,21 @@ export class GraphRenderer { } static getPathDraggingEdge : ko.PureComputed = ko.pureComputed(() => { - if (GraphRenderer.portDragSourceNode() === null){ + const portDragSourceNode = GraphRenderer.portDragSourceNode(); + + if (portDragSourceNode === null){ return ''; } - const srcNodeRadius: number = GraphRenderer.portDragSourceNode().getRadius(); + const srcNodeRadius: number = portDragSourceNode.getRadius(); const destNodeRadius: number = 0; - const srcX: number = GraphRenderer.portDragSourceNode().getPosition().x - srcNodeRadius; - const srcY: number = GraphRenderer.portDragSourceNode().getPosition().y - srcNodeRadius; + const srcX: number = portDragSourceNode.getPosition().x - srcNodeRadius; + const srcY: number = portDragSourceNode.getPosition().y - srcNodeRadius; const destX: number = GraphRenderer.mousePosX(); const destY: number = GraphRenderer.mousePosY(); - const srcField: Field = GraphRenderer.portDragSourcePort(); - const destField: Field = null; + const srcField: Field | null = GraphRenderer.portDragSourcePort(); + const destField: Field | null = null; //if we are dragging from an input port well pass the dragSrcPort(the input port) as the destination of edge. this is so the flow arrow on the edge is point in the correct direction in terms of graph flow if(GraphRenderer.portDragSourcePortIsInput){ @@ -922,7 +927,9 @@ export class GraphRenderer { }, this); static getPathSuggestedEdge : ko.PureComputed = ko.pureComputed(() => { - if (GraphRenderer.portDragSuggestedNode() === null){ + const portDragSuggestedNode = GraphRenderer.portDragSuggestedNode(); + + if (portDragSuggestedNode === null){ return ''; } @@ -932,13 +939,13 @@ export class GraphRenderer { } const srcNodeRadius: number = 0; - const destNodeRadius: number = GraphRenderer.portDragSuggestedNode().getRadius(); + const destNodeRadius: number = portDragSuggestedNode.getRadius(); const srcX: number = GraphRenderer.mousePosX(); const srcY: number = GraphRenderer.mousePosY(); - const destX = GraphRenderer.portDragSuggestedNode().getPosition().x - destNodeRadius; - const destY = GraphRenderer.portDragSuggestedNode().getPosition().y - destNodeRadius; - const srcField: Field = null; - const destField: Field = GraphRenderer.portDragSuggestedField(); + const destX = portDragSuggestedNode.getPosition().x - destNodeRadius; + const destY = portDragSuggestedNode.getPosition().y - destNodeRadius; + const srcField: Field | null = null; + const destField: Field | null = GraphRenderer.portDragSuggestedField(); return GraphRenderer.createBezier(true,false, null, srcNodeRadius, destNodeRadius, {x:srcX, y:srcY}, {x:destX, y:destY}, srcField, destField, GraphRenderer.portDragSourcePortIsInput); }, this); @@ -965,7 +972,7 @@ export class GraphRenderer { const e: WheelEvent = event.originalEvent as WheelEvent; const wheelDelta = e.deltaY; - const zoomDivisor = Setting.findValue(Setting.GRAPH_ZOOM_DIVISOR); + const zoomDivisor = Setting.findValue(Setting.GRAPH_ZOOM_DIVISOR, 1); const xsb = GraphRenderer.SCREEN_TO_GRAPH_POSITION_X(null) const ysb = GraphRenderer.SCREEN_TO_GRAPH_POSITION_Y(null) @@ -1011,10 +1018,15 @@ export class GraphRenderer { static preventBubbling () : void { //calling this function using native JS using onmousedown, onmouseup or onmousemove prevents bubbling these events up without loosing default event handling far any of those events //use this if you want only a click event and prevent any other ko events from being called aka drag, mousedown etc - event.stopPropagation() + event?.stopPropagation() } static startDrag(node: Node, event: MouseEvent) : void { + if (!event.target){ + console.warn("startDrag called with null target"); + return; + } + //if we click on the title of a node, cancel the drag handler if($(event.target).parent().parent().hasClass('header') || $(event.target).parent().hasClass('edgeComments') || $(event.target).parent().hasClass('commentIcons')){ event.preventDefault() @@ -1076,8 +1088,11 @@ export class GraphRenderer { eagle.setSelection(node, Eagle.FileType.Graph); } + const bottomWindowVisible = Setting.findValue(Setting.BOTTOM_WINDOW_VISIBLE, false); + const bottomWindowMode = Setting.findValue(Setting.BOTTOM_WINDOW_MODE, Eagle.BottomWindowMode.None); + //switch back to the node parameter table if a node is selected - if(Setting.findValue(Setting.BOTTOM_WINDOW_VISIBLE) === true && Setting.findValue(Setting.BOTTOM_WINDOW_MODE) !== Eagle.BottomWindowMode.NodeParameterTable){ + if(bottomWindowVisible && bottomWindowMode !== Eagle.BottomWindowMode.NodeParameterTable){ ParameterTable.openTable(Eagle.BottomWindowMode.NodeParameterTable, ParameterTable.SelectType.Normal) } }else{ @@ -1099,11 +1114,11 @@ export class GraphRenderer { static mouseMove(eagle: Eagle, event: JQuery.TriggeredEvent) : void { const e: MouseEvent = event.originalEvent as MouseEvent; - GraphRenderer.ctrlDrag = event.ctrlKey; + GraphRenderer.ctrlDrag = event.ctrlKey as boolean; //ive found that using the event.movementX and Y mouse tracking we were using, is not accurate when browser level zoom is applied. so i am calculating the movement per tick myself //this is done by comparing the current position, with the position recorded by the previous tick of this function - let moveDistance = {x:0,y:0} + let moveDistance: position2d = {x:0,y:0} if(GraphRenderer.dragCurrentPosition){ moveDistance = {x:e.pageX - GraphRenderer.dragCurrentPosition?.x, y: e.pageY - GraphRenderer.dragCurrentPosition?.y} } @@ -1112,8 +1127,10 @@ export class GraphRenderer { if (GraphRenderer.isDragging()){ if (GraphRenderer.draggingNode() !== null && !GraphRenderer.isDraggingSelectionRegion ){ + const dragStartPos = GraphRenderer.dragStartPosition ? GraphRenderer.dragStartPosition : {x:0,y:0} + //check and note if the mouse has moved - GraphRenderer.simpleSelect = GraphRenderer.dragStartPosition.x - moveDistance.x < 5 && GraphRenderer.dragStartPosition.y - moveDistance.y < 5 + GraphRenderer.simpleSelect = dragStartPos.x - moveDistance.x < 5 && dragStartPos.y - moveDistance.y < 5 //this is to prevent the de-parent transition effect, which we don't want in this case $('.node.transition').removeClass('transition') @@ -1150,6 +1167,11 @@ export class GraphRenderer { static endDrag(node: Node) : void { const eagle = Eagle.getInstance(); + if (GraphRenderer.selectionRegionStart === null || GraphRenderer.selectionRegionEnd === null){ + console.warn("endDrag called with null selection region points"); + return; + } + // if we dragged a selection region if (GraphRenderer.isDraggingSelectionRegion){ const nodes: Node[] = GraphRenderer.findNodesInRegion(GraphRenderer.selectionRegionStart.x, GraphRenderer.selectionRegionEnd.x, GraphRenderer.selectionRegionStart.y, GraphRenderer.selectionRegionEnd.y); @@ -1167,10 +1189,8 @@ export class GraphRenderer { //resetting some helper variables GraphRenderer.ctrlDrag = false; - GraphRenderer.selectionRegionStart.x = 0; - GraphRenderer.selectionRegionStart.y = 0; - GraphRenderer.selectionRegionEnd.x = 0; - GraphRenderer.selectionRegionEnd.y = 0; + GraphRenderer.selectionRegionStart = {x: 0, y: 0}; + GraphRenderer.selectionRegionEnd = {x: 0, y: 0}; GraphRenderer.isDraggingSelectionRegion = false; @@ -1212,6 +1232,11 @@ export class GraphRenderer { const containerWidth = $('#logicalGraph').width() const containerHeight = $('#logicalGraph').height() + if (typeof containerWidth === "undefined" || typeof containerHeight === "undefined"){ + console.warn("initiateDragSelection called with undefined container dimensions"); + return; + } + //turning the graph coordinates into a distance from bottom/right for css inset before applying const selectionBottomOffset = containerHeight - GraphRenderer.selectionRegionEnd.y const selectionRightOffset = containerWidth - GraphRenderer.selectionRegionEnd.x @@ -1222,6 +1247,16 @@ export class GraphRenderer { const containerWidth = $('#logicalGraph').width() const containerHeight = $('#logicalGraph').height() + if (GraphRenderer.selectionRegionStart === null || GraphRenderer.selectionRegionEnd === null){ + console.warn("drawSelectionRectangle called with null selection region points"); + return; + } + + if (typeof containerWidth === "undefined" || typeof containerHeight === "undefined"){ + console.warn("drawSelectionRectangle called with undefined container dimensions"); + return; + } + if(GraphRenderer.selectionRegionEnd.x>GraphRenderer.selectionRegionStart.x){ $('#selectionRectangle').css({'left':GraphRenderer.selectionRegionStart.x+'px','right':containerWidth - GraphRenderer.selectionRegionEnd.x+'px'}) }else{ @@ -1275,18 +1310,18 @@ export class GraphRenderer { const outermostNodes : Node[] = eagle.getOutermostSelectedNodes() for (const outermostNode of outermostNodes){ - const oldParent: Node = outermostNode.getParent(); + const oldParent = outermostNode.getParent(); let parentingSuccessful = false; //if the detected parent of one node in the selection changes, we assign the new parent to the whole selection and exit this loop // the parent construct is only allowed to grow by the amount specified(eagleConfig.construct_drag_out_distance) before allowing its children to escape - if(outermostNode.getParent() !== null && oldParent.getRadius()>GraphRenderer.nodeParentRadiusPreDrag+EagleConfig.CONSTRUCT_DRAG_OUT_DISTANCE){ + if(oldParent !== null && GraphRenderer.nodeParentRadiusPreDrag !== null && oldParent.getRadius()>GraphRenderer.nodeParentRadiusPreDrag+EagleConfig.CONSTRUCT_DRAG_OUT_DISTANCE){ $('#'+oldParent.getId()).addClass('transition') GraphRenderer.parentSelection(outermostNodes, null); parentingSuccessful = true; } // check for nodes underneath the node - const parent: Node = eagle.logicalGraph().checkForNodeAt(outermostNode.getPosition().x, outermostNode.getPosition().y, outermostNode.getRadius(), true); + const parent = eagle.logicalGraph().checkForNodeAt(outermostNode.getPosition().x, outermostNode.getPosition().y, outermostNode.getRadius(), true); // check if new candidate parent is already a descendent of the node, this would cause a circular hierarchy which would be bad const ancestorOfParent = GraphRenderer.isAncestor(parent, outermostNode); @@ -1311,9 +1346,9 @@ export class GraphRenderer { } } - static parentSelection(outermostNodes : Node[], parent:Node) : void { + static parentSelection(outermostNodes : Node[], parent: Node | null) : void { + const allowGraphEditing = Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false); - const allowGraphEditing = Setting.findValue(Setting.ALLOW_GRAPH_EDITING); outermostNodes.forEach(function(object){ if(object instanceof Node){ if(!object.isEmbedded() && parent === null){ @@ -1325,11 +1360,15 @@ export class GraphRenderer { }) // resizing the parent construct to fit its new children - GraphRenderer.resizeConstruct(parent) + if (parent !== null){ + GraphRenderer.resizeConstruct(parent) + } //updating the parent construct's "pre-drag" size at the end of parenting all the nodes - // TODO: check this line, could it be: GraphRenderer.nodeParentRadiusPreDrag = parent.getRadius() - GraphRenderer.nodeParentRadiusPreDrag = Eagle.getInstance().logicalGraph().getNodeById(parent?.getId())?.getRadius() + if (parent !== null){ + GraphRenderer.nodeParentRadiusPreDrag = parent.getRadius(); + } + //GraphRenderer.nodeParentRadiusPreDrag = Eagle.getInstance().logicalGraph().getNodeById(parent?.getId())?.getRadius() } static findNodesInRegion(left: number, right: number, top: number, bottom: number): Node[] { @@ -1401,15 +1440,18 @@ export class GraphRenderer { let destFound = false; for (const node of nodes){ + const inputApplication = node.getInputApplication(); + const outputApplication = node.getOutputApplication(); + if ((node.getId() === srcId) || - (node.hasInputApplication() && node.getInputApplication().getId() === srcId) || - (node.hasOutputApplication() && node.getOutputApplication().getId() === srcId)){ + (inputApplication !== null && inputApplication.getId() === srcId) || + (outputApplication !== null && outputApplication.getId() === srcId)){ srcFound = true; } if ((node.getId() === destId) || - (node.hasInputApplication() && node.getInputApplication().getId() === destId) || - (node.hasOutputApplication() && node.getOutputApplication().getId() === destId)){ + (inputApplication !== null && inputApplication.getId() === destId) || + (outputApplication !== null && outputApplication.getId() === destId)){ destFound = true; } } @@ -1445,7 +1487,7 @@ export class GraphRenderer { while(!finished){ let found = false for(const entry of constructsList){ - const parent: Node = entry.getParent(); + const parent = entry.getParent(); if(parent !== null && parent.getId() === findConstructId){ orderedConstructList.unshift(entry) @@ -1467,7 +1509,7 @@ export class GraphRenderer { // TODO: maybe move to LogicalGraph.ts // TODO: the graphNodes parameter probably should be a LogicalGraph - static centerConstruct(construct:Node, graphNodes:Node[]) : void { + static centerConstruct(construct:Node | null, graphNodes:Node[]) : void { if(!construct){ Utils.showNotification('Error','A single Construct node must be selected!',"warning") return @@ -1482,7 +1524,7 @@ export class GraphRenderer { // TODO: redo once we have node.children for (const node of graphNodes){ - const parent: Node = node.getParent(); + const parent = node.getParent(); if (!node.isEmbedded() && parent !== null && parent.getId() === construct.getId()){ childCount++ @@ -1520,6 +1562,7 @@ export class GraphRenderer { static setNewEmbeddedApp(nodeId: NodeId, mode: "addEmbeddedOutputApp" | "addEmbeddedInputApp") :void { const eagle = Eagle.getInstance() const parentNode = eagle.selectedNode() + RightClick.closeCustomContextMenu(true) // try to find the node (by nodeId) in the palettes @@ -1536,6 +1579,12 @@ export class GraphRenderer { return } + // abort if no parent node selected + if (parentNode === null){ + Utils.showNotification("Error", "No parent node selected to add embedded application to", "warning"); + return + } + const newNode: Node = Utils.duplicateNode(node); // add the node to the graph @@ -1558,15 +1607,19 @@ export class GraphRenderer { // loop through all nodes, if they belong to the parent's group, move them too for (const node of eagle.logicalGraph().getNodes()){ - if (node.getParent().getId() === parentId){ + const nodeParent = node.getParent(); + if (nodeParent === null){ + continue; + } + if (nodeParent.getId() === parentId){ node.changePosition(deltaX, deltaY); GraphRenderer.moveChildNodes(node, deltaX, deltaY); } } } - static isAncestor(node : Node, possibleAncestor : Node) : boolean { - let n : Node = node; + static isAncestor(node : Node | null, possibleAncestor : Node) : boolean { + let n : Node | null = node; let iterations = 0; if (n === null){ @@ -1576,7 +1629,7 @@ export class GraphRenderer { while (true){ if (iterations > 32){ console.error("too many iterations in isDescendent()"); - return null; + return false; } iterations += 1; @@ -1587,7 +1640,7 @@ export class GraphRenderer { } // otherwise keep traversing upwards - const newParent: Node = n.getParent(); + const newParent = n.getParent(); // if we reach a null parent, we are done looking if (newParent === null){ @@ -1601,8 +1654,9 @@ export class GraphRenderer { // update the parent of the given node // however, if allowGraphEditing is false, then don't update // TODO: check what to do if incoming parent is null (what happened if old parentId was null?) - static updateNodeParent(node: Node, parent: Node, allowGraphEditing: boolean): void { - if (node.getParent() === null || parent === null || (node.getParent().getId() !== parent.getId()) && allowGraphEditing){ + static updateNodeParent(node: Node, parent: Node | null, allowGraphEditing: boolean): void { + const oldNodeParent = node.getParent(); + if (oldNodeParent === null || parent === null || (oldNodeParent.getId() !== parent.getId()) && allowGraphEditing){ node.setParent(parent); Eagle.getInstance().checkGraph() } @@ -1628,7 +1682,7 @@ export class GraphRenderer { if(GraphRenderer.ctrlDrag && eagle.objectIsSelected(node) && !eagle.objectIsSelected(parent)){ continue } - if (node.getParent().getId() === construct.getId()){ + if (parent.getId() === construct.getId()){ const dx = construct.getPosition().x - node.getPosition().x; const dy = construct.getPosition().y - node.getPosition().y; const distance = Math.sqrt(dx*dx + dy*dy); @@ -1689,14 +1743,20 @@ export class GraphRenderer { } // build the list of all ports in the graph that are a valid end-point for an edge starting at this port - GraphRenderer.createEdgeSuggestedPorts = GraphRenderer.findMatchingPorts(GraphRenderer.portDragSourceNode(), GraphRenderer.portDragSourcePort()); + GraphRenderer.createEdgeSuggestedPorts = GraphRenderer.findMatchingPorts(port.getNode(), port); } static portDragging() : void { GraphRenderer.updateMousePos(); + const portDragSourceNode = GraphRenderer.portDragSourceNode(); + const portDragSourcePort = GraphRenderer.portDragSourcePort(); + if (portDragSourceNode === null || portDragSourcePort === null){ + console.error("source node or port is null in portDragging()"); + return; + } // check for nearest matching port in the nearby nodes - const match: {node: Node, field: Field, validity:Errors.Validity} = GraphRenderer.findNearestMatchingPort(GraphRenderer.mousePosX(), GraphRenderer.mousePosY(), GraphRenderer.portDragSourceNode(), GraphRenderer.portDragSourcePort(), GraphRenderer.portDragSourcePortIsInput); + const match: {node: Node | null, field: Field | null, validity:Errors.Validity} = GraphRenderer.findNearestMatchingPort(GraphRenderer.mousePosX(), GraphRenderer.mousePosY(), portDragSourceNode, portDragSourcePort, GraphRenderer.portDragSourcePortIsInput); if (match.field !== null){ GraphRenderer.portDragSuggestedNode(match.node); @@ -1713,29 +1773,35 @@ export class GraphRenderer { const eagle = Eagle.getInstance(); GraphRenderer.draggingPort = false; + + const srcNode = GraphRenderer.portDragSourceNode(); + const srcPort = GraphRenderer.portDragSourcePort(); + + if (srcNode === null || srcPort === null){ + console.error("source node or port is null in portDragEnd()"); + return; + } + // cleaning up the port drag event listeners $('#logicalGraphParent').off('mouseup.portDrag') $('.node .body').off('mouseup.portDrag') //here - if(Math.abs(GraphRenderer.portDragStartPos.x - GraphRenderer.SCREEN_TO_GRAPH_POSITION_X(null))+Math.abs(GraphRenderer.portDragStartPos.y - GraphRenderer.SCREEN_TO_GRAPH_POSITION_Y(null))<3){ + if(GraphRenderer.portDragStartPos !== null && Math.abs(GraphRenderer.portDragStartPos.x - GraphRenderer.SCREEN_TO_GRAPH_POSITION_X(null))+Math.abs(GraphRenderer.portDragStartPos.y - GraphRenderer.SCREEN_TO_GRAPH_POSITION_Y(null))<3){ //identify a click, if we click a port, we will open the parameter table and highlight the port - ParameterTable.openTableAndSelectField(GraphRenderer.portDragSourceNode(), GraphRenderer.portDragSourcePort()) + ParameterTable.openTableAndSelectField(srcNode, srcPort) GraphRenderer.clearEdgeVars(); }else{ if ((GraphRenderer.destinationPort !== null || GraphRenderer.portDragSuggestedField() !== null) && GraphRenderer.portMatchCloseEnough()){ - const srcNode: Node = GraphRenderer.portDragSourceNode(); - const srcPort: Field = GraphRenderer.portDragSourcePort(); + let destNode: Node | null = null; + let destPort: Field | null = null; - let destNode: Node = null; - let destPort: Field = null; - - if (GraphRenderer.destinationPort !== null){ + if (GraphRenderer.destinationNode !== null && GraphRenderer.destinationPort !== null){ destNode = GraphRenderer.destinationNode; destPort = GraphRenderer.destinationPort; } else { - destNode = GraphRenderer.portDragSuggestedNode(); - destPort = GraphRenderer.portDragSuggestedField(); + destNode = srcNode; + destPort = srcPort; } GraphRenderer.createEdge(srcNode, srcPort, destNode, destPort); @@ -1747,13 +1813,15 @@ export class GraphRenderer { if (GraphRenderer.destinationPort === null){ GraphRenderer.showUserNodeSelectionContextMenu(); } else { - // connect to destination port - const srcNode: Node = GraphRenderer.portDragSourceNode(); - const srcPort: Field = GraphRenderer.portDragSourcePort(); - const destNode: Node = GraphRenderer.destinationNode; - const destPort: Field = GraphRenderer.destinationPort; + let destNode: Node | null = null; + let destPort: Field | null = null; + + if (GraphRenderer.destinationNode !== null && GraphRenderer.destinationPort !== null){ + destNode = GraphRenderer.destinationNode; + destPort = GraphRenderer.destinationPort;// connect to destination port - GraphRenderer.createEdge(srcNode, srcPort, destNode, destPort); + GraphRenderer.createEdge(srcNode, srcPort, destNode, destPort); + } // we can stop rendering the dragging edge GraphRenderer.renderDraggingPortEdge(false); @@ -1793,7 +1861,8 @@ export class GraphRenderer { const linkValid : Errors.Validity = Edge.isValid(eagle, true, null, realSourceNode.getId(), realSourcePort.getId(), realDestinationNode.getId(), realDestinationPort.getId(), false, false, true, true, {errors:[], warnings:[]}); // abort if edge is invalid - if ((Setting.findValue(Setting.ALLOW_INVALID_EDGES) && linkValid === Errors.Validity.Error) || linkValid === Errors.Validity.Valid || linkValid === Errors.Validity.Warning || linkValid === Errors.Validity.Fixable){ + const allowInvalidEdges = Setting.findValue(Setting.ALLOW_INVALID_EDGES, false); + if ((allowInvalidEdges && linkValid === Errors.Validity.Error) || linkValid === Errors.Validity.Valid || linkValid === Errors.Validity.Warning || linkValid === Errors.Validity.Fixable){ if (linkValid === Errors.Validity.Warning){ GraphRenderer.addEdge(realSourceNode, realSourcePort, realDestinationNode, realDestinationPort, true, false); } else { @@ -1844,16 +1913,18 @@ export class GraphRenderer { } } + const portDragSourcePort = GraphRenderer.portDragSourcePort(); + //if enabled, filter the list - if (Setting.findValue(Setting.FILTER_NODE_SUGGESTIONS)){ + if (Setting.findValue(Setting.FILTER_NODE_SUGGESTIONS, false) && portDragSourcePort !== null){ // getting matches from both the graph and the palettes list - const filteredComponents = Utils.getComponentsWithMatchingPort(eligibleComponents, !GraphRenderer.portDragSourcePortIsInput, GraphRenderer.portDragSourcePort().getType()); + const filteredComponents = Utils.getComponentsWithMatchingPort(eligibleComponents, !GraphRenderer.portDragSourcePortIsInput, portDragSourcePort.getType()); eligibleComponents = filteredComponents } // check we found at least one eligible component - if (eligibleComponents.length === 0){ - Utils.showNotification("Not Found", "No eligible components found for connection to port of this type (" + GraphRenderer.portDragSourcePort().getType() + ")", "info"); + if (eligibleComponents.length === 0 && portDragSourcePort !== null){ + Utils.showNotification("Not Found", "No eligible components found for connection to port of this type (" + portDragSourcePort.getType() + ")", "info"); // stop rendering the dragging edge GraphRenderer.renderDraggingPortEdge(false); @@ -1885,18 +1956,27 @@ export class GraphRenderer { } } - static SCREEN_TO_GRAPH_POSITION_X(x:number) : number { + static SCREEN_TO_GRAPH_POSITION_X(x:number | null) : number { const eagle = Eagle.getInstance(); - if(x===null && GraphRenderer.dragCurrentPosition){ - x = GraphRenderer.dragCurrentPosition.x + if(x===null){ + if (GraphRenderer.dragCurrentPosition !== null){ + x = GraphRenderer.dragCurrentPosition.x + } else { + x = 0; + } } + return x/eagle.globalScale() - eagle.globalOffsetX(); } - static SCREEN_TO_GRAPH_POSITION_Y(y:number) : number { + static SCREEN_TO_GRAPH_POSITION_Y(y:number | null) : number { const eagle = Eagle.getInstance(); - if(y===null && GraphRenderer.dragCurrentPosition){ - y = GraphRenderer.dragCurrentPosition.y + if(y===null){ + if (GraphRenderer.dragCurrentPosition !== null){ + y = GraphRenderer.dragCurrentPosition.y + } else { + y = 0; + } } return (y-83.77)/eagle.globalScale() -eagle.globalOffsetY(); } @@ -1971,13 +2051,13 @@ export class GraphRenderer { } let depth : number = 0; - let node : Node = nodes[index]; + let node : Node | undefined = nodes[index]; let nodeId: NodeId; - let nodeParent: Node = node.getParent(); + let nodeParent: Node | null = node.getParent(); let iterations = 0; // follow the chain of parents - while (nodeParent != null){ + while (nodeParent !== null){ if (iterations > 10){ console.error("too many iterations in findDepthOfNode()"); break; @@ -1996,7 +2076,7 @@ export class GraphRenderer { // TODO: could we use something else here? node = GraphRenderer.findNodeWithId(nodeParent.getId(), nodes); - if (node === null){ + if (typeof node === "undefined"){ console.error("Node", nodeId, "has parent", nodeParent ? nodeParent.getName() : null, "but call to findNodeWithId(", nodeParent.getId(), ") returned null"); return depth; } @@ -2018,9 +2098,9 @@ export class GraphRenderer { } // TODO: can we just use LogicalGraph.findNodeById() instead of this function - static findNodeWithId(id: NodeId, nodes: Node[]) : Node { + static findNodeWithId(id: NodeId, nodes: Node[]) : Node | undefined{ if (id === null){ - return null; + return undefined; } for (const node of nodes){ @@ -2028,41 +2108,48 @@ export class GraphRenderer { return node; } + const inputApplication = node.getInputApplication() + const outputApplication = node.getOutputApplication() + // check if the node's inputApp has a matching key - if (node.hasInputApplication()){ - if (node.getInputApplication().getId() === id){ - return node.getInputApplication(); + if (inputApplication !== null){ + if (inputApplication.getId() === id){ + return inputApplication; } } // check if the node's outputApp has a matching key - if (node.hasOutputApplication()){ - if (node.getOutputApplication().getId() === id){ - return node.getOutputApplication(); + if (outputApplication !== null){ + if (outputApplication.getId() === id){ + return outputApplication; } } } console.warn("Cannot find node with id", id); - return null; + return undefined; } static findMatchingPorts(sourceNode: Node, sourcePort: Field): {node: Node, field: Field, validity: Errors.Validity}[]{ const eagle = Eagle.getInstance(); const result: {node: Node, field: Field,validity:Errors.Validity}[] = []; - const minValidity: Errors.Validity = Setting.findValue(Setting.AUTO_COMPLETE_EDGES_LEVEL); + const minValidity: Errors.Validity = Setting.findValue(Setting.AUTO_COMPLETE_EDGES_LEVEL, Errors.Validity.Unknown); const minValidityIndex: number = Object.values(Errors.Validity).indexOf(minValidity); const potentialNodes :Node[] = [] for (const node of eagle.logicalGraph().getNodes()){ potentialNodes.push(node) - if(node.isConstruct && node.getInputApplication()){ - potentialNodes.push(node.getInputApplication()) + + const inputApplication = node.getInputApplication() + const outputApplication = node.getOutputApplication() + + if(node.isConstruct() && inputApplication){ + potentialNodes.push(inputApplication) } - if(node.isConstruct && node.getOutputApplication()){ - potentialNodes.push(node.getOutputApplication()) + if(node.isConstruct() && outputApplication){ + potentialNodes.push(outputApplication) } } @@ -2090,10 +2177,10 @@ export class GraphRenderer { return result; } - static findNearestMatchingPort(positionX: number, positionY: number, sourceNode: Node, sourcePort: Field, sourcePortIsInput: boolean) : {node: Node, field: Field, validity: Errors.Validity} { + static findNearestMatchingPort(positionX: number, positionY: number, sourceNode: Node, sourcePort: Field, sourcePortIsInput: boolean) : {node: Node | null, field: Field | null, validity: Errors.Validity} { let minDistance: number = Number.MAX_SAFE_INTEGER; - let minNode: Node = null; - let minPort: Field = null; + let minNode: Node | null = null; + let minPort: Field | null = null; let minValidity: Errors.Validity = Errors.Validity.Unknown; GraphRenderer.portMatchCloseEnough(false) @@ -2143,6 +2230,15 @@ export class GraphRenderer { return; } + const portDragSourceNode = GraphRenderer.portDragSourceNode(); + const portDragSourcePort = GraphRenderer.portDragSourcePort(); + + // abort if portDragSourceNode or portDragSourcePort are null + if (portDragSourceNode === null || portDragSourcePort === null){ + console.error("portDragSourceNode or portDragSourcePort are null in mouseEnterPort()"); + return; + } + const eagle = Eagle.getInstance(); GraphRenderer.destinationPort = port; GraphRenderer.destinationNode = port.getNode(); @@ -2159,9 +2255,9 @@ export class GraphRenderer { let isValid: Errors.Validity if(!GraphRenderer.portDragSourcePortIsInput){ - isValid = Edge.isValid(eagle, true, null, GraphRenderer.portDragSourceNode().getId(), GraphRenderer.portDragSourcePort().getId(), GraphRenderer.destinationNode.getId(), GraphRenderer.destinationPort.getId(), false, false, false, false, {errors:[], warnings:[]}); + isValid = Edge.isValid(eagle, true, null, portDragSourceNode.getId(), portDragSourcePort.getId(), GraphRenderer.destinationNode.getId(), GraphRenderer.destinationPort.getId(), false, false, false, false, {errors:[], warnings:[]}); }else{ - isValid = Edge.isValid(eagle, true, null, GraphRenderer.destinationNode.getId(), GraphRenderer.destinationPort.getId(), GraphRenderer.portDragSourceNode().getId(), GraphRenderer.portDragSourcePort().getId(), false, false, false, false, {errors:[], warnings:[]}); + isValid = Edge.isValid(eagle, true, null, GraphRenderer.destinationNode.getId(), GraphRenderer.destinationPort.getId(), portDragSourceNode.getId(), portDragSourcePort.getId(), false, false, false, false, {errors:[], warnings:[]}); } GraphRenderer.isDraggingPortValid(isValid); } @@ -2259,14 +2355,17 @@ export class GraphRenderer { const eagle = Eagle.getInstance(); for (const node of eagle.logicalGraph().getNodes()){ if(node.isConstruct()){ - if(node.getInputApplication() != null){ - for (const inputAppField of node.getInputApplication().getFields()){ + const inputApplication = node.getInputApplication() + const outputApplication = node.getOutputApplication() + + if(inputApplication !== null){ + for (const inputAppField of inputApplication.getFields()){ inputAppField.setInputPeek(false) inputAppField.setOutputPeek(false) } } - if(node.getOutputApplication() != null){ - for (const outputAppField of node.getOutputApplication().getFields()){ + if(outputApplication !== null){ + for (const outputAppField of outputApplication.getFields()){ outputAppField.setInputPeek(false) outputAppField.setOutputPeek(false) } @@ -2301,7 +2400,7 @@ export class GraphRenderer { static edgeGetStrokeColor(edge: Edge) : string { const eagle = Eagle.getInstance(); - const showErrorsMode = Setting.findValue(Setting.SHOW_GRAPH_WARNINGS); + const showErrorsMode = Setting.findValue(Setting.SHOW_GRAPH_WARNINGS, Setting.ShowErrorsMode.None); let normalColor: string = EagleConfig.getColor('edgeDefault'); let selectedColor: string = EagleConfig.getColor('edgeDefaultSelected'); diff --git a/src/Hierarchy.ts b/src/Hierarchy.ts index ba9ead4f..5e6c7cf3 100644 --- a/src/Hierarchy.ts +++ b/src/Hierarchy.ts @@ -83,7 +83,7 @@ export class Hierarchy { //handle expanding groups that nodes get drawn to, and handle adding nodeRelative function setNodeRelatives(){ - nodeRelative.forEach(function(element:Node){ + nodeRelative.forEach(function(element:Node | null){ let iterations = 0; if (element === null){ @@ -96,10 +96,11 @@ export class Hierarchy { return } - if(element.isEmbedded()){ - const localPortGroup: Node = element.getEmbed(); - localPortGroup.setExpanded(true) - localPortGroup.setKeepExpanded(true) + const embedNode = element.getEmbed(); + + if(embedNode !== null){ + embedNode.setExpanded(true) + embedNode.setKeepExpanded(true) }else{ element.setExpanded(true) element.setKeepExpanded(true) @@ -126,7 +127,11 @@ export class Hierarchy { const innerItem = $('.hierarchy .hierarchyNodeIsSelected') const parentDiv = $('.hierarchy') if(innerItem.length > 0 && parentDiv.length > 0){ - parentDiv.scrollTop(parentDiv.scrollTop() + innerItem.position().top - parentDiv.height()/2 + innerItem.height()/2) + const parentDivScrollTop = parentDiv.scrollTop() || 0; + const parentDivHeight = parentDiv.height() || 0; + const innerItemHeight = innerItem.height() || 0; + + parentDiv.scrollTop(parentDivScrollTop + innerItem.position().top - parentDivHeight/2 + innerItemHeight/2) } },50) } @@ -172,7 +177,19 @@ export class Hierarchy { const srcNodePos = srcNodeElement.getBoundingClientRect() const destNodePos = destNodeElement.getBoundingClientRect() const parentPos = $("#rightWindowContainer")[0].getBoundingClientRect() - const parentScrollOffset = $(".rightWindowDisplay.hierarchy").scrollTop() + let parentScrollOffset = $(".rightWindowDisplay.hierarchy").scrollTop() + let nodeListWidth = $('#nodeList .col').width(); + + // set default value for parentScrollOffset + if (typeof parentScrollOffset === 'undefined'){ + console.warn("Hierarchy.drawEdge(): could not determine parent scroll offset, defaulting to 0"); + parentScrollOffset = 0; + } + + if (typeof nodeListWidth === 'undefined'){ + console.warn("Hierarchy.drawEdge(): could not determine node list width, defaulting to 300"); + nodeListWidth = 300; + } // determine colour of edge const colour: string = edgeSelected ? EagleConfig.getColor('hierarchyEdgeSelected') : EagleConfig.getColor('hierarchyEdgeDefault'); @@ -190,9 +207,9 @@ export class Hierarchy { $('#nodeList .col').append('
') }else if(use==="output"){ - p1x = ($('#nodeList .col').width() - (parentPos.right-srcNodePos.right))+29 + p1x = (nodeListWidth - (parentPos.right-srcNodePos.right))+29 p1y = ((srcNodePos.top - parentPos.top)+9)+parentScrollOffset - p2x = ($('#nodeList .col').width() - (parentPos.right-destNodePos.right))+39 + p2x = (nodeListWidth - (parentPos.right-destNodePos.right))+39 p2y = ((destNodePos.top - parentPos.top)+9)+parentScrollOffset arrowX = (parentPos.right-destNodePos.right) - 20 mpx = parentPos.right-srcNodePos.right+10 @@ -201,6 +218,10 @@ export class Hierarchy { $('#nodeList .col').append('
') }else{ console.log("error") + p1x = 0 + p1y = 0 + p2x = 0 + p2y = 0 } //Y values re-adjusted for edges diff --git a/src/KeyboardShortcut.ts b/src/KeyboardShortcut.ts index a5f4f1fa..4d9322d3 100644 --- a/src/KeyboardShortcut.ts +++ b/src/KeyboardShortcut.ts @@ -34,26 +34,26 @@ export class KeyboardShortcut { eventType: string; tags: string[]; // tags or key words that are associated with the function to help searchability icon: string; - run: (eagle: Eagle, event: KeyboardEvent) => void; + run: (eagle: Eagle, event: KeyboardEvent | null) => void; constructor(options: KeyboardShortcut.Options){ this.id = options.id; this.text = options.text; this.run = options.run; - if ("keys" in options){ + if (typeof options.keys !== 'undefined'){ this.keys = options.keys; this.eventType = "keydown"; } else { this.keys = []; this.eventType = ""; } - if ("tags" in options){ + if (typeof options.tags !== 'undefined'){ this.tags = options.tags; } else { this.tags = []; } - if ("icon" in options){ + if (typeof options.icon !== 'undefined'){ this.icon = options.icon; } else { this.icon = "build"; @@ -718,7 +718,7 @@ export class KeyboardShortcut { KeyboardShortcut.QUICK_ACTION_DOCS("docs_physicalGraph", "Physical Graph", ['documentation','help','components'], 'https://eagle-dlg.readthedocs.io/en/master/graphs.html#physical-graph'), ]; - static findById(id: string) : KeyboardShortcut { + static findById(id: string) : KeyboardShortcut | null { for (const shortcut of KeyboardShortcut.shortcuts){ if (shortcut.id === id){ return shortcut; @@ -743,7 +743,7 @@ export class KeyboardShortcut { return ks ? ks.text + ' ' + ks.getKeysText(true) : ""; } - static idToRun(id: string): (eagle: Eagle, event: KeyboardEvent) => void { + static idToRun(id: string): ((eagle: Eagle, event: KeyboardEvent) => void) | undefined { const ks = KeyboardShortcut.findById(id); return ks ? ks.run : undefined; } diff --git a/src/LogicalGraph.ts b/src/LogicalGraph.ts index 49f93654..579725f9 100644 --- a/src/LogicalGraph.ts +++ b/src/LogicalGraph.ts @@ -44,7 +44,7 @@ export class LogicalGraph { private nodes : ko.Observable>; private edges : ko.Observable>; private graphConfigs : ko.Observable>; - private activeGraphConfigId : ko.Observable; + private activeGraphConfigId : ko.Observable; private issues : ko.ObservableArray<{issue:Errors.Issue, validity:Errors.Validity}> //keeps track of higher level errors on the graph @@ -84,7 +84,7 @@ export class LogicalGraph { for (const edge of graph.edges().values()){ // depending on the settings and purpose, skip close-loop edges - if (forTranslation && Setting.findValue(Setting.SKIP_CLOSE_LOOP_EDGES)){ + if (forTranslation && Setting.findValue(Setting.SKIP_CLOSE_LOOP_EDGES, false)){ if (edge.isClosesLoop()){ continue; } @@ -109,11 +109,13 @@ export class LogicalGraph { } // for OJS format, we actually store links using the node keys of the construct, not the node keys of the embedded applications - if (srcNode.isEmbedded()){ - srcId = srcNode.getEmbed().getId(); + const srcEmbed = srcNode.getEmbed(); + const destEmbed = destNode.getEmbed(); + if (srcEmbed){ + srcId = srcEmbed.getId(); } - if (destNode.isEmbedded()){ - destId = destNode.getEmbed().getId(); + if (destEmbed){ + destId = destEmbed.getId(); } linkData.from = srcId; @@ -146,15 +148,16 @@ export class LogicalGraph { const nodeData : any = Node.toV4GraphJson(node); result.nodes[id] = nodeData; + const inputApplication = node.getInputApplication(); + const outputApplication = node.getOutputApplication(); + // add input and output applications to the top-level nodes dict - if (node.hasInputApplication()){ - const inputApp = node.getInputApplication(); - result.nodes[inputApp.getId()] = Node.toV4GraphJson(inputApp); + if (inputApplication !== null){ + result.nodes[inputApplication.getId()] = Node.toV4GraphJson(inputApplication); } - if (node.hasOutputApplication()){ - const outputApp = node.getOutputApplication(); - result.nodes[outputApp.getId()] = Node.toV4GraphJson(outputApp); + if (outputApplication !== null){ + result.nodes[outputApplication.getId()] = Node.toV4GraphJson(outputApplication); } } @@ -190,12 +193,13 @@ export class LogicalGraph { // if we are sending this graph for translation, then only provide the "active" graph configuration, or an empty array if none exist // otherwise, add all graph configurations if (forTranslation){ - if (graph.activeGraphConfigId() === null){ + const activeGraphConfig = graph.getActiveGraphConfig(); + + if (typeof activeGraphConfig === "undefined"){ result += '"graphConfigurations": {},\n'; } else { const graphConfigurations: any = {}; - graphConfigurations[graph.activeGraphConfigId().toString()] = GraphConfig.toJson(graph.getActiveGraphConfig()); - + graphConfigurations[activeGraphConfig.getId()] = GraphConfig.toJson(activeGraphConfig); result += '"graphConfigurations": ' + JSON.stringify(graphConfigurations, null, EagleConfig.JSON_INDENT) + ",\n"; } } else { @@ -222,12 +226,13 @@ export class LogicalGraph { // if we are sending this graph for translation, then only provide the "active" graph configuration, or an empty array if none exist // otherwise, add all graph configurations if (forTranslation){ - if (graph.activeGraphConfigId() === null){ + const activeGraphConfig = graph.getActiveGraphConfig(); + + if (typeof activeGraphConfig === "undefined"){ result += '"graphConfigurations": {},\n'; } else { const graphConfigurations: any = {}; - graphConfigurations[graph.activeGraphConfigId().toString()] = GraphConfig.toJson(graph.getActiveGraphConfig()); - + graphConfigurations[activeGraphConfig.getId()] = GraphConfig.toJson(activeGraphConfig); result += '"graphConfigurations": ' + JSON.stringify(graphConfigurations, null, EagleConfig.JSON_INDENT) + ",\n"; } } else { @@ -259,7 +264,7 @@ export class LogicalGraph { return result; } - static fromOJSJson(dataObject : any, filename: string, errorsWarnings : Errors.ErrorsWarnings) : LogicalGraph { + static fromOJSJson(dataObject : any, filename: string | null, errorsWarnings : Errors.ErrorsWarnings) : LogicalGraph { // create new logical graph object const result : LogicalGraph = new LogicalGraph(); const nodeDataIdToNodeId: Map = new Map(); @@ -271,6 +276,11 @@ export class LogicalGraph { // add nodes for (const nodeData of dataObject.nodeDataArray){ const nodeDataId = Node.determineNodeId(nodeData); + + if (nodeDataId === null){ + continue; + } + const newNode = Node.fromOJSJson(nodeData, errorsWarnings, false); if (newNode === null){ @@ -280,12 +290,15 @@ export class LogicalGraph { result.nodes().set(newNode.getId(), newNode); nodeDataIdToNodeId.set(nodeDataId, newNode.getId()); + const inputApplication = newNode.getInputApplication(); + const outputApplication = newNode.getOutputApplication(); + // add input and output applications to the top-level nodes list - if (newNode.hasInputApplication()){ - result.nodes().set(newNode.getInputApplication().getId(), newNode.getInputApplication()); + if (inputApplication !== null){ + result.nodes().set(inputApplication.getId(), inputApplication); } - if (newNode.hasOutputApplication()){ - result.nodes().set(newNode.getOutputApplication().getId(), newNode.getOutputApplication()); + if (outputApplication !== null){ + result.nodes().set(outputApplication.getId(), outputApplication); } result.nodes.valueHasMutated(); @@ -305,12 +318,25 @@ export class LogicalGraph { } const nodeDataId = Node.determineNodeId(nodeData); + + if (nodeDataId === null){ + continue; + } + const nodeId = nodeDataIdToNodeId.get(nodeDataId); const parentId = nodeDataIdToNodeId.get(parentDataId); + if (typeof nodeId === 'undefined' || typeof parentId === 'undefined'){ + continue; + } + const node = result.nodes().get(nodeId); const parent = result.nodes().get(parentId); + if (typeof node === 'undefined' || typeof parent === 'undefined'){ + continue; + } + node.setParent(parent); } @@ -377,15 +403,23 @@ export class LogicalGraph { // if source node or destination node is a construct, then something is wrong, constructs should not have ports if (sourceNode.getCategoryType() === Category.Type.Construct){ const srcIdAndPort = sourceNode.findPortInApplicationsById(edge.getSrcPort().getId()); - const warning = "Updated source node of edge " + edge.getId() + " from construct " + edge.getSrcNode().getId() + " to embedded application " + srcIdAndPort.node.getId(); - errorsWarnings.warnings.push(Errors.Message(warning)); - edge.getSrcNode().setId(srcIdAndPort.node.getId()); + if (typeof srcIdAndPort.node === 'undefined'){ + // TODO: add error + } else { + const warning = "Updated source node of edge " + edge.getId() + " from construct " + edge.getSrcNode().getId() + " to embedded application " + srcIdAndPort.node.getId(); + errorsWarnings.warnings.push(Errors.Message(warning)); + edge.getSrcNode().setId(srcIdAndPort.node.getId()); + } } if (destinationNode.getCategoryType() === Category.Type.Construct){ const destKeyAndPort = destinationNode.findPortInApplicationsById(edge.getDestPort().getId()); - const warning = "Updated destination node of edge " + edge.getId() + " from construct " + edge.getDestNode().getId() + " to embedded application " + destKeyAndPort.node.getId(); - errorsWarnings.warnings.push(Errors.Message(warning)); - edge.getDestNode().setId(destKeyAndPort.node.getId()); + if (typeof destKeyAndPort.node === 'undefined'){ + // TODO: add error + } else { + const warning = "Updated destination node of edge " + edge.getId() + " from construct " + edge.getDestNode().getId() + " to embedded application " + destKeyAndPort.node.getId(); + errorsWarnings.warnings.push(Errors.Message(warning)); + edge.getDestNode().setId(destKeyAndPort.node.getId()); + } } } @@ -413,12 +447,18 @@ export class LogicalGraph { // second pass through the nodes // used to set parent, embed, subject, inputApplication, outputApplication for (const [nodeId, nodeData] of Object.entries(dataObject.nodes)){ - const embed: Node = result.getNodeById((nodeData).embedId); - const parent: Node = result.getNodeById((nodeData).parentId); - const inputApplication: Node = result.getNodeById((nodeData).inputApplicationId); - const outputApplication: Node = result.getNodeById((nodeData).outputApplicationId); + const embed = result.getNodeById((nodeData).embedId); + const parent = result.getNodeById((nodeData).parentId); + const inputApplication = result.getNodeById((nodeData).inputApplicationId); + const outputApplication = result.getNodeById((nodeData).outputApplicationId); + + const node = result.getNodeById(nodeId as NodeId); + + if (typeof node === 'undefined'){ + console.error("No node found with id " + nodeId); + continue; + } - const node: Node = result.getNodeById(nodeId as NodeId); if (typeof embed !== 'undefined'){ node.setEmbed(embed); } @@ -490,11 +530,14 @@ export class LogicalGraph { this.nodes().forEach(function(node: Node){ nodes.push(node) if(node.isConstruct()){ - if(node.getInputApplication()!= null){ - nodes.push(node.getInputApplication()) + const inputApplication = node.getInputApplication(); + const outputApplication = node.getOutputApplication(); + + if(inputApplication != null){ + nodes.push(inputApplication) } - if(node.getOutputApplication() != null){ - nodes.push(node.getOutputApplication()) + if(outputApplication != null){ + nodes.push(outputApplication) } } }) @@ -628,10 +671,15 @@ export class LogicalGraph { } getActiveGraphConfig = (): GraphConfig | undefined => { - return this.getGraphConfigById(this.activeGraphConfigId()) + const activeGraphConfigId = this.activeGraphConfigId(); + if (activeGraphConfigId === null){ + return undefined; + } + + return this.getGraphConfigById(activeGraphConfigId); } - setActiveGraphConfig = (configId: GraphConfigId): void => { + setActiveGraphConfig = (configId: GraphConfigId | null): void => { this.activeGraphConfigId(configId) } @@ -757,7 +805,7 @@ export class LogicalGraph { return newNode; } - findNodeIdByNodeName = (name: string): NodeId => { + findNodeIdByNodeName = (name: string): NodeId | null=> { for (const [id, node] of this.nodes()){ if (node.getName() === name){ return id; @@ -835,12 +883,15 @@ export class LogicalGraph { // delete edges incident on this node this.removeEdgesById(id); + const inputApplication = node.getInputApplication(); + const outputApplication = node.getOutputApplication(); + // delete edges incident on the embedded apps of this node - if (node.hasInputApplication()){ - this.removeEdgesById(node.getInputApplication().getId()); + if (inputApplication !== null){ + this.removeEdgesById(inputApplication.getId()); } - if (node.hasOutputApplication()){ - this.removeEdgesById(node.getOutputApplication().getId()); + if (outputApplication !== null){ + this.removeEdgesById(outputApplication.getId()); } // search through nodes in graph, looking for one with the correct key @@ -855,17 +906,20 @@ export class LogicalGraph { break; } + const inputApplication = node.getInputApplication(); + const outputApplication = node.getOutputApplication(); + // delete the input application - if (node.hasInputApplication() && node.getInputApplication().getId() === id){ - this.nodes().delete(node.getInputApplication().getId()); + if (inputApplication !== null && inputApplication.getId() === id){ + this.nodes().delete(inputApplication.getId()); this.nodes.valueHasMutated(); node.setInputApplication(null); break; } // delete the output application - if (node.hasOutputApplication() && node.getOutputApplication().getId() === id){ - this.nodes().delete(node.getOutputApplication().getId()); + if (outputApplication !== null && outputApplication.getId() === id){ + this.nodes().delete(outputApplication.getId()); this.nodes.valueHasMutated(); node.setOutputApplication(null); break; @@ -878,12 +932,12 @@ export class LogicalGraph { } // remove inputApplication and outputApplication from the nodes map - if (node.hasInputApplication()){ - this.nodes().delete(node.getInputApplication().getId()); + if (inputApplication !== null){ + this.nodes().delete(inputApplication.getId()); this.nodes.valueHasMutated(); } - if (node.hasOutputApplication()){ - this.nodes().delete(node.getOutputApplication().getId()); + if (outputApplication !== null){ + this.nodes().delete(outputApplication.getId()); this.nodes.valueHasMutated(); } } @@ -981,7 +1035,7 @@ export class LogicalGraph { } findMultiplicity = (node : Node) : number => { - let n : Node = node; + let n : Node | null = node; let result : number = 1; let iterations : number = 0; @@ -1009,7 +1063,7 @@ export class LogicalGraph { return result; } - checkForNodeAt = (x: number, y: number, radius: number, findEligibleGroups: boolean = false) : Node => { + checkForNodeAt = (x: number, y: number, radius: number, findEligibleGroups: boolean = false) : Node | null => { const overlaps : Node[] = []; const eagle = Eagle.getInstance(); @@ -1054,7 +1108,7 @@ export class LogicalGraph { // once found all the overlaps, we return the most-leaf (highest depth) node let maxDepth: number = -1; - let maxDepthOverlap: Node = null; + let maxDepthOverlap: Node | null = null; for (const overlap of overlaps){ const depth = this.findDepthById(overlap.getId()); @@ -1071,7 +1125,13 @@ export class LogicalGraph { // TODO: we might be able to just make this findDepth(node: Node) findDepthById = (id: NodeId) : number => { const node = this.nodes().get(id); - let parent: Node = node.getParent(); + + if (typeof node === 'undefined'){ + console.error("findDepthById(): could not find node with id", id); + return -1; + } + + let parent = node.getParent(); let depth = 0; let iterations = 0; @@ -1143,7 +1203,10 @@ export class LogicalGraph { // write nodes to result in sorted order for (const idPlusDepth of idPlusDepths){ - result.push(this.nodes().get(idPlusDepth.id)); + const n = this.nodes().get(idPlusDepth.id); + if (n !== undefined) { + result.push(n); + } } return result; @@ -1355,13 +1418,13 @@ export class LogicalGraph { for (const graphConfig of graph.getGraphConfigs()){ for (const graphConfigNode of graphConfig.getNodes()){ // check that node exists in graph - const graphNode: Node = graph.nodes().get(graphConfigNode.getNode().getId()); + const graphNode = graph.nodes().get(graphConfigNode.getNode().getId()); if (typeof graphNode === 'undefined'){ const issue: Errors.Issue = Errors.Fix( "Node in graph config (" + graphConfig.fileInfo().name + ") is not present in Logical Graph", function(){ - graphConfig.removeNode(graphNode); + graphConfig.removeNodeById(graphConfigNode.getNode().getId()); }, "Delete node from graph config" ); @@ -1370,7 +1433,7 @@ export class LogicalGraph { } for (const graphConfigField of graphConfigNode.getFields()){ - const graphField: Field = graphNode.getFieldById(graphConfigField.getField().getId()); + const graphField = graphNode.getFieldById(graphConfigField.getField().getId()); if (typeof graphField === 'undefined'){ const issue: Errors.Issue = Errors.Fix( diff --git a/src/Modals.ts b/src/Modals.ts index 434a84a4..356f2da6 100644 --- a/src/Modals.ts +++ b/src/Modals.ts @@ -23,7 +23,8 @@ export class Modals { $('#inputModal').on('hidden.bs.modal', function(){ const returnType = $('#inputModal').data('returnType'); const completed: boolean = $('#inputModal').data('completed'); - const input: string = $('#inputModalInput').val().toString(); + const inputModalInputValue = $('#inputModalInput').val(); + const input: string = inputModalInputValue ? inputModalInputValue.toString() : ""; switch (returnType){ case "string": { @@ -74,7 +75,8 @@ export class Modals { console.log("No callback called when #inputTextModal hidden"); } else { const completed: boolean = $('#inputTextModal').data('completed'); - const input: string = $('#inputTextModalInput').val().toString(); + const inputModalInputValue = $('#inputTextModalInput').val(); + const input: string = inputModalInputValue ? inputModalInputValue.toString() : ""; callback(completed, input); } @@ -200,9 +202,11 @@ export class Modals { } else { // check selected option in select tag const choices : string[] = $('#choiceModal').data('choices'); - const choiceIndex : number = parseInt($('#choiceModalSelect').val().toString(), 10); + const choiceModalSelectValue = $('#choiceModalSelect').val(); + const choiceIndex : number = choiceModalSelectValue ? parseInt(choiceModalSelectValue.toString(), 10) : 0; const choice = $('#choiceModalSelect option:selected').text(); - const customChoice = $('#choiceModalString').val().toString(); + const customChoiceValue = $('#choiceModalString').val(); + const customChoice = customChoiceValue ? customChoiceValue.toString() : ""; // if the last item in the select was selected, then return the custom value, // otherwise return the selected choice @@ -227,10 +231,11 @@ export class Modals { }); $('#choiceModalSelect').on('change', function(){ - const choice : number = parseInt($('#choiceModalSelect').val().toString(), 10); + const choiceModalSelectValue = $('#choiceModalSelect').val(); + const choice : number = choiceModalSelectValue ? parseInt(choiceModalSelectValue.toString(), 10) : 0; //checking if the value of the select element is valid - if(!$('#choiceModalSelect').val() || choice > $('#choiceModalSelect option').length){ + if(!choiceModalSelectValue || choice > $('#choiceModalSelect option').length){ $('#choiceModalSelect').val(0) console.warn('Invalid selection value (', choice, '), resetting to 0'); } @@ -331,15 +336,15 @@ export class Modals { // check selected option in select tag const repositoryService : Repository.Service = $('#gitCommitModalRepositoryServiceSelect').val(); const repositories : Repository[] = $('#gitCommitModal').data('repositories'); - const repositoryNameChoice : number = parseInt($('#gitCommitModalRepositoryNameSelect').val().toString(), 10); + const repositoryNameChoice : number = parseInt(Utils.getUIValue('#gitCommitModalRepositoryNameSelect', "0"), 10); // split repository text (with form: "name (branch)") into name and branch strings const repositoryName : string = repositories[repositoryNameChoice].name; const repositoryBranch : string = repositories[repositoryNameChoice].branch; - const filePath : string = $('#gitCommitModalFilePathInput').val().toString(); - let fileName : string = $('#gitCommitModalFileNameInput').val().toString(); - const commitMessage : string = $('#gitCommitModalCommitMessageInput').val().toString(); + const filePath : string = Utils.getUIValue('#gitCommitModalFilePathInput', ""); + let fileName : string = Utils.getUIValue('#gitCommitModalFileNameInput', ""); + const commitMessage : string = Utils.getUIValue('#gitCommitModalCommitMessageInput', ""); // ensure that the graph filename ends with ".graph" or ".palette" as appropriate const fileType : Eagle.FileType = $('#gitCommitModal').data('fileType'); @@ -401,9 +406,9 @@ export class Modals { } else { // check selected option in select tag - const repositoryService : Repository.Service = $('#gitCustomRepositoryModalRepositoryServiceSelect').val(); - const repositoryName : string = $('#gitCustomRepositoryModalRepositoryNameInput').val().toString(); - const repositoryBranch : string = $('#gitCustomRepositoryModalRepositoryBranchInput').val().toString(); + const repositoryService : Repository.Service = Utils.getUIValue('#gitCustomRepositoryModalRepositoryServiceSelect', Repository.Service.Unknown); + const repositoryName : string = Utils.getUIValue('#gitCustomRepositoryModalRepositoryNameInput', ""); + const repositoryBranch : string = Utils.getUIValue('#gitCustomRepositoryModalRepositoryBranchInput', ""); callback(true, repositoryService, repositoryName, repositoryBranch); } @@ -521,30 +526,44 @@ export class Modals { static validateFieldModalValueInputText(data: Field, event: Event): void { const type: string = data.getType() - const value: any = $(event.target).val(); + const eventTarget = event.target; + + if (eventTarget === null){ + console.error("Event target is null in validateFieldModalValueInputText"); + return; + } + + const value: any = $(eventTarget).val(); const realType: string = Utils.translateStringToDataType(Utils.dataTypePrefix(type)); // only validate Json fields if (realType !== Daliuge.DataType.Json){ - $(event.target).removeClass('is-valid'); - $(event.target).removeClass('is-invalid'); + $(eventTarget).removeClass('is-valid'); + $(eventTarget).removeClass('is-invalid'); return; } const isValid = Utils.validateField(realType, value); - Modals._setValidClasses($(event.target), isValid); + Modals._setValidClasses($(eventTarget), isValid); } static validateCommitModalFileNameInputText(): void { const inputElement = $("#gitCommitModalFileNameInput"); + const inputElementValue = inputElement.val(); + + if (typeof inputElementValue === "undefined"){ + console.error("Input element value is undefined in validateCommitModalFileNameInputText"); + return; + } + const fileTypeData = $('#gitCommitModal').data('fileType'); const fileType: Eagle.FileType = fileTypeData ? fileTypeData : Eagle.FileType.Unknown; const isValid = (fileType === Eagle.FileType.Unknown) || - (fileType === Eagle.FileType.Graph && inputElement.val().toString().endsWith(".graph")) || - (fileType === Eagle.FileType.Palette && inputElement.val().toString().endsWith(".palette")) || - (fileType === Eagle.FileType.GraphConfig && inputElement.val().toString().endsWith(".graphConfig")); + (fileType === Eagle.FileType.Graph && inputElementValue.toString().endsWith(".graph")) || + (fileType === Eagle.FileType.Palette && inputElementValue.toString().endsWith(".palette")) || + (fileType === Eagle.FileType.GraphConfig && inputElementValue.toString().endsWith(".graphConfig")); Modals._setValidClasses(inputElement, isValid); } @@ -648,7 +667,7 @@ export class Modals { export namespace Modals { export type UserStringCallback = (completed: boolean, userString: string) => void; export type UserTextCallback = (completed: boolean, userText: string) => void; - export type UserFieldCallback = (field: Field) => void; // NOTE: completed is not required, since all changes happen to the field directly (immediately) + export type UserFieldCallback = (field: Field | null) => void; // NOTE: completed is not required, since all changes happen to the field directly (immediately) export type UserConfirmCallback = (completed: boolean, confirmed: boolean) => void; export type UserDockerHubCallback = (completed: boolean) => void; export type UserOptionsCallback = (selectedOptionIndex: number) => void; diff --git a/src/Node.ts b/src/Node.ts index 81bb2d10..1c6bd470 100644 --- a/src/Node.ts +++ b/src/Node.ts @@ -43,12 +43,12 @@ export class Node { private x : ko.Observable; private y : ko.Observable; - private parent : ko.Observable; // link to the node of which this node is a child - private embed : ko.Observable; // link to the node in which this node is embedded as an input or output application + private parent : ko.Observable; // link to the node of which this node is a child + private embed : ko.Observable; // link to the node in which this node is embedded as an input or output application private children : ko.Observable>; - private inputApplication : ko.Observable; - private outputApplication : ko.Observable; + private inputApplication : ko.Observable; + private outputApplication : ko.Observable; private fields : ko.Observable>; @@ -75,7 +75,7 @@ export class Node { // TODO: unused? shall we remove it? constructor(name : string, description : string, comment : string, category : Category){ - this.id = ko.observable(Utils.generateNodeId()); + this.id = ko.observable("" as NodeId);//Utils.generateNodeId()); this.name = ko.observable(name); this.description = ko.observable(description); this.comment = ko.observable(comment); @@ -93,8 +93,11 @@ export class Node { this.fields = ko.observable(new Map()); this.category = ko.observable(category); + // lookup category data + const categoryData = CategoryData.getCategoryData(category); + // lookup correct categoryType based on category - this.categoryType = ko.observable(CategoryData.getCategoryData(category).categoryType); + this.categoryType = ko.observable(categoryData.categoryType); this.repositoryUrl = ko.observable(""); this.commitHash = ko.observable(""); @@ -108,10 +111,10 @@ export class Node { this.keepExpanded = ko.observable(false); this.peek = ko.observable(false); - this.color = ko.observable(Utils.getColorForNode(this)); + this.color = ko.observable(categoryData.color);//Utils.getColorForNode(this)); this.drawOrderHint = ko.observable(0); - this.radius = ko.observable(Utils.getRadiusForNode(this)); + this.radius = ko.observable(categoryData.radius);//Utils.getRadiusForNode(this)); } getId = () : NodeId => { @@ -237,11 +240,11 @@ export class Node { return this; } - getParent = () : Node => { + getParent = () : Node | null => { return this.parent(); } - setParent = (node: Node) : Node => { + setParent = (node: Node | null) : Node => { // TODO: maybe we should allow this here and just check for the bad state in checkGraph() ? // check that we are not making this node its own parent if (node !== null && node.id() === this.id()){ @@ -250,9 +253,10 @@ export class Node { } // if this node already has a parent, remove this node from the parent's children - if (this.parent() !== null){ - this.parent().children().delete(this.getId()); - this.parent().children.valueHasMutated(); + const parent = this.parent(); + if (parent !== null){ + parent.children().delete(this.getId()); + parent.children.valueHasMutated(); } this.parent(node); @@ -268,11 +272,11 @@ export class Node { return this.parent() !== null; } - getEmbed = () : Node => { + getEmbed = () : Node | null => { return this.embed(); } - setEmbed = (node: Node) : Node => { + setEmbed = (node: Node | null) : Node => { this.embed(node); return this; } @@ -292,7 +296,7 @@ export class Node { isStreaming = () : boolean => { const streamingField = this.findFieldByDisplayText(Daliuge.FieldName.STREAMING); - if (streamingField !== null){ + if (typeof streamingField !== 'undefined'){ return streamingField.valIsTrue(streamingField.getValue()); } @@ -302,7 +306,7 @@ export class Node { isPersist = () : boolean => { const persistField = this.findFieldByDisplayText(Daliuge.FieldName.PERSIST); - if (persistField !== null){ + if (typeof persistField !== 'undefined'){ return persistField.valIsTrue(persistField.getValue()); } @@ -324,10 +328,10 @@ export class Node { isLocked : ko.PureComputed = ko.pureComputed(() => { if(Eagle.selectedLocation() === Eagle.FileType.Graph){ - const allowComponentEditing : boolean = Setting.findValue(Setting.ALLOW_COMPONENT_EDITING); + const allowComponentEditing : boolean = Setting.findValue(Setting.ALLOW_COMPONENT_EDITING, false); return !allowComponentEditing; }else{ - const allowPaletteEditing : boolean = Setting.findValue(Setting.ALLOW_PALETTE_EDITING); + const allowPaletteEditing : boolean = Setting.findValue(Setting.ALLOW_PALETTE_EDITING, false); return !allowPaletteEditing; } }, this); @@ -405,35 +409,39 @@ export class Node { } getInputApplicationInputPorts = () : Field[] => { - if (this.inputApplication() === null){ + const inputApplication = this.inputApplication(); + if (inputApplication === null){ return []; } - return this.inputApplication().getInputPorts(); + return inputApplication.getInputPorts(); } getInputApplicationOutputPorts = () : Field[] => { - if (this.inputApplication() === null){ + const inputApplication = this.inputApplication(); + if (inputApplication === null){ return []; } - return this.inputApplication().getOutputPorts(); + return inputApplication.getOutputPorts(); } getOutputApplicationInputPorts = () : Field[] => { - if (this.outputApplication() === null){ + const outputApplication = this.outputApplication(); + if (outputApplication === null){ return []; } - return this.outputApplication().getInputPorts(); + return outputApplication.getInputPorts(); } getOutputApplicationOutputPorts = () : Field[] => { - if (this.outputApplication() === null){ + const outputApplication = this.outputApplication(); + if (outputApplication === null){ return []; } - return this.outputApplication().getOutputPorts(); + return outputApplication.getOutputPorts(); } hasField = (id: FieldId) : boolean => { @@ -444,16 +452,6 @@ export class Node { return this.fields().get(id); } - getFieldByDisplayText = (displayText : string) : Field | null => { - for (const field of this.fields().values()){ - if (field.getDisplayText() === displayText){ - return field; - } - } - - return null; - } - // TODO: this looks similar to the function above (I think I prefer the name above) hasFieldWithDisplayText = (displayText : string) : boolean => { for (const field of this.fields().values()){ @@ -545,7 +543,7 @@ export class Node { } getDescriptionReadonly = () : boolean => { - const allowParam : boolean =Setting.findValue(Setting.ALLOW_COMPONENT_EDITING); + const allowParam : boolean = Setting.findValue(Setting.ALLOW_COMPONENT_EDITING, false); return !allowParam; } @@ -754,7 +752,7 @@ export class Node { return 'Edit Node Comment:
' + Utils.markdown2html(this.comment()); }, this); - setInputApplication = (inputApplication : Node) : Node => { + setInputApplication = (inputApplication : Node | null) : Node => { console.assert(this.isConstruct() || inputApplication === null, "Can't set non-null input application on node that is not a construct"); this.inputApplication(inputApplication); @@ -766,7 +764,7 @@ export class Node { return this; } - getInputApplication = () : Node => { + getInputApplication = () : Node | null => { return this.inputApplication(); } @@ -774,7 +772,7 @@ export class Node { return this.inputApplication() !== null; } - setOutputApplication = (outputApplication : Node) : Node => { + setOutputApplication = (outputApplication : Node | null) : Node => { console.assert(this.isConstruct() || outputApplication === null, "Can't set non-null output application on node that is not a construct"); this.outputApplication(outputApplication); @@ -786,7 +784,7 @@ export class Node { return this; } - getOutputApplication = () : Node => { + getOutputApplication = () : Node | null => { return this.outputApplication(); } @@ -794,40 +792,6 @@ export class Node { return this.outputApplication() !== null; } - clear = () : Node => { - this.id(null); - this.name(""); - this.description(""); - this.comment(""); - this.x(0); - this.y(0); - this.radius(EagleConfig.MINIMUM_CONSTRUCT_RADIUS); - this.color(EagleConfig.getColor('nodeDefault')); - this.drawOrderHint(0); - - this.parent(null); - this.embed(null); - - this.inputApplication(null); - this.outputApplication(null); - - this.fields().clear(); - this.fields.valueHasMutated(); - - this.category(Category.Unknown); - this.categoryType(Category.Type.Unknown); - - this.expanded(false); - this.keepExpanded(false) - - this.repositoryUrl(""); - this.commitHash(""); - this.paletteDownloadUrl(""); - this.dataHash(""); - - return this; - } - getGitHTML : ko.PureComputed = ko.pureComputed(() => { let url = "Unknown"; let hash = "Unknown"; @@ -842,36 +806,39 @@ export class Node { return '- Git -
Url: ' + url + '
Hash: ' + hash; }, this); - findPortInApplicationsById = (portId: FieldId) : {node: Node, port: Field} => { + findPortInApplicationsById = (portId: FieldId) : {node: Node | undefined, port: Field | undefined} => { + const inputApplication = this.inputApplication(); + const outputApplication = this.outputApplication(); + // if node has an inputApplication, check those ports too - if (this.hasInputApplication()){ - for (const inputPort of this.inputApplication().getInputPorts()){ + if (inputApplication !== null){ + for (const inputPort of inputApplication.getInputPorts()){ if (inputPort.getId() === portId){ - return {node: this.inputApplication(), port: inputPort}; + return {node: inputApplication, port: inputPort}; } } - for (const outputPort of this.inputApplication().getOutputPorts()){ + for (const outputPort of inputApplication.getOutputPorts()){ if (outputPort.getId() === portId){ - return {node: this.inputApplication(), port: outputPort}; + return {node: inputApplication, port: outputPort}; } } } // if node has an outputApplication, check those ports too - if (this.hasOutputApplication()){ - for (const inputPort of this.outputApplication().getInputPorts()){ + if (outputApplication !== null){ + for (const inputPort of outputApplication.getInputPorts()){ if (inputPort.getId() === portId){ - return {node: this.outputApplication(), port: inputPort}; + return {node: outputApplication, port: inputPort}; } } - for (const outputPort of this.outputApplication().getOutputPorts()){ + for (const outputPort of outputApplication.getOutputPorts()){ if (outputPort.getId() === portId){ - return {node: this.outputApplication(), port: outputPort}; + return {node: outputApplication, port: outputPort}; } } } - return {node: null, port: null}; + return {node: undefined, port: undefined}; } findPortIndexById = (portId: FieldId) : number => { @@ -906,7 +873,7 @@ export class Node { return '' } - findPortByDisplayText = (displayText : string, input : boolean, local : boolean) : Field | null => { + findPortByDisplayText = (displayText : string, input : boolean, local : boolean) : Field | undefined => { console.assert(!local); for (const field of this.fields().values()){ @@ -920,17 +887,17 @@ export class Node { } } - return null; + return undefined; } - findFieldByDisplayText = (displayText: string) : Field | null => { + findFieldByDisplayText = (displayText: string) : Field | undefined => { for (const field of this.fields().values()){ if (field.getDisplayText() === displayText){ return field; } } - return null; + return undefined; } @@ -969,7 +936,8 @@ export class Node { } // TODO: this seems similar to findPortTypeById(), maybe we can just use this one! - findPortIsInputById = (portId: string) : boolean => { + // TODO: can we avoid returning nulls here? + findPortIsInputById = (portId: string) : boolean | null => { // find the port within the node for (const inputPort of this.getInputPorts()){ if (inputPort.getId() === portId){ @@ -983,15 +951,18 @@ export class Node { } } + const inputApplication = this.inputApplication(); + const outputApplication = this.outputApplication(); + // check input application ports - if (this.hasInputApplication()){ - for (const inputPort of this.inputApplication().getInputPorts()){ + if (inputApplication !== null){ + for (const inputPort of inputApplication.getInputPorts()){ if (inputPort.getId() === portId){ return true; } } - for (const outputPort of this.inputApplication().getOutputPorts()){ + for (const outputPort of inputApplication.getOutputPorts()){ if (outputPort.getId() === portId){ return false; } @@ -999,14 +970,14 @@ export class Node { } // check output application ports - if (this.hasOutputApplication()){ - for (const inputPort of this.outputApplication().getInputPorts()){ + if (outputApplication !== null){ + for (const inputPort of outputApplication.getInputPorts()){ if (inputPort.getId() === portId){ return true; } } - for (const outputPort of this.outputApplication().getOutputPorts()){ + for (const outputPort of outputApplication.getOutputPorts()){ if (outputPort.getId() === portId){ return false; } @@ -1046,28 +1017,32 @@ export class Node { } setGroupStart = (value: boolean) : Node => { - if (!this.hasFieldWithDisplayText(Daliuge.FieldName.GROUP_START)){ + let groupStartField = this.findFieldByDisplayText(Daliuge.FieldName.GROUP_START); + + if (typeof groupStartField === 'undefined'){ // create a new groupStart field (clone from Daliuge) - const groupStartField: Field = Daliuge.groupStartField.clone().setId(Utils.generateFieldId()).setValue(value.toString()); + groupStartField = Daliuge.groupStartField.clone().setId(Utils.generateFieldId()).setValue(value.toString()); // add field to node this.addField(groupStartField); } else { - this.getFieldByDisplayText(Daliuge.FieldName.GROUP_START).setValue(value.toString()); + groupStartField.setValue(value.toString()); } return this; } setGroupEnd = (value: boolean) : Node => { - if (!this.hasFieldWithDisplayText(Daliuge.FieldName.GROUP_END)){ + let groupEndField = this.findFieldByDisplayText(Daliuge.FieldName.GROUP_END); + + if (typeof groupEndField === 'undefined'){ // create a new groupEnd field (clone from Daliuge) - const groupEndField: Field = Daliuge.groupEndField.clone().setId(Utils.generateFieldId()).setValue(value.toString()); + groupEndField = Daliuge.groupEndField.clone().setId(Utils.generateFieldId()).setValue(value.toString()); // add field to node this.addField(groupEndField); } else { - this.getFieldByDisplayText(Daliuge.FieldName.GROUP_END).setValue(value.toString()); + groupEndField.setValue(value.toString()); } return this; @@ -1163,11 +1138,14 @@ export class Node { result.paletteDownloadUrl(this.paletteDownloadUrl()); result.dataHash(this.dataHash()); - if (this.hasInputApplication()){ - result.inputApplication(this.inputApplication().clone()); + const inputApplication = this.inputApplication(); + const outputApplication = this.outputApplication(); + + if (inputApplication !== null){ + result.inputApplication(inputApplication.clone()); } - if (this.hasOutputApplication()){ - result.outputApplication(this.outputApplication().clone()); + if (outputApplication !== null){ + result.outputApplication(outputApplication.clone()); } return result; @@ -1220,12 +1198,13 @@ export class Node { getBorderColor : ko.PureComputed = ko.pureComputed(() => { const errorsWarnings = this.getAllErrorsWarnings() + const showGraphWarnings = Setting.findValue(Setting.SHOW_GRAPH_WARNINGS, Setting.ShowErrorsMode.None); if(this.isEmbedded()){ return '' //returning nothing lets the means we are not over writing the default css behaviour - }else if(errorsWarnings.errors.length>0 && Setting.findValue(Setting.SHOW_GRAPH_WARNINGS) != Setting.ShowErrorsMode.None){ + }else if(errorsWarnings.errors.length>0 && showGraphWarnings !== Setting.ShowErrorsMode.None){ return EagleConfig.getColor('graphError') - }else if(errorsWarnings.warnings.length>0 && Setting.findValue(Setting.SHOW_GRAPH_WARNINGS) === Setting.ShowErrorsMode.Warnings){ + }else if(errorsWarnings.warnings.length>0 && showGraphWarnings === Setting.ShowErrorsMode.Warnings){ return EagleConfig.getColor('graphWarning') }else{ return EagleConfig.getColor('bodyBorder') @@ -1246,10 +1225,11 @@ export class Node { getBackgroundColor : ko.PureComputed = ko.pureComputed(() => { const errorsWarnings = this.getAllErrorsWarnings() + const showGraphWarnings = Setting.findValue(Setting.SHOW_GRAPH_WARNINGS, Setting.ShowErrorsMode.None); - if(errorsWarnings.errors.length>0 && Setting.findValue(Setting.SHOW_GRAPH_WARNINGS) != Setting.ShowErrorsMode.None){ + if(errorsWarnings.errors.length>0 && showGraphWarnings !== Setting.ShowErrorsMode.None){ return EagleConfig.getColor('errorBackground'); - }else if(errorsWarnings.warnings.length>0 && Setting.findValue(Setting.SHOW_GRAPH_WARNINGS) === Setting.ShowErrorsMode.Warnings){ + }else if(errorsWarnings.warnings.length>0 && showGraphWarnings === Setting.ShowErrorsMode.Warnings){ return EagleConfig.getColor('warningBackground'); }else{ return '' //returning nothing lets the means we are not over writing the default css behaviour @@ -1305,7 +1285,7 @@ export class Node { } graphNodeTitleIsHidden = () : boolean => { - return (this.isData() || this.isGlobal()) && Setting.findValue(Setting.HIDE_DATA_NODE_TITLES); + return (this.isData() || this.isGlobal()) && Setting.findValue(Setting.HIDE_DATA_NODE_TITLES, false); } //get icon color @@ -1316,23 +1296,35 @@ export class Node { getLocalMultiplicity = () : number => { if (this.isMKN()){ - const k : Field = this.getFieldByDisplayText(Daliuge.FieldName.K); + const k = this.findFieldByDisplayText(Daliuge.FieldName.K); - if (k === null){ + if (typeof k === 'undefined'){ return 1; } - return parseInt(k.getValue(), 10); + const kValue = k.getValue(); + + if (kValue === null){ + return 1; + } + + return parseInt(kValue, 10); } if (this.isScatter()){ - const numCopies = this.getFieldByDisplayText(Daliuge.FieldName.NUM_OF_COPIES); + const numCopies = this.findFieldByDisplayText(Daliuge.FieldName.NUM_OF_COPIES); + + if (typeof numCopies === 'undefined'){ + return 1; + } + + const numCopiesValue = numCopies.getValue(); - if (numCopies === null){ + if (numCopiesValue === null){ return 1; } - return parseInt(numCopies.getValue(), 10); + return parseInt(numCopiesValue, 10); } // TODO: check this is correct! @@ -1341,20 +1333,26 @@ export class Node { } if (this.isLoop()){ - const numIter = this.getFieldByDisplayText(Daliuge.FieldName.NUM_OF_ITERATIONS); + const numIter = this.findFieldByDisplayText(Daliuge.FieldName.NUM_OF_ITERATIONS); - if (numIter === null){ + if (typeof numIter === 'undefined'){ return 1; } - return parseInt(numIter.getValue(), 10); + const numIterValue = numIter.getValue(); + + if (numIterValue === null){ + return 1; + } + + return parseInt(numIterValue, 10); } return 1; } addEmptyField = () : Node => { - const newField = new Field(Utils.generateFieldId(), "New Parameter", "", "", "", false, Daliuge.DataType.String, false, [], false, Daliuge.FieldType.Application, Daliuge.FieldUsage.NoPort); + const newField = new Field(this, Utils.generateFieldId(), "New Parameter", "", "", "", false, Daliuge.DataType.String, false, [], false, Daliuge.FieldType.Application, Daliuge.FieldUsage.NoPort); this.addField(newField); return this; } @@ -1415,7 +1413,7 @@ export class Node { } static fromOJSJson(nodeData : any, errorsWarnings: Errors.ErrorsWarnings, isPaletteNode: boolean) : Node { - let id: NodeId = Node.determineNodeId(nodeData); + let id = Node.determineNodeId(nodeData); if (id === null){ errorsWarnings.warnings.push(Errors.Message("Node has undefined id, generating new id")); @@ -1559,6 +1557,7 @@ export class Node { if (categoryData.categoryType !== Category.Type.Construct){ errorsWarnings.errors.push(Errors.Message("Attempt to add inputApplication to unsuitable node: " + category)); } else { + // TODO: should this use node.setInputApplication() instead? node.inputApplication(Node.createEmbeddedApplicationNode(nodeData.application, category, "", "", node)); } } @@ -1568,16 +1567,14 @@ export class Node { if (categoryData.categoryType !== Category.Type.Construct){ errorsWarnings.errors.push(Errors.Message("Attempt to add inputApplication to unsuitable node: " + category)); } else { - node.inputApplication(Node.fromOJSJson(nodeData.inputApplication, errorsWarnings, isPaletteNode)); - node.inputApplication().setEmbed(node); + node.setInputApplication(Node.fromOJSJson(nodeData.inputApplication, errorsWarnings, isPaletteNode)); } } if (typeof nodeData.outputApplication !== 'undefined' && nodeData.outputApplication !== null){ if (categoryData.categoryType !== Category.Type.Construct){ errorsWarnings.errors.push(Errors.Message("Attempt to add outputApplication to unsuitable node: " + category)); } else { - node.outputApplication(Node.fromOJSJson(nodeData.outputApplication, errorsWarnings, isPaletteNode)); - node.outputApplication().setEmbed(node); + node.setOutputApplication(Node.fromOJSJson(nodeData.outputApplication, errorsWarnings, isPaletteNode)); } } @@ -1596,13 +1593,14 @@ export class Node { // add fields if (typeof nodeData.fields !== 'undefined'){ for (const fieldData of nodeData.fields){ - const field = Field.fromOJSJson(fieldData); + const field = Field.fromOJSJson(fieldData, node); // if the parameter type is not specified, assume it is a ComponentParameter if (field.getParameterType() === Daliuge.FieldType.Unknown){ field.setParameterType(Daliuge.FieldType.Component); } + // TODO: required? probably better to set this in Field.fromOJSJson() node.addField(field); } } @@ -1610,7 +1608,7 @@ export class Node { // add application params if (typeof nodeData.applicationArgs !== 'undefined'){ for (const paramData of nodeData.applicationArgs){ - const field = Field.fromOJSJson(paramData); + const field = Field.fromOJSJson(paramData, node); field.setParameterType(Daliuge.FieldType.Application); node.addField(field); } @@ -1619,9 +1617,11 @@ export class Node { // add inputAppFields if (typeof nodeData.inputAppFields !== 'undefined'){ for (const fieldData of nodeData.inputAppFields){ - if (node.hasInputApplication()){ - const field = Field.fromOJSJson(fieldData); - node.inputApplication().addField(field); + const inputApplication = node.inputApplication(); + + if (inputApplication !== null){ + const field = Field.fromOJSJson(fieldData, inputApplication); + inputApplication.addField(field); } else { errorsWarnings.errors.push(Errors.Message("Can't add input app field " + fieldData.text + " to node " + node.getName() + ". No input application.")); } @@ -1631,9 +1631,11 @@ export class Node { // add outputAppFields if (typeof nodeData.outputAppFields !== 'undefined'){ for (const fieldData of nodeData.outputAppFields){ - if (node.hasOutputApplication()){ - const field = Field.fromOJSJson(fieldData); - node.outputApplication().addField(field); + const outputApplication = node.outputApplication(); + + if (outputApplication !== null){ + const field = Field.fromOJSJson(fieldData, outputApplication); + outputApplication.addField(field); } else { errorsWarnings.errors.push(Errors.Message("Can't add output app field " + fieldData.text + " to node " + node.getName() + ". No output application.")); } @@ -1643,7 +1645,7 @@ export class Node { // add input ports if (typeof nodeData.inputPorts !== 'undefined'){ for (const inputPort of nodeData.inputPorts){ - const port = Field.fromOJSJsonPort(inputPort); + const port = Field.fromOJSJsonPort(inputPort, node); port.setParameterType(Daliuge.FieldType.Application); port.setUsage(Daliuge.FieldUsage.InputPort); @@ -1662,7 +1664,7 @@ export class Node { // add output ports if (typeof nodeData.outputPorts !== 'undefined'){ for (const outputPort of nodeData.outputPorts){ - const port = Field.fromOJSJsonPort(outputPort); + const port = Field.fromOJSJsonPort(outputPort, node); port.setParameterType(Daliuge.FieldType.Application); port.setUsage(Daliuge.FieldUsage.OutputPort); @@ -1681,12 +1683,14 @@ export class Node { // add input local ports if (typeof nodeData.inputLocalPorts !== 'undefined'){ for (const inputLocalPort of nodeData.inputLocalPorts){ - if (node.hasInputApplication()){ - const port = Field.fromOJSJsonPort(inputLocalPort); + const inputApplication = node.inputApplication(); + + if (inputApplication !== null){ + const port = Field.fromOJSJsonPort(inputLocalPort, inputApplication); port.setParameterType(Daliuge.FieldType.Application); port.setUsage(Daliuge.FieldUsage.OutputPort); - node.inputApplication().addField(port); + inputApplication.addField(port); } else { errorsWarnings.errors.push(Errors.Message("Can't add inputLocal port " + inputLocalPort.IdText + " to node " + node.getName() + ". No input application.")); } @@ -1696,12 +1700,14 @@ export class Node { // add output local ports if (typeof nodeData.outputLocalPorts !== 'undefined'){ for (const outputLocalPort of nodeData.outputLocalPorts){ - const port = Field.fromOJSJsonPort(outputLocalPort); - port.setParameterType(Daliuge.FieldType.Application); - port.setUsage(Daliuge.FieldUsage.InputPort); + const outputApplication = node.outputApplication(); - if (node.hasOutputApplication()){ - node.outputApplication().addField(port); + if (outputApplication !== null){ + const port = Field.fromOJSJsonPort(outputLocalPort, outputApplication); + port.setParameterType(Daliuge.FieldType.Application); + port.setUsage(Daliuge.FieldUsage.InputPort); + + outputApplication.addField(port); } else { errorsWarnings.errors.push(Errors.Message("Can't add outputLocal port " + outputLocalPort.IdText + " to node " + node.getName() + ". No output application.")); } @@ -1750,7 +1756,8 @@ export class Node { // add fields for (const [id, fieldData] of Object.entries(nodeData.fields)){ - const field = Field.fromV4Json(fieldData); + const field = Field.fromV4Json(fieldData, node); + // TODO: required? probably better to set this in Field.fromV4Json() node.addField(field); } @@ -1776,29 +1783,35 @@ export class Node { // check that the node already has an appropriate embedded application, otherwise create it if (input){ - if (!node.hasInputApplication()){ - if (Setting.findValue(Setting.CREATE_APPLICATIONS_FOR_CONSTRUCT_PORTS)){ - node.setInputApplication(Node.createEmbeddedApplicationNode("Unknown", Category.UnknownApplication, "", "", node)); - errorsWarnings.errors.push(Errors.Message("Created new embedded input application (" + node.inputApplication().getName() + ") for node (" + node.getName() + "). Application category is " + node.inputApplication().getCategory() + " and may require user intervention.")); + let inputApplication = node.inputApplication(); + + if (inputApplication === null){ + if (Setting.findValue(Setting.CREATE_APPLICATIONS_FOR_CONSTRUCT_PORTS, false)){ + inputApplication = Node.createEmbeddedApplicationNode("Unknown", Category.UnknownApplication, "", "", node); + node.setInputApplication(inputApplication); + errorsWarnings.errors.push(Errors.Message("Created new embedded input application (" + inputApplication.getName() + ") for node (" + node.getName() + "). Application category is " + inputApplication.getCategory() + " and may require user intervention.")); } else { errorsWarnings.errors.push(Errors.Message("Cannot add input port to construct that doesn't support input ports (name:" + node.getName() + " category:" + node.getCategory() + ") port name" + port.getDisplayText() )); return; } } - node.inputApplication().addField(port); - errorsWarnings.warnings.push(Errors.Message("Moved input port (" + port.getDisplayText() + "," + port.getId().substring(0,4) + ") on construct node (" + node.getName() + ") to an embedded input application (" + node.inputApplication().getName() + ", " + node.inputApplication().getId() + ")")); + inputApplication.addField(port); + errorsWarnings.warnings.push(Errors.Message("Moved input port (" + port.getDisplayText() + "," + port.getId().substring(0,4) + ") on construct node (" + node.getName() + ") to an embedded input application (" + inputApplication.getName() + ", " + inputApplication.getId() + ")")); } else { - if (!node.hasOutputApplication()){ - if (Setting.findValue(Setting.CREATE_APPLICATIONS_FOR_CONSTRUCT_PORTS)){ - node.setOutputApplication(Node.createEmbeddedApplicationNode("Unknown", Category.UnknownApplication, "", "", node)); - errorsWarnings.errors.push(Errors.Message("Created new embedded output application (" + node.outputApplication().getName() + ") for node (" + node.getName() + "). Application category is " + node.outputApplication().getCategory() + " and may require user intervention.")); + let outputApplication = node.outputApplication(); + + if (outputApplication === null){ + if (Setting.findValue(Setting.CREATE_APPLICATIONS_FOR_CONSTRUCT_PORTS, false)){ + outputApplication = Node.createEmbeddedApplicationNode("Unknown", Category.UnknownApplication, "", "", node); + node.setOutputApplication(outputApplication); + errorsWarnings.errors.push(Errors.Message("Created new embedded output application (" + outputApplication.getName() + ") for node (" + node.getName() + "). Application category is " + outputApplication.getCategory() + " and may require user intervention.")); } else { errorsWarnings.errors.push(Errors.Message("Cannot add output port to construct that doesn't support output ports (name:" + node.getName() + " category:" + node.getCategory() + ") port name" + port.getDisplayText() )); return; } } - node.outputApplication().addField(port); - errorsWarnings.warnings.push(Errors.Message("Moved output port (" + port.getDisplayText() + "," + port.getId().substring(0,4) + ") on construct node (" + node.getName() + ") to an embedded output application (" + node.outputApplication().getName() + ", " + node.outputApplication().getId() + ")")); + outputApplication.addField(port); + errorsWarnings.warnings.push(Errors.Message("Moved output port (" + port.getDisplayText() + "," + port.getId().substring(0,4) + ") on construct node (" + node.getName() + ") to an embedded output application (" + outputApplication.getName() + ", " + outputApplication.getId() + ")")); } } @@ -1818,12 +1831,14 @@ export class Node { result.paletteDownloadUrl = node.paletteDownloadUrl(); result.dataHash = node.dataHash(); - if (node.parent() !== null){ - result.parentId = node.parent().getId(); + const parent = node.parent(); + if (parent !== null){ + result.parentId = parent.getId(); } - if (node.embed() !== null){ - result.embedId = node.embed().getId(); + const embed = node.embed(); + if (embed !== null){ + result.embedId = embed.getId(); } // add fields @@ -1832,29 +1847,32 @@ export class Node { result.fields.push(Field.toOJSJson(field)); } + const inputApplication = node.inputApplication(); + const outputApplication = node.outputApplication(); + // add fields from inputApplication result.inputAppFields = []; - if (node.hasInputApplication()){ - for (const field of node.inputApplication().fields().values()){ + if (inputApplication !== null){ + for (const field of inputApplication.fields().values()){ result.inputAppFields.push(Field.toOJSJson(field)); } } // add fields from outputApplication result.outputAppFields = []; - if (node.hasOutputApplication()){ - for (const field of node.outputApplication().fields().values()){ + if (outputApplication !== null){ + for (const field of outputApplication.fields().values()){ result.outputAppFields.push(Field.toOJSJson(field)); } } // write application names and types - if (node.hasInputApplication()){ - result.inputApplicationName = node.inputApplication().name(); - result.inputApplicationType = node.inputApplication().category(); - result.inputApplicationId = node.inputApplication().id(); - result.inputApplicationDescription = node.inputApplication().description(); - result.inputApplicationComment = node.inputApplication().comment(); + if (inputApplication !== null){ + result.inputApplicationName = inputApplication.name(); + result.inputApplicationType = inputApplication.category(); + result.inputApplicationId = inputApplication.id(); + result.inputApplicationDescription = inputApplication.description(); + result.inputApplicationComment = inputApplication.comment(); } else { result.inputApplicationName = ""; result.inputApplicationType = Category.None; @@ -1862,12 +1880,12 @@ export class Node { result.inputApplicationDescription = ""; result.inputApplicationComment = ""; } - if (node.hasOutputApplication()){ - result.outputApplicationName = node.outputApplication().name(); - result.outputApplicationType = node.outputApplication().category(); - result.outputApplicationId = node.outputApplication().id(); - result.outputApplicationDescription = node.outputApplication().description(); - result.outputApplicationComment = node.outputApplication().comment(); + if (outputApplication !== null){ + result.outputApplicationName = outputApplication.name(); + result.outputApplicationType = outputApplication.category(); + result.outputApplicationId = outputApplication.id(); + result.outputApplicationDescription = outputApplication.description(); + result.outputApplicationComment = outputApplication.comment(); } else { result.outputApplicationName = ""; result.outputApplicationType = Category.None; @@ -1900,12 +1918,14 @@ export class Node { result.paletteDownloadUrl = node.paletteDownloadUrl(); result.dataHash = node.dataHash(); - if (node.parent() !== null){ - result.parentId = node.parent().getId(); + const parent = node.parent(); + if (parent !== null){ + result.parentId = parent.getId(); } - if (node.embed() !== null){ - result.embedId = node.embed().getId(); + const embed = node.embed(); + if (embed !== null){ + result.embedId = embed.getId(); } // add fields @@ -1914,29 +1934,32 @@ export class Node { result.fields.push(Field.toOJSJson(field)); } + const inputApplication = node.inputApplication(); + const outputApplication = node.outputApplication(); + // add fields from inputApplication result.inputAppFields = []; - if (node.hasInputApplication()){ - for (const field of node.inputApplication().fields().values()){ + if (inputApplication !== null){ + for (const field of inputApplication.fields().values()){ result.inputAppFields.push(Field.toOJSJson(field)); } } // add fields from outputApplication result.outputAppFields = []; - if (node.hasOutputApplication()){ - for (const field of node.outputApplication().fields().values()){ + if (outputApplication !== null){ + for (const field of outputApplication.fields().values()){ result.outputAppFields.push(Field.toOJSJson(field)); } } // write application names and types - if (node.hasInputApplication()){ - result.inputApplicationName = node.inputApplication().name(); - result.inputApplicationType = node.inputApplication().category(); - result.inputApplicationId = node.inputApplication().id(); - result.inputApplicationDescription = node.inputApplication().description(); - result.inputApplicationComment = node.inputApplication().comment(); + if (inputApplication !== null){ + result.inputApplicationName = inputApplication.name(); + result.inputApplicationType = inputApplication.category(); + result.inputApplicationId = inputApplication.id(); + result.inputApplicationDescription = inputApplication.description(); + result.inputApplicationComment = inputApplication.comment(); } else { result.inputApplicationName = ""; result.inputApplicationType = Category.None; @@ -1944,12 +1967,12 @@ export class Node { result.inputApplicationDescription = ""; result.inputApplicationComment = ""; } - if (node.hasOutputApplication()){ - result.outputApplicationName = node.outputApplication().name(); - result.outputApplicationType = node.outputApplication().category(); - result.outputApplicationId = node.outputApplication().id(); - result.outputApplicationDescription = node.outputApplication().description(); - result.outputApplicationComment = node.outputApplication().comment(); + if (outputApplication !== null){ + result.outputApplicationName = outputApplication.name(); + result.outputApplicationType = outputApplication.category(); + result.outputApplicationId = outputApplication.id(); + result.outputApplicationDescription = outputApplication.description(); + result.outputApplicationComment = outputApplication.comment(); } else { result.outputApplicationName = ""; result.outputApplicationType = Category.None; @@ -1977,8 +2000,11 @@ export class Node { result.paletteDownloadUrl = node.paletteDownloadUrl(); result.dataHash = node.dataHash(); - result.parentId = node.parent() === null ? null : node.parent().getId(); - result.embedId = node.embed() === null ? null : node.embed().getId(); + const parent = node.parent(); + const embed = node.embed(); + + result.parentId = parent === null ? null : parent.getId(); + result.embedId = embed === null ? null : embed.getId(); // add fields result.fields = {}; @@ -1986,14 +2012,17 @@ export class Node { result.fields[field.getId()] = Field.toV4Json(field); } + const inputApplication = node.inputApplication(); + const outputApplication = node.outputApplication(); + // write application names and types - if (node.hasInputApplication()){ - result.inputApplicationId = node.inputApplication().id(); + if (inputApplication !== null){ + result.inputApplicationId = inputApplication.id(); } else { result.inputApplicationId = null; } - if (node.hasOutputApplication()){ - result.outputApplicationId = node.outputApplication().id(); + if (outputApplication !== null){ + result.outputApplicationId = outputApplication.id(); } else { result.outputApplicationId = null; } @@ -2012,6 +2041,11 @@ export class Node { const eagle = Eagle.getInstance() node.issues([])//clear old issues + const parent = node.parent(); + const embed = node.embed(); + const inputApplication = node.inputApplication(); + const outputApplication = node.outputApplication(); + // looping through and checking all the fields on the node for (const field of node.fields().values()){ Field.isValid(node,field, location) @@ -2025,21 +2059,21 @@ export class Node { if(node.isConstruct()){ //checking the input application if one is present - if(node.hasInputApplication()){ - Node.isValid(node.getInputApplication(), location) + if(inputApplication !== null){ + Node.isValid(inputApplication, location) } //checking the output application if one is present - if(node.hasOutputApplication()){ - Node.isValid(node.getOutputApplication(), location) + if(outputApplication !== null){ + Node.isValid(outputApplication, location) } } // check that parent has this as a child - if (node.parent() !== null){ - const parentsChild = node.parent().getChildById(node.getId()); + if (parent !== null){ + const parentsChild = parent.getChildById(node.getId()); if (typeof parentsChild === 'undefined'){ - const message: string = "Node (" + node.getName() + ") has parent (" + node.parent().getName() + "), but does not appear in that node's list of children."; + const message: string = "Node (" + node.getName() + ") has parent (" + parent.getName() + "), but does not appear in that node's list of children."; const issue: Errors.Issue = Errors.Show(message, function(){Utils.showNode(eagle, location, node)}); node.issues().push({issue:issue, validity:Errors.Validity.Error}); } @@ -2047,7 +2081,8 @@ export class Node { // check that children have this as the parent for (const child of node.children().values()){ - if (child.parent() === null || child.parent().getId() !== node.getId()){ + const childParent = child.parent(); + if (childParent === null || childParent.getId() !== node.getId()){ const message: string = "Node (" + node.getName() + ") has child (" + child.getName() + "), but is not that node's parent."; const issue: Errors.Issue = Errors.Show(message, function(){Utils.showNode(eagle, location, node)}); node.issues().push({issue:issue, validity:Errors.Validity.Error}); @@ -2055,27 +2090,32 @@ export class Node { } // check that embed has this as either inputApplication or outputApplication - if (node.embed() !== null){ - if ((node.embed().hasInputApplication() && node.embed().inputApplication().getId() !== node.getId()) && (node.embed().hasOutputApplication() && node.embed().outputApplication().getId() !== node.getId())){ - const message: string = "Node (" + node.getName() + ") has embed (" + node.embed().getName() + "), but is not that node's inputApplication or outputApplication."; + if (embed !== null){ + const embedInputApplication = embed.inputApplication(); + const embedOutputApplication = embed.outputApplication(); + + if ((embedInputApplication !== null && embedInputApplication.getId() !== node.getId()) && (embedOutputApplication !== null && embedOutputApplication.getId() !== node.getId())){ + const message: string = "Node (" + node.getName() + ") has embed (" + embed.getName() + "), but is not that node's inputApplication or outputApplication."; const issue: Errors.Issue = Errors.Show(message, function(){Utils.showNode(eagle, location, node)}); node.issues().push({issue:issue, validity:Errors.Validity.Error}); } } // check that inputApplication has this as embed - if (node.hasInputApplication()){ - if (node.inputApplication().embed().getId() !== node.getId()){ - const message: string = "Node (" + node.getName() + ") has inputApplication (" + node.inputApplication().getName() + "), but is not that node's embed."; + if (inputApplication !== null){ + const inputApplicationEmbed = inputApplication.embed(); + if (inputApplicationEmbed !== null && inputApplicationEmbed.getId() !== node.getId()){ + const message: string = "Node (" + node.getName() + ") has inputApplication (" + inputApplication.getName() + "), but is not that node's embed."; const issue: Errors.Issue = Errors.Show(message, function(){Utils.showNode(eagle, location, node)}); node.issues().push({issue:issue, validity:Errors.Validity.Error}); } } // check that outputApplication has this as embed - if (node.hasOutputApplication()){ - if (node.outputApplication().embed().getId() !== node.getId()){ - const message: string = "Node (" + node.getName() + ") has outputApplication (" + node.outputApplication().getName() + "), but is not that node's embed."; + if (outputApplication !== null){ + const outputApplicationEmbed = outputApplication.embed(); + if (outputApplicationEmbed !== null && outputApplicationEmbed.getId() !== node.getId()){ + const message: string = "Node (" + node.getName() + ") has outputApplication (" + outputApplication.getName() + "), but is not that node's embed."; const issue: Errors.Issue = Errors.Show(message, function(){Utils.showNode(eagle, location, node)}); node.issues().push({issue:issue, validity:Errors.Validity.Error}); } @@ -2117,7 +2157,7 @@ export class Node { const updatedCategory = Utils.getLegacyCategoryUpdate(node); if (typeof updatedCategory !== 'undefined'){ let updateMessage: string; - let updatedCategoryType: Category.Type = null; + let updatedCategoryType: Category.Type = Category.Type.Unknown; let issue: Errors.Issue; if (updatedCategory === null){ @@ -2168,8 +2208,8 @@ export class Node { // check that Memory and SharedMemory nodes have at least one input OR have a pydata field with a non-"None" value if (node.category() === Category.Memory || node.category() === Category.SharedMemory){ - const pydataField: Field = node.getFieldByDisplayText(Daliuge.FieldName.PYDATA); - const hasPydataValue: boolean = pydataField !== null && pydataField.getValue() !== Daliuge.DEFAULT_PYDATA_VALUE; + const pydataField = node.findFieldByDisplayText(Daliuge.FieldName.PYDATA); + const hasPydataValue: boolean = typeof pydataField !== 'undefined' && pydataField.getValue() !== Daliuge.DEFAULT_PYDATA_VALUE; if (!hasInputEdge && !hasPydataValue){ const message: string = node.category() + " node (" + node.getName() + ") has no connected input edges, and no data in its '" + Daliuge.FieldName.PYDATA + "' field. The node will not contain data."; @@ -2177,7 +2217,7 @@ export class Node { node.issues().push({issue:issue,validity:Errors.Validity.Warning}) } - if (hasInputEdge && hasPydataValue){ + if (hasInputEdge && pydataField && hasPydataValue){ const message: string = node.category() + " node (" + node.getName() + ") has a connected input edge, and also contains data in its '" + Daliuge.FieldName.PYDATA + "' field. The two sources of data could cause a conflict. Note that a " + Daliuge.FieldName.PYDATA + " field is considered a source of data if its value is NOT '" + Daliuge.DEFAULT_PYDATA_VALUE + "'."; const issue: Errors.Issue = Errors.ShowFix(message, function(){Utils.showNode(eagle, location, node)}, function(){if (pydataField.getValue() === ""){pydataField.setValue(Daliuge.DEFAULT_PYDATA_VALUE);}}, "Replace empty pydata with default value (" + Daliuge.DEFAULT_PYDATA_VALUE + ")"); node.issues().push({issue:issue,validity:Errors.Validity.Warning}) @@ -2185,17 +2225,17 @@ export class Node { } // check embedded application categories are not 'None' - if (node.hasInputApplication() && node.getInputApplication().getCategory() === Category.None){ + if (inputApplication !== null && inputApplication.getCategory() === Category.None){ const issue: Errors.Issue = Errors.Message("Node (" + node.getName() + ") has input application with category 'None'.") node.issues().push({issue:issue,validity:Errors.Validity.Error}); } - if (node.hasOutputApplication() && node.getOutputApplication().getCategory() === Category.None){ + if (outputApplication !== null && outputApplication.getCategory() === Category.None){ const issue : Errors.Issue = Errors.Message("Node (" + node.getName() + ") has output application with category 'None'.") node.issues().push({issue:issue,validity:Errors.Validity.Error}); } // check that Service nodes have inputApplications with no output ports! - if (node.getCategory() === Category.Service && node.hasInputApplication() && node.getInputApplication().getOutputPorts().length > 0){ + if (node.getCategory() === Category.Service && inputApplication !== null && inputApplication.getOutputPorts().length > 0){ const issue : Errors.Issue = Errors.Message("Node (" + node.getName() + ") is a Service node, but has an input application with at least one output.") node.issues().push({issue:issue,validity:Errors.Validity.Error}); } @@ -2221,13 +2261,16 @@ export class Node { // check PyFuncApp nodes to make sure contents of func_name field is actually found within the func_code field // check whether the value of func_name is also present in func_code should only be applied if func_code is not empty if (node.category() === Category.PyFuncApp){ - const funcCodeField = node.getFieldByDisplayText(Daliuge.FieldName.FUNC_CODE); - const funcNameField = node.getFieldByDisplayText(Daliuge.FieldName.FUNC_NAME); + const funcCodeField = node.findFieldByDisplayText(Daliuge.FieldName.FUNC_CODE); + const funcNameField = node.findFieldByDisplayText(Daliuge.FieldName.FUNC_NAME); if (funcCodeField && funcNameField){ - if (funcCodeField.getValue().trim() !== ""){ - if (!funcCodeField.getValue().includes(funcNameField.getValue())){ - const issue : Errors.Issue = Errors.Show("Node (" + node.getName() + ") has a value of func_name (" + funcNameField.getValue() + ") which does not appear in its func_code field.", function(){Utils.showNode(eagle, location, node)}); + const funcCodeValue = funcCodeField.getValue(); + const funcNameValue = funcNameField.getValue(); + + if (funcCodeValue !== null && funcNameValue !== null && funcCodeValue.trim() !== ""){ + if (!funcCodeValue.includes(funcNameValue)){ + const issue : Errors.Issue = Errors.Show("Node (" + node.getName() + ") has a value of func_name (" + funcNameValue + ") which does not appear in its func_code field.", function(){Utils.showNode(eagle, location, node)}); node.issues().push({issue:issue,validity:Errors.Validity.Error}); } } @@ -2276,11 +2319,11 @@ export class Node { private static _checkForField(eagle: Eagle, location: Eagle.FileType, node: Node, field: Field) : void { // check if the node already has this field - const existingField = node.getFieldByDisplayText(field.getDisplayText()); + const existingField = node.findFieldByDisplayText(field.getDisplayText()); // if not, create one by cloning the required field // if so, check the attributes of the field match - if (existingField === null){ + if (typeof existingField === 'undefined'){ const message = "Node (" + node.getName() + ":" + node.category() + ":" + node.categoryType() + ") does not have the required '" + field.getDisplayText() + "' field"; const issue : Errors.Issue = Errors.ShowFix(message, function(){Utils.showNode(eagle, location, node);}, function(){Utils.addMissingRequiredField(eagle, node, field);}, "Add missing " + field.getDisplayText() + " field.") node.issues().push({issue:issue,validity:Errors.Validity.Error}); diff --git a/src/Palette.ts b/src/Palette.ts index a278e87a..823d96cf 100644 --- a/src/Palette.ts +++ b/src/Palette.ts @@ -72,10 +72,11 @@ export class Palette { // read node const newNode : Node = Node.fromOJSJson(nodeData, errorsWarnings, true); + const newNodeParent = newNode.getParent(); // check that node has no group - if (newNode.getParent() !== null){ - const error : string = file.name + " Node " + i + " has parent: " + newNode.getParent().getName() + ". Setting parentKey to null."; + if (newNodeParent !== null){ + const error : string = file.name + " Node " + i + " has parent: " + newNodeParent.getName() + ". Setting parentKey to null."; errorsWarnings.warnings.push(Errors.Message(error)); newNode.setParent(null); @@ -131,12 +132,15 @@ export class Palette { // second pass through the nodes // used to set parent, embed, subject, inputApplication, outputApplication for (const [nodeId, nodeData] of Object.entries(dataObject.nodes)){ - const embed: Node = result.getNodeById((nodeData).embedId); - const parent: Node = result.getNodeById((nodeData).parentId); - const inputApplication: Node = result.getNodeById((nodeData).inputApplicationId); - const outputApplication: Node = result.getNodeById((nodeData).outputApplicationId); + const embed = result.getNodeById((nodeData).embedId); + const parent = result.getNodeById((nodeData).parentId); + const inputApplication = result.getNodeById((nodeData).inputApplicationId); + const outputApplication = result.getNodeById((nodeData).outputApplicationId); + const node = result.getNodeById(nodeId as NodeId); - const node: Node = result.getNodeById(nodeId as NodeId); + if (typeof node === 'undefined'){ + continue; + } if (typeof embed !== 'undefined'){ node.setEmbed(embed); } @@ -199,15 +203,16 @@ export class Palette { const nodeData : any = Node.toV4GraphJson(node); result.nodes[id] = nodeData; + const inputApplication = node.getInputApplication(); + const outputApplication = node.getOutputApplication(); + // add input and output applications to the top-level nodes dict - if (node.hasInputApplication()){ - const inputApp = node.getInputApplication(); - result.nodes[inputApp.getId()] = Node.toV4GraphJson(inputApp); + if (inputApplication){ + result.nodes[inputApplication.getId()] = Node.toV4GraphJson(inputApplication); } - if (node.hasOutputApplication()){ - const outputApp = node.getOutputApplication(); - result.nodes[outputApp.getId()] = Node.toV4GraphJson(outputApp); + if (outputApplication){ + result.nodes[outputApplication.getId()] = Node.toV4GraphJson(outputApplication); } } @@ -353,7 +358,7 @@ export class Palette { this.nodes.valueHasMutated(); } - findNodeById = (id: NodeId) : Node => { + findNodeById = (id: NodeId) : Node | undefined => { return this.nodes().get(id); } @@ -362,13 +367,13 @@ export class Palette { this.nodes.valueHasMutated(); } - findNodeByNameAndCategory = (nameAndCategory: Category) : Node => { + findNodeByNameAndCategory = (nameAndCategory: Category) : Node | undefined=> { for (const node of this.nodes().values()){ if (node.getName() === nameAndCategory && node.getCategory() === nameAndCategory){ return node; } } - return null; + return undefined; } getNodesByCategoryType = (categoryType: Category.Type) : Node[] => { @@ -398,30 +403,40 @@ export class Palette { break; } + const inputApplication = node.getInputApplication(); + const outputApplication = node.getOutputApplication(); + + if (typeof inputApplication === 'undefined' && typeof outputApplication === 'undefined'){ + continue; + } + // delete the input application - if (node.hasInputApplication() && node.getInputApplication().getId() === id){ - this.nodes().delete(node.getInputApplication().getId()); + if (inputApplication && inputApplication.getId() === id){ + this.nodes().delete(inputApplication.getId()); this.nodes.valueHasMutated(); node.setInputApplication(null); break; } // delete the output application - if (node.hasOutputApplication() && node.getOutputApplication().getId() === id){ - this.nodes().delete(node.getOutputApplication().getId()); + if (outputApplication && outputApplication.getId() === id){ + this.nodes().delete(outputApplication.getId()); this.nodes.valueHasMutated(); node.setOutputApplication(null); break; } } + const inputApplication = node.getInputApplication(); + const outputApplication = node.getOutputApplication(); + // remove inputApplication and outputApplication from the nodes map - if (node.hasInputApplication()){ - this.nodes().delete(node.getInputApplication().getId()); + if (inputApplication){ + this.nodes().delete(inputApplication.getId()); this.nodes.valueHasMutated(); } - if (node.hasOutputApplication()){ - this.nodes().delete(node.getOutputApplication().getId()); + if (outputApplication){ + this.nodes().delete(outputApplication.getId()); this.nodes.valueHasMutated(); } } diff --git a/src/ParameterTable.ts b/src/ParameterTable.ts index a8c3c54c..81f78be2 100644 --- a/src/ParameterTable.ts +++ b/src/ParameterTable.ts @@ -62,24 +62,35 @@ export class ParameterTable { } static formatTableInspectorSelection = () : string => { - if (ParameterTable.selection() === null){ + const selection = ParameterTable.selection(); + const selectionParent = ParameterTable.selectionParent(); + + if (selection === null){ + return ""; + } + + if (selectionParent === null){ return ""; } - if (Setting.findValue(Setting.BOTTOM_WINDOW_MODE) === Eagle.BottomWindowMode.NodeParameterTable || Setting.findValue(Setting.BOTTOM_WINDOW_MODE) === Eagle.BottomWindowMode.ConfigParameterTable){ - return ParameterTable.selectionParent().getDisplayText() + " - " + ParameterTable.selectionName(); + const bottomWindowMode = Setting.findValue(Setting.BOTTOM_WINDOW_MODE, Eagle.BottomWindowMode.None); + if ([Eagle.BottomWindowMode.NodeParameterTable, Eagle.BottomWindowMode.ConfigParameterTable].includes(bottomWindowMode)){ + return selectionParent.getDisplayText() + " - " + ParameterTable.selectionName(); } else { return "Unknown"; } } static formatTableInspectorValue = () : string => { - if (ParameterTable.selection() === null){ + const selection = ParameterTable.selection(); + + if (selection === null){ return ""; } - if (Setting.findValue(Setting.BOTTOM_WINDOW_MODE) === Eagle.BottomWindowMode.NodeParameterTable || Setting.findValue(Setting.BOTTOM_WINDOW_MODE) === Eagle.BottomWindowMode.ConfigParameterTable){ - return ParameterTable.selection(); + const bottomWindowMode = Setting.findValue(Setting.BOTTOM_WINDOW_MODE, Eagle.BottomWindowMode.None); + if ([Eagle.BottomWindowMode.NodeParameterTable, Eagle.BottomWindowMode.ConfigParameterTable].includes(bottomWindowMode)){ + return selection; } else { return "Unknown"; } @@ -93,6 +104,12 @@ export class ParameterTable { const selected = ParameterTable.selectionName() const selectedForm = ParameterTable.selectionParent() + + if (selectedForm === null){ + console.warn("ParameterTable.tableInspectorUpdateSelection(): Can't find selection parent to update"); + return; + } + if(selected === 'displayText'){ selectedForm.setDisplayText(value) } else if(selected === 'value'){ @@ -124,18 +141,20 @@ export class ParameterTable { //resets the table field selections used for the little editor at the top of the table ParameterTable.resetSelection() - switch (Setting.findValue(Setting.BOTTOM_WINDOW_MODE)){ + const bottomWindowMode = Setting.findValue(Setting.BOTTOM_WINDOW_MODE, Eagle.BottomWindowMode.None); + + switch (bottomWindowMode){ case Eagle.BottomWindowMode.NodeParameterTable: return ParameterTable.fields(); case Eagle.BottomWindowMode.ConfigParameterTable: const lg: LogicalGraph = eagle.logicalGraph(); - const config: GraphConfig = lg.getActiveGraphConfig(); + const config = lg.getActiveGraphConfig(); const displayedFields: Field[] = []; console.log("ParameterTable.getTableFields(): Displaying fields for config:", config ? config.fileInfo().name : ""); - if (!config){ + if (typeof config === 'undefined'){ return []; } @@ -144,8 +163,7 @@ export class ParameterTable { const lgNode = lg.getNodeById(graphConfigNode.getNode().getId()); if (typeof lgNode === 'undefined'){ - const missingNodeField: Field = new Field(graphConfigField.getField().getId(), "", graphConfigField.getValue(), "?", graphConfigField.getComment(), true, Daliuge.DataType.Unknown, false, [], false, Daliuge.FieldType.Unknown, Daliuge.FieldUsage.NoPort); - missingNodeField.setNode(lgNode); + const missingNodeField: Field = new Field(graphConfigNode.getNode(), graphConfigField.getField().getId(), "", graphConfigField.getValue(), "?", graphConfigField.getComment(), true, Daliuge.DataType.Unknown, false, [], false, Daliuge.FieldType.Unknown, Daliuge.FieldUsage.NoPort); displayedFields.push(missingNodeField); continue; } @@ -153,8 +171,7 @@ export class ParameterTable { const lgField = lgNode.getFieldById(graphConfigField.getField().getId()); if (typeof lgField === 'undefined'){ - const missingField: Field = new Field(graphConfigField.getField().getId(), "", graphConfigField.getValue(), "?", graphConfigField.getComment(), true, Daliuge.DataType.Unknown, false, [], false, Daliuge.FieldType.Unknown, Daliuge.FieldUsage.NoPort); - missingField.setNode(lgNode); + const missingField: Field = new Field(lgNode, graphConfigField.getField().getId(), "", graphConfigField.getValue(), "?", graphConfigField.getComment(), true, Daliuge.DataType.Unknown, false, [], false, Daliuge.FieldType.Unknown, Daliuge.FieldUsage.NoPort); displayedFields.push(missingField); continue; } @@ -259,17 +276,21 @@ export class ParameterTable { static getNodeLockedState = (field:Field) : boolean => { const eagle: Eagle = Eagle.getInstance(); + const bottomWindowMode = Setting.findValue(Setting.BOTTOM_WINDOW_MODE, Eagle.BottomWindowMode.None); + // this handles a special case where EAGLE is displaying the "Graph Configuration Attributes Table" // all the field names shown in that table should be locked (readonly) - if (Setting.find(Setting.BOTTOM_WINDOW_MODE).value() === Eagle.BottomWindowMode.ConfigParameterTable){ + if (bottomWindowMode === Eagle.BottomWindowMode.ConfigParameterTable){ return field.getNode().isLocked() } if(Eagle.selectedLocation() === Eagle.FileType.Palette){ - if(eagle.selectedNode() === null){ + const selectedNode = eagle.selectedNode(); + + if(selectedNode === null){ return false } - return eagle.selectedNode().isLocked() + return selectedNode.isLocked() }else{ if(field.getNode() === null){ return false @@ -281,9 +302,9 @@ export class ParameterTable { // TODO: move to Eagle.ts? only depends on Eagle state and settings static getParamsTableEditState = () : boolean => { if(Eagle.selectedLocation() === Eagle.FileType.Palette){ - return !Setting.findValue(Setting.ALLOW_PALETTE_EDITING) + return !Setting.findValue(Setting.ALLOW_PALETTE_EDITING, false) }else{ - return !Setting.findValue(Setting.ALLOW_GRAPH_EDITING) && !Setting.findValue(Setting.ALLOW_COMPONENT_EDITING); + return !Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false) && !Setting.findValue(Setting.ALLOW_COMPONENT_EDITING, false); } } @@ -325,10 +346,19 @@ export class ParameterTable { switch (Eagle.selectedLocation()){ case Eagle.FileType.Palette: { - const paletteNode: Node = eagle.selectedNode(); - console.assert(paletteNode instanceof Node) + const paletteNode = eagle.selectedNode(); + + if (paletteNode === null){ + console.warn("ParameterTable.fieldValueChanged(): Could not find selected palette node to mark as modified"); + return; + } + + const containingPalette = eagle.findPaletteContainingNode(paletteNode.getId()); - const containingPalette: Palette = eagle.findPaletteContainingNode(paletteNode.getId()); + if (typeof containingPalette === 'undefined'){ + console.warn("ParameterTable.fieldValueChanged(): Could not find containing palette to mark as modified"); + return; + } containingPalette.fileInfo().modified = true; break; @@ -413,7 +443,7 @@ export class ParameterTable { } static async _addRemoveField(currentField: Field, add: boolean): Promise { - let graphConfig: GraphConfig = Eagle.getInstance().logicalGraph().getActiveGraphConfig(); + let graphConfig = Eagle.getInstance().logicalGraph().getActiveGraphConfig(); if (graphConfig){ if (add){ @@ -463,10 +493,10 @@ export class ParameterTable { static async requestEditDescriptionInModal(field:Field): Promise { const eagle: Eagle = Eagle.getInstance(); - const node: Node = eagle.selectedNode(); + const selectedNode = eagle.selectedNode(); // check that we can actually find the node that this field belongs to - if (node === null){ + if (selectedNode === null){ Utils.showNotification("Warning", "Could not find node containing this field", "warning"); return; } @@ -479,7 +509,7 @@ export class ParameterTable { let fieldDescription: string; try { - fieldDescription = await Utils.requestUserText("Edit Field Description", "Please edit the description for: " + node.getName() + ' - ' + field.getDisplayText(), field.getDescription()); + fieldDescription = await Utils.requestUserText("Edit Field Description", "Please edit the description for: " + selectedNode.getName() + ' - ' + field.getDisplayText(), field.getDescription()); } catch (error) { console.error(error); return; @@ -491,14 +521,22 @@ export class ParameterTable { static async requestEditValueField(field:Field, defaultValue: boolean) : Promise { const eagle: Eagle = Eagle.getInstance(); - const node: Node = eagle.selectedNode(); + const selectedNode = eagle.selectedNode(); + + // check that we can actually find the node that this field belongs to + if (selectedNode === null){ + Utils.showNotification("Warning", "Could not find node containing this field", "warning"); + return; + } let editingField: Field | GraphConfigField // this will either be the normal field or the configured field if applicable - let editingValue: string // this will either be the value or default value or configured value + let editingValue: string | null // this will either be the value or default value or configured value + + const graphConfigField = field.getGraphConfigField(); //checking if the field is a configured field - if(!defaultValue && field.getGraphConfigField()){ - editingField = field.getGraphConfigField() + if(!defaultValue && graphConfigField){ + editingField = graphConfigField editingValue = editingField.getValue() }else{ editingField = field @@ -512,9 +550,9 @@ export class ParameterTable { let fieldValue: string; try { if (this.isCodeField(field.getDisplayText())){ - fieldValue = await Utils.requestUserCode("python", "Edit Value | Node: " + node.getName() + " - Field: " + field.getDisplayText(), editingValue, false); + fieldValue = await Utils.requestUserCode("python", "Edit Value | Node: " + selectedNode.getName() + " - Field: " + field.getDisplayText(), editingValue, false); }else { - fieldValue = await Utils.requestUserText("Edit Value | Node: " + node.getName() + " - Field: " + field.getDisplayText(), "Please edit the value for: " + node.getName() + ' - ' + field.getDisplayText(), editingValue, false); + fieldValue = await Utils.requestUserText("Edit Value | Node: " + selectedNode.getName() + " - Field: " + field.getDisplayText(), "Please edit the value for: " + selectedNode.getName() + ' - ' + field.getDisplayText(), editingValue, false); } } catch (error) { console.error(error); @@ -532,14 +570,29 @@ export class ParameterTable { static async requestEditCommentInModal(currentField:Field): Promise { const eagle: Eagle = Eagle.getInstance(); const currentNode: Node = currentField.getNode(); - const configField: GraphConfigField = eagle.logicalGraph().getActiveGraphConfig().getNodeById(currentNode.getId()).getFieldById(currentField.getId()); + const activeGraphConfig = eagle.logicalGraph().getActiveGraphConfig(); + + if (typeof activeGraphConfig === 'undefined'){ + console.warn("No active graph configuration to edit field comment in"); + return; + } + + //TODO: can we use: const configField = currentField.getGraphConfigField(); + const configField: GraphConfigField | undefined = activeGraphConfig?.getNodeById(currentNode.getId())?.getFieldById(currentField.getId()); + + if (typeof configField === 'undefined'){ + console.warn("Could not find configuration field to edit comment in"); + return; + } let fieldComment: string; try { fieldComment = await Utils.requestUserText("Edit Field Comment", "Please edit the comment for: " + currentNode.getName() + ' - ' + currentField.getDisplayText(), configField.getComment()); - } catch (error){ - // set the description on the field + // set the comment on the field configField.setComment(fieldComment); + } catch (error){ + console.error(error); + return; } } @@ -612,14 +665,14 @@ export class ParameterTable { static toggleTable = (mode: Eagle.BottomWindowMode, selectType: ParameterTable.SelectType) : void => { // if user in student mode, abort - const inStudentMode: boolean = Setting.findValue(Setting.STUDENT_SETTINGS_MODE); + const inStudentMode: boolean = Setting.findValue(Setting.STUDENT_SETTINGS_MODE, false); if (inStudentMode && mode === Eagle.BottomWindowMode.NodeParameterTable){ Utils.showNotification("Student Mode", "Unable to open Parameter Table in student mode", "danger", false); return; } //if we are already in the requested mode, we can toggle the bottom window - if(Setting.findValue(Setting.BOTTOM_WINDOW_MODE) === mode){ + if(Setting.findValue(Setting.BOTTOM_WINDOW_MODE, Eagle.BottomWindowMode.None) === mode){ SideWindow.toggleShown('bottom') }else{ this.openTable(mode,selectType) @@ -634,7 +687,8 @@ export class ParameterTable { $('.modal.show').modal('hide') } - Setting.find(Setting.BOTTOM_WINDOW_MODE).setValue(mode) + //set the bottom window mode setting to the requested mode + Setting.setValue(Setting.BOTTOM_WINDOW_MODE, mode); //open the bottom window SideWindow.setShown('bottom',true) @@ -668,7 +722,13 @@ export class ParameterTable { } static addEmptyTableRow = () : void => { - const selectedNode: Node = Eagle.getInstance().selectedNode(); + const selectedNode = Eagle.getInstance().selectedNode(); + + if (selectedNode === null){ + console.warn("No selected node to add field to"); + return; + } + selectedNode.addEmptyField(); const fieldIndex = selectedNode.getNumFields()-1; @@ -734,12 +794,18 @@ export class ParameterTable { static deleteTableRow = (field: Field) : void => { const eagle = Eagle.getInstance() - eagle.logicalGraph().removeFieldFromNodeById(eagle.selectedNode(),field.getId()) + const selectedNode = eagle.selectedNode(); + if(selectedNode === null){ + console.warn("No selected node to delete field from"); + return; + } + + eagle.logicalGraph().removeFieldFromNodeById(selectedNode, field.getId()) eagle.selectedObjects.valueHasMutated() eagle.flagActiveFileModified() - //update the parameter table fields array - ParameterTable.updateContent(eagle.selectedNode()) + // update the parameter table fields array + ParameterTable.updateContent(selectedNode) } static getCurrentParamReadonly = (field: Field) : boolean => { @@ -750,13 +816,13 @@ export class ParameterTable { } if(Eagle.selectedLocation() === Eagle.FileType.Palette){ - if(Setting.findValue(Setting.ALLOW_PALETTE_EDITING)){ + if(Setting.findValue(Setting.ALLOW_PALETTE_EDITING, false)){ return false; }else{ return field.isReadonly(); } }else{ - if(Setting.findValue(Setting.ALLOW_COMPONENT_EDITING)){ + if(Setting.findValue(Setting.ALLOW_COMPONENT_EDITING, false)){ return false; }else{ return field.isReadonly(); @@ -771,21 +837,22 @@ export class ParameterTable { return true; } - if(Eagle.selectedLocation() === Eagle.FileType.Palette && Setting.findValue(Setting.ALLOW_PALETTE_EDITING)){ + if(Eagle.selectedLocation() === Eagle.FileType.Palette && Setting.findValue(Setting.ALLOW_PALETTE_EDITING, false)){ return false; } - if (Eagle.selectedLocation() != Eagle.FileType.Palette && Setting.findValue(Setting.ALLOW_COMPONENT_EDITING)){ + if (Eagle.selectedLocation() != Eagle.FileType.Palette && Setting.findValue(Setting.ALLOW_COMPONENT_EDITING, false)){ return false; } - if(Setting.findValue(Setting.VALUE_EDITING_PERMS) === Setting.ValueEditingPermission.ReadOnly){ + const valueEditingPermissions = Setting.findValue(Setting.VALUE_EDITING_PERMS, Setting.ValueEditingPermission.Normal); + if(valueEditingPermissions === Setting.ValueEditingPermission.ReadOnly){ return false; } - if(Setting.findValue(Setting.VALUE_EDITING_PERMS) === Setting.ValueEditingPermission.Normal){ + if(valueEditingPermissions === Setting.ValueEditingPermission.Normal){ return field.isReadonly(); } - if(Setting.findValue(Setting.VALUE_EDITING_PERMS) === Setting.ValueEditingPermission.ConfigOnly){ + if(valueEditingPermissions === Setting.ValueEditingPermission.ConfigOnly){ return field.isReadonly(); } @@ -817,7 +884,7 @@ export class ParameterTable { } } - static updateContent = (node: Node) : void => { + static updateContent = (node: Node | null) : void => { if (node === null){ ParameterTable.copyFields([]); } else { @@ -897,14 +964,13 @@ export class ColumnVisibilities { return this.uiModeName; } - getModeByName = (name:string) : ColumnVisibilities => { - let columnVisibilityResult:ColumnVisibilities = null - columnVisibilities.forEach(function(columnVisibility){ + getModeByName = (name: string) : ColumnVisibilities | undefined => { + for (const columnVisibility of columnVisibilities){ if(columnVisibility.getModeName() === name){ - columnVisibilityResult = columnVisibility + return columnVisibility } - }) - return columnVisibilityResult + } + return undefined; } setModeName = (newUiModeName:string) : void => { @@ -1059,14 +1125,27 @@ export class ColumnVisibilities { } loadFromLocalStorage = () : void => { - const columnVisibilitiesObjArray : any[] = JSON.parse(localStorage.getItem('ColumnVisibilities')) + const columnVisibilities = localStorage.getItem('ColumnVisibilities') + if(columnVisibilities === null){ + console.warn("No saved column visibilities found in local storage"); + return; + } + + const columnVisibilitiesObjArray : any[] = JSON.parse(columnVisibilities) const that = ParameterTable.getActiveColumnVisibility() if(columnVisibilitiesObjArray === null){ return }else{ columnVisibilitiesObjArray.forEach(function(columnVisibility){ - const columnVisActual:ColumnVisibilities = that.getModeByName(columnVisibility.name) + const columnVisActual = that.getModeByName(columnVisibility.name) + + // check that we found a matching mode + if (typeof columnVisActual === 'undefined'){ + console.warn("Could not find column visibility mode with name: " + columnVisibility.name); + return; + } + if(columnVisibility.keyAttribute != null){ columnVisActual.setKeyAttribute(columnVisibility.keyAttribute) } diff --git a/src/QuickActions.ts b/src/QuickActions.ts index ef12d920..0e38fba7 100644 --- a/src/QuickActions.ts +++ b/src/QuickActions.ts @@ -14,7 +14,12 @@ export class QuickActions { setTimeout(function(){ if(QuickActions.open()){ - document.getElementById("quickActionSearchbar").focus(); + const element = document.getElementById("quickActionSearchbar"); + if (!element) { + console.warn("Element with ID 'quickActionSearchbar' not found."); + return; + } + element.focus(); QuickActions.initiateQuickActionQuickSelect() }else{ $('body').off('keydown.quickActions') diff --git a/src/Repositories.ts b/src/Repositories.ts index d8e65b34..80cc48cd 100644 --- a/src/Repositories.ts +++ b/src/Repositories.ts @@ -32,8 +32,8 @@ export class Repositories { isModified = eagle.logicalGraph().fileInfo().modified; break; case Eagle.FileType.Palette: { - const palette: Palette = eagle.findPalette(file.name, false); - isModified = palette !== null && palette.fileInfo().modified; + const palette = eagle.findPalette(file.name, false); + isModified = typeof palette !== "undefined" && palette.fileInfo().modified; break; } case Eagle.FileType.JSON: @@ -42,9 +42,10 @@ export class Repositories { } // if the file is modified, get the user to confirm they want to overwrite changes - const confirmDiscardChanges: Setting = Setting.find(Setting.CONFIRM_DISCARD_CHANGES); - if (isModified && confirmDiscardChanges.value()){ - const confirmed = await Utils.requestUserConfirm("Discard changes?", "Opening a new file will discard changes. Continue?", "OK", "Cancel", confirmDiscardChanges); + const confirmDiscardChangesSetting = Setting.find(Setting.CONFIRM_DISCARD_CHANGES); + const confirmDiscardChanges: boolean = confirmDiscardChangesSetting ? confirmDiscardChangesSetting.value() as boolean : true; // confirm by default if setting is undefined + if (isModified && confirmDiscardChanges){ + const confirmed = await Utils.requestUserConfirm("Discard changes?", "Opening a new file will discard changes. Continue?", "OK", "Cancel", confirmDiscardChangesSetting); if (confirmed){ eagle.openRemoteFile(file); } @@ -109,10 +110,11 @@ export class Repositories { } removeCustomRepository = async (repository : Repository): Promise => { - const confirmRemoveRepositories: Setting = Setting.find(Setting.CONFIRM_REMOVE_REPOSITORIES); + const confirmRemoveRepositories = Setting.find(Setting.CONFIRM_REMOVE_REPOSITORIES); + const confirmRemoveRepositoriesValue: boolean = confirmRemoveRepositories ? confirmRemoveRepositories.value() as boolean : true; // confirm by default if setting is undefined // if settings dictates that we don't confirm with user, remove immediately - if (!confirmRemoveRepositories.value()){ + if (!confirmRemoveRepositoriesValue){ this._removeCustomRepository(repository); return; } diff --git a/src/Repository.ts b/src/Repository.ts index 2182673b..10f6e54c 100644 --- a/src/Repository.ts +++ b/src/Repository.ts @@ -89,7 +89,7 @@ export class Repository { // browse down into a repository, along the path, and return the RepositoryFolder there // or if no path, just return the Repository // or if path not found, return null - findPath = (path: string): Repository | RepositoryFolder => { + findPath = (path: string): Repository | RepositoryFolder | null => { if (path === ""){ return this; } @@ -186,7 +186,7 @@ export class Repository { deleteFile = (file: RepositoryFile) : void => { let pointer: Repository | RepositoryFolder = this; - let lastPointer: Repository | RepositoryFolder = null; + let lastPointer: Repository | RepositoryFolder = pointer; const fileIsInTopLevelOfRepo: boolean = file.path === ""; if (!fileIsInTopLevelOfRepo){ diff --git a/src/RightClick.ts b/src/RightClick.ts index de57f2ba..4c7b2014 100644 --- a/src/RightClick.ts +++ b/src/RightClick.ts @@ -14,14 +14,14 @@ import { EagleConfig } from './EagleConfig'; export class RightClick { - static edgeDropSrcNode : Node - static edgeDropSrcPort : Field + static edgeDropSrcNode : Node | null + static edgeDropSrcPort : Field | null static edgeDropSrcIsInput : boolean constructor(){ RightClick.edgeDropSrcNode = null; RightClick.edgeDropSrcPort = null; - RightClick.edgeDropSrcIsInput = null; + RightClick.edgeDropSrcIsInput = false; } static openSubMenu(menuElement: HTMLElement) : void { @@ -34,17 +34,30 @@ export class RightClick { // TODO: global event static checkSearchField() : void { - const searchValue:string = $(event.target).val().toString().toLocaleLowerCase() + const searchField = $((event as any).target) + + if (typeof searchField === 'undefined') { + console.warn('Search field not found in checkSearchField()'); + return; + } + + const searchFieldValue = searchField.val(); + + if (typeof searchFieldValue === 'undefined') { + console.warn('Search field value is undefined in checkSearchField()'); + return; + } + + const searchValue:string = searchFieldValue.toString().toLocaleLowerCase() let paletteNodesHtml = '' let graphNodesHtml = '' - $(".rightClickFocus").removeClass('rightClickFocus') if(searchValue !== ''){ //if the search bar is not empty $('#rightClickPaletteList').hide() - $(event.target).parent().find('a').show() + $((event as any).target).parent().find('a').show() $('#paletteNodesSearchResult').remove() $('#customContextMenu .searchBarContainer').after("
") @@ -52,10 +65,17 @@ export class RightClick { dropDownOptions.each(function(index,dropdownOption){ const dropdownNode = $(dropdownOption).text().toLocaleLowerCase(); if(dropdownNode.toLocaleLowerCase().includes(searchValue)){ + const option = $(dropdownOption).clone().get(0); + + if (typeof option === 'undefined') { + console.warn('Could not clone dropdown option for search result:', dropdownOption); + return; + } + if($(dropdownOption).hasClass('graphNode')){ - graphNodesHtml= graphNodesHtml +$(dropdownOption).clone().get(0).outerHTML + graphNodesHtml= graphNodesHtml + option.outerHTML }else{ - paletteNodesHtml=paletteNodesHtml +$(dropdownOption).clone().get(0).outerHTML + paletteNodesHtml=paletteNodesHtml + option.outerHTML } } }) @@ -70,7 +90,7 @@ export class RightClick { } } else{ //if the search bar is empty - $(event.target).parent().find('a').hide() + $((event as any).target).parent().find('a').hide() $('#rightClickPaletteList').show() $('#paletteNodesSearchResult').remove() } @@ -109,11 +129,11 @@ export class RightClick { // add nodes from each palette palettes.forEach(function(palette){ - paletteList += RightClick.constructHtmlPaletteList(Array.from(palette.getNodes()),'addNode',null,palette.fileInfo().name,null) + paletteList += RightClick.constructHtmlPaletteList(Array.from(palette.getNodes()),'addNode', [], palette.fileInfo().name,null) }) // add nodes from the logical graph - paletteList += RightClick.constructHtmlPaletteList(Array.from(eagle.logicalGraph().getNodes()),'addNode',null,'Graph',null) + paletteList += RightClick.constructHtmlPaletteList(Array.from(eagle.logicalGraph().getNodes()),'addNode', [], 'Graph',null) return paletteList } @@ -132,7 +152,7 @@ export class RightClick { return paletteList } - static createHtmlEligibleEmbeddedNodesList(nodeType:Category.Type,passedObjectClass:string) : string { + static createHtmlEligibleEmbeddedNodesList(nodeType:Category.Type, passedObjectClass: "addEmbeddedOutputApp" | "addEmbeddedInputApp") : string { const eagle: Eagle = Eagle.getInstance(); let paletteList:string = '' @@ -147,7 +167,7 @@ export class RightClick { paletteNodes.push(paletteNode) } } - paletteList += RightClick.constructHtmlPaletteList(paletteNodes,'embedNode',null,palette.fileInfo().name,passedObjectClass) + paletteList += RightClick.constructHtmlPaletteList(paletteNodes,'embedNode', [], palette.fileInfo().name,passedObjectClass) } //sorting and adding compatible nodes from the graph @@ -157,12 +177,13 @@ export class RightClick { graphNodes.push(graphNode) } } - paletteList += RightClick.constructHtmlPaletteList(graphNodes,'embedNode',null,'Graph',passedObjectClass) + paletteList += RightClick.constructHtmlPaletteList(graphNodes,'embedNode', [], 'Graph',passedObjectClass) return paletteList } - static constructHtmlPaletteList(collectionOfNodes:Node[], mode: "addNode" | "addAndConnect" | "embedNode", compatibleNodesList:Node[],paletteName:string,embedMode:String) : string { + // TODO: include the "embed mode" options in the mode enum, since the embed mode is only relevant when mode is 'embedNode' + static constructHtmlPaletteList(collectionOfNodes:Node[], mode: "addNode" | "addAndConnect" | "embedNode", compatibleNodesList:Node[], paletteName:string, embedMode: "addEmbeddedOutputApp" | "addEmbeddedInputApp" | null) : string { let nodesHtml = '' let nodeFound = false let htmlPalette = ""+paletteName @@ -441,10 +462,11 @@ export class RightClick { // TODO: event var used in function is the deprecated global, we should get access to the event via some other method static edgeDropCreateNode = (data: Node[]) : void => { - RightClick.requestCustomContextMenu(data, 'edgeDropCreate') + RightClick.requestCustomContextMenu(data, 'edgeDropCreate'); // prevent bubbling events - event.stopPropagation(); + // TODO: get event from somewhere instead of global + (event as any).stopPropagation(); } static editNodeFuncCode = () : void => { @@ -456,7 +478,12 @@ export class RightClick { } const funcCodeField = rightClickObject.findFieldByDisplayText(Daliuge.FieldName.FUNC_CODE); - ParameterTable.requestEditValueField(funcCodeField, false) + + if (typeof funcCodeField === 'undefined'){ + console.warn("editNodeFuncCode() could not find " + Daliuge.FieldName.FUNC_CODE + " field on node:", rightClickObject); + } else { + ParameterTable.requestEditValueField(funcCodeField, false) + } } // TODO: event var used in function is the deprecated global, we should get access to the event via some other method @@ -489,13 +516,16 @@ export class RightClick { const minXMargin = 390 // this is the minimum amount of room we need on the right side of the click location to draw the context menu const minYMargin = 430 // this is the minimum amount of room we need on the bottom side of the click location to draw the context menu + const innerWidth = $(document).innerWidth() || 0 + const innerHeight = $(document).innerHeight() || 0 + //checking for screen real estate to the right and bottom, if we are too close to the edges of the window, we expand left, up or both - if($(document).innerWidth()-mouseX(Setting.ALLOW_GRAPH_EDITING, false)){ $('#customContextMenu').append(searchbar) $('#customContextMenu').append('
') @@ -570,7 +600,7 @@ export class RightClick { //edge drop menu options }else if(passedObjectClass === 'edgeDropCreate'){ - if(Setting.findValue(Setting.ALLOW_GRAPH_EDITING)){ + if(Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false)){ $('#customContextMenu').append(searchbar) $('#customContextMenu').append('
') @@ -587,9 +617,13 @@ export class RightClick { //construct embedded app options }else if(passedObjectClass === 'addEmbeddedOutputApp' || passedObjectClass === 'addEmbeddedInputApp'){ - if(Setting.findValue(Setting.ALLOW_GRAPH_EDITING)){ - - $(thisEvent.target).addClass('fullOpacity') + if(Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false)){ + const target = thisEvent.target; + if (target === null){ + console.warn("No target found for construct embedded app right click"); + } else { + $(target).addClass('fullOpacity') + } //making sure the construct we are trying to add an embedded node to is selected if(data instanceof Node){ @@ -614,7 +648,7 @@ export class RightClick { }else if(passedObjectClass === 'rightClick_paletteComponent'){ Eagle.selectedRightClickLocation(Eagle.FileType.Palette) - if(Setting.findValue(Setting.ALLOW_PALETTE_EDITING)){ + if(Setting.findValue(Setting.ALLOW_PALETTE_EDITING, false)){ $('#customContextMenu').append(RightClick.getNodeDescriptionDropdown()) $('#customContextMenu').append('Open Fields Table') $('#customContextMenu').append('Delete') @@ -629,7 +663,7 @@ export class RightClick { $('#customContextMenu').append('Open Fields Table') $('#customContextMenu').append('Graph Attributes') $('#customContextMenu').append('Delete') - if(Setting.findValue(Setting.ALLOW_PALETTE_EDITING)){ + if(Setting.findValue(Setting.ALLOW_PALETTE_EDITING, false)){ $('#customContextMenu').append('Add to palette') } $('#customContextMenu').append('Duplicate') @@ -651,7 +685,7 @@ export class RightClick { if(data.getCategory() === Category.Docker){ $('#customContextMenu').append('Browse DockerHub') } - if(Setting.findValue(Setting.ALLOW_PALETTE_EDITING)){ + if(Setting.findValue(Setting.ALLOW_PALETTE_EDITING, false)){ $('#customContextMenu').append('Add to palette') } $('#customContextMenu').append('Duplicate') @@ -669,6 +703,7 @@ export class RightClick { } } // adding a listener to function options that closes the menu if an option is clicked - $('#customContextMenu a').on('click',function(){if($(event.target).parents('.searchBarContainer').length){return}RightClick.closeCustomContextMenu(true)}) + // TODO: get event from somewhere instead of global + $('#customContextMenu a').on('click',function(){if($((event as any).target).parents('.searchBarContainer').length){return}RightClick.closeCustomContextMenu(true)}) } } diff --git a/src/Setting.ts b/src/Setting.ts index a046bebe..97c29977 100644 --- a/src/Setting.ts +++ b/src/Setting.ts @@ -2,11 +2,9 @@ import * as ko from "knockout"; import { Category } from "./Category"; import { CategoryData } from "./CategoryData"; -import { Daliuge } from "./Daliuge"; import { Eagle } from './Eagle'; import { Errors } from './Errors'; import { Palette } from "./Palette"; -import { Repository } from "./Repository"; import { UiModeSystem } from './UiModes'; import { Utils } from './Utils'; @@ -26,7 +24,7 @@ export class SettingsGroup { } isVisible = (eagle: Eagle) : boolean => { - return this.displayFunc(eagle) || Setting.findValue(Setting.SHOW_DEVELOPER_TAB); + return this.displayFunc(eagle) || Setting.findValue(Setting.SHOW_DEVELOPER_TAB, false) === true; } getSettings = () : Setting[] => { @@ -39,24 +37,27 @@ export class SettingsGroup { } } +type validValueTypes = string | number | boolean; + export class Setting { - value : ko.Observable; + + value : ko.Observable; private display : boolean; // if true, display setting in settings modal, otherwise do not display private name : string; private key : string; private description : string; private perpetual : boolean; // if true, then this setting will stay the same across all ui modes(always storing and using the data from the default ui mode) private type : Setting.Type; - private studentDefaultValue : any; - private minimalDefaultValue : any; - private graphDefaultValue : any; - private componentDefaultValue : any; - private expertDefaultValue : any; - private oldValue : any; - private options : string[]; // an optional list of possible values for this setting - private eventFunc : () => void; // optional function to be called when a settings button is clicked, or checkbox is toggled, or a input is changed - - constructor(display: boolean, name : string, key:string, description : string,perpetual:boolean, type : Setting.Type, studentDefaultValue : any, minimalDefaultValue : any,graphDefaultValue : any,componentDefaultValue : any,expertDefaultValue : any, options?: string[], eventFunc?: () => void){ + private studentDefaultValue : validValueTypes; + private minimalDefaultValue : validValueTypes; + private graphDefaultValue : validValueTypes; + private componentDefaultValue : validValueTypes; + private expertDefaultValue : validValueTypes; + private oldValue : validValueTypes; + private options : string[] | undefined; // an optional list of possible values for this setting + private eventFunc : (() => void) | undefined; // optional function to be called when a settings button is clicked, or checkbox is toggled, or a input is changed + + constructor(display: boolean, name: string, key: string, description: string, perpetual: boolean, type: Setting.Type, studentDefaultValue: validValueTypes, minimalDefaultValue: validValueTypes, graphDefaultValue: validValueTypes, componentDefaultValue: validValueTypes, expertDefaultValue: validValueTypes, options?: string[], eventFunc?: () => void){ this.display = display; this.name = name; this.key = key; @@ -95,31 +96,31 @@ export class Setting { return this.key; } - getOldValue = () : any => { + getOldValue = () : validValueTypes => { return this.oldValue; } - getStudentDefaultVal = () :any => { + getStudentDefaultVal = () : validValueTypes => { return this.studentDefaultValue } - getMinimalDefaultVal = () :any => { + getMinimalDefaultVal = () : validValueTypes => { return this.minimalDefaultValue } - getGraphDefaultVal = () :any => { + getGraphDefaultVal = () : validValueTypes => { return this.graphDefaultValue } - getComponentDefaultVal = () :any => { + getComponentDefaultVal = () : validValueTypes => { return this.componentDefaultValue } - getExpertDefaultVal = () :any => { + getExpertDefaultVal = () : validValueTypes => { return this.expertDefaultValue } - getPerpetualDefaultVal = () :any => { + getPerpetualDefaultVal = () : validValueTypes => { if(!this.perpetual){ console.warn(this.name + " is not a perpetual setting: ",this) } @@ -130,7 +131,7 @@ export class Setting { return this.perpetual; } - setValue = (value: any) : void => { + setValue = (value: validValueTypes) : void => { this.value(value); } @@ -169,10 +170,10 @@ export class Setting { this.eventFunc?.(); } - static find(key : string) : Setting { + static find(key : string) : Setting | undefined { // check if Eagle constructor has not been run (usually the case when this module is being used from a tools script) if (typeof Eagle.settings === 'undefined'){ - return null; + return undefined; } for (const group of Eagle.settings){ @@ -183,23 +184,24 @@ export class Setting { } } - return null; + return undefined; } - static findValue(key : string) : any { + static findValue(key : string, defaultValue: T) : T{ const setting = Setting.find(key); - if (setting === null){ - console.warn("No setting", key); - return null; + if (typeof setting === "undefined"){ + console.warn("No setting", key, ", returning default value:", defaultValue); + return defaultValue; } - return setting.value(); + // return the value cast to the expected type + return setting.value() as T; } - static setValue(key : string, value : any) : void { + static setValue(key : string, value : validValueTypes) : void { const setting = Setting.find(key); - if (setting === null){ + if (typeof setting === "undefined"){ console.warn("No setting", key); return; } @@ -207,6 +209,21 @@ export class Setting { return setting.value(value); } + static toggle(key : string) : void { + const setting = Setting.find(key); + if (typeof setting === "undefined"){ + console.warn("No setting", key); + return; + } + + if (setting.getType() !== Setting.Type.Boolean){ + console.warn("toggle() called on Setting that is not a boolean!" + setting.getName() + " " + setting.getType() + " " + setting.value()); + return; + } + + setting.toggle(); + } + resetDefault() : void { const activeUIModeName: string = UiModeSystem.getActiveUiMode().getName(); let value: any = this.graphDefaultValue; @@ -252,12 +269,18 @@ export class Setting { static showInspectorErrorsWarnings() : boolean { const eagle = Eagle.getInstance(); - - switch (Setting.findValue(Setting.SHOW_GRAPH_WARNINGS)){ + + const selectedNode = eagle.selectedNode(); + + if (selectedNode === null){ + return false; + } + + switch (Setting.findValue(Setting.SHOW_GRAPH_WARNINGS, Setting.ShowErrorsMode.None)){ case Setting.ShowErrorsMode.Warnings: - return eagle.selectedNode().getErrorsWarnings().errors.length + eagle.selectedNode().getErrorsWarnings().warnings.length > 0; + return selectedNode.getErrorsWarnings().errors.length + selectedNode.getErrorsWarnings().warnings.length > 0; case Setting.ShowErrorsMode.Errors: - return eagle.selectedNode().getErrorsWarnings().errors.length > 0; + return selectedNode.getErrorsWarnings().errors.length > 0; case Setting.ShowErrorsMode.None: default: return false; diff --git a/src/SideWindow.ts b/src/SideWindow.ts index 329886bd..b3412328 100644 --- a/src/SideWindow.ts +++ b/src/SideWindow.ts @@ -5,7 +5,7 @@ import { Utils } from './Utils'; import { Setting } from "./Setting"; import { UiModeSystem } from "./UiModes"; import { GraphRenderer } from "./GraphRenderer"; -import { Tutorial, TutorialSystem } from "./Tutorial"; +import { TutorialSystem } from "./Tutorial"; export class SideWindow { // The width remains on the sideWindow, this is because when we are dragging the width of a side window, there are frequent changes to the width. @@ -25,7 +25,7 @@ export class SideWindow { } // don't allow toggle if palette and graph editing are disabled - const editingAllowed: boolean = Setting.findValue(Setting.ALLOW_PALETTE_EDITING) || Setting.findValue(Setting.ALLOW_GRAPH_EDITING); + const editingAllowed: boolean = Setting.findValue(Setting.ALLOW_PALETTE_EDITING, false) || Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false); if (window === "left" && !editingAllowed){ Utils.notifyUserOfEditingIssue(Eagle.FileType.Unknown, "Toggle Window"); return; @@ -34,11 +34,11 @@ export class SideWindow { SideWindow.toggleTransition() if(window === 'left'){ - Setting.find(Setting.LEFT_WINDOW_VISIBLE).toggle() + Setting.toggle(Setting.LEFT_WINDOW_VISIBLE); }else if (window === 'right'){ - Setting.find(Setting.RIGHT_WINDOW_VISIBLE).toggle() + Setting.toggle(Setting.RIGHT_WINDOW_VISIBLE); }else if (window === 'bottom'){ - Setting.find(Setting.BOTTOM_WINDOW_VISIBLE).toggle() + Setting.toggle(Setting.BOTTOM_WINDOW_VISIBLE); }else{ console.warn("toggleShown(): Unknown window:", window); return; @@ -91,12 +91,25 @@ export class SideWindow { // retrieve data about the node being dragged // NOTE: I found that using $(e.target).data('palette-index'), using JQuery, sometimes retrieved a cached copy of the attribute value, which broke this functionality // Using the native javascript works better, it always fetches the current value of the attribute - Eagle.nodeDragPaletteIndex = parseInt(e.target.getAttribute('data-palette-index'), 10); - Eagle.nodeDragComponentId = e.target.getAttribute('data-component-id'); + const componentId = e.target.getAttribute('data-component-id'); + const paletteIndex = e.target.getAttribute('data-palette-index'); + + if (componentId === null || paletteIndex === null){ + console.warn("SideWindow.nodeDragStart(): data-component-id or data-palette-index is null!"); + return false; + } + + Eagle.nodeDragPaletteIndex = parseInt(paletteIndex, 10); + Eagle.nodeDragComponentId = componentId; //this is for dealing with drag and drop actions while there is already one or more palette components selected if (Eagle.selectedLocation() === Eagle.FileType.Palette){ - const draggedNode = eagle.palettes()[Eagle.nodeDragPaletteIndex].getNodeById(Eagle.nodeDragComponentId); + const draggedNode = eagle.palettes()[Eagle.nodeDragPaletteIndex].getNodeById(componentId); + + if (typeof draggedNode === 'undefined'){ + console.warn("Dragged node is undefined! Palette Index:", paletteIndex, "Component ID:", componentId); + return false; + } if(!eagle.objectIsSelected(draggedNode)){ $(e.target).find("div").trigger("click") @@ -109,7 +122,14 @@ export class SideWindow { // grab and set the node's icon and sets it as drag image. const drag = e.target.getElementsByClassName('input-group-prepend')[0] as HTMLElement; - (e.originalEvent as DragEvent).dataTransfer.setDragImage(drag, 0, 0); + const dataTransfer = (e.originalEvent as DragEvent).dataTransfer; + + if (dataTransfer === null){ + console.warn("SideWindow.nodeDragStart(): dataTransfer is null!"); + return false; + } + + dataTransfer.setDragImage(drag, 0, 0); return true; } @@ -132,6 +152,10 @@ export class SideWindow { static rightWindowAdjustStart(eagle: Eagle, event: JQuery.TriggeredEvent) : boolean { const e: DragEvent = event.originalEvent as DragEvent; + if (e.target === null){ + console.warn("SideWindow.rightWindowAdjustStart(): DragEvent is null!"); + return false; + } $(e.target).addClass('windowDragging') eagle.leftWindow().adjusting(false); eagle.rightWindow().adjusting(true); @@ -142,6 +166,11 @@ export class SideWindow { static leftWindowAdjustStart(eagle : Eagle, event : JQuery.TriggeredEvent) : boolean { const e: DragEvent = event.originalEvent as DragEvent; + if (e.target === null){ + console.warn("SideWindow.leftWindowAdjustStart(): DragEvent is null!"); + return false; + } + $(e.target).addClass('windowDragging') eagle.leftWindow().adjusting(true); eagle.rightWindow().adjusting(false); @@ -152,6 +181,11 @@ export class SideWindow { static bottomWindowAdjustStart(eagle: Eagle, event: JQuery.TriggeredEvent) : boolean { const e: DragEvent = event.originalEvent as DragEvent; + if (e.target === null){ + console.warn("SideWindow.bottomWindowAdjustStart(): DragEvent is null!"); + return false; + } + $(e.target).addClass('windowDragging') eagle.leftWindow().adjusting(false); eagle.rightWindow().adjusting(false); @@ -166,6 +200,11 @@ export class SideWindow { static sideWindowAdjustEnd = (eagle: Eagle, event: JQuery.TriggeredEvent) : boolean => { const e: DragEvent = event.originalEvent as DragEvent; + if (e.target === null){ + console.warn("SideWindow.sideWindowAdjustEnd(): DragEvent is null!"); + return false; + } + $(e.target).removeClass('windowDragging') eagle.leftWindow().adjusting(false); eagle.rightWindow().adjusting(false); @@ -184,17 +223,31 @@ export class SideWindow { return true; } + // get window settings + const leftWindowWidthSetting = Setting.find(Setting.LEFT_WINDOW_WIDTH); + const rightWindowWidthSetting = Setting.find(Setting.RIGHT_WINDOW_WIDTH); + const bottomWindowHeightSetting = Setting.find(Setting.BOTTOM_WINDOW_HEIGHT); + + if (typeof leftWindowWidthSetting === "undefined" || typeof rightWindowWidthSetting === "undefined" || typeof bottomWindowHeightSetting === "undefined"){ + console.warn("One or more window settings are undefined!"); + return false; + } + + const leftWindowWidth : number = leftWindowWidthSetting.value() as number; + const rightWindowWidth : number = rightWindowWidthSetting.value() as number; + const bottomWindowHeight : number = bottomWindowHeightSetting.value() as number; + if (isNaN(eagle.leftWindow().size())){ console.warn("Had to reset left window width from invalid state (NaN)!"); - eagle.leftWindow().size(Setting.find(Setting.LEFT_WINDOW_WIDTH).getPerpetualDefaultVal()); + eagle.leftWindow().size(leftWindowWidth); } if (isNaN(eagle.rightWindow().size())){ console.warn("Had to reset right window width from invalid state (NaN)!"); - eagle.rightWindow().size(Setting.find(Setting.RIGHT_WINDOW_WIDTH).getPerpetualDefaultVal()); + eagle.rightWindow().size(rightWindowWidth); } if (isNaN(eagle.bottomWindow().size())){ console.warn("Had to reset bottom window height from invalid state (NaN)!"); - eagle.bottomWindow().size(Setting.find(Setting.BOTTOM_WINDOW_HEIGHT).getPerpetualDefaultVal()); + eagle.bottomWindow().size(bottomWindowHeight); } let newSize : number; @@ -202,9 +255,9 @@ export class SideWindow { if (eagle.leftWindow().adjusting()){ newSize = e.clientX - if(newSize <= Setting.find(Setting.LEFT_WINDOW_WIDTH).getPerpetualDefaultVal()){ - eagle.leftWindow().size(Setting.find(Setting.LEFT_WINDOW_WIDTH).getPerpetualDefaultVal()); - Utils.setLeftWindowWidth(Setting.find(Setting.LEFT_WINDOW_WIDTH).getPerpetualDefaultVal()); + if(newSize <= leftWindowWidth){ + eagle.leftWindow().size(leftWindowWidth); + Utils.setLeftWindowWidth(leftWindowWidth); }else{ eagle.leftWindow().size(newSize); Utils.setLeftWindowWidth(newSize); @@ -212,9 +265,9 @@ export class SideWindow { } else if(eagle.rightWindow().adjusting()){ newSize = window.innerWidth - e.clientX - if(newSize <= Setting.find(Setting.RIGHT_WINDOW_WIDTH).getPerpetualDefaultVal()){ - eagle.rightWindow().size(Setting.find(Setting.RIGHT_WINDOW_WIDTH).getPerpetualDefaultVal()); - Utils.setRightWindowWidth(Setting.find(Setting.RIGHT_WINDOW_WIDTH).getPerpetualDefaultVal()); + if(newSize <= rightWindowWidth){ + eagle.rightWindow().size(rightWindowWidth); + Utils.setRightWindowWidth(rightWindowWidth); }else{ eagle.rightWindow().size(newSize); Utils.setRightWindowWidth(newSize); @@ -224,7 +277,7 @@ export class SideWindow { //we are only doing it for the bottom window, as it typically takes up a large part of the screen, causing it to become larger than the screen itself if switching from a 4k display to a smaller one. newSize = ((window.innerHeight - e.clientY)/window.innerHeight)*100 //making sure the height we are setting is not smaller than the minimum height - const minBottomWindowVh = Setting.find(Setting.BOTTOM_WINDOW_HEIGHT).getPerpetualDefaultVal() + const minBottomWindowVh = bottomWindowHeight; const maxBottomWindowVh = 80 if(newSize <= minBottomWindowVh){ diff --git a/src/StatusEntry.ts b/src/StatusEntry.ts index 2a819cae..8a50ec11 100644 --- a/src/StatusEntry.ts +++ b/src/StatusEntry.ts @@ -62,7 +62,7 @@ export class StatusEntry { new StatusEntry(KeyboardShortcut.idToKeysText('delete_selection', true),' delete selection.', Eagle.getInstance().selectedObjects().length > 0), //a node is selected new StatusEntry('[Right Click]',' on Objects in the graph for more options.', Eagle.getInstance().selectedNode() != null), - new StatusEntry(KeyboardShortcut.idToKeysText('open_parameter_table', true),' open fields table.', Eagle.getInstance().selectedNode() != null && Setting.findValue(Setting.ALLOW_GRAPH_EDITING)), + new StatusEntry(KeyboardShortcut.idToKeysText('open_parameter_table', true),' open fields table.', Eagle.getInstance().selectedNode() != null && Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false)), //more than one thing is selected new StatusEntry(KeyboardShortcut.idToKeysText('create_construct_from_selection', true),' Construct from selection.', Eagle.getInstance().selectedObjects().length >1), new StatusEntry('[Shift + Alt + Click]',' toggle selection of object', Eagle.getInstance().selectedObjects().length >1), diff --git a/src/Translator.ts b/src/Translator.ts index 7be296f0..2ac198e3 100644 --- a/src/Translator.ts +++ b/src/Translator.ts @@ -44,6 +44,8 @@ export class Translator { rmode : ko.Observable; isTranslating: ko.Observable + static readonly DEFAULT_TRANSLATION_ALGORITHM: string = "agl-1"; + constructor(){ this.numberOfIslands = ko.observable(0); this.numberOfNodes = ko.observable(1); @@ -61,8 +63,8 @@ export class Translator { submit = (translatorURL : string, formElements : { [index: string]: string }) : void => { // consult EAGLE settings to determine whether to open the translator in a new tab - const translateInCurrentTab: boolean = Setting.findValue(Setting.OPEN_TRANSLATOR_IN_CURRENT_TAB); - const overwriteTranslationTab: boolean = Setting.findValue(Setting.OVERWRITE_TRANSLATION_TAB); + const translateInCurrentTab: boolean = Setting.findValue(Setting.OPEN_TRANSLATOR_IN_CURRENT_TAB, false); + const overwriteTranslationTab: boolean = Setting.findValue(Setting.OVERWRITE_TRANSLATION_TAB, false); // create form element const form = document.createElement("form"); @@ -131,7 +133,7 @@ export class Translator { const isLocalFile: boolean = eagle.logicalGraph().fileInfo().location.repositoryService() === Repository.Service.File; // check if the graph is committed before translation - if (!Setting.findValue(Setting.TEST_TRANSLATE_MODE) && !isLocalFile && this._checkGraphModified(eagle)){ + if (!Setting.findValue(Setting.TEST_TRANSLATE_MODE, false) && !isLocalFile && this._checkGraphModified(eagle)){ Utils.showNotification("Saving graph", "Automatically saving modified graph prior to translation", "info"); // use the async function here, so that we can check isModified after saving @@ -148,14 +150,14 @@ export class Translator { } } - const translatorURL : string = Setting.findValue(Setting.TRANSLATOR_URL); + const translatorURL : string = Setting.findValue(Setting.TRANSLATOR_URL, ""); console.log("Eagle.getPGT() : ", "algorithm name:", algorithmName, "translator URL", translatorURL); this._genPGT(eagle, translatorURL, algorithmName, testingMode); } private _checkGraphModified = (eagle: Eagle): boolean => { - return eagle.logicalGraph().fileInfo().modified && !Setting.findValue(Setting.ALLOW_MODIFIED_GRAPH_TRANSLATION); + return eagle.logicalGraph().fileInfo().modified && !Setting.findValue(Setting.ALLOW_MODIFIED_GRAPH_TRANSLATION, false); } private _genPGT = (eagle: Eagle, translatorURL: string, algorithmName : string, testingMode: boolean) : void => { @@ -163,7 +165,7 @@ export class Translator { const lgClone: LogicalGraph = eagle.logicalGraph().clone(); // get the version of JSON we are using - const version: Setting.SchemaVersion = Setting.findValue(Setting.DALIUGE_SCHEMA_VERSION); + const version: Setting.SchemaVersion = Setting.findValue(Setting.DALIUGE_SCHEMA_VERSION, Setting.SchemaVersion.Unknown); // convert to JSON const jsonString: string = LogicalGraph.toJsonString(lgClone, true, version); @@ -181,7 +183,7 @@ export class Translator { eagle.translator().submit(translatorURL, translatorData); // if developer setting is enabled, write the translator-ready JSON to the console - if (Setting.findValue(Setting.PRINT_TRANSLATOR_JSON_TO_JS_CONSOLE)){ + if (Setting.findValue(Setting.PRINT_TRANSLATOR_JSON_TO_JS_CONSOLE, false)){ console.log("Translator Json"); console.log("---------"); console.log(translatorData); @@ -192,11 +194,11 @@ export class Translator { } algorithmVisible = (algorithm: string) : boolean => { - const normalTranslatorMode :boolean = Setting.findValue(Setting.USER_TRANSLATOR_MODE) === Setting.TranslatorMode.Normal; + const normalTranslatorMode :boolean = Setting.findValue(Setting.USER_TRANSLATOR_MODE, Setting.TranslatorMode.Normal) === Setting.TranslatorMode.Normal; if(!normalTranslatorMode){ return true } - if(algorithm === Setting.findValue(Setting.TRANSLATOR_ALGORITHM_DEFAULT)){ + if(algorithm === Setting.findValue(Setting.TRANSLATOR_ALGORITHM_DEFAULT, "agl-1")){ return true } @@ -204,16 +206,16 @@ export class Translator { } setUrl = async () : Promise => { - const translatorURLSetting : Setting = Setting.find(Setting.TRANSLATOR_URL); + const defaultUrl = Setting.findValue(Setting.TRANSLATOR_URL, ""); let userString: string; try { - userString = await Utils.requestUserString("Translator Url", "Enter the Translator Url", translatorURLSetting.value(), false); + userString = await Utils.requestUserString("Translator Url", "Enter the Translator Url", defaultUrl, false); } catch (error){ console.error(error); return; } - translatorURLSetting.value(userString); + Setting.setValue(Setting.TRANSLATOR_URL, userString); }; } diff --git a/src/Tutorial.ts b/src/Tutorial.ts index d7926f41..22cee966 100644 --- a/src/Tutorial.ts +++ b/src/Tutorial.ts @@ -3,13 +3,13 @@ import { Utils } from './Utils'; export class TutorialSystem { - static activeTut: Tutorial = null //current active tutorial + static activeTut: Tutorial | null = null //current active tutorial static activeTutCurrentStep: TutorialStep //current active tutorial step static activeTutNumSteps: number = 0; //total number of steps in the active tutorial static activeTutCurrentStepIndex: number = 0; //index of the current step in the active tutorial - static waitForElementTimer: number = null //this houses the time out timer when waiting for a target element to appear + static waitForElementTimer: number | undefined = undefined; //this houses the time out timer when waiting for a target element to appear static onCoolDown: boolean = false //boolean if the tutorial system is currently on cool down - static conditionCheck:number = null //this stores the condition interval function + static conditionCheck:number | undefined = undefined; //this stores the condition interval function static initiateTutorial(tutorialName: string): void { for (const tut of Eagle.tutorials){ @@ -95,14 +95,16 @@ export class TutorialSystem { } static initiateSimpleFindGraphNodeIdByNodeName(name:string) : string { - return Eagle.getInstance().logicalGraph().findNodeIdByNodeName(name) + const nodeId = Eagle.getInstance().logicalGraph().findNodeIdByNodeName(name) + return nodeId === null ? " JQuery) : TutorialStep =>{ - const x = new TutorialStep(title, description, TutorialStep.Type.Info, TutorialStep.Wait.None,null, selector, null, null,false,null,null,null) + const x = new TutorialStep(title, description, TutorialStep.Type.Info, TutorialStep.Wait.None,null, selector, null, null, false, "", null, null) this.tutorialSteps.push(x) return x } @@ -159,11 +161,18 @@ export class Tutorial { //the unlock happens after the waits for target elements in the ui, transitions of the tutorial visuals and changes of content and positioning has all been finished, this is when the tut system is ready to proceed. const eagle = Eagle.getInstance() - TutorialSystem.activeTutCurrentStep = TutorialSystem.activeTut.getTutorialSteps()[TutorialSystem.activeTutCurrentStepIndex] + const activeTutorial = TutorialSystem.activeTut + + // check that there is an active tutorial + if (activeTutorial === null) { + return; + } + + TutorialSystem.activeTutCurrentStep = activeTutorial.getTutorialSteps()[TutorialSystem.activeTutCurrentStepIndex] const tutStep = TutorialSystem.activeTutCurrentStep clearTimeout(TutorialSystem.conditionCheck); - TutorialSystem.conditionCheck = null; + TutorialSystem.conditionCheck = undefined; $('body').off('keydown.tutEventListener'); TutorialSystem.addTutKeyboardShortcuts() @@ -187,20 +196,18 @@ export class Tutorial { this.initiateStep(TutorialSystem.activeTutCurrentStep, null) } else if (tutStep.getWaitType() === TutorialStep.Wait.Delay) { //if a delay amount is not specified we will default to 4ms - let delay:number = 400 - if(TutorialSystem.activeTutCurrentStep.getDelayAmount()!=null){ - delay = TutorialSystem.activeTutCurrentStep.getDelayAmount() - } + const delay: number = TutorialSystem.activeTutCurrentStep.getDelayAmount() || 400; + setTimeout(function () { that.initiateStep(TutorialSystem.activeTutCurrentStep, null) }, delay) }else { //we set a two second timer, the wait will check every .1 seconds for two seconds at which point it is timed out and we abort the tut - TutorialSystem.waitForElementTimer = setInterval(function () { TutorialSystem.activeTut.waitForElementThenRun(tutStep.getWaitType()) }, 100); + TutorialSystem.waitForElementTimer = setInterval(function () { activeTutorial.waitForElementThenRun(tutStep.getWaitType()) }, 100); setTimeout(function () { - if (TutorialSystem.waitForElementTimer != null) { + if (TutorialSystem.waitForElementTimer !== undefined) { clearTimeout(TutorialSystem.waitForElementTimer); - TutorialSystem.waitForElementTimer = null; + TutorialSystem.waitForElementTimer = undefined; console.warn('waiting for next tutorial step element timed out') TutorialSystem.onCoolDown = false that.tutButtonPrev() @@ -212,8 +219,15 @@ export class Tutorial { waitForElementThenRun = (waitType: TutorialStep.Wait): void => { const tutStep = TutorialSystem.activeTutCurrentStep let elementAvailable: boolean = false - let targetElement: JQuery = tutStep.getTargetFunc()() - let autoAlternateHighlightTarget: JQuery = null // used for modals to automatically highlight the modal body, footer or header + const targetFunc = tutStep.getTargetFunc() + + if (targetFunc === null) { + console.warn('Tutorial.waitForElementThenRun(): no target function defined for this step'); + return; + } + + let targetElement: JQuery = targetFunc() + let autoAlternateHighlightTarget = null // used for modals to automatically highlight the modal body, footer or header if (waitType === TutorialStep.Wait.Modal) { //in case of a modal we make sure the selector is for the modal, we then check if it has the class 'show' @@ -250,16 +264,23 @@ export class Tutorial { if (elementAvailable) { this.initiateStep(tutStep, autoAlternateHighlightTarget) clearTimeout(TutorialSystem.waitForElementTimer); - TutorialSystem.waitForElementTimer = null; + TutorialSystem.waitForElementTimer = undefined; } else { return } } - initiateStep = (tutStep: TutorialStep, autoAlternateHighlightTarget: JQuery): void => { - const that = this; + initiateStep = (tutStep: TutorialStep, autoAlternateHighlightTarget: JQuery | null): void => { + const that = this; // TODO: needed? $(':focus').trigger("blur"); - tutStep.getTargetFunc()().trigger("focus"); + + const targetFunc = tutStep.getTargetFunc() + if (targetFunc === null) { + console.warn('Tutorial.initiateStep(): no target function defined for this step'); + return; + } + + targetFunc().trigger("focus"); //call the correct function depending on which type of tutorial step this is if (tutStep.getType() === TutorialStep.Type.Info) { @@ -274,12 +295,18 @@ export class Tutorial { } //normal info step - initiateInfoStep = (tutStep: TutorialStep, autoAlternateHighlightTarget: JQuery): void => { + initiateInfoStep = (tutStep: TutorialStep, autoAlternateHighlightTarget: JQuery | null): void => { //the alternate highlight selector is for modals in which case we highlight the whole modal while the arrow points at a specific child if (autoAlternateHighlightTarget != null && autoAlternateHighlightTarget.length > 0) { this.highlightStepTarget(autoAlternateHighlightTarget) } else { - this.highlightStepTarget(tutStep.getTargetFunc()()) + const targetFunc = tutStep.getTargetFunc() + if (targetFunc === null) { + console.warn('Tutorial.initiateInfoStep(): no target function defined for this step'); + return; + } + + this.highlightStepTarget(targetFunc()) } //the little wait is waiting for the css animation of the highlighting system @@ -290,8 +317,14 @@ export class Tutorial { } //a selector press step - initiatePressStep = (tutStep: TutorialStep, autoAlternateHighlightTarget: JQuery): void => { - const targetElement = tutStep.getTargetFunc()() + initiatePressStep = (tutStep: TutorialStep, autoAlternateHighlightTarget: JQuery | null): void => { + const targetFunc = tutStep.getTargetFunc() + if (targetFunc === null) { + console.warn('Tutorial.initiatePressStep(): no target function defined for this step'); + return; + } + + const targetElement = targetFunc() if (autoAlternateHighlightTarget != null) { this.highlightStepTarget(autoAlternateHighlightTarget) } else { @@ -309,11 +342,17 @@ export class Tutorial { } //these are ground work for future tutorial system functionality - initiateInputStep = (tutStep: TutorialStep, autoAlternateHighlightTarget: JQuery): void => { + initiateInputStep = (tutStep: TutorialStep, autoAlternateHighlightTarget: JQuery | null): void => { + const targetFunc = tutStep.getTargetFunc() + if (targetFunc === null) { + console.warn('Tutorial.initiateInputStep(): no target function defined for this step'); + return; + } + if (autoAlternateHighlightTarget != null) { this.highlightStepTarget(autoAlternateHighlightTarget) } else { - this.highlightStepTarget(tutStep.getTargetFunc()()) + this.highlightStepTarget(targetFunc()) } //the little wait is waiting for the css animation of the highlighting system @@ -323,17 +362,23 @@ export class Tutorial { }, 510); //attaching an input handler for checking input - tutStep.getTargetFunc()().on('keydown.tutInputCheckFunc',function(event: JQuery.TriggeredEvent){ + targetFunc().on('keydown.tutInputCheckFunc',function(event: JQuery.TriggeredEvent){ const e: KeyboardEvent = event.originalEvent as KeyboardEvent; - TutorialSystem.activeTut.tutInputCheckFunc(e, tutStep) + TutorialSystem.activeTut?.tutInputCheckFunc(e, tutStep) }) } - initiateConditionStep = (tutStep: TutorialStep, autoAlternateHighlightTarget: JQuery): void => { + initiateConditionStep = (tutStep: TutorialStep, autoAlternateHighlightTarget: JQuery | null): void => { if (autoAlternateHighlightTarget != null) { this.highlightStepTarget(autoAlternateHighlightTarget) } else { - this.highlightStepTarget(tutStep.getTargetFunc()()) + const targetFunc = tutStep.getTargetFunc() + if (targetFunc === null) { + console.warn('Tutorial.initiateConditionStep(): no target function defined for this step'); + return; + } + + this.highlightStepTarget(targetFunc()) } //the little wait is waiting for the css animation of the highlighting system @@ -341,7 +386,7 @@ export class Tutorial { TutorialSystem.activeTut?.openInfoPopUp() }, 510); - TutorialSystem.conditionCheck = setInterval(function(){TutorialSystem.activeTut.checkConditionFunction(tutStep)}, 100); + TutorialSystem.conditionCheck = setInterval(function(){TutorialSystem.activeTut?.checkConditionFunction(tutStep)}, 100); } highlightStepTarget = (target: JQuery): void => { @@ -350,13 +395,18 @@ export class Tutorial { //if the target element is not found, we end the tutorial gracefully if(target.length === 0){ console.warn('target highlight element not found: ', TutorialSystem.activeTutCurrentStep.getTargetFunc()) - TutorialSystem.activeTut.tutButtonEnd() + TutorialSystem.activeTut?.tutButtonEnd() return } - if(TutorialSystem.activeTutCurrentStep.getAlternateHighlightTargetFunc() != null){ - if(TutorialSystem.activeTutCurrentStep.getAlternateHighlightTargetFunc()().length > 0){ - target = TutorialSystem.activeTutCurrentStep.getAlternateHighlightTargetFunc()() + //check for an alternate highlight target function + const alternateHighlightTargetFunc = TutorialSystem.activeTutCurrentStep.getAlternateHighlightTargetFunc() + + if(alternateHighlightTargetFunc != null){ + const alternateHighlightTarget = alternateHighlightTargetFunc() + + if(alternateHighlightTarget.length > 0){ + target = alternateHighlightTarget }else{ console.warn('alternate highlight target not found using main target instead') } @@ -373,15 +423,36 @@ export class Tutorial { //in order to darken the screen save the selection target, we must add divs on each side of the element. const coords = target.offset() const docWidth = window.innerWidth + const targetOuterWidth = $(target).outerWidth() + const targetOuterHeight = $(target).outerHeight() + + // check that values are valid + if (coords === undefined) { + console.warn('Tutorial.highlightStepTarget(): target element has no offset'); + return; + } + if (docWidth === undefined) { + console.warn('Tutorial.highlightStepTarget(): document has no width'); + return; + } + if (targetOuterWidth === undefined) { + console.warn('Tutorial.highlightStepTarget(): target element has no outerWidth'); + return; + } + if (targetOuterHeight === undefined) { + console.warn('Tutorial.highlightStepTarget(): target element has no outerHeight'); + return; + } + const top_actual = Math.round(coords.top)//distance of the top of the element from the top of the document - let right = coords.left + $(target).outerWidth() + let right = coords.left + targetOuterWidth const left = docWidth - coords.left - let targetHeight = Math.round($(target).outerHeight()) + let targetHeight = Math.round(targetOuterHeight) let bottom_actual = Math.round(coords.top + targetHeight) //distance from the bottom of the target element to the bottom of the document if(target.parents('#logicalGraphParent').length){ targetHeight = Math.round(targetHeight*eagle.globalScale()) - right = coords.left+$(target).outerWidth() *eagle.globalScale() + right = coords.left+targetOuterWidth *eagle.globalScale() bottom_actual = Math.round(coords.top + targetHeight) } @@ -411,34 +482,85 @@ export class Tutorial { } openInfoPopUp = (): void => { - const step = TutorialSystem.activeTutCurrentStep - const currentTarget: JQuery = step.getTargetFunc()() + const stepTargetFunc = step.getTargetFunc() + + if (stepTargetFunc === null) { + console.warn('Tutorial.openInfoPopUp(): no target function defined for the current step'); + return; + } + + const currentTarget: JQuery = stepTargetFunc() + + if (currentTarget.length === 0) { + console.warn('Tutorial.openInfoPopUp(): no target element found for the current step'); + return; + } + + // get values of the target element + const targetOffset = currentTarget.offset(); + const targetWidth = currentTarget.width(); + const targetHeight = currentTarget.height(); + const targetOuterWidth = currentTarget.outerWidth(); + const targetOuterHeight = currentTarget.outerHeight(); + + // check that values are valid + if (targetOffset === undefined) { + console.warn('Tutorial.openInfoPopUp(): target element has no offset'); + return; + } + if (targetWidth === undefined) { + console.warn('Tutorial.openInfoPopUp(): target element has no width'); + return; + } + if (targetHeight === undefined) { + console.warn('Tutorial.openInfoPopUp(): target element has no height'); + return; + } + if (targetOuterWidth === undefined) { + console.warn('Tutorial.openInfoPopUp(): target element has no outerWidth'); + return; + } + if (targetOuterHeight === undefined) { + console.warn('Tutorial.openInfoPopUp(): target element has no outerHeight'); + return; + } + //figuring out where there is enough space to place the tutorial - let selectedLocationX = currentTarget.offset().left + (currentTarget.width() / 2) - let selectedLocationY = currentTarget.offset().top + currentTarget.outerHeight() + let selectedLocationX = targetOffset.left + (targetWidth / 2) + let selectedLocationY = targetOffset.top + targetOuterHeight const docWidth = $(document).width() const docHeight = $(document).height() + // check that docWidth and docHeight are valid + if (docWidth === undefined) { + console.warn('Tutorial.openInfoPopUp(): document has no width'); + return; + } + if (docHeight === undefined) { + console.warn('Tutorial.openInfoPopUp(): document has no height'); + return; + } + this.closeInfoPopUp() let orientation = 'tutorialRight' - if (currentTarget.outerWidth() === docWidth || currentTarget.outerHeight() / docHeight > 0.7) { + if (targetOuterWidth === docWidth || targetOuterHeight / docHeight > 0.7) { //if this is the case then we are looking at an object that is set to 100% of the screen //such as the navbar or canvas. we will then position the tutorial in the middle of the object - if ((docHeight - currentTarget.outerHeight()) < 250) { - selectedLocationY = selectedLocationY - (currentTarget.height() / 2) + if ((docHeight - targetOuterHeight) < 250) { + selectedLocationY = selectedLocationY - (targetHeight / 2) if (docWidth - selectedLocationX < 700) { orientation = 'tutorialLeft tutorialMiddle' - selectedLocationX = selectedLocationX - 600 - (currentTarget.width() / 2) + selectedLocationX = selectedLocationX - 600 - (targetWidth / 2) } else { orientation = 'tutorialRight tutorialMiddle' } } } else if (docWidth - selectedLocationX < 700) { orientation = 'tutorialLeft' - selectedLocationX = selectedLocationX - 660 - (currentTarget.width() / 2) + selectedLocationX = selectedLocationX - 660 - (targetWidth / 2) if (docHeight - selectedLocationY < 250) { orientation = 'tutorialLeftTop' selectedLocationY = selectedLocationY - 290 @@ -500,6 +622,11 @@ export class Tutorial { } tutButtonPrev = (): void => { + if (TutorialSystem.activeTut === null){ + console.warn('Tutorial.tutButtonPrev(): no active tutorial'); + return; + } + if (TutorialSystem.onCoolDown === false) { if (TutorialSystem.activeTutCurrentStepIndex > 0) { this.closeInfoPopUp() @@ -523,9 +650,9 @@ export class Tutorial { $('.forceShow').removeClass('forceShow') TutorialSystem.activeTut = null clearTimeout(TutorialSystem.conditionCheck); - TutorialSystem.conditionCheck = null; + TutorialSystem.conditionCheck = undefined; clearTimeout(TutorialSystem.waitForElementTimer); - TutorialSystem.waitForElementTimer = null; + TutorialSystem.waitForElementTimer = undefined; } tutPressStepListener = (): void => { @@ -550,28 +677,43 @@ export class Tutorial { return } + const targetFunc = tutStep.getTargetFunc() + if (targetFunc === null) { + console.warn('Tutorial.tutInputCheckFunc(): no target function defined for this step'); + return; + } + if(tutStep.getExpectedInput() === ''||tutStep.getExpectedInput() === null){ if(event.key === "Enter"){ event.preventDefault() event.stopImmediatePropagation() - TutorialSystem.activeTut.tutButtonNext() - tutStep.getTargetFunc()().off('keydown.tutInputCheckFunc') + if (TutorialSystem.activeTut !== null){ + TutorialSystem.activeTut.tutButtonNext() + } + targetFunc().off('keydown.tutInputCheckFunc') } }else{ - if(tutStep.getTargetFunc()().val() === tutStep.getExpectedInput()){ - TutorialSystem.activeTut.tutButtonNext() - tutStep.getTargetFunc()().off('keydown.tutInputCheckFunc') + if(targetFunc().val() === tutStep.getExpectedInput()){ + if (TutorialSystem.activeTut !== null){ + TutorialSystem.activeTut.tutButtonNext() + } + targetFunc().off('keydown.tutInputCheckFunc') } } } checkConditionFunction = (tutStep: TutorialStep): void => { - const eagle = Eagle.getInstance() - const conditionReturn: boolean = tutStep.getConditionFunction()(eagle) + const eagle = Eagle.getInstance() + const conditionFunc = tutStep.getConditionFunction() + if(conditionFunc === null){ + console.warn('Tutorial.checkConditionFunction(): no condition function defined for this step'); + return; + } + const conditionReturn: boolean = conditionFunc(eagle) if(conditionReturn){ clearTimeout(TutorialSystem.conditionCheck); - TutorialSystem.conditionCheck = null; + TutorialSystem.conditionCheck = undefined; this.tutButtonNext() } } @@ -582,18 +724,18 @@ export class TutorialStep { private text: string; private type: TutorialStep.Type; private waitType: TutorialStep.Wait; - private delayAmount : number; + private delayAmount : number | null; - private targetFunc: () => JQuery; - private alternateHighlightTargetFunc: () => JQuery; - private preFunc: (eagle: Eagle) => void; - private backPreFunc: (eagle: Eagle) => void; - private conditionFunc : (eagle: Eagle) => boolean; + private targetFunc: (() => JQuery) | null; + private alternateHighlightTargetFunc: (() => JQuery) | null; + private preFunc: ((eagle: Eagle) => void) | null; + private backPreFunc: ((eagle: Eagle) => void) | null; + private conditionFunc : ((eagle: Eagle) => boolean) | null; private backSkip : boolean; private expectedInput : string; - constructor(title: string, text: string, type: TutorialStep.Type, waitType: TutorialStep.Wait, delayAmount:number, targetFunc: () => JQuery, preFunc: (eagle: Eagle) => void, backPreFunc: (eagle: Eagle) => void, backSkip:boolean, expectedInput:string, conditionFunc:(eagle: Eagle) => boolean, alternateHighlightTargetFunc: () => JQuery) { + constructor(title: string, text: string, type: TutorialStep.Type, waitType: TutorialStep.Wait, delayAmount: number | null, targetFunc: () => JQuery, preFunc: ((eagle: Eagle) => void) | null, backPreFunc: ((eagle: Eagle) => void) | null, backSkip:boolean, expectedInput:string, conditionFunc:((eagle: Eagle) => boolean) | null, alternateHighlightTargetFunc: (() => JQuery) | null) { this.title = title; this.text = text; this.type = type; @@ -626,19 +768,19 @@ export class TutorialStep { return this.waitType; } - getDelayAmount = (): number => { + getDelayAmount = (): number | null => { return this.delayAmount; } - getTargetFunc = (): () => JQuery => { + getTargetFunc = (): (() => JQuery) | null => { return this.targetFunc; } - getPreFunc = (): (eagle: Eagle) => void => { + getPreFunc = (): ((eagle: Eagle) => void) | null => { return this.preFunc; } - getBackPreFunc = (): (eagle: Eagle) => void => { + getBackPreFunc = (): ((eagle: Eagle) => void) | null => { return this.backPreFunc; } @@ -650,11 +792,11 @@ export class TutorialStep { return this.expectedInput; } - getConditionFunction = (): (eagle: Eagle) => boolean => { + getConditionFunction = (): ((eagle: Eagle) => boolean) | null => { return this.conditionFunc; } - getAlternateHighlightTargetFunc = () : () => JQuery => { + getAlternateHighlightTargetFunc = () : (() => JQuery) | null => { return this.alternateHighlightTargetFunc; } @@ -702,6 +844,17 @@ export class TutorialStep { this.alternateHighlightTargetFunc = newAlternateHighlightTargetFunc; return this } + + forceShowTargetParent() { + const targetFunc = this.getTargetFunc(); + + if (targetFunc === null) { + console.warn('TutorialStep.forceShowTargetParent(): no target function defined'); + return; + } + + targetFunc().parent().addClass('forceShow'); + } } export namespace TutorialStep { diff --git a/src/UiModes.ts b/src/UiModes.ts index 454b4f04..5a6853ca 100644 --- a/src/UiModes.ts +++ b/src/UiModes.ts @@ -29,21 +29,17 @@ export class UiModeSystem { } static getFullUiModeNamesList() : string[] { - const uiModeNamesList : string[]= [] - UiModeSystem.getUiModes().forEach(function(uiMode){ - uiModeNamesList.push(uiMode.getName()) - }) - return uiModeNamesList + return UiModeSystem.getUiModes().map(x => x.getName()); } - static getUiModeByName(name:string) : UiMode { - let result = null - UiModeSystem.getUiModes().forEach(function(uiMode){ + static getUiModeByName(name:string) : UiMode | undefined { + for (const uiMode of UiModeSystem.getUiModes()){ if(name === uiMode.getName()){ - result = uiMode + return uiMode } - }) - return result + } + + return undefined; } static setActiveUiMode(newActiveUiMode:UiMode) : void { @@ -132,15 +128,20 @@ export class UiModeSystem { } static loadFromLocalStorage() : void { - const uiModesObj : any[] = JSON.parse(localStorage.getItem('UiModes')) + const uiModesString = localStorage.getItem('UiModes') + if(uiModesString === null){ + return + } + + const uiModesObj : any[] = JSON.parse(uiModesString); if(uiModesObj === null){ return } uiModesObj.forEach(function(uiModeObj){ - let destUiMode : UiMode = UiModeSystem.getUiModeByName(uiModeObj.name) - if(destUiMode===null){ + let destUiMode = UiModeSystem.getUiModeByName(uiModeObj.name) + if(typeof destUiMode === "undefined"){ const settings : SettingData[] = [] Setting.getSettings().forEach(function(settingsGroup){ @@ -154,19 +155,18 @@ export class UiModeSystem { uiModeObj.settingValues.forEach(function(settingObj:any){ destUiMode.setSettingByKey(settingObj.key,settingObj.value) } ) - }) } - static setActiveSetting(settingName:string,newValue:any) : void { - let activeSetting: SettingData = null + static setActiveSetting(settingName:string, newValue:any) : void { + let activeSetting: SettingData | null = null //if a setting is marked perpetual we will write the value to all ui modes, this means it stays the same regardless of which ui mode is active - UiModeSystem.getActiveUiMode().getSettings().forEach(function(setting){ - if(setting.getKey() === settingName){ + for (const setting of UiModeSystem.getActiveUiMode().getSettings()){ + if (setting.getKey() === settingName){ activeSetting = setting } - }) + } if(activeSetting === null){ console.warn('Requested setting key to change: "'+ settingName+'" can not be found') diff --git a/src/Undo.ts b/src/Undo.ts index abd55e67..8844e7b7 100644 --- a/src/Undo.ts +++ b/src/Undo.ts @@ -45,7 +45,7 @@ class Snapshot { export class Undo { static readonly MEMORY_SIZE : number = 10; - memory: ko.ObservableArray; + memory: ko.ObservableArray; front: ko.Observable; // place where next snapshot will go rear: ko.Observable; current: ko.Observable; // snapshot currently in use, normally equal to front @@ -73,7 +73,7 @@ export class Undo { pushSnapshot = (eagle: Eagle, description: string) : void => { const previousIndex = (this.current() + Undo.MEMORY_SIZE - 1) % Undo.MEMORY_SIZE; - const previousSnapshot : Snapshot = this.memory()[previousIndex]; + const previousSnapshot : Snapshot | null = this.memory()[previousIndex]; const newContent: object = LogicalGraph.toOJSJson(eagle.logicalGraph(), false) // check if newContent matches old content, if so, no need to push @@ -104,7 +104,7 @@ export class Undo { this.memory()[index] = null; } - if (Setting.findValue(Setting.PRINT_UNDO_STATE_TO_JS_CONSOLE)){ + if (Setting.findValue(Setting.PRINT_UNDO_STATE_TO_JS_CONSOLE, false)){ Undo.printTable(); } } @@ -119,13 +119,18 @@ export class Undo { const prevprevIndex = (this.current() + Undo.MEMORY_SIZE - 2) % Undo.MEMORY_SIZE; // user notification - const description = this.memory()[prevIndex].description(); + const prevSnapshot = this.memory()[prevIndex]; + if (prevSnapshot === null){ + console.warn("Undo.prevSnapshot(): snapshot at index", prevIndex, "is null"); + return; + } + const description = prevSnapshot.description(); Utils.showNotification("Undo", description, "info", false); this._loadFromIndex(prevprevIndex, eagle); this.current((this.current() + Undo.MEMORY_SIZE - 1) % Undo.MEMORY_SIZE); - if (Setting.findValue(Setting.PRINT_UNDO_STATE_TO_JS_CONSOLE)){ + if (Setting.findValue(Setting.PRINT_UNDO_STATE_TO_JS_CONSOLE, false)){ Undo.printTable(); } @@ -149,13 +154,18 @@ export class Undo { } // user notification - const description = this.memory()[this.current()].description(); + const currentSnapshot = this.memory()[this.current()]; + if (currentSnapshot === null){ + console.warn("Undo.nextSnapshot(): snapshot at index", this.current(), "is null"); + return; + } + const description = currentSnapshot.description(); Utils.showNotification("Redo", description, "info", false); this._loadFromIndex(this.current(), eagle); this.current((this.current() + 1) % Undo.MEMORY_SIZE); - if (Setting.findValue(Setting.PRINT_UNDO_STATE_TO_JS_CONSOLE)){ + if (Setting.findValue(Setting.PRINT_UNDO_STATE_TO_JS_CONSOLE, false)){ Undo.printTable(); } @@ -195,7 +205,7 @@ export class Undo { } _loadFromIndex = (index: number, eagle: Eagle) : void => { - const snapshot : Snapshot = this.memory()[index]; + const snapshot : Snapshot | null = this.memory()[index]; if (snapshot === null){ console.warn("Undo memory at index", index, "is null"); @@ -229,7 +239,7 @@ export class Undo { const object = node || edge; // abort if no edge or node exists fot that id - if (typeof node === 'undefined' && typeof edge === 'undefined'){ + if (typeof object === 'undefined'){ continue; } diff --git a/src/Utils.ts b/src/Utils.ts index c36fb1b9..33b8785c 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -155,17 +155,20 @@ export class Utils { // loop through nodes, look for embedded nodes with null id, create new id for (const node of lg.getNodes()){ + const inputApplication = node.getInputApplication(); + const outputApplication = node.getOutputApplication(); + // if this node has inputApp, set the inputApp id - if (node.hasInputApplication()){ - if (node.getInputApplication().getId() === null){ - node.getInputApplication().setId(Utils.generateNodeId()); + if (inputApplication !== null){ + if (inputApplication.getId() === null){ + inputApplication.setId(Utils.generateNodeId()); } } // if this node has outputApp, set the outputApp id - if (node.hasOutputApplication()){ - if (node.getOutputApplication().getId() === null){ - node.getOutputApplication().setId(Utils.generateNodeId()); + if (outputApplication !== null){ + if (outputApplication.getId() === null){ + outputApplication.setId(Utils.generateNodeId()); } } } @@ -226,9 +229,14 @@ export class Utils { * @param path File name. */ static getFileExtension(path : string) : string { - const basename = path.split(/[\\/]/).pop(), // extract file name from full path ... - // (supports `\\` and `/` separators) - pos = basename.lastIndexOf("."); // get last position of `.` + const basename = path.split(/[\\/]/).pop(); // extract file name from full path ... + // (supports `\\` and `/` separators) + + if (typeof basename === 'undefined'){ + return ""; + } + + const pos = basename.lastIndexOf("."); // get last position of `.` if (basename === "" || pos < 1) // if file name is empty or ... return ""; // `.` not found (-1) or comes first (0) @@ -515,7 +523,7 @@ export class Utils { } // if this is a message intended for developers, check whether display of those messages is enabled - if (developer && !Setting.findValue(Setting.SHOW_DEVELOPER_NOTIFICATIONS)){ + if (developer && !Setting.findValue(Setting.SHOW_DEVELOPER_NOTIFICATIONS, false)){ return; } @@ -574,12 +582,12 @@ export class Utils { }); } - static requestUserText(title : string, message : string, defaultText: string, readonly: boolean = false) : Promise { + static requestUserText(title : string, message : string, defaultText: string | null, readonly: boolean = false) : Promise { return new Promise(async(resolve, reject) => { $('#inputTextModalTitle').text(title); $('#inputTextModalMessage').html(message); - $('#inputTextModalInput').val(defaultText); + $('#inputTextModalInput').val(defaultText ? defaultText : ''); $('#inputTextModalInput').prop('readonly', readonly); // store the callback, result on the modal HTML element @@ -598,7 +606,7 @@ export class Utils { }); } - static requestUserCode(language: "json"|"python"|"text", title: string, defaultText: string, readonly: boolean = false): Promise { + static requestUserCode(language: "json"|"python"|"text", title: string, defaultText: string | null, readonly: boolean = false): Promise { return new Promise(async(resolve, reject) => { // set title $('#inputCodeModalTitle').text(title); @@ -624,7 +632,7 @@ export class Utils { const editor = $('#inputCodeModal').data('editor'); editor.setOption('readOnly', readonly); editor.setOption('mode', mode); - editor.setValue(defaultText); + editor.setValue(defaultText ? defaultText : ''); // store the callback, result on the modal HTML element // so that the info is available to event handlers @@ -752,7 +760,7 @@ export class Utils { }); } - static async requestUserConfirm(title : string, message : string, affirmativeAnswer : string, negativeAnswer : string, confirmSetting: Setting): Promise { + static async requestUserConfirm(title : string, message : string, affirmativeAnswer : string, negativeAnswer : string, confirmSetting: Setting | undefined): Promise { return new Promise(async(resolve, reject) => { $('#confirmModalTitle').text(title); $('#confirmModalMessage').html(message); @@ -760,7 +768,7 @@ export class Utils { $('#confirmModalNegativeAnswer').text(negativeAnswer); $('#confirmModalDontShowAgain button').off() - if(confirmSetting === null){ + if(typeof confirmSetting === 'undefined'){ $('#confirmModalDontShowAgain').hide() }else{ $('#confirmModalDontShowAgain').show() @@ -929,7 +937,7 @@ export class Utils { return true; } - static updateGitCommitRepositoriesList(repositories: Repository[], defaultRepository: Repository) : void { + static updateGitCommitRepositoriesList(repositories: Repository[], defaultRepository: Repository | null) : void { // remove existing options from the repository name select tag $('#gitCommitModalRepositoryNameSelect').empty(); @@ -1040,17 +1048,20 @@ export class Utils { } } + const inputApplication = node.getInputApplication(); + const outputApplication = node.getOutputApplication(); + // add input application input and output ports - if (node.hasInputApplication()){ + if (inputApplication !== null){ // input ports - for (const port of node.getInputApplication().getInputPorts()) { + for (const port of inputApplication.getInputPorts()) { if (!port.getIsEvent()) { Utils._addFieldIfUnique(uniquePorts, port.clone()); } } // output ports - for (const port of node.getInputApplication().getOutputPorts()) { + for (const port of inputApplication.getOutputPorts()) { if (!port.getIsEvent()) { Utils._addFieldIfUnique(uniquePorts, port.clone()); } @@ -1058,16 +1069,16 @@ export class Utils { } // add output application input and output ports - if (node.hasOutputApplication()){ + if (outputApplication !== null){ // input ports - for (const port of node.getOutputApplication().getInputPorts()) { + for (const port of outputApplication.getInputPorts()) { if (!port.getIsEvent()) { Utils._addFieldIfUnique(uniquePorts, port.clone()); } } // output ports - for (const port of node.getOutputApplication().getOutputPorts()) { + for (const port of outputApplication.getOutputPorts()) { if (!port.getIsEvent()) { Utils._addFieldIfUnique(uniquePorts, port.clone()); } @@ -1150,8 +1161,8 @@ export class Utils { const eagle = Eagle.getInstance(); // get a reference to the builtin palette - const builtinPalette: Palette = eagle.findPalette(Palette.BUILTIN_PALETTE_NAME, false); - if (builtinPalette === null){ + const builtinPalette = eagle.findPalette(Palette.BUILTIN_PALETTE_NAME, false); + if (typeof builtinPalette === "undefined"){ // if no built-in palette is found, then build a list from the EAGLE categoryData console.warn("Could not find builtin palette", Palette.BUILTIN_PALETTE_NAME); return Utils.buildComponentList((cData: Category.CategoryData) => {return cData.categoryType === categoryType}); @@ -1210,8 +1221,15 @@ export class Utils { static getComponentsWithMatchingPort(nodes:Node[], input: boolean, type: string) : Node[] { const result: Node[] = []; + const portDragSourceNode = GraphRenderer.portDragSourceNode(); + + if (portDragSourceNode === null){ + console.warn("getComponentsWithMatchingPort(): port drag source node is null"); + return result; + } + // no destination, ask user to choose a new node - const isData: boolean = GraphRenderer.portDragSourceNode().getCategoryType() === Category.Type.Data; + const isData: boolean = portDragSourceNode.getCategoryType() === Category.Type.Data; for (const node of nodes){ // skip data nodes if not eligible @@ -1297,7 +1315,7 @@ export class Utils { static getLegacyCategoryUpdate(node: Node): Category | undefined { // first check for the special case of PythonApp, which should be upgraded to either a DALiuGEApp or a PyFuncApp, depending on the dropclass field value if (node.getCategory() === Category.PythonApp){ - const dropClassField = node.getFieldByDisplayText(Daliuge.FieldName.DROP_CLASS); + const dropClassField = node.findFieldByDisplayText(Daliuge.FieldName.DROP_CLASS); // by default, update PythonApp to a DALiuGEApp, unless dropclass field value indicates it is a PyFuncApp if (dropClassField && dropClassField.getValue() === Daliuge.DEFAULT_PYFUNCAPP_DROPCLASS_VALUE){ @@ -1327,94 +1345,91 @@ export class Utils { ![Category.Type.Unknown].map(x => x as string).includes(categoryType); } - static getColorForNode(node: Node) : string { - return CategoryData.getCategoryData(node.getCategory()).color; - } - - static getRadiusForNode(node: Node) : number { - if(node.isData() || node.isGlobal()){ - return EagleConfig.DATA_NODE_RADIUS; - }else if (node.isBranch()){ - return EagleConfig.BRANCH_NODE_RADIUS; - }else if (node.isConstruct()){ - return EagleConfig.NORMAL_NODE_RADIUS; - }else if (node.isConstruct()){ - return EagleConfig.MINIMUM_CONSTRUCT_RADIUS; - }else if (node.isComment()){ - return EagleConfig.COMMENT_NODE_WIDTH; - }else{ - return EagleConfig.NORMAL_NODE_RADIUS; - } - } - static getRightWindowWidth() : number { - if(Eagle.getInstance().eagleIsReady() && !Setting.findValue(Setting.RIGHT_WINDOW_VISIBLE)){ + if(Eagle.getInstance().eagleIsReady() && !Setting.findValue(Setting.RIGHT_WINDOW_VISIBLE, false)){ return 0 } - return Setting.findValue(Setting.RIGHT_WINDOW_WIDTH) + + return Setting.findValue(Setting.RIGHT_WINDOW_WIDTH, 0); } static setRightWindowWidth(width : number) : void { - Setting.find(Setting.RIGHT_WINDOW_WIDTH).setValue(width) + Setting.setValue(Setting.RIGHT_WINDOW_WIDTH, width); UiModeSystem.saveToLocalStorage() } static getLeftWindowWidth() : number { - const leftWindowDisabled = !Setting.findValue(Setting.ALLOW_GRAPH_EDITING) && !Setting.findValue(Setting.ALLOW_PALETTE_EDITING) + const leftWindowDisabled = !Setting.findValue(Setting.ALLOW_GRAPH_EDITING, false) && !Setting.findValue(Setting.ALLOW_PALETTE_EDITING, false) - if(Eagle.getInstance().eagleIsReady() && !Setting.findValue(Setting.LEFT_WINDOW_VISIBLE) || leftWindowDisabled){ + if(Eagle.getInstance().eagleIsReady() && !Setting.findValue(Setting.LEFT_WINDOW_VISIBLE, false) || leftWindowDisabled){ return 0 } - return Setting.findValue(Setting.LEFT_WINDOW_WIDTH) + + return Setting.findValue(Setting.LEFT_WINDOW_WIDTH, 0); } static setLeftWindowWidth(width : number) : void { - Setting.find(Setting.LEFT_WINDOW_WIDTH).setValue(width) + Setting.setValue(Setting.LEFT_WINDOW_WIDTH, width); UiModeSystem.saveToLocalStorage() } static calculateBottomWindowHeight() : number { //this function exists to prevent the bottom window height value from exceeding its max height value. - //if eagle isnt ready or the window is hidden just return 0 - //TODO This function is only needed for the transition perdiod from pixels to vh. We can get rid of this in the future. + //if eagle isn't ready or the window is hidden just return 0 + //TODO This function is only needed for the transition period from pixels to vh. We can get rid of this in the future. if(!Eagle.getInstance().eagleIsReady()){ return 0 } + const bottomWindowHeight = Setting.findValue(Setting.BOTTOM_WINDOW_HEIGHT, 0); + //if the bottom window height set is too large, just return the max allowed height - if(Setting.findValue(Setting.BOTTOM_WINDOW_HEIGHT)>80){ + if(bottomWindowHeight > 80){ return 80 } //else return the actual height - return Setting.findValue(Setting.BOTTOM_WINDOW_HEIGHT) + return bottomWindowHeight; } static getBottomWindowHeight() : number { - if(Eagle.getInstance().eagleIsReady() && !Setting.findValue(Setting.BOTTOM_WINDOW_VISIBLE)){ + if(Eagle.getInstance().eagleIsReady() && !Setting.findValue(Setting.BOTTOM_WINDOW_VISIBLE, false)){ return 0 } - return Setting.findValue(Setting.BOTTOM_WINDOW_HEIGHT) + + return Setting.findValue(Setting.BOTTOM_WINDOW_HEIGHT, 0); } + // TODO: I don't think this is needed, since Setting.setValue() will already save to local storage static setBottomWindowHeight(height : number) : void { - Setting.find(Setting.BOTTOM_WINDOW_HEIGHT).setValue(height) + Setting.setValue(Setting.BOTTOM_WINDOW_HEIGHT, height); UiModeSystem.saveToLocalStorage() } static getInspectorOffset() : number { const offset = 10 - const statusBarAndOffsetHeightVH = ((($('#statusBar').height() + offset) / window.innerHeight)*100) + let statusBarElementHeight: number = 0; + const statusBarElement = $('#statusBar') + + if (statusBarElement.length) { + const height = statusBarElement.height(); + if (typeof height === 'number') { + statusBarElementHeight = height; + } + } + + const statusBarAndOffsetHeightVH = ((statusBarElementHeight + offset) / window.innerHeight)*100 return this.getBottomWindowHeight() + statusBarAndOffsetHeightVH } - static getLocalStorageKey(repositoryService : Repository.Service, repositoryName : string, repositoryBranch : string) : string { + static getLocalStorageKey(repositoryService : Repository.Service, repositoryName : string, repositoryBranch : string) : string | null{ switch (repositoryService){ case Repository.Service.GitHub: return repositoryName + "|" + repositoryBranch + ".github_repository_and_branch"; case Repository.Service.GitLab: return repositoryName + "|" + repositoryBranch + ".gitlab_repository_and_branch"; default: + console.warn("Utils.getLocalStorageKey(): unknown repository service:", repositoryService); return null; } } @@ -1613,28 +1628,31 @@ export class Utils { //gather all the errors //from nodes - for(const node of graph.getNodes()){ + for (const node of graph.getNodes()){ graphIssues.push(...node.getIssues()) //from fields - for(const field of node.getFields()){ + for (const field of node.getFields()){ graphIssues.push(...field.getIssues()) } + const inputApplication = node.getInputApplication(); + const outputApplication = node.getOutputApplication(); + //embedded input applications and their fields - if(node.hasInputApplication()){ - graphIssues.push(...node.getInputApplication().getIssues().values()) - - for(const field of node.getInputApplication().getFields()){ + if (inputApplication !== null){ + graphIssues.push(...inputApplication.getIssues().values()) + + for (const field of inputApplication.getFields()){ graphIssues.push(...field.getIssues()) } } //embedded output applications and their fields - if(node.hasOutputApplication()){ - graphIssues.push(...node.getOutputApplication().getIssues().values()) - - for( const field of node.getOutputApplication().getFields()){ + if (outputApplication !== null){ + graphIssues.push(...outputApplication.getIssues().values()) + + for (const field of outputApplication.getFields()){ graphIssues.push(...field.getIssues()) } } @@ -1663,7 +1681,7 @@ export class Utils { // validate json static validateJSON(jsonString: string, fileType: Eagle.FileType, version: Setting.SchemaVersion){ // if validation disabled, just return true - if (Setting.findValue(Setting.DISABLE_JSON_VALIDATION)){ + if (Setting.findValue(Setting.DISABLE_JSON_VALIDATION, false)){ return; } @@ -1892,8 +1910,8 @@ export class Utils { return html; } - static asBool(value: string) : boolean { - if(value === undefined){ + static asBool(value: string | undefined | null) : boolean { + if(value === undefined || value === null){ return false } return value.toLowerCase() === "true"; @@ -1918,8 +1936,12 @@ export class Utils { static fixNodeCategory(eagle: Eagle, node: Node, category: Category, categoryType: Category.Type){ node.setCategory(category); node.setCategoryType(categoryType); - node.setRadius(Utils.getRadiusForNode(node)); - node.setColor(Utils.getColorForNode(node)); + + // lookup category data + const categoryData = CategoryData.getCategoryData(category); + + node.setRadius(categoryData.radius); + node.setColor(categoryData.color); } // NOTE: merges field1 into field0 @@ -2009,10 +2031,10 @@ export class Utils { } static fixFieldValue(eagle: Eagle, node: Node, exampleField: Field, value: string){ - let field : Field = node.getFieldByDisplayText(exampleField.getDisplayText()); + let field = node.findFieldByDisplayText(exampleField.getDisplayText()); // if a field was not found, clone one from the example and add to node - if (field === null){ + if (typeof field === 'undefined'){ field = exampleField .clone() .setId(Utils.generateFieldId()); @@ -2102,7 +2124,11 @@ export class Utils { } } - static addSourcePortToSourceNode(eagle: Eagle, edge: Edge){ + static addSourcePortToSourceNode(eagle: Eagle, edge: Edge | undefined){ + if (typeof edge === 'undefined'){ + console.warn("fixAddSourcePortToSourceNode(): edge is undefined"); + return; + } const srcNode = edge.getSrcNode(); const destPort = edge.getDestPort(); @@ -2115,13 +2141,17 @@ export class Utils { const srcPortType = destPort.getType() === undefined ? Daliuge.DataType.Object : destPort.getType(); // create new source port - const srcPort = new Field(edge.getSrcPort().getId(), destPort.getDisplayText(), "", "", "", false, srcPortType, false, [], false, Daliuge.FieldType.Application, Daliuge.FieldUsage.OutputPort); + const srcPort = new Field(srcNode, edge.getSrcPort().getId(), destPort.getDisplayText(), "", "", "", false, srcPortType, false, [], false, Daliuge.FieldType.Application, Daliuge.FieldUsage.OutputPort); // add port to source node srcNode.addField(srcPort); } - static addDestinationPortToDestinationNode(eagle: Eagle, edge: Edge){ + static addDestinationPortToDestinationNode(eagle: Eagle, edge: Edge | undefined){ + if (typeof edge === 'undefined'){ + console.warn("fixAddDestinationPortToDestinationNode(): edge is undefined"); + return; + } const destNode = edge.getDestNode(); const srcPort = edge.getSrcPort(); @@ -2134,13 +2164,17 @@ export class Utils { const destPortType = srcPort.getType() === undefined ? Daliuge.DataType.Object : srcPort.getType(); // create new destination port - const destPort = new Field(edge.getDestPort().getId(), srcPort.getDisplayText(), "", "", "", false, destPortType, false, [], false, Daliuge.FieldType.Application, Daliuge.FieldUsage.OutputPort); + const destPort = new Field(destNode, edge.getDestPort().getId(), srcPort.getDisplayText(), "", "", "", false, destPortType, false, [], false, Daliuge.FieldType.Application, Daliuge.FieldUsage.OutputPort); // add port to destination node destNode.addField(destPort); } - static fixMoveEdgeToEmbeddedApplication(eagle: Eagle, edge: Edge){ + static fixMoveEdgeToEmbeddedApplication(eagle: Eagle, edge: Edge | undefined){ + if (typeof edge === 'undefined'){ + console.warn("fixMoveEdgeToEmbeddedApplication(): edge is undefined"); + return; + } const srcNode = edge.getSrcNode(); const destNode = edge.getDestNode(); @@ -2148,7 +2182,7 @@ export class Utils { if (srcNode.getCategoryType() === Category.Type.Construct){ const embeddedApplicationKeyAndPort = srcNode.findPortInApplicationsById(edge.getSrcPort().getId()); - if (embeddedApplicationKeyAndPort.node !== null){ + if (typeof embeddedApplicationKeyAndPort.node !== 'undefined'){ edge.setSrcNode(embeddedApplicationKeyAndPort.node); } } @@ -2157,7 +2191,7 @@ export class Utils { if (destNode.getCategoryType() === Category.Type.Construct){ const embeddedApplicationKeyAndPort = destNode.findPortInApplicationsById(edge.getDestPort().getId()); - if (embeddedApplicationKeyAndPort.node !== null){ + if (typeof embeddedApplicationKeyAndPort.node !== 'undefined'){ edge.setDestNode(embeddedApplicationKeyAndPort.node); } } @@ -2172,7 +2206,12 @@ export class Utils { field.setParameterType(newType); } - static fixAppToAppEdge(eagle: Eagle, edge: Edge){ + static fixAppToAppEdge(eagle: Eagle, edge: Edge | undefined){ + if (typeof edge === 'undefined'){ + console.warn("fixAppToAppEdge(): edge is undefined"); + return; + } + const srcNode: Node = edge.getSrcNode(); const destNode: Node = edge.getDestNode(); const srcPort: Field = edge.getSrcPort(); @@ -2185,11 +2224,10 @@ export class Utils { static addMissingRequiredField(eagle: Eagle, node: Node, requiredField: Field){ // if requiredField is "dropclass", and node already contains an "appclass" field, then just rename it if (requiredField.getDisplayText() === Daliuge.FieldName.DROP_CLASS){ - const appClassField = node.getFieldByDisplayText("appclass"); + const appClassField = node.findFieldByDisplayText("appclass"); - if (appClassField !== null){ + if (typeof appClassField !== 'undefined'){ appClassField.setDisplayText(Daliuge.FieldName.DROP_CLASS); - return; } } @@ -2205,10 +2243,14 @@ export class Utils { case Daliuge.FieldName.DROP_CLASS: // look up component in palette - const paletteComponent: Node = Utils.getPaletteComponentByName(node.getCategory()); + const paletteComponent = Utils.getPaletteComponentByName(node.getCategory()); - if (paletteComponent !== null){ - const dropClassField: Field = paletteComponent.findFieldByDisplayText(Daliuge.FieldName.DROP_CLASS); + if (typeof paletteComponent !== 'undefined'){ + const dropClassField = paletteComponent.findFieldByDisplayText(Daliuge.FieldName.DROP_CLASS); + if (typeof dropClassField === 'undefined'){ + console.warn("Could not find dropclass field in palette component:", paletteComponent.getName()); + break; + } field.setValue(dropClassField.getDefaultValue()); field.setDefaultValue(dropClassField.getDefaultValue()); @@ -2276,11 +2318,13 @@ export class Utils { graph.updateGraphConfigId(graphConfigId, Utils.generateGraphConfigId()); } - static showEdge(eagle: Eagle, edge: Edge): void { + static showEdge(eagle: Eagle, edge: Edge | undefined): void { // close errors modal if visible $('#issuesDisplay').modal("hide"); - eagle.setSelection(edge, Eagle.FileType.Graph); + if (typeof edge !== 'undefined'){ + eagle.setSelection(edge, Eagle.FileType.Graph); + } } static showNode(eagle: Eagle, location: Eagle.FileType, node: Node): void { @@ -2289,7 +2333,7 @@ export class Utils { // check that we found the node if (node === null){ - console.warn("Could not show node with id", node.getId()); + console.warn("Could not show null node"); return; } @@ -2309,7 +2353,12 @@ export class Utils { // open the graph configs table GraphConfigurationsTable.openTable(); - const graphConfig: GraphConfig = eagle.logicalGraph().getGraphConfigById(graphConfigId); + const graphConfig = eagle.logicalGraph().getGraphConfigById(graphConfigId); + + if (typeof graphConfig === 'undefined'){ + console.warn("Could not find graph config with id:", graphConfigId); + return; + } // highlight the name of the graph config setTimeout(() => { @@ -2380,11 +2429,18 @@ export class Utils { numFieldIssues += field.getIssues().length; } + const parent = node.getParent(); + const embed = node.getEmbed(); + const inputApplication = node.getInputApplication(); + const outputApplication = node.getOutputApplication(); + const inputApplicationEmbed = inputApplication === null ? null : inputApplication.getEmbed(); + const outputApplicationEmbed = outputApplication === null ? null : outputApplication.getEmbed(); + tableData.push({ "name":node.getName(), "id":node.getId(), - "parent":node.getParent() === null ? null : node.getParent().getId(), - "embed":node.getEmbed() === null ? null : node.getEmbed().getId(), + "parent":parent === null ? null : parent.getId(), + "embed":embed === null ? null : embed.getId(), "comment":node.getComment(), "children":children.toString(), "category":node.getCategory(), @@ -2394,12 +2450,12 @@ export class Utils { "x":node.getPosition().x, "y":node.getPosition().y, "radius":node.getRadius(), - "inputAppId":node.getInputApplication() === null ? null : node.getInputApplication().getId(), - "inputAppCategory":node.getInputApplication() === null ? null : node.getInputApplication().getCategory(), - "inputAppEmbedId":node.getInputApplication() === null ? null : node.getInputApplication().getEmbed().getId(), - "outputAppId":node.getOutputApplication() === null ? null : node.getOutputApplication().getId(), - "outputAppCategory":node.getOutputApplication() === null ? null : node.getOutputApplication().getCategory(), - "outputAppEmbedId":node.getOutputApplication() === null ? null : node.getOutputApplication().getEmbed().getId(), + "inputAppId":inputApplication === null ? null : inputApplication.getId(), + "inputAppCategory":inputApplication === null ? null : inputApplication.getCategory(), + "inputAppEmbedId":inputApplicationEmbed === null ? null : inputApplicationEmbed.getId(), + "outputAppId":outputApplication === null ? null : outputApplication.getId(), + "outputAppCategory":outputApplication === null ? null : outputApplication.getCategory(), + "outputAppEmbedId":outputApplicationEmbed === null ? null : outputApplicationEmbed.getId(), "nodeIssues": node.getIssues().length, "fieldIssues": numFieldIssues }); @@ -2444,11 +2500,13 @@ export class Utils { // add logical graph nodes to table for (const palette of eagle.palettes()){ for (const node of palette.getNodes()){ + const embed = node.getEmbed(); + tableData.push({ "id":node.getId(), "palette":palette.fileInfo().name, "name":node.getName(), - "embedId":node.getEmbed().getId(), + "embedId":embed === null ? null : embed.getId(), "category":node.getCategory(), "categoryType":node.getCategoryType(), "numFields":node.getNumFields(), @@ -2467,14 +2525,16 @@ export class Utils { const tableData : any[] = []; const eagle : Eagle = Eagle.getInstance(); + const node = eagle.logicalGraph().getNodeById(nodeId); + // check that node at nodeIndex exists - if (!eagle.logicalGraph().hasNode(nodeId)){ + if (typeof node === 'undefined'){ console.warn("Unable to print node fields table, node", nodeId, "does not exist."); return; } // add logical graph nodes to table - for (const field of eagle.logicalGraph().getNodeById(nodeId).getFields()){ + for (const field of node.getFields()){ tableData.push({ "id":field.getId(), "displayText":field.getDisplayText(), @@ -2495,11 +2555,16 @@ export class Utils { static printGraphConfigurationTable() : void { const tableData : any[] = []; const eagle : Eagle = Eagle.getInstance(); - const activeConfig: GraphConfig = eagle.logicalGraph().getActiveGraphConfig(); + const activeConfig = eagle.logicalGraph().getActiveGraphConfig(); + + if (typeof activeConfig === 'undefined'){ + console.warn("No active graph configuration to print."); + return; + } // add logical graph nodes to table for (const graphConfigNode of activeConfig.getNodes()){ - const graphNode: Node = eagle.logicalGraph().getNodeById(graphConfigNode.getNode().getId()); + const graphNode = eagle.logicalGraph().getNodeById(graphConfigNode.getNode().getId()); if (typeof graphNode === 'undefined'){ // TODO: what to do here? blank row, console warning? @@ -2507,7 +2572,7 @@ export class Utils { } for (const graphConfigField of graphConfigNode.getFields()){ - const graphField: Field = graphNode.getFieldById(graphConfigField.getField().getId()); + const graphField = graphNode.getFieldById(graphConfigField.getField().getId()); if (typeof graphField === 'undefined'){ // TODO: what to do here? blank row, console warning? @@ -2556,7 +2621,20 @@ export class Utils { static copyInputTextModalInput(): void { - navigator.clipboard.writeText($('#inputTextModalInput').val().toString()); + const input = $('#inputTextModalInput'); + + if (typeof input === 'undefined'){ + console.error("No input element found in modal"); + return; + } + + const inputValue = input.val(); + if (typeof inputValue === 'undefined'){ + console.error("No value found in modal input element"); + return; + } + + navigator.clipboard.writeText(inputValue.toString()); } static copyInputCodeModalInput(): void { @@ -2641,7 +2719,7 @@ export class Utils { setFunc(dataObject); // write to localStorage - localStorage.setItem(localStorageKey, data); + localStorage.setItem(localStorageKey, JSON.stringify(dataObject)); } } @@ -2651,7 +2729,9 @@ export class Utils { } static snapToGrid(coord: number, offset: number) : number { - const gridSize = Setting.findValue(Setting.SNAP_TO_GRID_SIZE); + const gridSizeSetting = Setting.find(Setting.SNAP_TO_GRID_SIZE); + const gridSize : number = gridSizeSetting ? gridSizeSetting.value() as number : 10; + return (gridSize * Math.round((coord + offset)/gridSize)) - offset; } @@ -2694,17 +2774,17 @@ export class Utils { const eagle: Eagle = Eagle.getInstance(); // get a reference to the builtin palette - const palette: Palette = eagle.findPalette(paletteName, false); - if (palette === null){ + const palette = eagle.findPalette(paletteName, false); + if (typeof palette === "undefined"){ console.warn("Could not find palette", paletteName); return; } // find node with new type in builtinPalette - const newCategoryPrototype: Node = palette.findNodeByNameAndCategory(category); + const newCategoryPrototype = palette.findNodeByNameAndCategory(category); // check that category was found - if (newCategoryPrototype === null){ + if (typeof newCategoryPrototype === 'undefined'){ console.warn("Prototypes for new category could not be found in palettes", category); return; } @@ -2719,7 +2799,7 @@ export class Utils { let destField = node.findFieldByDisplayText(field.getDisplayText()); // if dest field could not be found, then go ahead and add a NEW field to the dest node - if (destField === null){ + if (typeof destField === 'undefined'){ destField = field.clone(); node.addField(destField); } @@ -2787,7 +2867,7 @@ export class Utils { let destField = node.findFieldByDisplayText(field.getDisplayText()); // if dest field could not be found, then go ahead and add a NEW field to the node - if (destField === null){ + if (typeof destField === "undefined"){ destField = field.clone(); node.addField(destField); } @@ -2816,10 +2896,18 @@ export class Utils { // search for custom repositories, and add them into the list. for (let i = 0; i < localStorage.length; i++) { - const key : string = localStorage.key(i); - const value : string = localStorage.getItem(key); + const key : string | null = localStorage.key(i); + if (key === null) { + continue; + } + const keyExtension : string = key.substring(key.lastIndexOf('.') + 1); + const value : string | null = localStorage.getItem(key); + if (value === null) { + continue; + } + // handle legacy repositories where the branch is not specified (assume master) if (keyExtension === "github_repository"){ customRepositories.push(new Repository(Repository.Service.GitHub, value, "master", false)); @@ -2902,4 +2990,9 @@ export class Utils { // This regex covers most OS restrictions (Windows, macOS, Linux) return name.replace(/[^a-zA-Z0-9_\-\.]/g, "_"); } + + static getUIValue(selector: string, defaultValue: string): string { + const value = $(selector).val(); + return value ? value.toString() : defaultValue; + } } diff --git a/src/bindingHandlers/eagleTooltip.ts b/src/bindingHandlers/eagleTooltip.ts index 2a049164..8b2c13d9 100644 --- a/src/bindingHandlers/eagleTooltip.ts +++ b/src/bindingHandlers/eagleTooltip.ts @@ -33,6 +33,7 @@ ko.bindingHandlers.eagleTooltip = { // manual tooltip open system to allow for hovering on the tooltips let stillHovering = false jQueryElement.on('mouseenter', function () { + const event = window.event as MouseEvent; event.stopImmediatePropagation() event.stopPropagation() event.preventDefault() @@ -45,7 +46,7 @@ ko.bindingHandlers.eagleTooltip = { jQueryElement.attr("data-bs-placement", "right"); } - let html = ko.unwrap(valueAccessor()) + const html = ko.unwrap(valueAccessor()) let result = '' let size = EagleConfig.EAGLE_TOOLTIP_DEFAULT_MAX_WIDTH + 'px' //default size let content = '' diff --git a/src/main.ts b/src/main.ts index c2595b62..95f9c387 100644 --- a/src/main.ts +++ b/src/main.ts @@ -133,16 +133,14 @@ $(function(){ } // hide the ?mode=x part of the url - window.history.replaceState(null, null, window.location.origin + window.location.pathname); + window.history.replaceState(null, "", window.location.origin + window.location.pathname); } // load the default palette eagle.loadDefaultPalettes(); // set other state based on settings values - if (Setting.findValue(Setting.SNAP_TO_GRID)){ - eagle.snapToGrid(Setting.findValue(Setting.SNAP_TO_GRID)); - } + eagle.snapToGrid(Setting.findValue(Setting.SNAP_TO_GRID, false)); // load schemas Utils.loadSchemas(); @@ -154,7 +152,7 @@ $(function(){ Modals.init(eagle); // add a listener for the beforeunload event, helps warn users before leaving webpage with unsaved changes - window.onbeforeunload = () => (eagle.areAnyFilesModified() && Setting.findValue(Setting.CONFIRM_DISCARD_CHANGES)) ? "Check graph" : null; + window.onbeforeunload = () => (eagle.areAnyFilesModified() && Setting.findValue(Setting.CONFIRM_DISCARD_CHANGES, true)) ? "Check graph" : null; // keyboard shortcut event listener document.onkeydown = KeyboardShortcut.processKey; @@ -348,8 +346,8 @@ async function autoLoad() { } // if developer setting enabled, fetch the repository that this graph belongs to (if the repository is in the list of known repositories) - if (serviceIsGit && Setting.findValue(Setting.FETCH_REPOSITORY_FOR_URLS)){ - let repo: Repository = Repositories.get(service, repository, branch); + if (serviceIsGit && Setting.findValue(Setting.FETCH_REPOSITORY_FOR_URLS, false)){ + let repo: Repository | null = Repositories.get(service, repository, branch); // check whether the source repository is already known to EAGLE if (repo === null){ diff --git a/src/tutorials/graphBuilding.ts b/src/tutorials/graphBuilding.ts index 25fe95fb..1b35e7d6 100644 --- a/src/tutorials/graphBuilding.ts +++ b/src/tutorials/graphBuilding.ts @@ -16,14 +16,14 @@ newTut.newTutStep("Creating a New Graph", "First we are going to create a new gr newTut.newTutStep("Creating a New Graph", "Click on 'New'.", function(){return $("#navbarDropdownGraph").parent().find('.dropdown-item').first()}) .setType(TutorialStep.Type.Press) -.setPreFunction(function(){TutorialSystem.activeTutCurrentStep.getTargetFunc()().parent().addClass('forceShow')}) //keeping the navbar graph dropdown open +.setPreFunction(function(){TutorialSystem.activeTutCurrentStep.forceShowTargetParent()}) //keeping the navbar graph dropdown open .setBackPreFunction(function(){$("#navbarDropdownGraph").parent().find('#createNewGraph').removeClass('forceShow')})//allowing the 'new' drop drop down section to close .setBackSkip(true) newTut.newTutStep("Creating a New Graph", "Click on 'Create new graph'", function(){return $("#navbarDropdownGraph").parent().find('#createNewGraph')}) .setType(TutorialStep.Type.Press) -.setPreFunction(function(){TutorialSystem.activeTutCurrentStep.getTargetFunc()().parent().addClass('forceShow')})//keeping the 'new' drop drop down section open as well -.setBackPreFunction(function(){$("#navbarDropdownGraph").parent().find('.dropdown-item').first().parent().addClass('forceShow');TutorialSystem.activeTutCurrentStep.getTargetFunc()().parent().addClass('forceShow')})//force showing both of the navbar graph drop downs +.setPreFunction(function(){TutorialSystem.activeTutCurrentStep.forceShowTargetParent()})//keeping the 'new' drop drop down section open as well +.setBackPreFunction(function(){$("#navbarDropdownGraph").parent().find('.dropdown-item').first().parent().addClass('forceShow');TutorialSystem.activeTutCurrentStep.forceShowTargetParent()})//force showing both of the navbar graph drop downs .setBackSkip(true) newTut.newTutStep("Creating a new graph", "Then just give it a name and press enter", function(){return $("#inputModalInput")}) @@ -79,7 +79,7 @@ newTut.newTutStep("Graph Nodes", "Once added into your graph, the component is i newTut.newTutStep("Editing Components", "The inspector panel provides access to the complete set of specifications of a component.", function(){return $("#inspector")}) .setWaitType(TutorialStep.Wait.Delay) .setDelayAmount(200) -.setPreFunction(function(){Setting.find(Setting.INSPECTOR_COLLAPSED_STATE).setValue(false)}) +.setPreFunction(function(){Setting.setValue(Setting.INSPECTOR_COLLAPSED_STATE, false)}) newTut.newTutStep("The Parameter Table", "Click to open the node fields table and continue.", function(){return $("#inspector #openNodeParamsTable")}) .setWaitType(TutorialStep.Wait.Element) @@ -134,7 +134,7 @@ newTut.newTutStep("Saving a Graph", "Options to save your graph are available in .setBackPreFunction(function(){$('.forceShow').removeClass('forceShow');$(".dropdown-toggle").removeClass("show");$(".dropdown-menu").removeClass("show")}) //allowing the graph navbar dropdown to hide newTut.newTutStep("Saving a Graph", "You are able to download the graph in the 'local storage' section, or save the graph into your github repository under 'git storage'", function(){return $("#navbarDropdownGraph").parent().find('.dropdown-menu')}) -.setPreFunction(function(){TutorialSystem.activeTutCurrentStep.getTargetFunc()().addClass('forceShow')}) //keeping the navbar graph dropdown open +.setPreFunction(function(){TutorialSystem.activeTutCurrentStep.forceShowTargetParent()}) //keeping the navbar graph dropdown open .setBackSkip(true) newTut.newTutStep("Well Done!", "You have completed the Hello world graph creation tutorial! Be sure to check our online documentation for additional help and guidance.", function(){return $("#logicalGraphParent")}) diff --git a/src/tutorials/graphConfigs.ts b/src/tutorials/graphConfigs.ts index e998c6b5..0f00c757 100644 --- a/src/tutorials/graphConfigs.ts +++ b/src/tutorials/graphConfigs.ts @@ -13,14 +13,14 @@ newTut.newTutStep("Creating a New Graph", "Lets once again create a new graph. < newTut.newTutStep("Creating a New Graph", "Click on 'New'.", function(){return $("#navbarDropdownGraph").parent().find('.dropdown-item').first()}) .setType(TutorialStep.Type.Press) -.setPreFunction(function(){TutorialSystem.activeTutCurrentStep.getTargetFunc()().parent().addClass('forceShow')}) //keeping the navbar graph dropdown open +.setPreFunction(function(){TutorialSystem.activeTutCurrentStep.forceShowTargetParent()}) //keeping the navbar graph dropdown open .setBackPreFunction(function(){$("#navbarDropdownGraph").parent().find('#createNewGraph').removeClass('forceShow')})//allowing the 'new' drop drop down section to close .setBackSkip(true) newTut.newTutStep("Creating a New Graph", "Click on 'Create new graph'", function(){return $("#navbarDropdownGraph").parent().find('#createNewGraph')}) .setType(TutorialStep.Type.Press) -.setPreFunction(function(){TutorialSystem.activeTutCurrentStep.getTargetFunc()().parent().addClass('forceShow')})//keeping the 'new' drop drop down section open as well -.setBackPreFunction(function(){$("#navbarDropdownGraph").parent().find('.dropdown-item').first().parent().addClass('forceShow');TutorialSystem.activeTutCurrentStep.getTargetFunc()().parent().addClass('forceShow')})//force showing both of the navbar graph drop downs +.setPreFunction(function(){TutorialSystem.activeTutCurrentStep.forceShowTargetParent()})//keeping the 'new' drop drop down section open as well +.setBackPreFunction(function(){$("#navbarDropdownGraph").parent().find('.dropdown-item').first().parent().addClass('forceShow');TutorialSystem.activeTutCurrentStep.forceShowTargetParent()})//force showing both of the navbar graph drop downs .setBackSkip(true) newTut.newTutStep("Creating a new graph", "Then just give it a name and press enter on you keyboard", function(){return $("#inputModalInput")}) @@ -77,7 +77,7 @@ newTut.newTutStep("Adding Graph Configuration Fields", "Click on the HelloWo newTut.newTutStep("Adding Graph Configuration Fields", "Click to open the node fields table and continue.", function(){return $("#inspector #openNodeParamsTable")}) .setWaitType(TutorialStep.Wait.Element) .setType(TutorialStep.Type.Press) -.setPreFunction(function(){Setting.find(Setting.INSPECTOR_COLLAPSED_STATE).setValue(false)}) +.setPreFunction(function(){Setting.setValue(Setting.INSPECTOR_COLLAPSED_STATE, false)}) .setWaitType(TutorialStep.Wait.Delay) .setDelayAmount(400) diff --git a/src/tutorials/quickStart.ts b/src/tutorials/quickStart.ts index 82bc511c..3f922225 100644 --- a/src/tutorials/quickStart.ts +++ b/src/tutorials/quickStart.ts @@ -1,4 +1,4 @@ -import {TutorialStep, TutorialSystem} from '../Tutorial'; +import { TutorialStep, TutorialSystem } from '../Tutorial'; import { Utils } from '../Utils'; const newTut = TutorialSystem.newTutorial('Quick Start', 'This tutorial is an introductory tour around Eagle to get the user familiar with the user interface.') @@ -23,7 +23,7 @@ newTut.newTutStep("Help", "This is where you can find various documentation to a .setBackPreFunction(function(){$('.forceShow').removeClass('forceShow');$('.modal').modal("hide");}) //allowing the graph navbar dropdown to hide newTut.newTutStep("Tutorials", "All the of our tutorials, including this one, will always be available here.", function(){return $("#navTutorials")}) -.setPreFunction(function(){TutorialSystem.activeTutCurrentStep.getTargetFunc()().parent().addClass('forceShow');$("#navTutorials").find('.dropDropDown').addClass('forceShow')}) //keeping the navbar graph dropdown open and showing the contents of the tutorials drop down +.setPreFunction(function(){TutorialSystem.activeTutCurrentStep.forceShowTargetParent();$("#navTutorials").find('.dropDropDown').addClass('forceShow')}) //keeping the navbar graph dropdown open and showing the contents of the tutorials drop down .setBackPreFunction(function(){$("#navTutorials").find('.dropDropDown').addClass('forceShow')})//showing the contents of the tutorials drop down newTut.newTutStep("Read The Docs", "This is a link to our in depth documentation on Read The Docs.", function(){return $("#onlineDocs")}) @@ -31,7 +31,7 @@ newTut.newTutStep("Read The Docs", "This is a link to our in depth documentation .setBackPreFunction(function(){$('.modal').modal("hide");}) //hide the modal in case it has been opened by the user newTut.newTutStep("Keyboard Shortcuts", "Eagle has many keyboard shortcuts to boost productivity, this is the cheat sheet. The shortcut for this is [K]", function(){return $("#keyboardShortcuts")}) -.setBackPreFunction(function(){TutorialSystem.activeTutCurrentStep.getTargetFunc()().parent().addClass('forceShow');}) //hide the modal in case it has been opened by the user +.setBackPreFunction(function(){TutorialSystem.activeTutCurrentStep.forceShowTargetParent()}) //hide the modal in case it has been opened by the user // TODO: check comment is correct newTut.newTutStep("Quick Actions", "Use this tool to look up and run functions or discover documentation available in EAGLE.", function(){return $("#quickAction")}) .setPreFunction(function(){$(".forceShow").removeClass("forceShow"); $("#navbarDropdownHelp").trigger('mouseleave');}) //hide the quickaction tooltip diff --git a/tsconfig.json b/tsconfig.json index ab96f776..da25b7d6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,7 @@ "noFallthroughCasesInSwitch": true, "noUnusedLocals": false, // NOTE: would like to set this to true, but sometimes functions are only used within knockout html "noUnusedParameters": false, - "strictNullChecks" : false, + "strictNullChecks" : true, "strictFunctionTypes": false, "strictPropertyInitialization": false, "module": "umd",