Browse Source

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.

pull/204/head
Alex Van Dyke 8 years ago
parent
commit
390ce3ecd1
  1. 19
      src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs
  2. 5
      src/Squidex.Infrastructure/Geocoding/GoogleMapsGeocoder.cs
  3. 5
      src/Squidex.Infrastructure/Geocoding/IGeocoder.cs
  4. 5
      src/Squidex.Infrastructure/Geocoding/OSMGeocoder.cs
  5. 4
      src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  6. 5
      src/Squidex/Areas/Api/Controllers/Apps/Models/AppCreatedDto.cs
  7. 5
      src/Squidex/Areas/Api/Controllers/Apps/Models/AppDto.cs
  8. 6
      src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs
  9. 8
      src/Squidex/Areas/Api/Controllers/UI/UIController.cs
  10. 14
      src/Squidex/app/shared/components/app-context.ts
  11. 32
      src/Squidex/app/shared/components/geolocation-editor.component.html
  12. 20
      src/Squidex/app/shared/components/geolocation-editor.component.scss
  13. 102
      src/Squidex/app/shared/components/geolocation-editor.component.ts
  14. 50
      src/Squidex/app/shared/services/apps-store.service.spec.ts
  15. 18
      src/Squidex/app/shared/services/apps-store.service.ts
  16. 15
      src/Squidex/app/shared/services/apps.service.spec.ts
  17. 8
      src/Squidex/app/shared/services/apps.service.ts
  18. 2
      src/Squidex/app/shared/services/ui.service.spec.ts
  19. 3
      src/Squidex/app/shared/services/ui.service.ts
  20. 32
      tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs
  21. 2
      tests/Squidex.Domain.Apps.Write.Tests/Contents/ContentCommandMiddlewareTests.cs

19
src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs

@ -69,7 +69,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
List<string> addressString = new List<string>(); List<string> addressString = new List<string>();
var validProperties = new string[] var validProperties = new string[]
{ {
"latitude", "longitude", "address1", "address2", "city", "state", "zip" "latitude", "longitude", "address"
}; };
foreach (var property in geolocation.Properties()) foreach (var property in geolocation.Properties())
@ -78,19 +78,16 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{ {
throw new InvalidCastException("Geolocation must have proper properties."); throw new InvalidCastException("Geolocation must have proper properties.");
} }
addressString.Add(geolocation[property.Name.ToLower()]?.ToString());
} }
var lat = geolocation["latitude"]; var lat = geolocation["latitude"];
var lon = geolocation["longitude"]; var lon = geolocation["longitude"];
var state = geolocation["state"]?.ToString(); var address = geolocation["address"]?.ToString();
var zip = geolocation["zip"]?.ToString();
if (lat == null || lon == null || if (lat == null || lon == null ||
((JValue)lat).Value == null || ((JValue)lon).Value == 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); lat = response.TryGetPropertyValue("Latitude", (JToken)null);
lon = response.TryGetPropertyValue("Longitude", (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."); 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; return geolocation;
} }

5
src/Squidex.Infrastructure/Geocoding/GoogleMapsGeocoder.cs

@ -1,6 +1,9 @@
// ========================================================================== // ==========================================================================
// GoogleMapsGeocoder.cs // GoogleMapsGeocoder.cs
// CivicPlus implementation of Squidex Headless CMS // Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ========================================================================== // ==========================================================================
using System; using System;

5
src/Squidex.Infrastructure/Geocoding/IGeocoder.cs

@ -1,6 +1,9 @@
// ========================================================================== // ==========================================================================
// IGeocoder.cs // IGeocoder.cs
// CivicPlus implementation of Squidex Headless CMS // Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ========================================================================== // ==========================================================================
namespace Squidex.Infrastructure.Geocoding namespace Squidex.Infrastructure.Geocoding

5
src/Squidex.Infrastructure/Geocoding/OSMGeocoder.cs

@ -1,6 +1,9 @@
// ========================================================================== // ==========================================================================
// OSMGeocoder.cs // OSMGeocoder.cs
// CivicPlus implementation of Squidex Headless CMS // Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ========================================================================== // ==========================================================================
using System; using System;

4
src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs

@ -69,7 +69,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
var response = apps.Select(a => 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]; dto.Permission = a.Contributors[subject];
@ -108,7 +108,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
var result = context.Result<EntityCreatedResult<Guid>>(); var result = context.Result<EntityCreatedResult<Guid>>();
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; response.Permission = AppContributorPermission.Owner;

5
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. /// Gets the next plan name.
/// </summary> /// </summary>
public string PlanUpgrade { get; set; } public string PlanUpgrade { get; set; }
/// <summary>
/// The geocoding api key for the application
/// </summary>
public string GeocoderKey { get; set; }
} }
} }

