Пример #1
0
        private static void WriteApiEndpointFunction(TypeScriptCodeBuilder b, ClassViewModel model, MethodViewModel method)
        {
            var    returnIsListResult = method.ReturnsListResult;
            string signature          =
                string.Concat(method.ClientParameters.Select(f => $"{f.Name}: {new VueType(f.Type).TsType("$models")} | null, "))
                + "$config?: AxiosRequestConfig";

            if (method.IsModelInstanceMethod)
            {
                signature = $"id: {new VueType(model.PrimaryKey.Type).TsType(null)}, " + signature;
            }

            using (b.Block($"public {method.JsVariable}({signature})"))
            {
                string resultType = method.TransportTypeGenericParameter.IsVoid
                    ? $"{method.TransportType}<void>"
                    : $"{method.TransportType}<{new VueType(method.TransportTypeGenericParameter).TsType("$models")}>";

                b.Line($"const $method = this.$metadata.methods.{method.JsVariable}");
                using (b.Block($"const $params = this.$mapParams($method,", ')'))
                {
                    if (method.IsModelInstanceMethod)
                    {
                        b.Line($"id,");
                    }
                    foreach (var param in method.ClientParameters)
                    {
                        b.Line($"{param.JsVariable},");
                    }
                }

                b.Line("return AxiosClient");
                using (b.Indented())
                {
                    b.Line($".{method.ApiActionHttpMethodName.ToLower()}(");
                    b.Indented($"`/${{this.$metadata.controllerRoute}}/{method.Name}`,");
                    switch (method.ApiActionHttpMethod)
                    {
                    case DataAnnotations.HttpMethod.Get:
                    case DataAnnotations.HttpMethod.Delete:
                        b.Indented($"this.$options(undefined, $config, $params)");
                        break;

                    default:
                        b.Indented($"qs.stringify($params),");
                        b.Indented($"this.$options(undefined, $config)");
                        break;
                    }
                    b.Line(")");

                    b.Line($".then<AxiosResponse<{resultType}>>(r => this.$hydrate{method.TransportType}(r, $method.return))");
                }
            }

            // Line between methods
            b.Line();
        }
Пример #2
0
        private static void WriteListViewModel(TypeScriptCodeBuilder b, ClassViewModel model)
        {
            string name = model.ViewModelClassName;

            string viewModelName = $"{model.ListViewModelClassName}ViewModel";
            string metadataName  = $"$metadata.{name}";

            using (b.Block($"export class {viewModelName} extends ListViewModel<$models.{name}, $apiClients.{name}ApiClient>"))
            {
                foreach (var method in model.ClientMethods.Where(m => m.IsStatic))
                {
                    string signature =
                        string.Concat(method.ClientParameters.Select(f => $", {f.Name}: {new VueType(f.Type).TsType("$models")} | null"));

                    // "item" or "list"
                    var transportTypeSlug = method.TransportType.ToString().Replace("Result", "").ToLower();

                    b.DocComment(method.Comment, true);
                    b.Line($"public {method.JsVariable} = this.$apiClient.$makeCaller(\"{transportTypeSlug}\", ");
                    b.Indented($"(c{signature}) => c.{method.JsVariable}({string.Join(", ", method.ClientParameters.Select(p => p.Name))}))");
                }

                b.Line();
                using (b.Block($"constructor()"))
                {
                    b.Line($"super({metadataName}, new $apiClients.{name}ApiClient())");
                }
            }
            b.Line();
        }
Пример #3
0
        private void WriteListViewModelClass(TypeScriptCodeBuilder b)
        {
            using (b.Block($"export class {Model.ListViewModelClassName} extends Coalesce.BaseListViewModel<{ViewModelFullName}>"))
            {
                b.Line($"public readonly modelName: string = \"{Model.ClientTypeName}\";");
                b.Line($"public readonly apiController: string = \"/{Model.ApiRouteControllerPart}\";");
                b.Line($"public modelKeyName: string = \"{Model.PrimaryKey.JsVariable}\";");
                b.Line($"public itemClass: new () => {ViewModelFullName} = {ViewModelFullName};");

                b.Line();
                b.Line("public filter: {");
                foreach (var prop in Model.BaseViewModel.ClientProperties.Where(f => f.IsUrlFilterParameter))
                {
                    b.Indented($"{prop.JsonName}?: string;");
                }
                b.Line("} | null = null;");

                b.DocComment("The namespace containing all possible values of this.dataSource.");
                b.Line($"public dataSources: typeof {Model.ClientTypeName}DataSources = {Model.ClientTypeName}DataSources;");

                b.DocComment("The data source on the server to use when retrieving objects. Valid values are in this.dataSources.");
                b.Line($"public dataSource: Coalesce.DataSource<{ViewModelFullName}> = new this.dataSources.{DataSourceFactory.DefaultSourceName}();");

                b.DocComment($"Configuration for all instances of {Model.ListViewModelClassName}. Can be overidden on each instance via instance.coalesceConfig.");
                b.Line($"public static coalesceConfig = new Coalesce.ListViewModelConfiguration<{Model.ListViewModelClassName}, {ViewModelFullName}>(Coalesce.GlobalConfiguration.listViewModel);");

                b.DocComment($"Configuration for this {Model.ListViewModelClassName} instance.");
                b.Line($"public coalesceConfig: Coalesce.ListViewModelConfiguration<{Model.ListViewModelClassName}, {ViewModelFullName}>");
                b.Indented($"= new Coalesce.ListViewModelConfiguration<{Model.ListViewModelClassName}, {ViewModelFullName}>({Model.ListViewModelClassName}.coalesceConfig);");

                // Write client methods
                b.Line();
                foreach (var method in Model.ClientMethods.Where(m => m.IsStatic))
                {
                    b.Line();
                    WriteClientMethodDeclaration(b, method, Model.ListViewModelClassName);
                }

                b.Line();
                b.Line($"protected createItem = (newItem?: any, parent?: any) => new {ViewModelFullName}(newItem, parent);");

                b.Line();
                b.Line("constructor() {");
                b.Indented("super();");
                b.Line("}");
            }
        }
