From 68ea315fc1f7254b7364bfe40947ceb33046aa78 Mon Sep 17 00:00:00 2001 From: Alex Van Dyke Date: Thu, 7 Dec 2017 09:27:20 -0600 Subject: [PATCH 1/2] Adding the option to use Google Maps for the geolocation map. --- .../ContentValidationExtensions.cs | 9 +- .../ValidateContent/ContentValidator.cs | 9 +- .../ValidateContent/FieldExtensions.cs | 15 +- .../ValidateContent/JsonValueConverter.cs | 58 +- .../Contents/ContentCommandMiddleware.cs | 10 +- .../Contents/ContentOperationContext.cs | 31 +- .../Geocoding/GoogleMapsGeocoder.cs | 57 ++ .../Geocoding/IGeocoder.cs | 14 + .../Geocoding/OSMGeocoder.cs | 19 + .../Api/Controllers/Apps/AppsController.cs | 10 +- .../Controllers/Apps/Models/AppCreatedDto.cs | 5 + .../Api/Controllers/Apps/Models/AppDto.cs | 5 + .../Config/Domain/InfrastructureServices.cs | 20 + .../angular/geolocation-editor.component.html | 19 - .../angular/geolocation-editor.component.scss | 14 - .../angular/geolocation-editor.component.ts | 217 -------- src/Squidex/app/framework/declarations.ts | 1 - src/Squidex/app/framework/module.ts | 3 - .../geolocation-editor.component.html | 47 ++ .../geolocation-editor.component.scss | 68 +++ .../geolocation-editor.component.ts | 496 ++++++++++++++++++ src/Squidex/app/shared/declarations.ts | 1 + src/Squidex/app/shared/module.ts | 3 + .../services/apps-store.service.spec.ts | 6 +- .../app/shared/services/apps.service.spec.ts | 15 +- .../app/shared/services/apps.service.ts | 8 +- src/Squidex/appsettings.json | 10 + .../ValidateContent/ContentValidationTests.cs | 33 +- .../ValidateContent/DateTimeFieldTests.cs | 2 +- .../ValidateContent/GeolocationFieldTests.cs | 38 +- .../ValidationTestExtensions.cs | 9 +- .../Contents/ContentCommandMiddlewareTests.cs | 56 +- 32 files changed, 978 insertions(+), 330 deletions(-) create mode 100644 src/Squidex.Infrastructure/Geocoding/GoogleMapsGeocoder.cs create mode 100644 src/Squidex.Infrastructure/Geocoding/IGeocoder.cs create mode 100644 src/Squidex.Infrastructure/Geocoding/OSMGeocoder.cs delete mode 100644 src/Squidex/app/framework/angular/geolocation-editor.component.html delete mode 100644 src/Squidex/app/framework/angular/geolocation-editor.component.scss delete mode 100644 src/Squidex/app/framework/angular/geolocation-editor.component.ts create mode 100644 src/Squidex/app/shared/components/geolocation-editor.component.html create mode 100644 src/Squidex/app/shared/components/geolocation-editor.component.scss create mode 100644 src/Squidex/app/shared/components/geolocation-editor.component.ts diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs index e08089efc..4f20ad771 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidationExtensions.cs @@ -11,14 +11,15 @@ using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; +using Squidex.Infrastructure.Geocoding; namespace Squidex.Domain.Apps.Core.ValidateContent { public static class ContentValidationExtensions { - public static async Task ValidateAsync(this NamedContentData data, ValidationContext context, Schema schema, PartitionResolver partitionResolver, IList errors) + public static async Task ValidateAsync(this NamedContentData data, ValidationContext context, Schema schema, PartitionResolver partitionResolver, IList errors, IGeocoder geocoder) { - var validator = new ContentValidator(schema, partitionResolver, context); + var validator = new ContentValidator(schema, partitionResolver, context, geocoder); await validator.ValidateAsync(data); @@ -28,9 +29,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent } } - public static async Task ValidatePartialAsync(this NamedContentData data, ValidationContext context, Schema schema, PartitionResolver partitionResolver, IList errors) + public static async Task ValidatePartialAsync(this NamedContentData data, ValidationContext context, Schema schema, PartitionResolver partitionResolver, IList errors, IGeocoder geocoder) { - var validator = new ContentValidator(schema, partitionResolver, context); + var validator = new ContentValidator(schema, partitionResolver, context, geocoder); await validator.ValidatePartialAsync(data); diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs index d9470234f..76d7ddb47 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs @@ -13,6 +13,7 @@ using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; +using Squidex.Infrastructure.Geocoding; #pragma warning disable 168 @@ -22,6 +23,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { private readonly Schema schema; private readonly PartitionResolver partitionResolver; + private readonly IGeocoder geocoder; private readonly ValidationContext context; private readonly ConcurrentBag errors = new ConcurrentBag(); @@ -30,10 +32,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent get { return errors; } } - public ContentValidator(Schema schema, PartitionResolver partitionResolver, ValidationContext context) + public ContentValidator(Schema schema, PartitionResolver partitionResolver, ValidationContext context, IGeocoder geocoder) { Guard.NotNull(schema, nameof(schema)); Guard.NotNull(partitionResolver, nameof(partitionResolver)); + Guard.NotNull(geocoder, nameof(geocoder)); this.schema = schema; this.context = context; @@ -74,7 +77,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { if (partition.TryGetItem(partitionValues.Key, out var item)) { - tasks.Add(field.ValidateAsync(partitionValues.Value, context.Optional(item.IsOptional), m => errors.AddError(m, field, item))); + tasks.Add(field.ValidateAsync(partitionValues.Value, context.Optional(item.IsOptional), m => errors.AddError(m, field, item), geocoder)); } else { @@ -133,7 +136,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { var value = fieldData.GetOrCreate(item.Key, k => JValue.CreateNull()); - tasks.Add(field.ValidateAsync(value, context.Optional(item.IsOptional), m => errors.AddError(m, field, item))); + tasks.Add(field.ValidateAsync(value, context.Optional(item.IsOptional), m => errors.AddError(m, field, item), geocoder)); } return Task.WhenAll(tasks); diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldExtensions.cs index b56bf1908..c2fc2e0eb 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldExtensions.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldExtensions.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; +using Squidex.Infrastructure.Geocoding; using Squidex.Infrastructure.Json; namespace Squidex.Domain.Apps.Core.ValidateContent @@ -38,20 +39,26 @@ namespace Squidex.Domain.Apps.Core.ValidateContent errors.Add(new ValidationError(message.Replace("", displayName), fieldName)); } - public static async Task ValidateAsync(this Field field, JToken value, ValidationContext context, Action addError) + public static async Task ValidateAsync(this Field field, JToken value, ValidationContext context, Action addError, IGeocoder geocoder) { try { - var typedValue = value.IsNull() ? null : JsonValueConverter.ConvertValue(field, value); + var typedValue = value.IsNull() ? null : JsonValueConverter.ConvertValue(field, value, geocoder); foreach (var validator in ValidatorsFactory.CreateValidators(field)) { await validator.ValidateAsync(typedValue, context, addError); } } - catch + catch (InvalidCastException ex) { - addError(" is not a valid value."); + var error = ex.Message; + addError($" is not a valid value. {error}".Trim()); + } + catch (Exception ex) + { + var error = (ex as AggregateException)?.InnerException?.Message; + addError($" is not a valid value. {error ?? string.Empty}".Trim()); } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs index 00f4f3e9d..e5dcc8575 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs @@ -8,25 +8,32 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; using Newtonsoft.Json.Linq; +using NJsonSchema.Infrastructure; using NodaTime.Text; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; +using Squidex.Infrastructure.Geocoding; namespace Squidex.Domain.Apps.Core.ValidateContent { public sealed class JsonValueConverter : IFieldVisitor { + private IGeocoder geocoder; + public JToken Value { get; } - private JsonValueConverter(JToken value) + private JsonValueConverter(JToken value, IGeocoder geocoder) { this.Value = value; + this.geocoder = geocoder; } - public static object ConvertValue(Field field, JToken json) + public static object ConvertValue(Field field, JToken json, IGeocoder geocoder) { - return field.Accept(new JsonValueConverter(json)); + return field.Accept(new JsonValueConverter(json, geocoder)); } public object Visit(AssetsField field) @@ -59,30 +66,59 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public object Visit(GeolocationField field) { var geolocation = (JObject)Value; + List addressString = new List(); + var validProperties = new string[] + { + "latitude", "longitude", "address1", "address2", "city", "state", "zip" + }; foreach (var property in geolocation.Properties()) { - if (!string.Equals(property.Name, "latitude", StringComparison.OrdinalIgnoreCase) && - !string.Equals(property.Name, "longitude", StringComparison.OrdinalIgnoreCase)) + if (!validProperties.Contains(property.Name.ToLower())) { - throw new InvalidCastException("Geolocation can only have latitude and longitude property."); + throw new InvalidCastException("Geolocation must have proper properties."); } + + addressString.Add(geolocation[property.Name.ToLower()]?.ToString()); } - var lat = (double)geolocation["latitude"]; - var lon = (double)geolocation["longitude"]; + var lat = geolocation["latitude"]; + var lon = geolocation["longitude"]; + var state = geolocation["state"]?.ToString(); + var zip = geolocation["zip"]?.ToString(); + + if (lat == null || lon == null || + ((JValue)lat).Value == null || ((JValue)lon).Value == null) + { + var response = geocoder.GeocodeAddress(string.Join(string.Empty, addressString)); + lat = response.TryGetPropertyValue("Latitude", (JToken)null); + lon = response.TryGetPropertyValue("Longitude", (JToken)null); + + geolocation["latitude"] = lat; + geolocation["longitude"] = lon; + } - if (!lat.IsBetween(-90, 90)) + if (!((double)lat).IsBetween(-90, 90)) { throw new InvalidCastException("Latitude must be between -90 and 90."); } - if (!lon.IsBetween(-180, 180)) + if (!((double)lon).IsBetween(-180, 180)) { throw new InvalidCastException("Longitude must be between -180 and 180."); } - return Value; + if (!string.IsNullOrWhiteSpace(state) && !Regex.IsMatch(state, "[A-Z]{2}")) + { + throw new InvalidCastException("State must be two capital letters."); + } + + if (!string.IsNullOrWhiteSpace(zip) && !Regex.IsMatch(zip, "[0-9]{5}(\\-[0-9]{4})?")) + { + throw new InvalidCastException("ZIP Code must match postal code with optional suffix pattern."); + } + + return geolocation; } public object Visit(JsonField field) diff --git a/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs b/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs index 5c111c5bd..a063f0dc8 100644 --- a/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Write/Contents/ContentCommandMiddleware.cs @@ -17,6 +17,7 @@ using Squidex.Domain.Apps.Write.Contents.Guards; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.Geocoding; namespace Squidex.Domain.Apps.Write.Contents { @@ -27,25 +28,29 @@ namespace Squidex.Domain.Apps.Write.Contents private readonly IAssetRepository assetRepository; private readonly IContentRepository contentRepository; private readonly IScriptEngine scriptEngine; + private readonly IGeocoder geocoder; public ContentCommandMiddleware( IAggregateHandler handler, IAppProvider appProvider, IAssetRepository assetRepository, IScriptEngine scriptEngine, - IContentRepository contentRepository) + IContentRepository contentRepository, + IGeocoder geocoder) { Guard.NotNull(handler, nameof(handler)); Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(scriptEngine, nameof(scriptEngine)); Guard.NotNull(assetRepository, nameof(assetRepository)); Guard.NotNull(contentRepository, nameof(contentRepository)); + Guard.NotNull(geocoder, nameof(geocoder)); this.handler = handler; this.appProvider = appProvider; this.scriptEngine = scriptEngine; this.assetRepository = assetRepository; this.contentRepository = contentRepository; + this.geocoder = geocoder; } protected async Task On(CreateContent command, CommandContext context) @@ -151,7 +156,8 @@ namespace Squidex.Domain.Apps.Write.Contents appProvider, assetRepository, scriptEngine, - message); + message, + geocoder); return operationContext; } diff --git a/src/Squidex.Domain.Apps.Write/Contents/ContentOperationContext.cs b/src/Squidex.Domain.Apps.Write/Contents/ContentOperationContext.cs index 530c7505e..9689dbfca 100644 --- a/src/Squidex.Domain.Apps.Write/Contents/ContentOperationContext.cs +++ b/src/Squidex.Domain.Apps.Write/Contents/ContentOperationContext.cs @@ -20,6 +20,7 @@ using Squidex.Domain.Apps.Read.Contents.Repositories; using Squidex.Domain.Apps.Read.Schemas; using Squidex.Domain.Apps.Write.Contents.Commands; using Squidex.Infrastructure; +using Squidex.Infrastructure.Geocoding; using Squidex.Infrastructure.Tasks; #pragma warning disable IDE0017 // Simplify object initialization @@ -36,6 +37,7 @@ namespace Squidex.Domain.Apps.Write.Contents private ISchemaEntity schemaEntity; private IAppEntity appEntity; private Func message; + private IGeocoder geocoder; public static async Task CreateAsync( IContentRepository contentRepository, @@ -44,20 +46,23 @@ namespace Squidex.Domain.Apps.Write.Contents IAppProvider appProvider, IAssetRepository assetRepository, IScriptEngine scriptEngine, - Func message) + Func message, + IGeocoder geocoder) { var (appEntity, schemaEntity) = await appProvider.GetAppWithSchemaAsync(command.AppId.Name, command.SchemaId.Id); - var context = new ContentOperationContext(); - - context.appEntity = appEntity; - context.assetRepository = assetRepository; - context.contentRepository = contentRepository; - context.content = content; - context.command = command; - context.message = message; - context.schemaEntity = schemaEntity; - context.scriptEngine = scriptEngine; + var context = new ContentOperationContext + { + appEntity = appEntity, + assetRepository = assetRepository, + contentRepository = contentRepository, + content = content, + command = command, + message = message, + schemaEntity = schemaEntity, + scriptEngine = scriptEngine, + geocoder = geocoder + }; return context; } @@ -93,11 +98,11 @@ namespace Squidex.Domain.Apps.Write.Contents if (partial) { - await dataCommand.Data.ValidatePartialAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), errors); + await dataCommand.Data.ValidatePartialAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), errors, geocoder); } else { - await dataCommand.Data.ValidateAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), errors); + await dataCommand.Data.ValidateAsync(ctx, schemaEntity.SchemaDef, appEntity.PartitionResolver(), errors, geocoder); } if (errors.Count > 0) diff --git a/src/Squidex.Infrastructure/Geocoding/GoogleMapsGeocoder.cs b/src/Squidex.Infrastructure/Geocoding/GoogleMapsGeocoder.cs new file mode 100644 index 000000000..2dfa7c671 --- /dev/null +++ b/src/Squidex.Infrastructure/Geocoding/GoogleMapsGeocoder.cs @@ -0,0 +1,57 @@ +// ========================================================================== +// GoogleMapsGeocoder.cs +// CivicPlus implementation of Squidex Headless CMS +// ========================================================================== + +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Squidex.Infrastructure.Geocoding +{ + public class GoogleMapsGeocoder : IGeocoder + { + private readonly string geoCodeUrl = "https://maps.googleapis.com/maps/api/geocode/json"; + private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(2); + + public GoogleMapsGeocoder(string key) + { + Key = key; + } + + public string Key { get; } + + public object GeocodeAddress(string address) + { + var requestUrl = $"{geoCodeUrl}?key={Key}&address={address}"; + var request = new HttpRequestMessage(HttpMethod.Get, requestUrl); + + return GetLatLong(request).Result; + } + + private async Task GetLatLong(HttpRequestMessage request) + { + try + { + HttpResponseMessage response; + using (var client = new HttpClient { Timeout = Timeout }) + { + response = await client.SendAsync(request); + } + + var result = JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().Result); + var innerResults = ((JObject)result["results"].FirstOrDefault()); + var geometry = ((JObject)innerResults["geometry"]); + var location = ((JObject)geometry["location"]); + return new { Latitude = location["lat"], Longitude = location["lng"] }; + } + catch + { + throw new InvalidCastException("Latitude and Longitude could not be calculated. Please enter a valid address."); + } + } + } +} diff --git a/src/Squidex.Infrastructure/Geocoding/IGeocoder.cs b/src/Squidex.Infrastructure/Geocoding/IGeocoder.cs new file mode 100644 index 000000000..c159ce466 --- /dev/null +++ b/src/Squidex.Infrastructure/Geocoding/IGeocoder.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// IGeocoder.cs +// CivicPlus implementation of Squidex Headless CMS +// ========================================================================== + +namespace Squidex.Infrastructure.Geocoding +{ + public interface IGeocoder + { + string Key { get; } + + object GeocodeAddress(string address); + } +} diff --git a/src/Squidex.Infrastructure/Geocoding/OSMGeocoder.cs b/src/Squidex.Infrastructure/Geocoding/OSMGeocoder.cs new file mode 100644 index 000000000..099d265b4 --- /dev/null +++ b/src/Squidex.Infrastructure/Geocoding/OSMGeocoder.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// OSMGeocoder.cs +// CivicPlus implementation of Squidex Headless CMS +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.Geocoding +{ + public class OSMGeocoder : IGeocoder + { + public string Key { get; } + + public object GeocodeAddress(string address) + { + throw new InvalidCastException("Latitude and Longitude must be provided."); + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs index 71aa34709..796a98265 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -17,6 +17,7 @@ using Squidex.Domain.Apps.Read; using Squidex.Domain.Apps.Read.Apps.Services; using Squidex.Domain.Apps.Write.Apps.Commands; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Geocoding; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Security; using Squidex.Pipeline; @@ -33,14 +34,17 @@ namespace Squidex.Areas.Api.Controllers.Apps { private readonly IAppProvider appProvider; private readonly IAppPlansProvider appPlansProvider; + private readonly IGeocoder geocoder; public AppsController(ICommandBus commandBus, IAppProvider appProvider, - IAppPlansProvider appPlansProvider) + IAppPlansProvider appPlansProvider, + IGeocoder geocoder) : base(commandBus) { this.appProvider = appProvider; this.appPlansProvider = appPlansProvider; + this.geocoder = geocoder; } /// @@ -65,7 +69,7 @@ namespace Squidex.Areas.Api.Controllers.Apps var response = apps.Select(a => { - var dto = SimpleMapper.Map(a, new AppDto()); + var dto = SimpleMapper.Map(a, new AppDto() { GeocoderKey = geocoder.Key ?? string.Empty }); dto.Permission = a.Contributors[subject]; @@ -104,7 +108,7 @@ namespace Squidex.Areas.Api.Controllers.Apps var context = await CommandBus.PublishAsync(command); var result = context.Result>(); - var response = new AppCreatedDto { Id = result.IdOrValue.ToString(), Version = result.Version }; + var response = new AppCreatedDto { Id = result.IdOrValue.ToString(), Version = result.Version, GeocoderKey = geocoder.Key ?? string.Empty }; response.Permission = AppContributorPermission.Owner; diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs index 30b3de2c9..e19b4b5f8 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs @@ -41,5 +41,10 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// Gets the next plan name. /// public string PlanUpgrade { get; set; } + + /// + /// The geocoding api key for the application + /// + public string GeocoderKey { get; set; } } } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs index dbc6e513e..dca6f5782 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs @@ -59,5 +59,10 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// Gets the next plan name. /// public string PlanUpgrade { get; set; } + + /// + /// The geocoding api key for the application + /// + public string GeocoderKey { get; set; } } } diff --git a/src/Squidex/Config/Domain/InfrastructureServices.cs b/src/Squidex/Config/Domain/InfrastructureServices.cs index 5552c561a..1ef8c683e 100644 --- a/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -18,6 +18,7 @@ using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Assets.ImageSharp; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Geocoding; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.States; using Squidex.Infrastructure.UsageTracking; @@ -47,6 +48,25 @@ namespace Squidex.Config.Domain .As(); } + var geocoder = config.GetRequiredValue("geolocation:type"); + + if (string.Equals(geocoder, "GoogleMaps", StringComparison.OrdinalIgnoreCase)) + { + var geocoderKey = config.GetRequiredValue("geolocation:key"); + + services.AddSingletonAs(c => new GoogleMapsGeocoder(geocoderKey)) + .As(); + } + else if (string.Equals(geocoder, "OSM", StringComparison.OrdinalIgnoreCase)) + { + services.AddSingletonAs() + .As(); + } + else + { + throw new ConfigurationException($"Unsupported value '{geocoder}' for 'geocoder:type', supported: GoogleMaps, OSM."); + } + services.AddSingletonAs(c => new ApplicationInfoLogAppender(typeof(Program).Assembly, Guid.NewGuid())) .As(); diff --git a/src/Squidex/app/framework/angular/geolocation-editor.component.html b/src/Squidex/app/framework/angular/geolocation-editor.component.html deleted file mode 100644 index fd3392b28..000000000 --- a/src/Squidex/app/framework/angular/geolocation-editor.component.html +++ /dev/null @@ -1,19 +0,0 @@ -
-
- -
-
-
- -
-
- -
-
- -
- - -
-
-
\ No newline at end of file diff --git a/src/Squidex/app/framework/angular/geolocation-editor.component.scss b/src/Squidex/app/framework/angular/geolocation-editor.component.scss deleted file mode 100644 index ffa89e6ba..000000000 --- a/src/Squidex/app/framework/angular/geolocation-editor.component.scss +++ /dev/null @@ -1,14 +0,0 @@ -@import '_mixins'; -@import '_vars'; - -.editor { - height: 30rem; -} - -.form-inline { - margin-top: .5rem; -} - -.latitude-group { - margin-right: .25rem; -} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/geolocation-editor.component.ts b/src/Squidex/app/framework/angular/geolocation-editor.component.ts deleted file mode 100644 index 6226b8ac3..000000000 --- a/src/Squidex/app/framework/angular/geolocation-editor.component.ts +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Sebastian Stehle. All rights reserved - */ - -import { AfterViewInit, Component, ElementRef, forwardRef, ViewChild } from '@angular/core'; -import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; - -import { Types } from './../utils/types'; - -import { ResourceLoaderService } from './../services/resource-loader.service'; -import { ValidatorsEx } from './validators'; - -declare var L: any; - -export const SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR: any = { - provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => GeolocationEditorComponent), multi: true -}; - -interface Geolocation { - latitude: number; - longitude: number; -} - -@Component({ - selector: 'sqx-geolocation-editor', - styleUrls: ['./geolocation-editor.component.scss'], - templateUrl: './geolocation-editor.component.html', - providers: [SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR] -}) -export class GeolocationEditorComponent implements ControlValueAccessor, AfterViewInit { - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; - private marker: any; - private map: any; - private value: Geolocation | null = null; - - public get hasValue() { - return !!this.value; - } - - public geolocationForm = - this.formBuilder.group({ - latitude: ['', - [ - ValidatorsEx.between(-90, 90) - ]], - longitude: ['', - [ - ValidatorsEx.between(-180, 180) - ]] - }); - - @ViewChild('editor') - public editor: ElementRef; - - public isDisabled = false; - - constructor( - private readonly resourceLoader: ResourceLoaderService, - private readonly formBuilder: FormBuilder - ) { - } - - public writeValue(value: Geolocation) { - if (Types.isObject(value) && Types.isNumber(value.latitude) && Types.isNumber(value.longitude)) { - this.value = value; - } else { - this.value = null; - } - - if (this.marker) { - this.updateMarker(true, false); - } - } - - public setDisabledState(isDisabled: boolean): void { - this.isDisabled = isDisabled; - - if (isDisabled) { - if (this.map) { - this.map.zoomControl.disable(); - - this.map._handlers.forEach((handler: any) => { - handler.disable(); - }); - } - - if (this.marker) { - this.marker.dragging.disable(); - } - - this.geolocationForm.disable(); - } else { - if (this.map) { - this.map.zoomControl.enable(); - - this.map._handlers.forEach((handler: any) => { - handler.enable(); - }); - } - - if (this.marker) { - this.marker.dragging.enable(); - } - - this.geolocationForm.enable(); - } - } - - public registerOnChange(fn: any) { - this.callChange = fn; - } - - public registerOnTouched(fn: any) { - this.callTouched = fn; - } - - public updateValueByInput() { - if (this.geolocationForm.controls['latitude'].value !== null && - this.geolocationForm.controls['longitude'].value !== null && - this.geolocationForm.valid) { - this.value = this.geolocationForm.value; - } else { - this.value = null; - } - - this.updateMarker(true, true); - } - - public ngAfterViewInit() { - this.resourceLoader.loadStyle('https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.3/leaflet.css'); - this.resourceLoader.loadScript('https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.3/leaflet.js').then(() => { - this.map = L.map(this.editor.nativeElement).fitWorld(); - - L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', { - attribution: '© OpenStreetMap contributors' - }).addTo(this.map); - - this.map.on('click', (event: any) => { - if (!this.marker && !this.isDisabled) { - const latlng = event.latlng.wrap(); - - this.value = { latitude: latlng.lat, longitude: latlng.lng }; - - this.updateMarker(false, true); - } - }); - - this.updateMarker(true, false); - - if (this.isDisabled) { - this.map.zoomControl.disable(); - - this.map._handlers.forEach((handler: any) => { - handler.disable(); - }); - } - }); - } - - public reset() { - this.value = null; - - this.updateMarker(true, true); - } - - private updateMarker(zoom: boolean, fireEvent: boolean) { - if (this.value) { - if (!this.marker) { - this.marker = L.marker([0, 90], { draggable: true }).addTo(this.map); - - this.marker.on('drag', (event: any) => { - const latlng = event.latlng.wrap(); - - this.value = { latitude: latlng.lat, longitude: latlng.lng }; - }); - - this.marker.on('dragend', () => { - this.updateMarker(false, true); - }); - - if (this.isDisabled) { - this.marker.dragging.disable(); - } - } - - const latLng = L.latLng(this.value.latitude, this.value.longitude); - - if (zoom) { - this.map.setView(latLng, 8); - } else { - this.map.panTo(latLng); - } - - this.marker.setLatLng(latLng); - - this.geolocationForm.setValue(this.value, { emitEvent: false, onlySelf: false }); - } else { - if (this.marker) { - this.marker.removeFrom(this.map); - this.marker = null; - } - - this.map.fitWorld(); - - this.geolocationForm.reset(undefined, { emitEvent: false, onlySelf: false }); - } - - if (fireEvent) { - this.callChange(this.value); - this.callTouched(); - } - } -} \ No newline at end of file diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts index 51fcb48ac..1b4ab3b68 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/src/Squidex/app/framework/declarations.ts @@ -17,7 +17,6 @@ export * from './angular/dialog-renderer.component'; export * from './angular/dropdown.component'; export * from './angular/file-drop.directive'; export * from './angular/focus-on-init.directive'; -export * from './angular/geolocation-editor.component'; export * from './angular/http-extensions-impl'; export * from './angular/image-source.directive'; export * from './angular/indeterminate-value.directive'; diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts index 1edc0e5a5..f162d4f6b 100644 --- a/src/Squidex/app/framework/module.ts +++ b/src/Squidex/app/framework/module.ts @@ -32,7 +32,6 @@ import { FileSizePipe, FocusOnInitDirective, FromNowPipe, - GeolocationEditorComponent, ImageSourceDirective, IndeterminateValueDirective, JscriptEditorComponent, @@ -99,7 +98,6 @@ import { FileSizePipe, FocusOnInitDirective, FromNowPipe, - GeolocationEditorComponent, ImageSourceDirective, IndeterminateValueDirective, JscriptEditorComponent, @@ -149,7 +147,6 @@ import { FileSizePipe, FocusOnInitDirective, FromNowPipe, - GeolocationEditorComponent, ImageSourceDirective, IndeterminateValueDirective, JscriptEditorComponent, diff --git a/src/Squidex/app/shared/components/geolocation-editor.component.html b/src/Squidex/app/shared/components/geolocation-editor.component.html new file mode 100644 index 000000000..c5be81bb3 --- /dev/null +++ b/src/Squidex/app/shared/components/geolocation-editor.component.html @@ -0,0 +1,47 @@ +
+
+
+ +
+
+
+ + +
+ + + +
+
+ + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
\ No newline at end of file diff --git a/src/Squidex/app/shared/components/geolocation-editor.component.scss b/src/Squidex/app/shared/components/geolocation-editor.component.scss new file mode 100644 index 000000000..b82dca5e8 --- /dev/null +++ b/src/Squidex/app/shared/components/geolocation-editor.component.scss @@ -0,0 +1,68 @@ +@import '_mixins'; +@import '_vars'; + +.editor { + height: 30rem; +} + +.form-group { + margin-top: .5rem; + margin-bottom: .5rem; + + + &.city-group { + display: inline-block; + width: 70%; + + &.hasClear{ + width: 55%; + } + } + + &.state-group { + display: inline-block; + width: 10%; + } + + &.zip-group { + display: inline-block; + width: 18.5%; + } + + &.clear-group { + display: inline-block; + &.hidden{ + display: none; + } + } +} + +.latitude-group { + margin-right: .25rem; +} + +#pac-input { + background-color: #fff; + font-size: 15px; + font-weight: 300; + margin-left: 12px; + padding: 0 11px 0 13px; + text-overflow: ellipsis; + width: 400px; + height: 40px; + + &:focus { + border-color: #4d90fe; + } +} + +#type-selector { + color: #fff; + background-color: #4d90fe; + padding: 5px 11px 0px 11px; +} + +#type-selector label { + font-size: 13px; + font-weight: 300; +} \ No newline at end of file diff --git a/src/Squidex/app/shared/components/geolocation-editor.component.ts b/src/Squidex/app/shared/components/geolocation-editor.component.ts new file mode 100644 index 000000000..b825912a5 --- /dev/null +++ b/src/Squidex/app/shared/components/geolocation-editor.component.ts @@ -0,0 +1,496 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { AfterViewInit, Component, ElementRef, forwardRef, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; + +import { Types } from './../../framework/utils/types'; + +import { ResourceLoaderService } from './../../framework/services/resource-loader.service'; +import { ValidatorsEx } from './../../framework/angular/validators'; + +import { AppContext } from './app-context'; + +declare var L: any; +declare var google: any; + +export const SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR: any = { + provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => GeolocationEditorComponent), multi: true +}; + +interface Geolocation { + latitude: number; + longitude: number; + address1: string; + address2: string; + city: string; + state: string; + zip: string; +} + +@Component({ + selector: 'sqx-geolocation-editor', + styleUrls: ['./geolocation-editor.component.scss'], + templateUrl: './geolocation-editor.component.html', + providers: [SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR] +}) +export class GeolocationEditorComponent implements ControlValueAccessor, AfterViewInit { + private callChange = (v: any) => { /* NOOP */ }; + private callTouched = () => { /* NOOP */ }; + private marker: any; + private map: any; + private searchBox: any; + private value: Geolocation | null = null; + public useGoogleMaps = false; + + public get hasValue() { + return !!this.value; + } + + public geolocationForm = + this.formBuilder.group({ + address1: ['', []], + address2: ['', []], + city: ['', []], + state: [ + '', [ + ValidatorsEx.pattern('[A-Z]{2}', 'This field must be a valid state abbreviation.') + ] + ], + zip: [ + '', [ + ValidatorsEx.pattern('[0-9]{5}(\-[0-9]{4})?', 'This field must be a valid ZIP Code.') + ] + ], + latitude: [ + '', + [ + ValidatorsEx.between(-90, 90) + ] + ], + longitude: [ + '', + [ + ValidatorsEx.between(-180, 180) + ] + ] + }); + + @ViewChild('editor') + public editor: ElementRef; + + @ViewChild('searchBox') + public searchBoxInput: ElementRef; + + public isDisabled = false; + + constructor( + private readonly resourceLoader: ResourceLoaderService, + private readonly formBuilder: FormBuilder, + private readonly ctx: AppContext + ) { + this.useGoogleMaps = this.ctx.app.geocoderKey !== ''; + } + + public writeValue(value: Geolocation) { + if (Types.isObject(value) && Types.isNumber(value.latitude) && Types.isNumber(value.longitude)) { + this.value = value; + } else { + this.value = null; + } + + if (this.marker || (!this.marker && this.map && this.value)) { + this.updateMarker(true, false); + } + } + + public setDisabledState(isDisabled: boolean): void { + if (this.ctx.app.geocoderKey === '') { + this.setDisabledStateOSM(isDisabled); + } else { + this.setDisabledStateGoogle(isDisabled); + } + } + + private setDisabledStateOSM(isDisabled: boolean): void { + this.isDisabled = isDisabled; + + if (isDisabled) { + if (this.map) { + this.map.zoomControl.disable(); + + this.map._handlers.forEach((handler: any) => { + handler.disable(); + }); + } + + if (this.marker) { + this.marker.dragging.disable(); + } + + this.geolocationForm.disable(); + } else { + if (this.map) { + this.map.zoomControl.enable(); + + this.map._handlers.forEach((handler: any) => { + handler.enable(); + }); + } + + if (this.marker) { + this.marker.dragging.enable(); + } + + this.geolocationForm.enable(); + } + } + + private setDisabledStateGoogle(isDisabled: boolean): void { + this.isDisabled = isDisabled; + + if (isDisabled) { + if (this.map) { + this.map.setOptions({ + draggable: false, + zoomControl: false + }); + } + + if (this.marker) { + this.marker.setDraggable(false); + } + + this.geolocationForm.disable(); + this.searchBoxInput.nativeElement.disabled = true; + } else { + if (this.map) { + this.map.setOptions({ + draggable: true, + zoomControl: true + }); + } + + if (this.marker) { + this.marker.setDraggable(true); + } + + this.geolocationForm.enable(); + this.searchBoxInput.nativeElement.disabled = false; + } + } + + public registerOnChange(fn: any) { + this.callChange = fn; + } + + public registerOnTouched(fn: any) { + this.callTouched = fn; + } + + public updateValueByInput() { + let updateMap = this.geolocationForm.controls['latitude'].value !== null && + this.geolocationForm.controls['longitude'].value !== null; + + this.value = this.geolocationForm.value; + + if (updateMap) { + this.updateMarker(true, true); + } else { + this.callChange(this.value); + this.callTouched(); + } + } + + public ngAfterViewInit() { + if (!this.useGoogleMaps) { + this.ngAfterViewInitOSM(); + } else { + this.ngAfterViewInitGoogle(); + } + } + + private ngAfterViewInitOSM() { + this.searchBoxInput.nativeElement.remove(); + + this.resourceLoader.loadStyle('https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.3/leaflet.css'); + this.resourceLoader.loadScript('https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.3/leaflet.js').then( + () => { + this.map = L.map(this.editor.nativeElement).fitWorld(); + + L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', + { + attribution: '© OpenStreetMap contributors' + }).addTo(this.map); + + this.map.on('click', + (event: any) => { + if (!this.marker && !this.isDisabled) { + const latlng = event.latlng.wrap(); + + this.value = { + latitude: latlng.lat, + longitude: latlng.lng, + address1: '', + address2: '', + city: '', + state: '', + zip: '' + }; + + this.updateMarker(false, true); + } + }); + + this.updateMarker(true, false); + + if (this.isDisabled) { + this.map.zoomControl.disable(); + + this.map._handlers.forEach((handler: any) => { + handler.disable(); + }); + } + }); + } + + private ngAfterViewInitGoogle() { + this.resourceLoader.loadScript(`https://maps.googleapis.com/maps/api/js?key=${this.ctx.app.geocoderKey}&libraries=places`).then( + () => { + this.map = new google.maps.Map(this.editor.nativeElement, + { + zoom: 1, + mapTypeControl: false, + streetViewControl: false, + center: { lat: 0, lng: 0 } + }); + + this.searchBox = new google.maps.places.SearchBox(this.searchBoxInput.nativeElement); + this.map.controls[google.maps.ControlPosition.LEFT_TOP].push(this.searchBoxInput.nativeElement); + + this.map.addListener('click', + (event: any) => { + if (!this.isDisabled) { + this.value = { + latitude: event.latLng.lat(), + longitude: event.latLng.lng(), + address1: this.value == null ? '' : this.value.address1, + address2: this.value == null ? '' : this.value.address2, + city: this.value == null ? '' : this.value.city, + state: this.value == null ? '' : this.value.state, + zip: this.value == null ? '' : this.value.zip + }; + + this.updateMarker(false, true); + } + }); + + this.map.addListener('bounds_changed', (event: any) => { + this.searchBox.setBounds(this.map.getBounds()); + }); + + this.searchBox.addListener('places_changed', (event: any) => { + let places = this.searchBox.getPlaces(); + + if (places.length === 1) { + let place = places[0]; + + if (!place.geometry) { + console.log('Returned place contains no geometry'); + return; + } + + if (!this.isDisabled) { + this.value = this.parseAddress(place); + + this.updateMarker(false, true); + } + } + }); + + this.updateMarker(true, false); + + if (this.isDisabled) { + this.map.setOptions({ + draggable: false, + zoomControl: false + }); + } + }); + } + + public reset() { + this.value = null; + this.searchBoxInput.nativeElement.value = null; + + this.updateMarker(true, true); + } + + private parseAddress(value: any): Geolocation { + let latitude = value.geometry.location.lat(); + let longitude = value.geometry.location.lng(); + let address1 = this.getAddressValue(value.address_components.find((a: any) => a.types.indexOf('street_number') > -1)) + + ' ' + this.getAddressValue(value.address_components.find((a: any) => a.types.indexOf('route') > -1)); + let address2 = this.getAddressValue(value.address_components.find((a: any) => a.types.indexOf('subpremise') > -1)); + let city = this.getAddressValue(value.address_components.find((a: any) => a.types.indexOf('locality') > -1)); + let state = this.getAddressValue(value.address_components.find((a: any) => a.types.indexOf('administrative_area_level_1') > -1)).toUpperCase(); + + let zipCode = this.getAddressValue(value.address_components.find((a: any) => a.types.indexOf('postal_code') > -1)); + let zipCodeSuffix = this.getAddressValue(value.address_components.find((a: any) => a.types.indexOf('postal_code_suffix') > -1)); + let zip = zipCodeSuffix === '' ? zipCode : zipCode + '-' + zipCodeSuffix; + + return { latitude: latitude, longitude: longitude, address1: address1, address2: address2, city: city, state: state, zip: zip }; + } + + private getAddressValue(value: any) { + return value == null ? '' : value.short_name; + } + + private fillMissingParameters() { + return { + latitude: this.value.latitude, + longitude: this.value.longitude, + address1: this.value.address1 ? this.value.address1 : '', + address2: this.value.address2 ? this.value.address2 : '', + city: this.value.city ? this.value.city : '', + state: this.value.state ? this.value.state : '', + zip: this.value.zip ? this.value.zip : '' + }; + } + + private updateMarker(zoom: boolean, fireEvent: boolean) { + if (this.ctx.app.geocoderKey === '') { + this.updateMarkerOSM(zoom, fireEvent); + } else { + this.updateMarkerGoogle(zoom, fireEvent); + } + } + + private updateMarkerOSM(zoom: boolean, fireEvent: boolean) { + if (this.value) { + if (!this.marker) { + this.marker = L.marker([0, 90], { draggable: true }).addTo(this.map); + + this.marker.on('drag', (event: any) => { + const latlng = event.latlng.wrap(); + + this.value = { + latitude: latlng.lat, + longitude: latlng.lng, + address1: '', + address2: '', + city: '', + state: '', + zip: '' }; + }); + + this.marker.on('dragend', () => { + this.updateMarker(false, true); + }); + + if (this.isDisabled) { + this.marker.dragging.disable(); + } + } + + const latLng = L.latLng(this.value.latitude, this.value.longitude); + + if (zoom) { + this.map.setView(latLng, 8); + } else { + this.map.panTo(latLng); + } + + this.marker.setLatLng(latLng); + + this.geolocationForm.setValue(this.fillMissingParameters(), { emitEvent: false, onlySelf: false }); + } else { + if (this.marker) { + this.marker.removeFrom(this.map); + this.marker = null; + } + + this.map.fitWorld(); + + this.geolocationForm.reset(undefined, { emitEvent: false, onlySelf: false }); + } + + if (fireEvent) { + this.callChange(this.value); + this.callTouched(); + } + } + + private updateMarkerGoogle(zoom: boolean, fireEvent: boolean) { + if (this.value) { + if (!this.marker) { + this.marker = new google.maps.Marker({ + position: { lat: 0, lng: 0 }, + map: this.map, + draggable: true + }); + + this.marker.addListener('drag', (event: any) => { + if (!this.isDisabled) { + this.value = { + latitude: event.latLng.lat(), + longitude: event.latLng.lng(), + address1: this.value == null ? '' : this.value.address1, + address2: this.value == null ? '' : this.value.address2, + city: this.value == null ? '' : this.value.city, + state: this.value == null ? '' : this.value.state, + zip: this.value == null ? '' : this.value.zip + }; + } + }); + this.marker.addListener('dragend', (event: any) => { + if (!this.isDisabled) { + this.value = { + latitude: event.latLng.lat(), + longitude: event.latLng.lng(), + address1: this.value == null ? '' : this.value.address1, + address2: this.value == null ? '' : this.value.address2, + city: this.value == null ? '' : this.value.city, + state: this.value == null ? '' : this.value.state, + zip: this.value == null ? '' : this.value.zip + }; + + this.updateMarker(false, true); + } + }); + } + + const latLng = { lat: this.value.latitude, lng: this.value.longitude }; + + if (zoom) { + this.map.setCenter(latLng); + } else { + this.map.panTo(latLng); + } + + this.marker.setPosition(latLng); + this.map.setZoom(16); + + this.geolocationForm.setValue(this.fillMissingParameters(), { emitEvent: false, onlySelf: false }); + } else { + if (this.marker) { + this.marker.setMap(null); + this.marker = null; + } + + this.map.setCenter({ lat: 0, lng: 0 }); + this.map.setZoom(1); + + this.geolocationForm.reset(undefined, { emitEvent: false, onlySelf: false }); + } + + if (fireEvent) { + this.callChange(this.value); + this.callTouched(); + } + } +} \ No newline at end of file diff --git a/src/Squidex/app/shared/declarations.ts b/src/Squidex/app/shared/declarations.ts index 0d7371c00..f8b2efe39 100644 --- a/src/Squidex/app/shared/declarations.ts +++ b/src/Squidex/app/shared/declarations.ts @@ -9,6 +9,7 @@ export * from './components/app-context'; export * from './components/app-form.component'; export * from './components/asset.component'; export * from './components/help.component'; +export * from './components/geolocation-editor.component'; export * from './components/history.component'; export * from './components/language-selector.component'; export * from './components/pipes'; diff --git a/src/Squidex/app/shared/module.ts b/src/Squidex/app/shared/module.ts index 3cc68672f..7d60668a8 100644 --- a/src/Squidex/app/shared/module.ts +++ b/src/Squidex/app/shared/module.ts @@ -28,6 +28,7 @@ import { ContentsService, EventConsumersService, FileIconPipe, + GeolocationEditorComponent, GraphQlService, HelpComponent, HelpService, @@ -73,6 +74,7 @@ import { AssetPreviewUrlPipe, AssetUrlPipe, FileIconPipe, + GeolocationEditorComponent, HelpComponent, HistoryComponent, LanguageSelectorComponent, @@ -92,6 +94,7 @@ import { AssetPreviewUrlPipe, AssetUrlPipe, FileIconPipe, + GeolocationEditorComponent, HelpComponent, HistoryComponent, LanguageSelectorComponent, diff --git a/src/Squidex/app/shared/services/apps-store.service.spec.ts b/src/Squidex/app/shared/services/apps-store.service.spec.ts index 96f86dded..e64ed5cb1 100644 --- a/src/Squidex/app/shared/services/apps-store.service.spec.ts +++ b/src/Squidex/app/shared/services/apps-store.service.spec.ts @@ -20,10 +20,10 @@ describe('AppsStoreService', () => { const now = DateTime.now(); const oldApps = [ - new AppDto('id', 'old-name', 'Owner', now, now, 'Free', 'Plan'), - new AppDto('id', 'old-name', 'Owner', now, now, 'Free', 'Plan') + new AppDto('id', 'old-name', 'Owner', now, now, 'Free', 'Plan', ''), + new AppDto('id', 'old-name', 'Owner', now, now, 'Free', 'Plan', '') ]; - const newApp = new AppDto('id', 'new-name', 'Owner', now, now, 'Free', 'Plan'); + const newApp = new AppDto('id', 'new-name', 'Owner', now, now, 'Free', 'Plan', ''); let appsService: IMock; diff --git a/src/Squidex/app/shared/services/apps.service.spec.ts b/src/Squidex/app/shared/services/apps.service.spec.ts index d255b7f23..09b204ab7 100644 --- a/src/Squidex/app/shared/services/apps.service.spec.ts +++ b/src/Squidex/app/shared/services/apps.service.spec.ts @@ -59,7 +59,8 @@ describe('AppsService', () => { created: '2016-01-01', lastModified: '2016-02-02', planName: 'Free', - planUpgrade: 'Basic' + planUpgrade: 'Basic', + geocoderKey: '' }, { id: '456', @@ -68,13 +69,14 @@ describe('AppsService', () => { created: '2017-01-01', lastModified: '2017-02-02', planName: 'Basic', - planUpgrade: 'Enterprise' + planUpgrade: 'Enterprise', + geocoderKey: '' } ]); expect(apps).toEqual([ - new AppDto('123', 'name1', 'Owner', DateTime.parseISO('2016-01-01'), DateTime.parseISO('2016-02-02'), 'Free', 'Basic'), - new AppDto('456', 'name2', 'Owner', DateTime.parseISO('2017-01-01'), DateTime.parseISO('2017-02-02'), 'Basic', 'Enterprise') + new AppDto('123', 'name1', 'Owner', DateTime.parseISO('2016-01-01'), DateTime.parseISO('2016-02-02'), 'Free', 'Basic', ''), + new AppDto('456', 'name2', 'Owner', DateTime.parseISO('2017-01-01'), DateTime.parseISO('2017-02-02'), 'Basic', 'Enterprise', '') ]); })); @@ -98,9 +100,10 @@ describe('AppsService', () => { id: '123', permission: 'Reader', planName: 'Basic', - planUpgrade: 'Enterprise' + planUpgrade: 'Enterprise', + geocoderKey: '' }); - expect(app).toEqual(new AppDto('123', dto.name, 'Reader', now, now, 'Basic', 'Enterprise')); + expect(app).toEqual(new AppDto('123', dto.name, 'Reader', now, now, 'Basic', 'Enterprise', '')); })); }); \ No newline at end of file diff --git a/src/Squidex/app/shared/services/apps.service.ts b/src/Squidex/app/shared/services/apps.service.ts index d1f99b348..12ed810ee 100644 --- a/src/Squidex/app/shared/services/apps.service.ts +++ b/src/Squidex/app/shared/services/apps.service.ts @@ -26,7 +26,8 @@ export class AppDto { public readonly created: DateTime, public readonly lastModified: DateTime, public readonly planName: string, - public readonly planUpgrade: string + public readonly planUpgrade: string, + public readonly geocoderKey: string ) { } } @@ -64,7 +65,8 @@ export class AppsService { DateTime.parseISO(item.created), DateTime.parseISO(item.lastModified), item.planName, - item.planUpgrade); + item.planUpgrade, + item.geocoderKey); }); }) .pretifyError('Failed to load apps. Please reload.'); @@ -79,7 +81,7 @@ export class AppsService { now = now || DateTime.now(); - return new AppDto(body.id, dto.name, body.permission, now, now, body.planName, body.planUpgrade); + return new AppDto(body.id, dto.name, body.permission, now, now, body.planName, body.planUpgrade, body.geocoderKey); }) .do(() => { this.analytics.trackEvent('App', 'Created', dto.name); diff --git a/src/Squidex/appsettings.json b/src/Squidex/appsettings.json index 2709ed69d..7650fd432 100644 --- a/src/Squidex/appsettings.json +++ b/src/Squidex/appsettings.json @@ -22,6 +22,16 @@ } }, + "geolocation": { + /* + * Define the type of the geolocation service. + * + * Supported: GoogleMaps, OSM + */ + "type": "OSM", + "key": "" + }, + "logging": { /* * Setting the flag to true, enables well formatteds json logs. diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs index a85b1f562..c092631ef 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs @@ -8,12 +8,14 @@ using System.Collections.Generic; using System.Threading.Tasks; +using FakeItEasy; using FluentAssertions; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Infrastructure; +using Squidex.Infrastructure.Geocoding; using Xunit; namespace Squidex.Domain.Apps.Core.Operations.ValidateContent @@ -22,6 +24,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE, Language.EN); private readonly List errors = new List(); + private readonly IGeocoder geocoder = A.Fake(); private readonly ValidationContext context = ValidationTestExtensions.ValidContext; private Schema schema = new Schema("my-schema"); @@ -33,7 +36,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent .AddField("unknown", new ContentFieldData()); - await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors, geocoder); errors.ShouldBeEquivalentTo( new List @@ -54,7 +57,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent new ContentFieldData() .AddValue(1000)); - await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors, geocoder); errors.ShouldBeEquivalentTo( new List @@ -75,7 +78,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent .AddValue("es", 1) .AddValue("it", 1)); - await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors, geocoder); errors.ShouldBeEquivalentTo( new List @@ -94,7 +97,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent var data = new NamedContentData(); - await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors, geocoder); errors.ShouldBeEquivalentTo( new List @@ -113,7 +116,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent var data = new NamedContentData(); - await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors, geocoder); errors.ShouldBeEquivalentTo( new List @@ -134,7 +137,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent .AddValue("de", 1) .AddValue("xx", 1)); - await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors, geocoder); errors.ShouldBeEquivalentTo( new List @@ -160,7 +163,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent new ContentFieldData() .AddValue("es", "value")); - await data.ValidateAsync(context, schema, optionalConfig.ToResolver(), errors); + await data.ValidateAsync(context, schema, optionalConfig.ToResolver(), errors, geocoder); Assert.Empty(errors); } @@ -177,7 +180,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent .AddValue("es", 1) .AddValue("it", 1)); - await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors, geocoder); errors.ShouldBeEquivalentTo( new List @@ -195,7 +198,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent .AddField("unknown", new ContentFieldData()); - await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors, geocoder); errors.ShouldBeEquivalentTo( new List @@ -216,7 +219,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent new ContentFieldData() .AddValue(1000)); - await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors, geocoder); errors.ShouldBeEquivalentTo( new List @@ -237,7 +240,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent .AddValue("es", 1) .AddValue("it", 1)); - await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors, geocoder); errors.ShouldBeEquivalentTo( new List @@ -256,7 +259,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent var data = new NamedContentData(); - await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors, geocoder); Assert.Empty(errors); } @@ -270,7 +273,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent var data = new NamedContentData(); - await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors, geocoder); Assert.Empty(errors); } @@ -287,7 +290,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent .AddValue("de", 1) .AddValue("xx", 1)); - await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors, geocoder); errors.ShouldBeEquivalentTo( new List @@ -308,7 +311,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent .AddValue("es", 1) .AddValue("it", 1)); - await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors); + await data.ValidatePartialAsync(context, schema, languagesConfig.ToResolver(), errors, geocoder); errors.ShouldBeEquivalentTo( new List diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs index 5580c1883..aaabde7b2 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs @@ -91,7 +91,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent await sut.ValidateAsync(CreateValue(123), errors); errors.ShouldBeEquivalentTo( - new[] { " is not a valid value." }); + new[] { " is not a valid value. Invalid json type, expected string." }); } private static Instant FutureDays(int days) diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs index c8c275fe2..3269c1d64 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs @@ -63,7 +63,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent await sut.ValidateAsync(CreateValue(geolocation), errors); errors.ShouldBeEquivalentTo( - new[] { " is not a valid value." }); + new[] { " is not a valid value. Latitude must be between -90 and 90." }); } [Fact] @@ -78,7 +78,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent await sut.ValidateAsync(CreateValue(geolocation), errors); errors.ShouldBeEquivalentTo( - new[] { " is not a valid value." }); + new[] { " is not a valid value. Longitude must be between -180 and 180." }); } [Fact] @@ -94,7 +94,39 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent await sut.ValidateAsync(CreateValue(geolocation), errors); errors.ShouldBeEquivalentTo( - new[] { " is not a valid value." }); + new[] { " is not a valid value. Geolocation must have proper properties." }); + } + + [Fact] + public async Task Should_add_errors_if_geolocation_has_invalid_zip() + { + var sut = new GeolocationField(1, "my-geolocation", Partitioning.Invariant, new GeolocationFieldProperties { IsRequired = true }); + + var geolocation = new JObject( + new JProperty("latitude", 0), + new JProperty("longitude", 0), + new JProperty("zip", "1234")); + + await sut.ValidateAsync(CreateValue(geolocation), errors); + + errors.ShouldBeEquivalentTo( + new[] { " is not a valid value. ZIP Code must match postal code with optional suffix pattern." }); + } + + [Fact] + public async Task Should_add_errors_if_geolocation_has_invalid_state() + { + var sut = new GeolocationField(1, "my-geolocation", Partitioning.Invariant, new GeolocationFieldProperties { IsRequired = true }); + + var geolocation = new JObject( + new JProperty("latitude", 0), + new JProperty("longitude", 0), + new JProperty("state", "1")); + + await sut.ValidateAsync(CreateValue(geolocation), errors); + + errors.ShouldBeEquivalentTo( + new[] { " is not a valid value. State must be two capital letters." }); } [Fact] diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs index c08bef25f..c42a0790a 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs @@ -14,6 +14,7 @@ using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Core.ValidateContent.Validators; +using Squidex.Infrastructure.Geocoding; namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { @@ -34,14 +35,14 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent return validator.ValidateAsync(value, (context ?? ValidContext).Optional(true), errors.Add); } - public static Task ValidateAsync(this Field field, JToken value, IList errors, ValidationContext context = null) + public static Task ValidateAsync(this Field field, JToken value, IList errors, ValidationContext context = null, IGeocoder geocoder = null) { - return field.ValidateAsync(value, context ?? ValidContext, errors.Add); + return field.ValidateAsync(value, context ?? ValidContext, errors.Add, geocoder); } - public static Task ValidateOptionalAsync(this Field field, JToken value, IList errors, ValidationContext context = null) + public static Task ValidateOptionalAsync(this Field field, JToken value, IList errors, ValidationContext context = null, IGeocoder geocoder = null) { - return field.ValidateAsync(value, (context ?? ValidContext).Optional(true), errors.Add); + return field.ValidateAsync(value, (context ?? ValidContext).Optional(true), errors.Add, geocoder); } public static ValidationContext Assets(params IAssetInfo[] assets) diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs index 21d668f7e..ecd6678fa 100644 --- a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs @@ -10,6 +10,7 @@ using System; using System.Security.Claims; using System.Threading.Tasks; using FakeItEasy; +using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Contents; @@ -24,6 +25,7 @@ using Squidex.Domain.Apps.Write.Contents.Commands; using Squidex.Domain.Apps.Write.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Geocoding; using Xunit; namespace Squidex.Domain.Apps.Write.Contents @@ -36,6 +38,7 @@ namespace Squidex.Domain.Apps.Write.Contents private readonly IScriptEngine scriptEngine = A.Fake(); private readonly IAppProvider appProvider = A.Fake(); private readonly IAppEntity app = A.Fake(); + private readonly IGeocoder geocoder = A.Fake(); private readonly ClaimsPrincipal user = new ClaimsPrincipal(); private readonly LanguagesConfig languagesConfig = LanguagesConfig.Build(Language.DE); private readonly Guid contentId = Guid.NewGuid(); @@ -57,6 +60,14 @@ namespace Squidex.Domain.Apps.Write.Contents .AddField("my-field1", new ContentFieldData() .AddValue(1)); + private readonly NamedContentData invalidOSMLatLongData = new NamedContentData() + .AddField("my-geolocation-field1", new ContentFieldData() + .AddValue(JObject.FromObject(new { latitude = 0, longitude = (double?)null }))); + + private readonly NamedContentData invalidGoogleMapsLatLongData = new NamedContentData() + .AddField("my-geolocation-field1", new ContentFieldData() + .AddValue(JObject.FromObject(new { latitude = 0, longitude = (double?)null, address1 = "baddata" }))); + public ContentCommandMiddlewareTests() { var schemaDef = @@ -68,7 +79,7 @@ namespace Squidex.Domain.Apps.Write.Contents content = new ContentDomainObject(contentId, -1); - sut = new ContentCommandMiddleware(Handler, appProvider, A.Dummy(), scriptEngine, A.Dummy()); + sut = new ContentCommandMiddleware(Handler, appProvider, A.Dummy(), scriptEngine, A.Dummy(), geocoder); A.CallTo(() => app.LanguagesConfig).Returns(languagesConfig); @@ -239,6 +250,49 @@ namespace Squidex.Domain.Apps.Write.Contents A.CallTo(() => scriptEngine.Execute(A.Ignored, "")).MustHaveHappened(); } + [Fact] + public async Task Create_geolocation_should_throw_exception_if_OSM_data_is_invalid() + { + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) + .Returns(invalidOSMLatLongData); + SetupGeolocationTest(); + + var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = invalidOSMLatLongData, User = user }); + + await TestCreate(content, async _ => + { + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + }, false); + } + + [Fact] + public async Task Create_geolocation_should_throw_exception_if_Google_Maps_data_does_not_return_lat_long() + { + A.CallTo(() => scriptEngine.ExecuteAndTransform(A.Ignored, A.Ignored)) + .Returns(invalidGoogleMapsLatLongData); + SetupGeolocationTest("key"); + + var context = CreateContextForCommand(new CreateContent { ContentId = contentId, Data = invalidOSMLatLongData, User = user }); + + await TestCreate(content, async _ => + { + await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + }, false); + } + + private void SetupGeolocationTest(string googleApiKey = "") + { + var geolocationSchemaDef = + new Schema("my-geolocation-schema") + .AddField(new GeolocationField(1, "my-geolocation-field1", Partitioning.Invariant, + new GeolocationFieldProperties { IsRequired = true })); + + A.CallTo(() => schema.SchemaDef).Returns(geolocationSchemaDef); + A.CallTo(() => geocoder.Key).Returns(googleApiKey); + A.CallTo(() => geocoder.GeocodeAddress("baddata")).Throws(); + A.CallTo(() => geocoder.GeocodeAddress(string.Empty)).Throws(); + } + private void CreateContent() { content.Create(new CreateContent { Data = data }); From 390ce3ecd1bf4540c25f1cc3362b1743927131b6 Mon Sep 17 00:00:00 2001 From: Alex Van Dyke Date: Tue, 12 Dec 2017 07:46:04 -0600 Subject: [PATCH 2/2] Removing the US Specific fields and mostly just making this a map change. Users can also use the API and submit an address without lat/long that Google will try to geocode. --- .../ValidateContent/JsonValueConverter.cs | 19 +--- .../Geocoding/GoogleMapsGeocoder.cs | 5 +- .../Geocoding/IGeocoder.cs | 5 +- .../Geocoding/OSMGeocoder.cs | 5 +- .../Api/Controllers/Apps/AppsController.cs | 4 +- .../Controllers/Apps/Models/AppCreatedDto.cs | 5 - .../Api/Controllers/Apps/Models/AppDto.cs | 5 - .../Controllers/UI/Models/UISettingsDto.cs | 6 ++ .../Areas/Api/Controllers/UI/UIController.cs | 8 +- .../app/shared/components/app-context.ts | 14 ++- .../geolocation-editor.component.html | 32 +----- .../geolocation-editor.component.scss | 20 ---- .../geolocation-editor.component.ts | 102 +++--------------- .../services/apps-store.service.spec.ts | 50 ++++++--- .../app/shared/services/apps-store.service.ts | 18 +++- .../app/shared/services/apps.service.spec.ts | 15 ++- .../app/shared/services/apps.service.ts | 8 +- .../app/shared/services/ui.service.spec.ts | 2 +- src/Squidex/app/shared/services/ui.service.ts | 3 +- .../ValidateContent/GeolocationFieldTests.cs | 32 ------ .../Contents/ContentCommandMiddlewareTests.cs | 2 +- 21 files changed, 125 insertions(+), 235 deletions(-) diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs index e5dcc8575..e3db375d5 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs @@ -69,7 +69,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent List addressString = new List(); var validProperties = new string[] { - "latitude", "longitude", "address1", "address2", "city", "state", "zip" + "latitude", "longitude", "address" }; foreach (var property in geolocation.Properties()) @@ -78,19 +78,16 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { throw new InvalidCastException("Geolocation must have proper properties."); } - - addressString.Add(geolocation[property.Name.ToLower()]?.ToString()); } var lat = geolocation["latitude"]; var lon = geolocation["longitude"]; - var state = geolocation["state"]?.ToString(); - var zip = geolocation["zip"]?.ToString(); + var address = geolocation["address"]?.ToString(); if (lat == null || lon == null || ((JValue)lat).Value == null || ((JValue)lon).Value == null) { - var response = geocoder.GeocodeAddress(string.Join(string.Empty, addressString)); + var response = geocoder.GeocodeAddress(address); lat = response.TryGetPropertyValue("Latitude", (JToken)null); lon = response.TryGetPropertyValue("Longitude", (JToken)null); @@ -108,16 +105,6 @@ namespace Squidex.Domain.Apps.Core.ValidateContent throw new InvalidCastException("Longitude must be between -180 and 180."); } - if (!string.IsNullOrWhiteSpace(state) && !Regex.IsMatch(state, "[A-Z]{2}")) - { - throw new InvalidCastException("State must be two capital letters."); - } - - if (!string.IsNullOrWhiteSpace(zip) && !Regex.IsMatch(zip, "[0-9]{5}(\\-[0-9]{4})?")) - { - throw new InvalidCastException("ZIP Code must match postal code with optional suffix pattern."); - } - return geolocation; } diff --git a/src/Squidex.Infrastructure/Geocoding/GoogleMapsGeocoder.cs b/src/Squidex.Infrastructure/Geocoding/GoogleMapsGeocoder.cs index 2dfa7c671..62a2787b6 100644 --- a/src/Squidex.Infrastructure/Geocoding/GoogleMapsGeocoder.cs +++ b/src/Squidex.Infrastructure/Geocoding/GoogleMapsGeocoder.cs @@ -1,6 +1,9 @@ // ========================================================================== // GoogleMapsGeocoder.cs -// CivicPlus implementation of Squidex Headless CMS +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. // ========================================================================== using System; diff --git a/src/Squidex.Infrastructure/Geocoding/IGeocoder.cs b/src/Squidex.Infrastructure/Geocoding/IGeocoder.cs index c159ce466..41434dae4 100644 --- a/src/Squidex.Infrastructure/Geocoding/IGeocoder.cs +++ b/src/Squidex.Infrastructure/Geocoding/IGeocoder.cs @@ -1,6 +1,9 @@ // ========================================================================== // IGeocoder.cs -// CivicPlus implementation of Squidex Headless CMS +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. // ========================================================================== namespace Squidex.Infrastructure.Geocoding diff --git a/src/Squidex.Infrastructure/Geocoding/OSMGeocoder.cs b/src/Squidex.Infrastructure/Geocoding/OSMGeocoder.cs index 099d265b4..bb29f597c 100644 --- a/src/Squidex.Infrastructure/Geocoding/OSMGeocoder.cs +++ b/src/Squidex.Infrastructure/Geocoding/OSMGeocoder.cs @@ -1,6 +1,9 @@ // ========================================================================== // OSMGeocoder.cs -// CivicPlus implementation of Squidex Headless CMS +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. // ========================================================================== using System; diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs index 796a98265..efd6359ae 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -69,7 +69,7 @@ namespace Squidex.Areas.Api.Controllers.Apps var response = apps.Select(a => { - var dto = SimpleMapper.Map(a, new AppDto() { GeocoderKey = geocoder.Key ?? string.Empty }); + var dto = SimpleMapper.Map(a, new AppDto()); dto.Permission = a.Contributors[subject]; @@ -108,7 +108,7 @@ namespace Squidex.Areas.Api.Controllers.Apps var context = await CommandBus.PublishAsync(command); var result = context.Result>(); - var response = new AppCreatedDto { Id = result.IdOrValue.ToString(), Version = result.Version, GeocoderKey = geocoder.Key ?? string.Empty }; + var response = new AppCreatedDto { Id = result.IdOrValue.ToString(), Version = result.Version }; response.Permission = AppContributorPermission.Owner; diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs index e19b4b5f8..30b3de2c9 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs @@ -41,10 +41,5 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// Gets the next plan name. /// public string PlanUpgrade { get; set; } - - /// - /// The geocoding api key for the application - /// - public string GeocoderKey { get; set; } } } diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs index dca6f5782..dbc6e513e 100644 --- a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs @@ -59,10 +59,5 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// Gets the next plan name. /// public string PlanUpgrade { get; set; } - - /// - /// The geocoding api key for the application - /// - public string GeocoderKey { get; set; } } } diff --git a/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs b/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs index 6c051df7f..39e318c6f 100644 --- a/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs @@ -18,5 +18,11 @@ namespace Squidex.Areas.Api.Controllers.UI.Models /// [Required] public List RegexSuggestions { get; set; } + + /// + /// The geocoder key. + /// + [Required] + public string GeocoderKey { get; set; } } } diff --git a/src/Squidex/Areas/Api/Controllers/UI/UIController.cs b/src/Squidex/Areas/Api/Controllers/UI/UIController.cs index 7d4e75afc..e29905773 100644 --- a/src/Squidex/Areas/Api/Controllers/UI/UIController.cs +++ b/src/Squidex/Areas/Api/Controllers/UI/UIController.cs @@ -14,6 +14,7 @@ using NSwag.Annotations; using Squidex.Areas.Api.Controllers.UI.Models; using Squidex.Config; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Geocoding; using Squidex.Pipeline; namespace Squidex.Areas.Api.Controllers.UI @@ -26,11 +27,13 @@ namespace Squidex.Areas.Api.Controllers.UI public sealed class UIController : ApiController { private readonly MyUIOptions uiOptions; + private readonly IGeocoder geocoder; - public UIController(ICommandBus commandBus, IOptions uiOptions) + public UIController(ICommandBus commandBus, IOptions uiOptions, IGeocoder geocoder) : base(commandBus) { this.uiOptions = uiOptions.Value; + this.geocoder = geocoder; } /// @@ -50,7 +53,8 @@ namespace Squidex.Areas.Api.Controllers.UI !string.IsNullOrWhiteSpace(x.Key) && !string.IsNullOrWhiteSpace(x.Value)) .Select(x => new UIRegexSuggestionDto { Name = x.Key, Pattern = x.Value }).ToList() - ?? new List() + ?? new List(), + GeocoderKey = geocoder.Key ?? string.Empty }; return Ok(dto); diff --git a/src/Squidex/app/shared/components/app-context.ts b/src/Squidex/app/shared/components/app-context.ts index bee564796..b693e8800 100644 --- a/src/Squidex/app/shared/components/app-context.ts +++ b/src/Squidex/app/shared/components/app-context.ts @@ -18,13 +18,16 @@ import { DialogService, ErrorDto, Notification, - Profile + Profile, + UISettingsDto } from './../declarations-base'; @Injectable() export class AppContext implements OnDestroy { private readonly appSubscription: Subscription; + private readonly uiSettingsSubscription: Subscription; private appField: AppDto; + private uiSettingsField: UISettingsDto; public get app(): AppDto { return this.appField; @@ -38,6 +41,10 @@ export class AppContext implements OnDestroy { return this.appField ? this.appField.name : ''; } + public get uiSettings() { + return this.uiSettingsField; + } + public get userToken(): string { return this.authService.user!.token; } @@ -61,6 +68,11 @@ export class AppContext implements OnDestroy { this.appsStore.selectedApp.take(1).subscribe(app => { this.appField = app; }); + + this.uiSettingsSubscription = + this.appsStore.uiSettings.subscribe(settings => { + this.uiSettingsField = settings; + }); } public ngOnDestroy() { diff --git a/src/Squidex/app/shared/components/geolocation-editor.component.html b/src/Squidex/app/shared/components/geolocation-editor.component.html index c5be81bb3..e77bffd9f 100644 --- a/src/Squidex/app/shared/components/geolocation-editor.component.html +++ b/src/Squidex/app/shared/components/geolocation-editor.component.html @@ -4,38 +4,16 @@
-
- - -
+ +
- +
-
+
- -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - +
diff --git a/src/Squidex/app/shared/components/geolocation-editor.component.scss b/src/Squidex/app/shared/components/geolocation-editor.component.scss index b82dca5e8..2d77f58c0 100644 --- a/src/Squidex/app/shared/components/geolocation-editor.component.scss +++ b/src/Squidex/app/shared/components/geolocation-editor.component.scss @@ -9,26 +9,6 @@ margin-top: .5rem; margin-bottom: .5rem; - - &.city-group { - display: inline-block; - width: 70%; - - &.hasClear{ - width: 55%; - } - } - - &.state-group { - display: inline-block; - width: 10%; - } - - &.zip-group { - display: inline-block; - width: 18.5%; - } - &.clear-group { display: inline-block; &.hidden{ diff --git a/src/Squidex/app/shared/components/geolocation-editor.component.ts b/src/Squidex/app/shared/components/geolocation-editor.component.ts index b825912a5..b8f3bbfdf 100644 --- a/src/Squidex/app/shared/components/geolocation-editor.component.ts +++ b/src/Squidex/app/shared/components/geolocation-editor.component.ts @@ -25,11 +25,6 @@ export const SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR: any = { interface Geolocation { latitude: number; longitude: number; - address1: string; - address2: string; - city: string; - state: string; - zip: string; } @Component({ @@ -53,19 +48,6 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi public geolocationForm = this.formBuilder.group({ - address1: ['', []], - address2: ['', []], - city: ['', []], - state: [ - '', [ - ValidatorsEx.pattern('[A-Z]{2}', 'This field must be a valid state abbreviation.') - ] - ], - zip: [ - '', [ - ValidatorsEx.pattern('[0-9]{5}(\-[0-9]{4})?', 'This field must be a valid ZIP Code.') - ] - ], latitude: [ '', [ @@ -93,7 +75,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi private readonly formBuilder: FormBuilder, private readonly ctx: AppContext ) { - this.useGoogleMaps = this.ctx.app.geocoderKey !== ''; + this.useGoogleMaps = this.ctx.uiSettings.geocoderKey !== ''; } public writeValue(value: Geolocation) { @@ -109,7 +91,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi } public setDisabledState(isDisabled: boolean): void { - if (this.ctx.app.geocoderKey === '') { + if (!this.useGoogleMaps) { this.setDisabledStateOSM(isDisabled); } else { this.setDisabledStateGoogle(isDisabled); @@ -234,12 +216,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi this.value = { latitude: latlng.lat, - longitude: latlng.lng, - address1: '', - address2: '', - city: '', - state: '', - zip: '' + longitude: latlng.lng }; this.updateMarker(false, true); @@ -259,7 +236,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi } private ngAfterViewInitGoogle() { - this.resourceLoader.loadScript(`https://maps.googleapis.com/maps/api/js?key=${this.ctx.app.geocoderKey}&libraries=places`).then( + this.resourceLoader.loadScript(`https://maps.googleapis.com/maps/api/js?key=${this.ctx.uiSettings.geocoderKey}&libraries=places`).then( () => { this.map = new google.maps.Map(this.editor.nativeElement, { @@ -277,12 +254,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi if (!this.isDisabled) { this.value = { latitude: event.latLng.lat(), - longitude: event.latLng.lng(), - address1: this.value == null ? '' : this.value.address1, - address2: this.value == null ? '' : this.value.address2, - city: this.value == null ? '' : this.value.city, - state: this.value == null ? '' : this.value.state, - zip: this.value == null ? '' : this.value.zip + longitude: event.latLng.lng() }; this.updateMarker(false, true); @@ -305,7 +277,10 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi } if (!this.isDisabled) { - this.value = this.parseAddress(place); + let latitude = place.geometry.location.lat(); + let longitude = place.geometry.location.lng(); + + this.value = { latitude: latitude, longitude: longitude }; this.updateMarker(false, true); } @@ -330,40 +305,8 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi this.updateMarker(true, true); } - private parseAddress(value: any): Geolocation { - let latitude = value.geometry.location.lat(); - let longitude = value.geometry.location.lng(); - let address1 = this.getAddressValue(value.address_components.find((a: any) => a.types.indexOf('street_number') > -1)) - + ' ' + this.getAddressValue(value.address_components.find((a: any) => a.types.indexOf('route') > -1)); - let address2 = this.getAddressValue(value.address_components.find((a: any) => a.types.indexOf('subpremise') > -1)); - let city = this.getAddressValue(value.address_components.find((a: any) => a.types.indexOf('locality') > -1)); - let state = this.getAddressValue(value.address_components.find((a: any) => a.types.indexOf('administrative_area_level_1') > -1)).toUpperCase(); - - let zipCode = this.getAddressValue(value.address_components.find((a: any) => a.types.indexOf('postal_code') > -1)); - let zipCodeSuffix = this.getAddressValue(value.address_components.find((a: any) => a.types.indexOf('postal_code_suffix') > -1)); - let zip = zipCodeSuffix === '' ? zipCode : zipCode + '-' + zipCodeSuffix; - - return { latitude: latitude, longitude: longitude, address1: address1, address2: address2, city: city, state: state, zip: zip }; - } - - private getAddressValue(value: any) { - return value == null ? '' : value.short_name; - } - - private fillMissingParameters() { - return { - latitude: this.value.latitude, - longitude: this.value.longitude, - address1: this.value.address1 ? this.value.address1 : '', - address2: this.value.address2 ? this.value.address2 : '', - city: this.value.city ? this.value.city : '', - state: this.value.state ? this.value.state : '', - zip: this.value.zip ? this.value.zip : '' - }; - } - private updateMarker(zoom: boolean, fireEvent: boolean) { - if (this.ctx.app.geocoderKey === '') { + if (!this.useGoogleMaps) { this.updateMarkerOSM(zoom, fireEvent); } else { this.updateMarkerGoogle(zoom, fireEvent); @@ -380,12 +323,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi this.value = { latitude: latlng.lat, - longitude: latlng.lng, - address1: '', - address2: '', - city: '', - state: '', - zip: '' }; + longitude: latlng.lng}; }); this.marker.on('dragend', () => { @@ -407,7 +345,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi this.marker.setLatLng(latLng); - this.geolocationForm.setValue(this.fillMissingParameters(), { emitEvent: false, onlySelf: false }); + this.geolocationForm.setValue(this.value, { emitEvent: false, onlySelf: false }); } else { if (this.marker) { this.marker.removeFrom(this.map); @@ -438,12 +376,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi if (!this.isDisabled) { this.value = { latitude: event.latLng.lat(), - longitude: event.latLng.lng(), - address1: this.value == null ? '' : this.value.address1, - address2: this.value == null ? '' : this.value.address2, - city: this.value == null ? '' : this.value.city, - state: this.value == null ? '' : this.value.state, - zip: this.value == null ? '' : this.value.zip + longitude: event.latLng.lng() }; } }); @@ -451,12 +384,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi if (!this.isDisabled) { this.value = { latitude: event.latLng.lat(), - longitude: event.latLng.lng(), - address1: this.value == null ? '' : this.value.address1, - address2: this.value == null ? '' : this.value.address2, - city: this.value == null ? '' : this.value.city, - state: this.value == null ? '' : this.value.state, - zip: this.value == null ? '' : this.value.zip + longitude: event.latLng.lng() }; this.updateMarker(false, true); @@ -475,7 +403,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi this.marker.setPosition(latLng); this.map.setZoom(16); - this.geolocationForm.setValue(this.fillMissingParameters(), { emitEvent: false, onlySelf: false }); + this.geolocationForm.setValue(this.value, { emitEvent: false, onlySelf: false }); } else { if (this.marker) { this.marker.setMap(null); diff --git a/src/Squidex/app/shared/services/apps-store.service.spec.ts b/src/Squidex/app/shared/services/apps-store.service.spec.ts index e64ed5cb1..ceff06c51 100644 --- a/src/Squidex/app/shared/services/apps-store.service.spec.ts +++ b/src/Squidex/app/shared/services/apps-store.service.spec.ts @@ -13,30 +13,39 @@ import { AppsService, AppsStoreService, CreateAppDto, - DateTime + DateTime, + UIService, + UISettingsDto } from './../'; describe('AppsStoreService', () => { const now = DateTime.now(); const oldApps = [ - new AppDto('id', 'old-name', 'Owner', now, now, 'Free', 'Plan', ''), - new AppDto('id', 'old-name', 'Owner', now, now, 'Free', 'Plan', '') + new AppDto('id', 'old-name', 'Owner', now, now, 'Free', 'Plan'), + new AppDto('id', 'old-name', 'Owner', now, now, 'Free', 'Plan') ]; - const newApp = new AppDto('id', 'new-name', 'Owner', now, now, 'Free', 'Plan', ''); + const newApp = new AppDto('id', 'new-name', 'Owner', now, now, 'Free', 'Plan'); + + const settings: UISettingsDto = { regexSuggestions: [], geocoderKey: '' }; let appsService: IMock; + let uiService: IMock; beforeEach(() => { appsService = Mock.ofType(AppsService); - }); + uiService = Mock.ofType(UIService); - it('should load automatically', () => { appsService.setup(x => x.getApps()) .returns(() => Observable.of(oldApps)) .verifiable(Times.once()); + uiService.setup(x => x.getSettings()) + .returns(() => Observable.of(settings)) + .verifiable(Times.once()); + }); - const store = new AppsStoreService(appsService.object); + it('should load automatically', () => { + const store = new AppsStoreService(appsService.object, uiService.object); let result1: AppDto[] | null = null; let result2: AppDto[] | null = null; @@ -56,15 +65,11 @@ describe('AppsStoreService', () => { }); it('should add app to cache when created', () => { - appsService.setup(x => x.getApps()) - .returns(() => Observable.of(oldApps)) - .verifiable(Times.once()); - appsService.setup(x => x.postApp(It.isAny())) .returns(() => Observable.of(newApp)) .verifiable(Times.once()); - const store = new AppsStoreService(appsService.object); + const store = new AppsStoreService(appsService.object, uiService.object); let result1: AppDto[] | null = null; let result2: AppDto[] | null = null; @@ -86,11 +91,7 @@ describe('AppsStoreService', () => { }); it('should select app', (done) => { - appsService.setup(x => x.getApps()) - .returns(() => Observable.of(oldApps)) - .verifiable(Times.once()); - - const store = new AppsStoreService(appsService.object); + const store = new AppsStoreService(appsService.object, uiService.object); store.selectApp('old-name').subscribe(isSelected => { expect(isSelected).toBeTruthy(); @@ -104,4 +105,19 @@ describe('AppsStoreService', () => { done(); }); }); + + + it('should load uisettings', + () => { + const store = new AppsStoreService(appsService.object, uiService.object); + + store.uiSettings.subscribe(result => { + expect(result).toEqual(settings); + + uiService.verifyAll(); + }, + err => { + expect(err).toBeNull(); + }); + }); }); \ No newline at end of file diff --git a/src/Squidex/app/shared/services/apps-store.service.ts b/src/Squidex/app/shared/services/apps-store.service.ts index 4ef9fe436..6b3b37f79 100644 --- a/src/Squidex/app/shared/services/apps-store.service.ts +++ b/src/Squidex/app/shared/services/apps-store.service.ts @@ -16,10 +16,13 @@ import { CreateAppDto } from './apps.service'; +import { UIService, UISettingsDto } from './ui.service'; + @Injectable() export class AppsStoreService { private readonly apps$ = new ReplaySubject(1); private readonly app$ = new BehaviorSubject(null); + private readonly uiSettings$ = new BehaviorSubject(null); public get apps(): Observable { return this.apps$; @@ -29,8 +32,13 @@ export class AppsStoreService { return this.app$; } + public get uiSettings(): Observable { + return this.uiSettings$; + } + constructor( - private readonly appsService: AppsService + private readonly appsService: AppsService, + private readonly uiService: UIService ) { if (!appsService) { return; @@ -42,6 +50,14 @@ export class AppsStoreService { }, error => { this.apps$.next([]); }); + + this.uiService.getSettings() + .subscribe(settings => { + this.uiSettings$.next(settings); + }, + error => { + this.uiSettings$.next(null); + }); } public selectApp(name: string | null): Observable { diff --git a/src/Squidex/app/shared/services/apps.service.spec.ts b/src/Squidex/app/shared/services/apps.service.spec.ts index 09b204ab7..d255b7f23 100644 --- a/src/Squidex/app/shared/services/apps.service.spec.ts +++ b/src/Squidex/app/shared/services/apps.service.spec.ts @@ -59,8 +59,7 @@ describe('AppsService', () => { created: '2016-01-01', lastModified: '2016-02-02', planName: 'Free', - planUpgrade: 'Basic', - geocoderKey: '' + planUpgrade: 'Basic' }, { id: '456', @@ -69,14 +68,13 @@ describe('AppsService', () => { created: '2017-01-01', lastModified: '2017-02-02', planName: 'Basic', - planUpgrade: 'Enterprise', - geocoderKey: '' + planUpgrade: 'Enterprise' } ]); expect(apps).toEqual([ - new AppDto('123', 'name1', 'Owner', DateTime.parseISO('2016-01-01'), DateTime.parseISO('2016-02-02'), 'Free', 'Basic', ''), - new AppDto('456', 'name2', 'Owner', DateTime.parseISO('2017-01-01'), DateTime.parseISO('2017-02-02'), 'Basic', 'Enterprise', '') + new AppDto('123', 'name1', 'Owner', DateTime.parseISO('2016-01-01'), DateTime.parseISO('2016-02-02'), 'Free', 'Basic'), + new AppDto('456', 'name2', 'Owner', DateTime.parseISO('2017-01-01'), DateTime.parseISO('2017-02-02'), 'Basic', 'Enterprise') ]); })); @@ -100,10 +98,9 @@ describe('AppsService', () => { id: '123', permission: 'Reader', planName: 'Basic', - planUpgrade: 'Enterprise', - geocoderKey: '' + planUpgrade: 'Enterprise' }); - expect(app).toEqual(new AppDto('123', dto.name, 'Reader', now, now, 'Basic', 'Enterprise', '')); + expect(app).toEqual(new AppDto('123', dto.name, 'Reader', now, now, 'Basic', 'Enterprise')); })); }); \ No newline at end of file diff --git a/src/Squidex/app/shared/services/apps.service.ts b/src/Squidex/app/shared/services/apps.service.ts index 12ed810ee..d1f99b348 100644 --- a/src/Squidex/app/shared/services/apps.service.ts +++ b/src/Squidex/app/shared/services/apps.service.ts @@ -26,8 +26,7 @@ export class AppDto { public readonly created: DateTime, public readonly lastModified: DateTime, public readonly planName: string, - public readonly planUpgrade: string, - public readonly geocoderKey: string + public readonly planUpgrade: string ) { } } @@ -65,8 +64,7 @@ export class AppsService { DateTime.parseISO(item.created), DateTime.parseISO(item.lastModified), item.planName, - item.planUpgrade, - item.geocoderKey); + item.planUpgrade); }); }) .pretifyError('Failed to load apps. Please reload.'); @@ -81,7 +79,7 @@ export class AppsService { now = now || DateTime.now(); - return new AppDto(body.id, dto.name, body.permission, now, now, body.planName, body.planUpgrade, body.geocoderKey); + return new AppDto(body.id, dto.name, body.permission, now, now, body.planName, body.planUpgrade); }) .do(() => { this.analytics.trackEvent('App', 'Created', dto.name); diff --git a/src/Squidex/app/shared/services/ui.service.spec.ts b/src/Squidex/app/shared/services/ui.service.spec.ts index 129dc3598..daa7bd121 100644 --- a/src/Squidex/app/shared/services/ui.service.spec.ts +++ b/src/Squidex/app/shared/services/ui.service.spec.ts @@ -41,7 +41,7 @@ describe('UIService', () => { settings1 = result; }); - const response: UISettingsDto = { regexSuggestions: [] }; + const response: UISettingsDto = { regexSuggestions: [], geocoderKey: '' }; const req = httpMock.expectOne('http://service/p/api/ui/settings'); diff --git a/src/Squidex/app/shared/services/ui.service.ts b/src/Squidex/app/shared/services/ui.service.ts index fe9d999fc..0d9f7fbc9 100644 --- a/src/Squidex/app/shared/services/ui.service.ts +++ b/src/Squidex/app/shared/services/ui.service.ts @@ -15,6 +15,7 @@ import { ApiUrlConfig } from 'framework'; export interface UISettingsDto { regexSuggestions: UIRegexSuggestionDto[]; + geocoderKey: string; } export interface UIRegexSuggestionDto { @@ -39,7 +40,7 @@ export class UIService { return this.http.get(url) .catch(error => { - return Observable.of({ regexSuggestions: [] }); + return Observable.of({ regexSuggestions: [], geocoderKey: '' }); }) .do(settings => { this.settings = settings; diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs index 3269c1d64..41ec7b66b 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs @@ -97,38 +97,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent new[] { " is not a valid value. Geolocation must have proper properties." }); } - [Fact] - public async Task Should_add_errors_if_geolocation_has_invalid_zip() - { - var sut = new GeolocationField(1, "my-geolocation", Partitioning.Invariant, new GeolocationFieldProperties { IsRequired = true }); - - var geolocation = new JObject( - new JProperty("latitude", 0), - new JProperty("longitude", 0), - new JProperty("zip", "1234")); - - await sut.ValidateAsync(CreateValue(geolocation), errors); - - errors.ShouldBeEquivalentTo( - new[] { " is not a valid value. ZIP Code must match postal code with optional suffix pattern." }); - } - - [Fact] - public async Task Should_add_errors_if_geolocation_has_invalid_state() - { - var sut = new GeolocationField(1, "my-geolocation", Partitioning.Invariant, new GeolocationFieldProperties { IsRequired = true }); - - var geolocation = new JObject( - new JProperty("latitude", 0), - new JProperty("longitude", 0), - new JProperty("state", "1")); - - await sut.ValidateAsync(CreateValue(geolocation), errors); - - errors.ShouldBeEquivalentTo( - new[] { " is not a valid value. State must be two capital letters." }); - } - [Fact] public async Task Should_add_errors_if_geolocation_is_required() { diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs index ecd6678fa..42572377c 100644 --- a/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs @@ -66,7 +66,7 @@ namespace Squidex.Domain.Apps.Write.Contents private readonly NamedContentData invalidGoogleMapsLatLongData = new NamedContentData() .AddField("my-geolocation-field1", new ContentFieldData() - .AddValue(JObject.FromObject(new { latitude = 0, longitude = (double?)null, address1 = "baddata" }))); + .AddValue(JObject.FromObject(new { latitude = 0, longitude = (double?)null, address = "baddata" }))); public ContentCommandMiddlewareTests() {