5
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. /// Gets the next plan name.
/// </summary> /// </summary>
public string PlanUpgrade { get; set; } public string PlanUpgrade { get; set; }
/// <summary>
/// The geocoding api key for the application
/// </summary>
public string GeocoderKey { get; set; }
} }
} }

6
src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs

@ -18,5 +18,11 @@ namespace Squidex.Areas.Api.Controllers.UI.Models
/// </summary> /// </summary>
[Required] [Required]
public List<UIRegexSuggestionDto> RegexSuggestions { get; set; } public List<UIRegexSuggestionDto> RegexSuggestions { get; set; }
/// <summary>
/// The geocoder key.
/// </summary>
[Required]
public string GeocoderKey { get; set; }
} }
} }

8
src/Squidex/Areas/Api/Controllers/UI/UIController.cs

@ -14,6 +14,7 @@ using NSwag.Annotations;
using Squidex.Areas.Api.Controllers.UI.Models; using Squidex.Areas.Api.Controllers.UI.Models;
using Squidex.Config; using Squidex.Config;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Geocoding;
using Squidex.Pipeline; using Squidex.Pipeline;
namespace Squidex.Areas.Api.Controllers.UI namespace Squidex.Areas.Api.Controllers.UI
@ -26,11 +27,13 @@ namespace Squidex.Areas.Api.Controllers.UI
public sealed class UIController : ApiController public sealed class UIController : ApiController
{ {
private readonly MyUIOptions uiOptions; private readonly MyUIOptions uiOptions;
private readonly IGeocoder geocoder;
public UIController(ICommandBus commandBus, IOptions<MyUIOptions> uiOptions) public UIController(ICommandBus commandBus, IOptions<MyUIOptions> uiOptions, IGeocoder geocoder)
: base(commandBus) : base(commandBus)
{ {
this.uiOptions = uiOptions.Value; this.uiOptions = uiOptions.Value;
this.geocoder = geocoder;
} }
/// <summary> /// <summary>
@ -50,7 +53,8 @@ namespace Squidex.Areas.Api.Controllers.UI
!string.IsNullOrWhiteSpace(x.Key) && !string.IsNullOrWhiteSpace(x.Key) &&
!string.IsNullOrWhiteSpace(x.Value)) !string.IsNullOrWhiteSpace(x.Value))
.Select(x => new UIRegexSuggestionDto { Name = x.Key, Pattern = x.Value }).ToList() .Select(x => new UIRegexSuggestionDto { Name = x.Key, Pattern = x.Value }).ToList()
?? new List<UIRegexSuggestionDto>() ?? new List<UIRegexSuggestionDto>(),
GeocoderKey = geocoder.Key ?? string.Empty
}; };
return Ok(dto); return Ok(dto);

14
src/Squidex/app/shared/components/app-context.ts