Пример #4
0
        private void WriteEnumMetadata(TypeScriptCodeBuilder b, TypeViewModel model)
        {
            using (b.Block($"export const {model.Name} = domain.enums.{model.Name} ="))
            {
                b.StringProp("name", model.Name.ToCamelCase());
                b.StringProp("displayName", model.DisplayName);
                b.StringProp("type", "enum");

                string enumShape = string.Join("|", model.EnumValues.Select(ev => $"\"{ev.Value}\""));
                b.Line($"...getEnumMeta<{enumShape}>([");
                foreach (var value in model.EnumValues)
                {
                    // TODO: allow for localization of displayName
                    b.Indented($"{{ value: {value.Key}, strValue: '{value.Value}', displayName: '{value.Value.ToProperCase()}' }},");
                }
                b.Line("]),");
            }
        }
Пример #5
0
        private static void WriteViewModel(TypeScriptCodeBuilder b, ClassViewModel model)
        {
            string name          = model.ViewModelClassName;
            string viewModelName = $"{name}ViewModel";
            string metadataName  = $"$metadata.{name}";

            b.Line($"export interface {viewModelName} extends $models.{name} {{}}");
            using (b.Block($"export class {viewModelName} extends ViewModel<$models.{name}, $apiClients.{name}ApiClient>"))
            {
                // This is an alternative to calling defineProps() on each class that causes larger and more cluttered files:
                //foreach (var prop in model.ClientProperties)
                //{
                //    b.Line($"get {prop.JsVariable}() {{ return this.$data.{prop.JsVariable} }}");
                //    b.Line($"set {prop.JsVariable}(val) {{ this.$data.{prop.JsVariable} = val }}");
                //}

                foreach (var method in model.ClientMethods.Where(m => !m.IsStatic))
                {
                    string signature =
                        string.Concat(method.ClientParameters.Select(f => $", {f.Name}: {new VueType(f.Type).TsType("$models")} | null"));

                    // "item" or "list"
                    var transportTypeSlug = method.TransportType.ToString().Replace("Result", "").ToLower();

                    b.DocComment(method.Comment, true);
                    b.Line($"public {method.JsVariable} = this.$apiClient.$makeCaller(\"{transportTypeSlug}\", ");
                    b.Indented($"(c{signature}) => c.{method.JsVariable}(this.$primaryKey{string.Concat(method.ClientParameters.Select(p => ", " + p.Name))}))");
                }

                b.Line();
                using (b.Block($"constructor(initialData?: $models.{name})"))
                {
                    b.Line($"super({metadataName}, new $apiClients.{name}ApiClient(), initialData)");
                }
            }
            b.Line($"defineProps({viewModelName}, $metadata.{name})");
            b.Line();
        }
Пример #6
0
        public override void BuildOutput(TypeScriptCodeBuilder b)
        {
            using (b.Block($"module {ModuleName(ModuleKind.Service)}"))
            {
                using (b.Block($"export class {Model.ServiceClientClassName}"))
                {
                    b.Line();
                    b.Line($"public readonly apiController: string = \"/{Model.ApiRouteControllerPart}\";");

                    b.DocComment($"Configuration for all instances of {Model.ServiceClientClassName}. Can be overidden on each instance via instance.coalesceConfig.");
                    b.Line($"public static coalesceConfig = new Coalesce.ServiceClientConfiguration<{Model.ServiceClientClassName}>(Coalesce.GlobalConfiguration.serviceClient);");

                    b.DocComment($"Configuration for this {Model.ServiceClientClassName} instance.");
                    b.Line($"public coalesceConfig: Coalesce.ServiceClientConfiguration<{Model.ServiceClientClassName}>");
                    b.Indented($"= new Coalesce.ServiceClientConfiguration<{Model.ServiceClientClassName}>({Model.ServiceClientClassName}.coalesceConfig);");

                    b.Line();
                    foreach (var method in Model.ClientMethods)
                    {
                        WriteClientMethodDeclaration(b, method, Model.ServiceClientClassName);
                    }
                }
            }
        }
Пример #7
0
        private void WriteMethod_LoadFromDto(TypeScriptCodeBuilder b)
        {
            b.DocComment(new[] {
                "Load the ViewModel object from the DTO.",
                "@param data: The incoming data object to load.",
                "@param force: Will override the check against isLoading that is done to prevent recursion. False is default.",
                "@param allowCollectionDeletes: Set true when entire collections are loaded. True is the default. ",
                "In some cases only a partial collection is returned, set to false to only add/update collections.",
            });
            using (b.Block("public loadFromDto = (data: any, force: boolean = false, allowCollectionDeletes: boolean = true): void =>", ';'))
            {
                b.Line("if (!data || (!force && this.isLoading())) return;");
                b.Line("this.isLoading(true);");

                b.Line("// Set the ID ");
                b.Line($"this.myId = data.{Model.PrimaryKey.JsonName};");
                b.Line($"this.{Model.PrimaryKey.JsVariable}(data.{Model.PrimaryKey.JsonName});");

                b.Line("// Load the lists of other objects");
                foreach (PropertyViewModel prop in Model.ClientProperties.Where(p => p.Type.IsCollection))
                {
                    using (b.Block($"if (data.{prop.JsonName} != null)"))
                    {
                        if (prop.Object.PrimaryKey != null)
                        {
                            b.Line("// Merge the incoming array");
                            b.Line($"Coalesce.KnockoutUtilities.RebuildArray(this.{prop.JsVariable}, data.{prop.JsonName}, '{prop.Object.PrimaryKey.JsonName}', {prop.Object.ViewModelClassName}, this, allowCollectionDeletes);");
                            if (prop.IsManytoManyCollection)
                            {
                                b.Line("// Add many-to-many collection");
                                b.Line("let objs: any[] = [];");
                                using (b.Block($"$.each(data.{prop.JsonName}, (index, item) =>", ");"))
                                {
                                    b.Line($"if (item.{prop.ManyToManyCollectionProperty.JsonName}) {{");
                                    b.Indented($"objs.push(item.{prop.ManyToManyCollectionProperty.JsonName});");
                                    b.Line($"}}");
                                }
                                b.Line($"Coalesce.KnockoutUtilities.RebuildArray(this.{prop.ManyToManyCollectionName.ToCamelCase()}, objs, '{prop.ManyToManyCollectionProperty.ForeignKeyProperty.JsVariable}', {prop.ManyToManyCollectionProperty.Object.ViewModelClassName}, this, allowCollectionDeletes);");
                            }
                        }
                        else if (prop.PureType.IsPrimitive)
                        {
                            b.Line($"this.{prop.JsVariable}(data.{prop.JsVariable});");
                        }
                        else
                        {
                            b.Line($"Coalesce.KnockoutUtilities.RebuildArray(this.{prop.JsVariable}, data.{prop.JsonName}, null, {prop.Object.ViewModelClassName}, this, allowCollectionDeletes);");
                        }
                    }
                }

                // Objects are loaded first so that they are available when the IDs get loaded.
                // This handles the issue with populating select lists with correct data because we now have the object.
                foreach (PropertyViewModel prop in Model.ClientProperties.Where(p => p.IsPOCO))
                {
                    b.Line($"if (!data.{prop.JsonName}) {{ ");
                    using (b.Indented())
                    {
                        if (prop.ForeignKeyProperty != null)
                        {
                            // Prop is a reference navigation prop. The incoming foreign key doesn't match our existing foreign key, so clear out the property.
                            // If the incoming key and existing key DOES match, we assume that the data just wasn't loaded, but is still valid, so we do nothing.
                            b.Line($"if (data.{prop.ForeignKeyProperty.JsonName} != this.{prop.ForeignKeyProperty.JsVariable}()) {{");
                            b.Indented($"this.{prop.JsVariable}(null);");
                            b.Line("}");
                        }
                        else
                        {
                            b.Line($"this.{prop.JsVariable}(null);");
                        }
                    }
                    b.Line("} else {");
                    using (b.Indented())
                    {
                        b.Line($"if (!this.{prop.JsVariable}()){{");
                        b.Indented($"this.{prop.JsVariable}(new {prop.Object.ViewModelClassName}(data.{prop.JsonName}, this));");
                        b.Line("} else {");
                        b.Indented($"this.{prop.JsVariable}()!.loadFromDto(data.{prop.JsonName});");
                        b.Line("}");
                        if (prop.Object.IsDbMappedType)
                        {
                            b.Line($"if (this.parent instanceof {prop.Object.ViewModelClassName} && this.parent !== this.{prop.JsVariable}() && this.parent.{prop.Object.PrimaryKey.JsVariable}() == this.{prop.JsVariable}()!.{prop.Object.PrimaryKey.JsVariable}())");
                            b.Line("{");
                            b.Indented($"this.parent.loadFromDto(data.{prop.JsonName}, undefined, false);");
                            b.Line("}");
                        }
                    }
                    b.Line("}");
                }

                b.Line();
                b.Line("// The rest of the objects are loaded now.");
                foreach (PropertyViewModel prop in Model.ClientProperties.Where(p => p.Object == null && !p.IsPrimaryKey))
                {
                    if (prop.Type.IsDate)
                    {   // Using valueOf/getTime here is a 20x performance increase over moment.isSame(). moment(new Date(...)) is also a 10x perf increase.
                        b.Line($"if (data.{prop.JsonName} == null) this.{prop.JsVariable}(null);");
                        b.Line($"else if (this.{prop.JsVariable}() == null || this.{prop.JsVariable}()!.valueOf() != new Date(data.{prop.JsonName}).getTime()){{");
                        b.Indented($"this.{prop.JsVariable}(moment(new Date(data.{prop.JsonName})));");
                        b.Line("}");
                    }
                    else
                    {
                        b.Line($"this.{prop.JsVariable}(data.{prop.JsonName});");
                    }
                }

                b.Line("if (this.coalesceConfig.onLoadFromDto()){");
                b.Indented("this.coalesceConfig.onLoadFromDto()(this as any);");
                b.Line("}");
                b.Line("this.isLoading(false);");
                b.Line("this.isDirty(false);");
                b.Line("if (this.coalesceConfig.validateOnLoadFromDto()) this.validate();");
            }
        }