@ -18,13 +18,16 @@ import {
DialogService, DialogService,
ErrorDto, ErrorDto,
Notification, Notification,
Profile Profile,
UISettingsDto
} from './../declarations-base'; } from './../declarations-base';
@Injectable() @Injectable()
export class AppContext implements OnDestroy { export class AppContext implements OnDestroy {
private readonly appSubscription: Subscription; private readonly appSubscription: Subscription;
private readonly uiSettingsSubscription: Subscription;
private appField: AppDto; private appField: AppDto;
private uiSettingsField: UISettingsDto;
public get app(): AppDto { public get app(): AppDto {
return this.appField; return this.appField;
@ -38,6 +41,10 @@ export class AppContext implements OnDestroy {
return this.appField ? this.appField.name : ''; return this.appField ? this.appField.name : '';
} }
public get uiSettings() {
return this.uiSettingsField;
}
public get userToken(): string { public get userToken(): string {
return this.authService.user!.token; return this.authService.user!.token;
} }
@ -61,6 +68,11 @@ export class AppContext implements OnDestroy {
this.appsStore.selectedApp.take(1).subscribe(app => { this.appsStore.selectedApp.take(1).subscribe(app => {
this.appField = app; this.appField = app;
}); });
this.uiSettingsSubscription =
this.appsStore.uiSettings.subscribe(settings => {
this.uiSettingsField = settings;
});
} }
public ngOnDestroy() { public ngOnDestroy() {

32
src/Squidex/app/shared/components/geolocation-editor.component.html

@ -4,38 +4,16 @@
<input id="pac-input" class="form-control controls" type="text" placeholder="Search Google Maps" #searchBox /> <input id="pac-input" class="form-control controls" type="text" placeholder="Search Google Maps" #searchBox />
</form> </form>
<div> <div>
<form [formGroup]="geolocationForm" (change)="updateValueByInput()" [class.form-inline]="!useGoogleMaps"> <form class="form-inline" [formGroup]="geolocationForm" (change)="updateValueByInput()" (submit)="updateValueByInput()">
<sqx-control-errors for="state"></sqx-control-errors> <div class="form-group latitude-group">
<sqx-control-errors for="zip"></sqx-control-errors>
<div class="form-group latitude-group" [hidden]="useGoogleMaps">
<sqx-control-errors for="latitude" style="z-index: 10000;"></sqx-control-errors> <sqx-control-errors for="latitude" style="z-index: 10000;"></sqx-control-errors>
<label for="latitude">Lat. </label> <label for="latitude">Lat. </label>
<input type="number" class="form-control" formControlName="latitude" step="any"/> <input type="number" class="form-control" formControlName="latitude" step="any" />
</div> </div>
<div class="form-group longitude-group" [hidden]="useGoogleMaps"> <div class="form-group longitude-group">
<sqx-control-errors for="longitude" style="z-index: 10000;"></sqx-control-errors> <sqx-control-errors for="longitude" style="z-index: 10000;"></sqx-control-errors>
<label for="longitude">Long. </label> <label for="longitude">Long. </label>
<input type="number" class="form-control" formControlName="longitude" step="any"/> <input type="number" class="form-control" formControlName="longitude" step="any" />
</div>
<div class="form-group address1-group" [hidden]="!useGoogleMaps">
<label for="latitude">Address 1 </label>
<input type="text" class="form-control" formControlName="address1"/>
</div>
<div class="form-group address2-group" [hidden]="!useGoogleMaps">
<label for="longitude">Address 2 </label>
<input type="text" class="form-control" formControlName="address2"/>
</div>
<div class="form-group city-group" [hidden]="!useGoogleMaps" [class.hasClear]="hasValue">
<label for="latitude">City </label>
<input type="text" class="form-control" formControlName="city"/>
</div>
<div class="form-group state-group" [hidden]="!useGoogleMaps">
<label for="longitude">State </label>
<input type="text" class="form-control" formControlName="state" sqxUpperCaseInput/>
</div>
<div class="form-group zip-group" [hidden]="!useGoogleMaps">
<label for="longitude">ZIP Code </label>
<input type="text" class="form-control" formControlName="zip"/>
</div> </div>
<div class="form-group clear-group" [class.hidden]="!hasValue"> <div class="form-group clear-group" [class.hidden]="!hasValue">
<button type="reset" class="btn btn-link clear" [disabled]="isDisabled" (click)="reset()">Clear</button> <button type="reset" class="btn btn-link clear" [disabled]="isDisabled" (click)="reset()">Clear</button>

20
src/Squidex/app/shared/components/geolocation-editor.component.scss

@ -9,26 +9,6 @@
margin-top: .5rem; margin-top: .5rem;
margin-bottom: .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 { &.clear-group {
display: inline-block; display: inline-block;
&.hidden{ &.hidden{

102
src/Squidex/app/shared/components/geolocation-editor.component.ts

@ -25,11 +25,6 @@ export const SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
interface Geolocation { interface Geolocation {
latitude: number; latitude: number;
longitude: number; longitude: number;
address1: string;
address2: string;
city: string;
state: string;
zip: string;
} }
@Component({ @Component({
@ -53,19 +48,6 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
public geolocationForm = public geolocationForm =
this.formBuilder.group({ 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: [ latitude: [
'', '',
[ [
@ -93,7 +75,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
private readonly formBuilder: FormBuilder, private readonly formBuilder: FormBuilder,
private readonly ctx: AppContext private readonly ctx: AppContext
) { ) {
this.useGoogleMaps = this.ctx.app.geocoderKey !== ''; this.useGoogleMaps = this.ctx.uiSettings.geocoderKey !== '';
} }
public writeValue(value: Geolocation) { public writeValue(value: Geolocation) {
@ -109,7 +91,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
} }
public setDisabledState(isDisabled: boolean): void { public setDisabledState(isDisabled: boolean): void {
if (this.ctx.app.geocoderKey === '') { if (!this.useGoogleMaps) {
this.setDisabledStateOSM(isDisabled); this.setDisabledStateOSM(isDisabled);
} else { } else {
this.setDisabledStateGoogle(isDisabled); this.setDisabledStateGoogle(isDisabled);
@ -234,12 +216,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
this.value = { this.value = {
latitude: latlng.lat, latitude: latlng.lat,
longitude: latlng.lng, longitude: latlng.lng
address1: '',
address2: '',
city: '',
state: '',
zip: ''
}; };
this.updateMarker(false, true); this.updateMarker(false, true);
@ -259,7 +236,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
} }
private ngAfterViewInitGoogle() { 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, this.map = new google.maps.Map(this.editor.nativeElement,
{ {
@ -277,12 +254,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
if (!this.isDisabled) { if (!this.isDisabled) {
this.value = { this.value = {
latitude: event.latLng.lat(), latitude: event.latLng.lat(),
longitude: event.latLng.lng(), 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.updateMarker(false, true);
@ -305,7 +277,10 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
} }
if (!this.isDisabled) { 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); this.updateMarker(false, true);
} }
@ -330,40 +305,8 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
this.updateMarker(true, true); 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) { private updateMarker(zoom: boolean, fireEvent: boolean) {
if (this.ctx.app.geocoderKey === '') { if (!this.useGoogleMaps) {
this.updateMarkerOSM(zoom, fireEvent); this.updateMarkerOSM(zoom, fireEvent);
} else { } else {
this.updateMarkerGoogle(zoom, fireEvent); this.updateMarkerGoogle(zoom, fireEvent);
@ -380,12 +323,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
this.value = { this.value = {
latitude: latlng.lat, latitude: latlng.lat,
longitude: latlng.lng, longitude: latlng.lng};
address1: '',
address2: '',
city: '',
state: '',
zip: '' };
}); });
this.marker.on('dragend', () => { this.marker.on('dragend', () => {
@ -407,7 +345,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
this.marker.setLatLng(latLng); this.marker.setLatLng(latLng);
this.geolocationForm.setValue(this.fillMissingParameters(), { emitEvent: false, onlySelf: false }); this.geolocationForm.setValue(this.value, { emitEvent: false, onlySelf: false });
} else { } else {
if (this.marker) { if (this.marker) {
this.marker.removeFrom(this.map); this.marker.removeFrom(this.map);
@ -438,12 +376,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
if (!this.isDisabled) { if (!this.isDisabled) {
this.value = { this.value = {
latitude: event.latLng.lat(), latitude: event.latLng.lat(),
longitude: event.latLng.lng(), 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
}; };
} }
}); });
@ -451,12 +384,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
if (!this.isDisabled) { if (!this.isDisabled) {
this.value = { this.value = {
latitude: event.latLng.lat(), latitude: event.latLng.lat(),
longitude: event.latLng.lng(), 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.updateMarker(false, true);
@ -475,7 +403,7 @@ export class GeolocationEditorComponent implements ControlValueAccessor, AfterVi
this.marker.setPosition(latLng); this.marker.setPosition(latLng);
this.map.setZoom(16); this.map.setZoom(16);
this.geolocationForm.setValue(this.fillMissingParameters(), { emitEvent: false, onlySelf: false }); this.geolocationForm.setValue(this.value, { emitEvent: false, onlySelf: false });
} else { } else {
if (this.marker) { if (this.marker) {
this.marker.setMap(null); this.marker.setMap(null);

50
src/Squidex/app/shared/services/apps-store.service.spec.ts

@ -13,30 +13,39 @@ import {
AppsService, AppsService,
AppsStoreService, AppsStoreService,
CreateAppDto, CreateAppDto,
DateTime DateTime,
UIService,
UISettingsDto
} from './../'; } from './../';
describe('AppsStoreService', () => { describe('AppsStoreService', () => {
const now = DateTime.now(); const now = DateTime.now();
const oldApps = [ 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<AppsService>; let appsService: IMock<AppsService>;
let uiService: IMock<UIService>;
beforeEach(() => { beforeEach(() => {
appsService = Mock.ofType(AppsService); appsService = Mock.ofType(AppsService);
}); uiService = Mock.ofType(UIService);
it('should load automatically', () => {
appsService.setup(x => x.getApps()) appsService.setup(x => x.getApps())
.returns(() => Observable.of(oldApps)) .returns(() => Observable.of(oldApps))
.verifiable(Times.once()); .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 result1: AppDto[] | null = null;
let result2: AppDto[] | null = null; let result2: AppDto[] | null = null;
@ -56,15 +65,11 @@ describe('AppsStoreService', () => {
}); });
it('should add app to cache when created', () => { 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())) appsService.setup(x => x.postApp(It.isAny()))
.returns(() => Observable.of(newApp)) .returns(() => Observable.of(newApp))
.verifiable(Times.once()); .verifiable(Times.once());
const store = new AppsStoreService(appsService.object); const store = new AppsStoreService(appsService.object, uiService.object);
let result1: AppDto[] | null = null; let result1: AppDto[] | null = null;
let result2: AppDto[] | null = null; let result2: AppDto[] | null = null;
@ -86,11 +91,7 @@ describe('AppsStoreService', () => {
}); });
it('should select app', (done) => { it('should select app', (done) => {
appsService.setup(x => x.getApps()) const store = new AppsStoreService(appsService.object, uiService.object);
.returns(() => Observable.of(oldApps))
.verifiable(Times.once());
const store = new AppsStoreService(appsService.object);
store.selectApp('old-name').subscribe(isSelected => { store.selectApp('old-name').subscribe(isSelected => {
expect(isSelected).toBeTruthy(); expect(isSelected).toBeTruthy();
@ -104,4 +105,19 @@ describe('AppsStoreService', () => {
done(); 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();
});
});
}); });

18
src/Squidex/app/shared/services/apps-store.service.ts

@ -16,10 +16,13 @@ import {
CreateAppDto CreateAppDto
} from './apps.service'; } from './apps.service';
import { UIService, UISettingsDto } from './ui.service';
@Injectable() @Injectable()
export class AppsStoreService { export class AppsStoreService {
private readonly apps$ = new ReplaySubject<AppDto[]>(1); private readonly apps$ = new ReplaySubject<AppDto[]>(1);
private readonly app$ = new BehaviorSubject<AppDto | null>(null); private readonly app$ = new BehaviorSubject<AppDto | null>(null);
private readonly uiSettings$ = new BehaviorSubject<UISettingsDto | null>(null);
public get apps(): Observable<AppDto[]> { public get apps(): Observable<AppDto[]> {
return this.apps$; return this.apps$;
@ -29,8 +32,13 @@ export class AppsStoreService {
return this.app$; return this.app$;
} }
public get uiSettings(): Observable<UISettingsDto | null> {
return this.uiSettings$;
}
constructor( constructor(
private readonly appsService: AppsService private readonly appsService: AppsService,
private readonly uiService: UIService
) { ) {
if (!appsService) { if (!appsService) {
return; return;
@ -42,6 +50,14 @@ export class AppsStoreService {
}, error => { }, error => {
this.apps$.next([]); this.apps$.next([]);
}); });
this.uiService.getSettings()
.subscribe(settings => {
this.uiSettings$.next(settings);
},
error => {
this.uiSettings$.next(null);
});
} }
public selectApp(name: string | null): Observable<boolean> { public selectApp(name: string | null): Observable<boolean> {

15
src/Squidex/app/shared/services/apps.service.spec.ts

@ -59,8 +59,7 @@ describe('AppsService', () => {
created: '2016-01-01', created: '2016-01-01',
lastModified: '2016-02-02', lastModified: '2016-02-02',
planName: 'Free', planName: 'Free',
planUpgrade: 'Basic', planUpgrade: 'Basic'
geocoderKey: ''
}, },
{ {
id: '456', id: '456',
@ -69,14 +68,13 @@ describe('AppsService', () => {
created: '2017-01-01', created: '2017-01-01',
lastModified: '2017-02-02', lastModified: '2017-02-02',
planName: 'Basic', planName: 'Basic',
planUpgrade: 'Enterprise', planUpgrade: 'Enterprise'
geocoderKey: ''
} }
]); ]);
expect(apps).toEqual([ expect(apps).toEqual([
new AppDto('123', 'name1', 'Owner', DateTime.parseISO('2016-01-01'), DateTime.parseISO('2016-02-02'), 'Free', 'Basic', ''), 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('456', 'name2', 'Owner', DateTime.parseISO('2017-01-01'), DateTime.parseISO('2017-02-02'), 'Basic', 'Enterprise')
]); ]);
})); }));
@ -100,10 +98,9 @@ describe('AppsService', () => {
id: '123', id: '123',
permission: 'Reader', permission: 'Reader',
planName: 'Basic', 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'));
})); }));
}); });

8
src/Squidex/app/shared/services/apps.service.ts

@ -26,8 +26,7 @@ export class AppDto {
public readonly created: DateTime, public readonly created: DateTime,
public readonly lastModified: DateTime, public readonly lastModified: DateTime,
public readonly planName: string, public readonly planName: string,
public readonly planUpgrade: string, public readonly planUpgrade: string
public readonly geocoderKey: string
) { ) {
} }
} }
@ -65,8 +64,7 @@ export class AppsService {
DateTime.parseISO(item.created), DateTime.parseISO(item.created),
DateTime.parseISO(item.lastModified), DateTime.parseISO(item.lastModified),
item.planName, item.planName,
item.planUpgrade, item.planUpgrade);
item.geocoderKey);
}); });
}) })
.pretifyError('Failed to load apps. Please reload.'); .pretifyError('Failed to load apps. Please reload.');
@ -81,7 +79,7 @@ export class AppsService {
now = now || DateTime.now(); 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(() => { .do(() => {
this.analytics.trackEvent('App', 'Created', dto.name); this.analytics.trackEvent('App', 'Created', dto.name);

2
src/Squidex/app/shared/services/ui.service.spec.ts

@ -41,7 +41,7 @@ describe('UIService', () => {
settings1 = result; settings1 = result;
}); });
const response: UISettingsDto = { regexSuggestions: [] }; const response: UISettingsDto = { regexSuggestions: [], geocoderKey: '' };
const req = httpMock.expectOne('http://service/p/api/ui/settings'); const req = httpMock.expectOne('http://service/p/api/ui/settings');

3
src/Squidex/app/shared/services/ui.service.ts

@ -15,6 +15,7 @@ import { ApiUrlConfig } from 'framework';
export interface UISettingsDto { export interface UISettingsDto {
regexSuggestions: UIRegexSuggestionDto[]; regexSuggestions: UIRegexSuggestionDto[];
geocoderKey: string;
} }
export interface UIRegexSuggestionDto { export interface UIRegexSuggestionDto {
@ -39,7 +40,7 @@ export class UIService {
return this.http.get<UISettingsDto>(url) return this.http.get<UISettingsDto>(url)
.catch(error => { .catch(error => {
return Observable.of({ regexSuggestions: [] }); return Observable.of({ regexSuggestions: [], geocoderKey: '' });
}) })
.do(settings => { .do(settings => {
this.settings = settings; this.settings = settings;

32
tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs

@ -97,38 +97,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
new[] { "<FIELD> is not a valid value. Geolocation must have proper properties." }); new[] { "<FIELD> 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[] { "<FIELD> 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[] { "<FIELD> is not a valid value. State must be two capital letters." });
}
[Fact] [Fact]
public async Task Should_add_errors_if_geolocation_is_required() public async Task Should_add_errors_if_geolocation_is_required()
{ {

2
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() private readonly NamedContentData invalidGoogleMapsLatLongData = new NamedContentData()
.AddField("my-geolocation-field1", new ContentFieldData() .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() public ContentCommandMiddlewareTests()
{ {

Loading…
Cancel
Save