Пример #8
0
        private void WriteViewModelClass(TypeScriptCodeBuilder b)
        {
            using (b.Block($"export class {Model.ViewModelGeneratedClassName} extends Coalesce.BaseViewModel"))
            {
                b.Line($"public readonly modelName = \"{Model.ClientTypeName}\";");
                b.Line($"public readonly primaryKeyName = \"{Model.PrimaryKey.JsVariable}\";");
                b.Line($"public readonly modelDisplayName = \"{Model.DisplayName}\";");
                b.Line($"public readonly apiController = \"/{Model.ApiRouteControllerPart}\";");
                b.Line($"public readonly viewController = \"/{Model.ControllerName}\";");

                b.DocComment($"Configuration for all instances of {Model.ViewModelClassName}. Can be overidden on each instance via instance.coalesceConfig.");
                b.Line($"public static coalesceConfig: Coalesce.ViewModelConfiguration<{Model.ViewModelClassName}>");
                b.Indented($"= new Coalesce.ViewModelConfiguration<{Model.ViewModelClassName}>(Coalesce.GlobalConfiguration.viewModel);");

                b.DocComment($"Configuration for the current {Model.ViewModelClassName} instance.");
                b.Line("public coalesceConfig: Coalesce.ViewModelConfiguration<this>");
                b.Indented($"= new Coalesce.ViewModelConfiguration<{Model.ViewModelGeneratedClassName}>({Model.ViewModelClassName}.coalesceConfig);");

                b.DocComment("The namespace containing all possible values of this.dataSource.");
                b.Line($"public dataSources: typeof ListViewModels.{Model.ClientTypeName}DataSources = ListViewModels.{Model.ClientTypeName}DataSources;");

                b.Line();
                b.Line();
                foreach (PropertyViewModel prop in Model.ClientProperties)
                {
                    b.DocComment(prop.Comment);
                    b.Line($"public {prop.JsVariable}: {prop.Type.TsKnockoutType(true)} = {prop.Type.ObservableConstructorCall()};");
                    if (prop.Type.IsEnum)
                    {
                        b.DocComment($"Text value for enumeration {prop.Name}");
                        b.Line($"public {prop.JsTextPropertyName}: KnockoutComputed<string | null> = ko.pureComputed(() => {{");
                        b.Line($"    for(var i = 0; i < this.{prop.JsVariable}Values.length; i++){{");
                        b.Line($"        if (this.{prop.JsVariable}Values[i].id == this.{prop.JsVariable}()){{");
                        b.Line($"            return this.{prop.JsVariable}Values[i].value;");
                        b.Line("        }");
                        b.Line("    }");
                        b.Line("    return null;");
                        b.Line("});");
                    }
                    if (prop.IsManytoManyCollection)
                    {
                        if (prop.Comment.Length > 0)
                        {
                            b.DocComment($"Collection of related objects for many-to-many relationship {prop.ManyToManyCollectionName} via {prop.Name}");
                        }
                        b.Line($"public {prop.ManyToManyCollectionName.ToCamelCase()}: KnockoutObservableArray<ViewModels.{prop.ManyToManyCollectionProperty.Object.ViewModelClassName}> = ko.observableArray([]);");
                    }
                }

                b.Line();
                foreach (PropertyViewModel prop in Model.ClientProperties.Where(f => f.IsPOCO))
                {
                    b.DocComment($"Display text for {prop.Name}");
                    b.Line($"public {prop.JsTextPropertyName}: KnockoutComputed<string>;");
                }

                b.Line();
                foreach (PropertyViewModel prop in Model.ClientProperties.Where(f => f.Role == PropertyRole.CollectionNavigation && !f.IsManytoManyCollection))
                {
                    b.DocComment($"Add object to {prop.JsVariable}");
                    using (b.Block($"public addTo{prop.Name} = (autoSave?: boolean | null): {prop.Object.ViewModelClassName} =>", ';'))
                    {
                        b.Line($"var newItem = new {prop.Object.ViewModelClassName}();");
                        b.Line($"if (typeof(autoSave) == 'boolean'){{");
                        b.Line($"    newItem.coalesceConfig.autoSaveEnabled(autoSave);");
                        b.Line($"}}");
                        b.Line($"newItem.parent = this;");
                        b.Line($"newItem.parentCollection = this.{prop.JsVariable};");
                        b.Line($"newItem.isExpanded(true);");
                        if (prop.InverseIdProperty != null)
                        {
                            b.Line($"newItem.{prop.InverseIdProperty.JsVariable}(this.{Model.PrimaryKey.JsVariable}());");
                        }
                        else if (prop.Object.PropertyByName(Model.PrimaryKey.JsVariable) != null)
                        {
                            // TODO: why do we do this? Causes weird behavior simply because key names line up.
                            // If all primary keys are just named "Id", this will copy the PK of the parent to the PK of the child,
                            // which doesn't make sense.
                            b.Line($"newItem.{Model.PrimaryKey.JsVariable}(this.{Model.PrimaryKey.JsVariable}());");
                        }
                        b.Line($"this.{prop.JsVariable}.push(newItem);");
                        b.Line($"return newItem;");
                    }

                    b.DocComment($"ListViewModel for {prop.Name}. Allows for loading subsets of data.");
                    b.Line($"public {prop.JsVariable}List: (loadImmediate?: boolean) => {ListViewModelModuleName}.{prop.Object.ListViewModelClassName};");
                }

                b.Line();
                foreach (PropertyViewModel prop in Model.ClientProperties.Where(f => f.Role == PropertyRole.CollectionNavigation))
                {
                    b.DocComment($"Url for a table view of all members of collection {prop.Name} for the current object.");
                    b.Line($"public {prop.ListEditorUrlName()}: KnockoutComputed<string> = ko.computed(");
                    if (prop.ListEditorUrl() == null)
                    {
                        b.Indented($"() => \"Inverse property not set on {Model.ClientTypeName} for property {prop.Name}\",");
                    }
                    else
                    {
                        b.Indented($"() => this.coalesceConfig.baseViewUrl() + '/{prop.ListEditorUrl()}' + this.{Model.PrimaryKey.JsVariable}(),");
                    }
                    b.Indented("null, { deferEvaluation: true }");
                    b.Line(");");
                }

                b.Line();
                foreach (PropertyViewModel prop in Model.ClientProperties.Where(f => f.Role == PropertyRole.ReferenceNavigation))
                {
                    b.DocComment($"Pops up a stock editor for object {prop.JsVariable}");
                    b.Line($"public show{prop.Name}Editor: (callback?: any) => void;");
                }

                b.Line();
                foreach (PropertyViewModel prop in Model.ClientProperties.Where(f => f.Type.IsEnum))
                {
                    b.DocComment($"Array of all possible names & values of enum {prop.JsVariable}");
                    b.Line($"public {prop.JsVariable}Values: Coalesce.EnumValue[] = [ ");
                    foreach (var kvp in prop.Type.EnumValues)
                    {
                        b.Indented($"{{ id: {kvp.Key}, value: '{kvp.Value.ToProperCase()}' }},");
                    }
                    b.Line("];");
                }

                b.Line();
                foreach (var method in Model.ClientMethods.Where(m => !m.IsStatic || m.ResultType.EqualsType(Model.Type)))
                {
                    WriteClientMethodDeclaration(b, method, Model.ViewModelGeneratedClassName, true, true);
                }

                WriteMethod_LoadFromDto(b);

                WriteMethod_SaveToDto(b);

                b.DocComment(new[]
                {
                    "Loads any child objects that have an ID set, but not the full object.",
                    "This is useful when creating an object that has a parent object and the ID is set on the new child."
                });
                using (b.Block("public loadChildren = (callback?: () => void): void =>", ';'))
                {
                    b.Line("var loadingCount = 0;");
                    // AES 4/14/18 - unsure of the reason for the !IsReadOnly check here. Perhaps just redundant?
                    foreach (PropertyViewModel prop in Model.ClientProperties.Where(f => f.Role == PropertyRole.ReferenceNavigation && !f.IsReadOnly))
                    {
                        b.Line($"// See if this.{prop.JsVariable} needs to be loaded.");
                        using (b.Block($"if (this.{prop.JsVariable}() == null && this.{prop.ForeignKeyProperty.JsVariable}() != null)"))
                        {
                            b.Line("loadingCount++;");
                            b.Line($"var {prop.JsVariable}Obj = new {prop.Object.ViewModelClassName}();");
                            b.Line($"{prop.JsVariable}Obj.load(this.{prop.ForeignKeyProperty.JsVariable}(), () => {{");
                            b.Indented("loadingCount--;");
                            b.Indented($"this.{prop.JsVariable}({prop.JsVariable}Obj);");
                            b.Indented("if (loadingCount == 0 && typeof(callback) == \"function\") { callback(); }");
                            b.Line("});");
                        }
                    }
                    b.Line("if (loadingCount == 0 && typeof(callback) == \"function\") { callback(); }");
                }


                b.Line();
                using (b.Block("public setupValidation(): void"))
                {
                    b.Line("if (this.errors !== null) return;");

                    var validatableProps = Model.ClientProperties.Where(p => !string.IsNullOrWhiteSpace(p.ClientValidationKnockoutJs()));

                    b.Line("this.errors = ko.validation.group([");
                    foreach (PropertyViewModel prop in validatableProps.Where(p => !p.ClientValidationAllowSave))
                    {
                        b.Indented($"this.{prop.JsVariable}.extend({{ {prop.ClientValidationKnockoutJs()} }}),");
                    }
                    b.Line("]);");

                    b.Line("this.warnings = ko.validation.group([");
                    foreach (PropertyViewModel prop in validatableProps.Where(p => p.ClientValidationAllowSave))
                    {
                        b.Indented($"this.{prop.JsVariable}.extend({{ {prop.ClientValidationKnockoutJs()} }}),");
                    }
                    b.Line("]);");
                }



                b.Line();
                using (b.Block($"constructor(newItem?: object, parent?: Coalesce.BaseViewModel | {ListViewModelModuleName}.{Model.ListViewModelClassName})"))
                {
                    b.Line("super(parent);");
                    b.Line("this.baseInitialize();");
                    b.Line("const self = this;");

                    // Create computeds for display for objects
                    b.Line();
                    foreach (PropertyViewModel prop in Model.ClientProperties.Where(f => f.IsPOCO))
                    {
                        // If the object exists, use the text value. Otherwise show 'None'
                        using (b.Block($"this.{prop.JsTextPropertyName} = ko.pureComputed(function()", ");"))
                        {
                            b.Line($"if (self.{prop.JsVariable}() && self.{prop.JsVariable}()!.{prop.Object.ListTextProperty.JsVariable}()) {{");
                            b.Indented($"return self.{prop.JsVariable}()!.{prop.Object.ListTextProperty.JsVariable}()!.toString();");
                            b.Line("} else {");
                            b.Indented("return \"None\";");
                            b.Line("}");
                        }
                    }

                    b.Line();
                    b.Line();
                    foreach (PropertyViewModel prop in Model.ClientProperties.Where(f => f.Role == PropertyRole.CollectionNavigation && !f.IsManytoManyCollection))
                    {
                        b.Line($"// List Object model for {prop.Name}. Allows for loading subsets of data.");
                        var childListVar = $"_{prop.JsVariable}List";
                        b.Line($"var {childListVar}: {ListViewModelModuleName}.{prop.Object.ListViewModelClassName};");

                        using (b.Block($"this.{prop.JsVariable}List = function(loadImmediate = true)"))
                        {
                            using (b.Block($"if (!{childListVar})"))
                            {
                                b.Line($"{childListVar} = new {ListViewModelModuleName}.{prop.Object.ListViewModelClassName}();");
                                b.Line($"if (loadImmediate) load{prop.Name}List();");
                                b.Line($"self.{prop.Parent.PrimaryKey.JsVariable}.subscribe(load{prop.Name}List)");
                            }
                            b.Line($"return {childListVar};");
                        }

                        using (b.Block($"function load{prop.Name}List()"))
                        {
                            using (b.Block($"if (self.{prop.Parent.PrimaryKey.JsVariable}())"))
                            {
                                if (prop.InverseIdProperty != null)
                                {
                                    b.Line($"{childListVar}.queryString = \"filter.{prop.InverseIdProperty.Name}=\" + self.{prop.Parent.PrimaryKey.JsVariable}();");
                                }
                                else
                                {
                                    b.Line($"{childListVar}.queryString = \"filter.{Model.PrimaryKey.Name}=\" + self.{prop.Parent.PrimaryKey.JsVariable}();");
                                }
                                b.Line($"{childListVar}.load();");
                            }
                        }
                    }

                    b.Line();
                    b.Line();
                    foreach (PropertyViewModel prop in Model.ClientProperties.Where(f => f.Role == PropertyRole.ReferenceNavigation))
                    {
                        using (b.Block($"this.show{prop.Name}Editor = function(callback: any)", ';'))
                        {
                            b.Line($"if (!self.{prop.JsVariable}()) {{");
                            b.Indented($"self.{prop.JsVariable}(new {prop.Object.ViewModelClassName}());");
                            b.Line("}");
                            b.Line($"self.{prop.JsVariable}()!.showEditor(callback)");
                        }
                    }

                    // Register autosave subscriptions on all autosavable properties.
                    // Must be done after everything else in the ctor.
                    b.Line();
                    foreach (PropertyViewModel prop in Model.ClientProperties.Where(p => p.IsClientWritable && !p.Type.IsCollection))
                    {
                        b.Line($"self.{prop.JsVariable}.subscribe(self.autoSave);");
                    }

                    foreach (PropertyViewModel prop in Model.ClientProperties.Where(p => p.IsClientWritable && p.IsManytoManyCollection))
                    {
                        b.Line();
                        b.Line($"self.{prop.ManyToManyCollectionName.ToCamelCase()}.subscribe<KnockoutArrayChange<{prop.ManyToManyCollectionProperty.Object.ViewModelClassName}>[]>(changes => {{");
                        using (b.Indented())
                        {
                            using (b.Block("for (var i in changes)"))
                            {
                                b.Line("var change = changes[i];");
                                b.Line("self.autoSaveCollection(");
                                b.Indented("change.status, ");
                                b.Indented($"this.{prop.JsVariable}, ");
                                b.Indented($"{prop.Object.ViewModelClassName}, ");
                                b.Indented($"'{prop.Object.ClientProperties.First(f => f.Type.EqualsType(Model.Type)).ForeignKeyProperty.JsVariable}',");
                                b.Indented($"'{prop.ManyToManyCollectionProperty.ForeignKeyProperty.JsVariable}',");
                                b.Indented($"change.value.{prop.ManyToManyCollectionProperty.Object.PrimaryKey.JsVariable}()");
                                b.Line(");");
                            }
                        }
                        b.Line("}, null, \"arrayChange\");");
                    }

                    b.Line();
                    b.Line("if (newItem) {");
                    b.Indented("self.loadFromDto(newItem, true);");
                    b.Line("}");
                }
            }
        }
Пример #9
0
        /// <summary>
        /// Writes the class for invoking the given client method,
        /// as well as the property for the default instance of this class.
        /// </summary>
        /// <param name="b"></param>
        /// <param name="method"></param>
        /// <param name="parentClassName"></param>
        /// <param name="methodLoadsParent">
        /// If true, code emitted for methods that load their parents' type should
        /// load the method's parent with the results of the method when called.
        /// </param>
        /// <param name="parentLoadHasIdParameter">
        /// If true, calls to this.parent.load() will pass null as the first arg and a callback as the second.
        /// </param>
        public void WriteClientMethodDeclaration(
            TypeScriptCodeBuilder b, MethodViewModel method, string parentClassName,
            bool methodLoadsParent = false, bool parentLoadHasIdParameter = false
            )
        {
            var model              = this.Model;
            var isService          = method.Parent.IsService;
            var returnIsListResult = method.ReturnsListResult;

            string reloadArg              = !isService ? ", reload" : "";
            string reloadParam            = !isService ? ", reload: boolean = true" : "";
            string callbackAndReloadParam = $"callback?: (result: {method.ResultType.TsType}) => void{reloadParam}";

            // Default instance of the method class.
            b.Line("/**");
            b.Indented($"Methods and properties for invoking server method {method.Name}.");
            if (method.Comment.Length > 0)
            {
                b.Indented(method.Comment);
            }
            b.Line($"*/");

            b.Line($"public readonly {method.JsVariable} = new {parentClassName}.{method.Name}(this);");

            string methodBaseClass = returnIsListResult
                ? "ClientListMethod"
                : "ClientMethod";

            // Not wrapping this in a using since it is used by nearly this entire method. Will manually dispose.
            var classBlock = b.Block(
                $"public static {method.Name} = class {method.Name} extends Coalesce.{methodBaseClass}<{parentClassName}, {method.ResultType.TsType}>", ';');

            b.Line($"public readonly name = '{method.Name}';");
            b.Line($"public readonly verb = '{method.ApiActionHttpMethodName}';");

            if (method.ResultType.IsCollection || method.ResultType.IsArray)
            {
                b.Line($"public result: {method.ResultType.TsKnockoutType()} = {method.ResultType.ObservableConstructorCall()};");
            }

            // ----------------------
            // Standard invoke method - all CS method parameters as TS method parameters.
            // ----------------------
            b.Line();
            b.Line($"/** Calls server method ({method.Name}) with the given arguments */");

            string parameters = "";

            parameters = string.Join(", ", method.ClientParameters.Select(f => f.Type.TsDeclarationPlain(f.Name) + " | null"));
            if (!string.IsNullOrWhiteSpace(parameters))
            {
                parameters = parameters + ", ";
            }
            parameters = parameters + callbackAndReloadParam;

            using (b.Block($"public invoke = ({parameters}): JQueryPromise<any> =>", ';'))
            {
                string jsPostObject = "{ ";
                if (method.IsModelInstanceMethod)
                {
                    jsPostObject = jsPostObject + "id: this.parent[this.parent.primaryKeyName]()";
                    if (method.Parameters.Any())
                    {
                        jsPostObject = jsPostObject + ", ";
                    }
                }

                string TsConversion(ParameterViewModel param)
                {
                    string argument = param.JsVariable;

                    if (param.Type.HasClassViewModel)
                    {
                        return($"{argument} ? {argument}.saveToDto() : null");
                    }
                    if (param.Type.IsDate)
                    {
                        return($"{argument} ? {argument}.format() : null");
                    }
                    return(argument);
                }

                jsPostObject += string.Join(", ", method.ClientParameters.Select(f => $"{f.JsVariable}: {TsConversion(f)}"));
                jsPostObject += " }";

                b.Line($"return this.invokeWithData({jsPostObject}, callback{reloadArg});");
            }


            // ----------------------
            // Members for methods with parameters only.
            if (method.ClientParameters.Any())
            {
                b.Line();


                // ----------------------
                // Args class, and default instance
                b.Line($"/** Object that can be easily bound to fields to allow data entry for the method's parameters */");
                b.Line($"public args = new {method.Name}.Args(); ");

                using (b.Block("public static Args = class Args", ';'))
                {
                    foreach (var arg in method.ClientParameters)
                    {
                        b.Line($@"public {arg.JsVariable}: {arg.Type.TsKnockoutType(true)} = {arg.Type.ObservableConstructorCall()};");
                    }
                }



                // Gets the js arguments to pass to a call to this.invoke(...)
                string JsArguments(string obj = "")
                {
                    string result;

                    if (obj != "")
                    {
                        result = string.Join(", ", method.ClientParameters.Select(f => $"{obj}.{f.JsVariable}()"));
                    }
                    else
                    {
                        result = string.Join(", ", method.ClientParameters.Select(f => obj + f.JsVariable));
                    }

                    if (!string.IsNullOrEmpty(result))
                    {
                        result = result + ", ";
                    }
                    result = result + "callback";

                    return(result);
                }

                // ----------------------
                // invokeWithArgs method
                // ----------------------
                b.Line();
                b.Line($"/** Calls server method ({method.Name}) with an instance of {method.Name}.Args, or the value of this.args if not specified. */");
                // We can't explicitly declare the type of the args parameter here - TypeScript doesn't allow it.
                // Thankfully, we can implicitly type using the default.
                using (b.Block($"public invokeWithArgs = (args = this.args, {callbackAndReloadParam}): JQueryPromise<any> =>"))
                {
                    b.Line($"return this.invoke({JsArguments("args")}{reloadArg});");
                }


                // ----------------------
                // invokeWithPrompts method
                // ----------------------
                b.Line();
                b.Line("/** Invokes the method after displaying a browser-native prompt for each argument. */");
                using (b.Block($"public invokeWithPrompts = ({callbackAndReloadParam}): JQueryPromise<any> | undefined =>", ';'))
                {
                    b.Line($"var $promptVal: string | null = null;");
                    foreach (var param in method.ClientParameters.Where(f => f.ConvertsFromJsString))
                    {
                        b.Line($"$promptVal = {$"prompt('{param.Name.ToProperCase()}')"};");
                        // AES 1/23/2018 - why do we do this? what about optional params where no value is desired?
                        b.Line($"if ($promptVal === null) return;");
                        b.Line($"var {param.Name}: {param.Type.TsType} = {param.Type.TsConvertFromString("$promptVal")};");
                    }

                    // For all parameters that can't convert from a string (is this even possible with what we support for method parameters now?),
                    // pass null as the value. I guess we just let happen what's going to happen? Again, not sure when this case would ever be hit.
                    foreach (var param in method.ClientParameters.Where(f => !f.ConvertsFromJsString))
                    {
                        b.Line($"var {param.Name}: null = null;");
                    }
                    b.Line($"return this.invoke({JsArguments("")}{reloadArg});");
                }
            }



            // ----------------------
            // Method response handler - highly dependent on what the response type actually is.
            // ----------------------
            b.Line();
            using (b.Block($"protected loadResponse = (data: Coalesce.{(returnIsListResult ? "List" : "Item")}Result, {callbackAndReloadParam}) =>", ';'))
            {
                var incomingMainData = returnIsListResult
                    ? "data.list || []"
                    : "data.object";

                if (method.ResultType.IsCollection && method.ResultType.PureType.HasClassViewModel)
                {
                    // Collection of objects that have TypeScript ViewModels. This could be an entity or an external type.

                    // If the models have a key, rebuild our collection using that key so that we can reuse objects when the data matches.
                    var keyNameArg = method.ResultType.PureType.ClassViewModel.PrimaryKey != null
                        ? $"'{method.ResultType.PureType.ClassViewModel.PrimaryKey.JsVariable}'"
                        : "null";

                    b.Line($"Coalesce.KnockoutUtilities.RebuildArray(this.result, {incomingMainData}, {keyNameArg}, ViewModels.{method.ResultType.PureType.ClassViewModel.ClientTypeName}, this, true);");
                }
                else if (method.ResultType.HasClassViewModel)
                {
                    // Single view model return type.

                    b.Line("if (!this.result()) {");
                    b.Indented($"this.result(new ViewModels.{method.ResultType.PureType.ClassViewModel.ClientTypeName}({incomingMainData}));");
                    b.Line("} else {");
                    b.Indented($"this.result().loadFromDto({incomingMainData});");
                    b.Line("}");
                }
                else
                {
                    // Uninteresting return type. Either void, a primitive, or a collection of primitives.
                    // In any case, regardless of the type of the 'result' observable, this is how we set the results.
                    b.Line($"this.result({incomingMainData});");
                }

                if (isService)
                {
                    b.Line("if (typeof(callback) == 'function')");
                    b.Indented("callback(this.result());");
                }
                else if (method.ResultType.EqualsType(method.Parent.Type) && methodLoadsParent)
                {
                    // The return type is the type of the method's parent. Load the parent with the results of the method.
                    // Parameter 'reload' has no meaning here, since we're reloading the object with the result of the method.
                    b.Line($"this.parent.loadFromDto({incomingMainData}, true)");
                    using (b.Block("if (typeof(callback) == 'function')"))
                    {
                        b.Line($"callback(this.result());");
                    }
                }
                else
                {
                    // We're not loading the results into the method's parent. This is by far the more common case.
                    b.Line("if (reload) {");
                    // If reload is true, invoke the load function on the method's parent to reload its latest state from the server.
                    // Only after that's done do we call the method-completion callback.
                    // Store the result locally in case it somehow gets changed by us reloading the parent.
                    b.Indented("var result = this.result();");
                    b.Indented($"this.parent.load({(parentLoadHasIdParameter ? "null, " : "")}typeof(callback) == 'function' ? () => callback(result) : undefined);");
                    b.Line("} else if (typeof(callback) == 'function') {");
                    // If we're not reloading, just call the callback now.
                    b.Indented("callback(this.result());");
                    b.Line("}");
                }
            }


            // End of the method class declaration.
            classBlock.Dispose();
        }
Пример #10
0
        private void WriteMethod_LoadFromDto(TypeScriptCodeBuilder b)
        {
            b.DocComment(new[] {
                "Load the object from the DTO.",
                "@param data: The incoming data object to load.",
            });
            using (b.Block("public loadFromDto = (data: any) =>", ';'))
            {
                b.Line("if (!data) return;");

                if (Model.PrimaryKey != null)
                {
                    b.Line("// Set the ID");
                    b.Line($"this.myId = data.{Model.PrimaryKey.JsonName};");
                }

                b.Line();
                b.Line("// Load the properties.");
                foreach (PropertyViewModel prop in Model.ClientProperties)
                {
                    // DB-mapped viewmodels can't have an external type as a parent.
                    // There's no strong reason for this restriction other than that
                    // the code just doesn't support it at the moment.
                    var parentVar = prop.PureTypeOnContext ? "undefined" : "this";

                    if (prop.Type.IsCollection && prop.Object != null)
                    {
                        b.Line($"if (data.{prop.JsonName} != null) {{");
                        b.Line("// Merge the incoming array");
                        if (prop.Object.PrimaryKey != null)
                        {
                            b.Indented($"Coalesce.KnockoutUtilities.RebuildArray(this.{prop.JsVariable}, data.{prop.JsonName}, \'{prop.Object.PrimaryKey.JsVariable}\', ViewModels.{prop.Object.ViewModelClassName}, {parentVar}, true);");
                        }
                        else
                        {
                            b.Indented($"Coalesce.KnockoutUtilities.RebuildArray(this.{prop.JsVariable}, data.{prop.JsonName}, null, ViewModels.{prop.Object.ViewModelClassName}, {parentVar}, true);");
                        }
                        b.Line("}");
                    }
                    else if (prop.Type.IsDate)
                    {
                        b.Line($"if (data.{prop.JsVariable} == null) this.{prop.JsVariable}(null);");
                        b.Line($"else if (this.{prop.JsVariable}() == null || !this.{prop.JsVariable}()!.isSame(moment(data.{prop.JsVariable}))) {{");
                        b.Indented($"this.{prop.JsVariable}(moment(data.{prop.JsVariable}));");
                        b.Line("}");
                    }
                    else if (prop.IsPOCO)
                    {
                        b.Line($"if (!this.{prop.JsVariable}()){{");
                        b.Indented($"this.{prop.JsVariable}(new {prop.Object.ViewModelClassName}(data.{prop.JsonName}, {parentVar}));");
                        b.Line("} else {");
                        b.Indented($"this.{prop.JsVariable}()!.loadFromDto(data.{prop.JsonName});");
                        b.Line("}");
                    }
                    else
                    {
                        b.Line($"this.{prop.JsVariable}(data.{prop.JsVariable});");
                    }
                }
                b.Line();
            }
        }
Пример #11
0
        public override Task <string> BuildOutputAsync()
        {
            var b = new TypeScriptCodeBuilder(indentSize: 2);

            b.Line("import * as metadata from './metadata.g'");
            b.Line("import { Model, DataSource, convertToModel, mapToModel } from 'coalesce-vue/lib/model'");
            //   b.Line("import { Domain, getEnumMeta, ModelType, ExternalType } from './coalesce/core/metadata' ");
            b.Line();

            foreach (var model in Model.ClientEnums.OrderBy(e => e.Name))
            {
                using (b.Block($"export enum {model.Name}"))
                {
                    foreach (var value in model.EnumValues)
                    {
                        b.Line($"{value.Value} = {value.Key},");
                    }
                }

                b.Line();
                b.Line();
            }

            foreach (var model in Model.ClientClasses
                     .OrderByDescending(c => c.IsDbMappedType) // Entity types first
                     .ThenBy(e => e.ClientTypeName))
            {
                var name = model.ViewModelClassName;

                using (b.Block($"export interface {name} extends Model<typeof metadata.{name}>"))
                {
                    foreach (var prop in model.ClientProperties)
                    {
                        b.DocComment(prop.Comment);
                        var typeString = new VueType(prop.Type).TsType();
                        b.Line($"{prop.JsVariable}: {typeString} | null");
                    }
                }

                using (b.Block($"export class {name}"))
                {
                    b.DocComment($"Mutates the input object and its descendents into a valid {name} implementation.");
                    using (b.Block($"static convert(data?: Partial<{name}>): {name}"))
                    {
                        b.Line($"return convertToModel(data || {{}}, metadata.{name}) ");
                    }

                    b.DocComment($"Maps the input object and its descendents to a new, valid {name} implementation.");
                    using (b.Block($"static map(data?: Partial<{name}>): {name}"))
                    {
                        b.Line($"return mapToModel(data || {{}}, metadata.{name}) ");
                    }

                    b.DocComment($"Instantiate a new {name}, optionally basing it on the given data.");
                    using (b.Block($"constructor(data?: Partial<{name}> | {{[k: string]: any}})"))
                    {
                        b.Indented($"Object.assign(this, {name}.map(data || {{}}));");
                    }
                }

                var dataSources = model.ClientDataSources(Model);
                if (model.IsDbMappedType && dataSources.Any())
                {
                    using (b.Block($"export namespace {name}"))
                    {
                        using (b.Block("export namespace DataSources"))
                        {
                            foreach (var source in dataSources)
                            {
                                b.DocComment(source.Comment, true);
                                var sourceMeta = $"metadata.{name}.dataSources.{source.ClientTypeName.ToCamelCase()}";

                                using (b.Block($"export class {source.ClientTypeName} implements DataSource<typeof {sourceMeta}>"))
                                {
                                    b.Line($"readonly $metadata = {sourceMeta}");
                                    foreach (var param in source.DataSourceParameters)
                                    {
                                        b.DocComment(param.Comment);
                                        var typeString = new VueType(param.Type).TsType();
                                        b.Line($"{param.JsVariable}: {typeString} | null = null");
                                    }
                                }
                            }
                        }
                    }
                }

                b.Line();
                b.Line();
            }

            return(Task.FromResult(b.ToString()));
        }