Browse Source

1) Google Maps integrated

2) Versioning fix
pull/208/head
Sebastian Stehle 8 years ago
parent
commit
c09cf212b2
  1. 2
      src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs
  2. 10
      src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs
  3. 16
      src/Squidex.Infrastructure/Commands/CommandExtensions.cs
  4. 4
      src/Squidex.Infrastructure/Commands/DomainObjectBase.cs
  5. 2
      src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs
  6. 6
      src/Squidex.Infrastructure/Migrations/Migrator.cs
  7. 12
      src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs
  8. 4
      src/Squidex/Areas/Api/Controllers/UI/UIController.cs
  9. 14
      src/Squidex/Config/MyUIOptions.cs
  10. 19
      src/Squidex/app/framework/angular/geolocation-editor.component.html
  11. 14
      src/Squidex/app/framework/angular/geolocation-editor.component.scss
  12. 217
      src/Squidex/app/framework/angular/geolocation-editor.component.ts
  13. 1
      src/Squidex/app/framework/declarations.ts
  14. 3
      src/Squidex/app/framework/module.ts
  15. 31
      src/Squidex/app/shared/components/geolocation-editor.component.html
  16. 20
      src/Squidex/app/shared/components/geolocation-editor.component.scss
  17. 396
      src/Squidex/app/shared/components/geolocation-editor.component.ts
  18. 1
      src/Squidex/app/shared/declarations.ts
  19. 3
      src/Squidex/app/shared/module.ts
  20. 12
      src/Squidex/app/shared/services/apps-store.service.spec.ts
  21. 2
      src/Squidex/app/shared/services/ui.service.spec.ts
  22. 4
      src/Squidex/app/shared/services/ui.service.ts
  23. 21
      src/Squidex/appsettings.json
  24. 4
      tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs

2
src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs

@ -54,7 +54,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas
public async Task<Guid> FindSchemaIdAsync(Guid appId, string name) public async Task<Guid> FindSchemaIdAsync(Guid appId, string name)
{ {
var schemaEntity = var schemaEntity =
await Collection.Find(x => x.Name == name).Only(x => x.Id) await Collection.Find(x => x.AppId == appId && x.Name == name).Only(x => x.Id)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
return schemaEntity != null ? Guid.Parse(schemaEntity["_id"].AsString) : Guid.Empty; return schemaEntity != null ? Guid.Parse(schemaEntity["_id"].AsString) : Guid.Empty;

10
src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs

@ -31,17 +31,19 @@ namespace Squidex.Infrastructure.Migrations
{ {
var entity = await Collection.Find(x => x.Id == DefaultId).FirstOrDefaultAsync(); var entity = await Collection.Find(x => x.Id == DefaultId).FirstOrDefaultAsync();
return entity?.Version ?? 0; return entity.Version;
} }
public async Task<bool> TryLockAsync() public async Task<bool> TryLockAsync(int currentVersion)
{ {
var entity = var entity =
await Collection.FindOneAndUpdateAsync<MongoMigrationEntity>(x => x.Id == DefaultId, await Collection.FindOneAndUpdateAsync<MongoMigrationEntity>(x => x.Id == DefaultId,
Update.Set(x => x.IsLocked, true), Update
.Set(x => x.IsLocked, true)
.Set(x => x.Version, currentVersion),
UpsertFind); UpsertFind);
return entity?.IsLocked == false; return entity == null || entity.IsLocked == false;
} }
public Task UnlockAsync(int newVersion) public Task UnlockAsync(int newVersion)

16
src/Squidex.Infrastructure/Commands/CommandExtensions.cs

@ -14,24 +14,24 @@ namespace Squidex.Infrastructure.Commands
{ {
public static class CommandExtensions public static class CommandExtensions
{ {
public static Task CreateAsync<T>(this IAggregateHandler handler, CommandContext context, Action<T> creator) where T : class, IDomainObject public static Task<T> CreateAsync<T>(this IAggregateHandler handler, CommandContext context, Action<T> creator) where T : class, IDomainObject
{ {
return handler.CreateAsync<T>(context, creator.ToAsync()); return handler.CreateAsync(context, creator.ToAsync());
} }
public static Task UpdateAsync<T>(this IAggregateHandler handler, CommandContext context, Action<T> updater) where T : class, IDomainObject public static Task<T> UpdateAsync<T>(this IAggregateHandler handler, CommandContext context, Action<T> updater) where T : class, IDomainObject
{ {
return handler.UpdateAsync<T>(context, updater.ToAsync()); return handler.UpdateAsync(context, updater.ToAsync());
} }
public static Task CreateSyncedAsync<T>(this IAggregateHandler handler, CommandContext context, Action<T> creator) where T : class, IDomainObject public static Task<T> CreateSyncedAsync<T>(this IAggregateHandler handler, CommandContext context, Action<T> creator) where T : class, IDomainObject
{ {
return handler.CreateSyncedAsync<T>(context, creator.ToAsync()); return handler.CreateSyncedAsync(context, creator.ToAsync());
} }
public static Task UpdateSyncedAsync<T>(this IAggregateHandler handler, CommandContext context, Action<T> updater) where T : class, IDomainObject public static Task<T> UpdateSyncedAsync<T>(this IAggregateHandler handler, CommandContext context, Action<T> updater) where T : class, IDomainObject
{ {
return handler.UpdateSyncedAsync<T>(context, updater.ToAsync()); return handler.UpdateSyncedAsync(context, updater.ToAsync());
} }
public static Task HandleAsync(this ICommandMiddleware commandMiddleware, CommandContext context) public static Task HandleAsync(this ICommandMiddleware commandMiddleware, CommandContext context)

4
src/Squidex.Infrastructure/Commands/DomainObjectBase.cs

@ -70,6 +70,8 @@ namespace Squidex.Infrastructure.Commands
OnRaised(@event.To<IEvent>()); OnRaised(@event.To<IEvent>());
state.Version++;
uncomittedEvents.Add(@event.To<IEvent>()); uncomittedEvents.Add(@event.To<IEvent>());
} }
@ -95,8 +97,6 @@ namespace Squidex.Infrastructure.Commands
if (events.Length > 0) if (events.Length > 0)
{ {
state.Version += events.Length;
foreach (var @event in events) foreach (var @event in events)
{ {
@event.SetSnapshotVersion(state.Version); @event.SetSnapshotVersion(state.Version);

2
src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs

@ -14,7 +14,7 @@ namespace Squidex.Infrastructure.Migrations
{ {
Task<int> GetVersionAsync(); Task<int> GetVersionAsync();
Task<bool> TryLockAsync(); Task<bool> TryLockAsync(int currentVersion);
Task UnlockAsync(int newVersion); Task UnlockAsync(int newVersion);
} }

6
src/Squidex.Infrastructure/Migrations/Migrator.cs

@ -40,7 +40,9 @@ namespace Squidex.Infrastructure.Migrations
try try
{ {
while (!await migrationStatus.TryLockAsync()) var lastMigrator = migrations.FirstOrDefault();
while (!await migrationStatus.TryLockAsync(lastMigrator.ToVersion))
{ {
log.LogInformation(w => w log.LogInformation(w => w
.WriteProperty("action", "Migrate") .WriteProperty("action", "Migrate")
@ -49,8 +51,6 @@ namespace Squidex.Infrastructure.Migrations
await Task.Delay(LockWaitMs); await Task.Delay(LockWaitMs);
} }
var lastMigrator = migrations.FirstOrDefault();
version = await migrationStatus.GetVersionAsync(); version = await migrationStatus.GetVersionAsync();
if (lastMigrator != null && lastMigrator.ToVersion != version) if (lastMigrator != null && lastMigrator.ToVersion != version)

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

@ -18,5 +18,17 @@ 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 type of the map control.
/// </summary>
[Required]
public string MapType { get; set; }
/// <summary>
/// The key for the map control.
/// </summary>
[Required]
public string MapKey { get; set; }
} }
} }

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

@ -50,7 +50,9 @@ 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>(),
MapType = uiOptions.Map?.Type ?? "OSM",
MapKey = uiOptions.Map?.GoogleMaps?.Key
}; };
return Ok(dto); return Ok(dto);

14
src/Squidex/Config/MyUIOptions.cs

@ -13,5 +13,19 @@ namespace Squidex.Config
public sealed class MyUIOptions public sealed class MyUIOptions
{ {
public Dictionary<string, string> RegexSuggestions { get; set; } public Dictionary<string, string> RegexSuggestions { get; set; }
public MapOptions Map { get; set; }
public sealed class MapOptions
{
public string Type { get; set; }
public MapGoogleOptions GoogleMaps { get; set; }
}
public sealed class MapGoogleOptions
{
public string Key { get; set; }
}
} }
} }

19
src/Squidex/app/framework/angular/geolocation-editor.component.html

@ -1,19 +0,0 @@
<div>
<div class="editor" #editor></div>
<div>
<form class="form-inline" [formGroup]="geolocationForm" (submit)="updateValueByInput()">
<div class="form-group latitude-group">
<input type="number" class="form-control" formControlName="latitude" step="any" #dateInput />
</div>
<div class="form-group longitude-group">
<input type="number" class="form-control" formControlName="longitude" step="any" />
</div>
<div class="form-group" [class.hidden]="!hasValue">
<button type="reset" class="btn btn-link clear" [disabled]="isDisabled" (click)="reset()">Clear</button>
</div>
<button type="submit" class="hidden"></button>
</form>
</div>
</div>

14
src/Squidex/app/framework/angular/geolocation-editor.component.scss

@ -1,14 +0,0 @@
@import '_mixins';
@import '_vars';
.editor {
height: 30rem;
}
.form-inline {
margin-top: .5rem;
}
.latitude-group {
margin-right: .25rem;
}

217
src/Squidex/app/framework/angular/geolocation-editor.component.ts

@ -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: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> 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();
}
}
}

1
src/Squidex/app/framework/declarations.ts

@ -17,7 +17,6 @@ export * from './angular/dialog-renderer.component';
export * from './angular/dropdown.component'; export * from './angular/dropdown.component';
export * from './angular/file-drop.directive'; export * from './angular/file-drop.directive';
export * from './angular/focus-on-init.directive'; export * from './angular/focus-on-init.directive';
export * from './angular/geolocation-editor.component';
export * from './angular/http-extensions-impl'; export * from './angular/http-extensions-impl';
export * from './angular/image-source.directive'; export * from './angular/image-source.directive';
export * from './angular/indeterminate-value.directive'; export * from './angular/indeterminate-value.directive';

3
src/Squidex/app/framework/module.ts

@ -32,7 +32,6 @@ import {
FileSizePipe, FileSizePipe,
FocusOnInitDirective, FocusOnInitDirective,
FromNowPipe, FromNowPipe,
GeolocationEditorComponent,
ImageSourceDirective, ImageSourceDirective,
IndeterminateValueDirective, IndeterminateValueDirective,
JscriptEditorComponent, JscriptEditorComponent,
@ -98,7 +97,6 @@ import {
FileSizePipe, FileSizePipe,
FocusOnInitDirective, FocusOnInitDirective,
FromNowPipe, FromNowPipe,
GeolocationEditorComponent,
ImageSourceDirective, ImageSourceDirective,
IndeterminateValueDirective, IndeterminateValueDirective,
JscriptEditorComponent, JscriptEditorComponent,
@ -148,7 +146,6 @@ import {
FileSizePipe, FileSizePipe,
FocusOnInitDirective, FocusOnInitDirective,
FromNowPipe, FromNowPipe,
GeolocationEditorComponent,
ImageSourceDirective, ImageSourceDirective,
IndeterminateValueDirective, IndeterminateValueDirective,
JscriptEditorComponent, JscriptEditorComponent,

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

@ -0,0 +1,31 @@
<div class="editor-container">
<form>
<div class="editor" #editor></div>
<input [class.hidden]="!isGoogleMaps" class="form-control search-control" type="text" [disabled]="isDisabled" placeholder="Search Google Maps" #searchBox />
</form>
<div>
<form class="form-inline no-gutters" [formGroup]="geolocationForm" (change)="updateValueByInput()" (submit)="updateValueByInput()">
<div class="form-group col-auto pr-2">
<label for="latitude">Latitude: </label>
</div>
<div class="form-group">
<sqx-control-errors for="latitude" style="z-index: 10000;"></sqx-control-errors>
<input type="number" class="form-control" formControlName="latitude" step="any" />
</div>
<div class="form-group col-auto pr-2 pl-2">
<label for="longitude">Longitude: </label>
</div>
<div class="form-group">
<sqx-control-errors for="longitude" style="z-index: 10000;"></sqx-control-errors>
<input type="number" class="form-control" formControlName="longitude" step="any" />
</div>
<div class="form-group col-auto">
<button [class.hidden]="!hasValue" type="reset" class="btn btn-link clear" [disabled]="isDisabled" (click)="reset()">Clear</button>
</div>
</form>
</div>
</div>

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

@ -0,0 +1,20 @@
@import '_mixins';
@import '_vars';
.editor {
height: 30rem;
}
.editor-container {
position: relative;
}
.search-control {
@include absolute(.5rem, auto, auto, .5rem);
@include box-shadow;
width: 40%;
}
.form-inline {
margin-top: .5rem;
}

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

@ -0,0 +1,396 @@
/*
* 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 { UIService } from './../services/ui.service';
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;
}
@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;
@ViewChild('searchBox')
public searchBoxInput: ElementRef;
public isGoogleMaps = false;
public isDisabled = false;
constructor(
private readonly resourceLoader: ResourceLoaderService,
private readonly formBuilder: FormBuilder,
private readonly uiService: UIService
) {
}
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 {
this.isDisabled = isDisabled;
if (!this.isGoogleMaps) {
this.setDisabledStateOSM(isDisabled);
} else {
this.setDisabledStateGoogle(isDisabled);
}
if (isDisabled) {
this.geolocationForm.disable();
} else {
this.geolocationForm.enable();
}
}
private setDisabledStateOSM(isDisabled: boolean): void {
const update: (t: any) => any =
isDisabled ?
x => x.enable() :
x => x.disable();
if (this.map) {
update(this.map.zoomControl);
this.map._handlers.forEach((handler: any) => {
update(handler);
});
}
if (this.marker) {
update(this.marker.dragging);
}
}
private setDisabledStateGoogle(isDisabled: boolean): void {
const enabled = !isDisabled;
if (this.map) {
this.map.setOptions({ draggable: enabled, zoomControl: enabled });
}
if (this.marker) {
this.marker.setDraggable(enabled);
}
}
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() {
this.uiService.getSettings()
.subscribe(settings => {
this.isGoogleMaps = settings.mapType === 'GoogleMaps';
if (!this.isGoogleMaps) {
this.ngAfterViewInitOSM();
} else {
this.ngAfterViewInitGoogle(settings.mapKey);
}
});
}
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: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> 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();
});
}
});
}
private ngAfterViewInitGoogle(apiKey: string) {
this.resourceLoader.loadScript(`https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places`).then(
() => {
this.map = new google.maps.Map(this.editor.nativeElement,
{
zoom: 1,
mapTypeControl: false,
streetViewControl: false,
center: { lat: 0, lng: 0 }
});
const searchBox = new google.maps.places.SearchBox(this.searchBoxInput.nativeElement);
this.map.addListener('click',
(event: any) => {
if (!this.isDisabled) {
this.value = {
latitude: event.latLng.lat(),
longitude: event.latLng.lng()
};
this.updateMarker(false, true);
}
});
this.map.addListener('bounds_changed', (event: any) => {
searchBox.setBounds(this.map.getBounds());
});
searchBox.addListener('places_changed', (event: any) => {
let places = searchBox.getPlaces();
if (places.length === 1) {
let place = places[0];
if (!place.geometry) {
return;
}
if (!this.isDisabled) {
let lat = place.geometry.location.lat();
let lng = place.geometry.location.lng();
this.value = { latitude: lat, longitude: lng };
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 updateMarker(zoom: boolean, fireEvent: boolean) {
if (!this.isGoogleMaps) {
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
};
});
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();
}
}
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()
};
}
});
this.marker.addListener('dragend', (event: any) => {
if (!this.isDisabled) {
this.value = {
latitude: event.latLng.lat(),
longitude: event.latLng.lng()
};
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(12);
this.geolocationForm.setValue(this.value, { emitEvent: false, onlySelf: false });
} else {
if (this.marker) {
this.marker.setMap(null);
this.marker = null;
}
this.map.setCenter({ lat: 0, lng: 0 });
this.geolocationForm.reset(undefined, { emitEvent: false, onlySelf: false });
}
if (fireEvent) {
this.callChange(this.value);
this.callTouched();
}
}
}

1
src/Squidex/app/shared/declarations.ts

@ -9,6 +9,7 @@ export * from './components/app-context';
export * from './components/app-form.component'; export * from './components/app-form.component';
export * from './components/asset.component'; export * from './components/asset.component';
export * from './components/help.component'; export * from './components/help.component';
export * from './components/geolocation-editor.component';
export * from './components/history.component'; export * from './components/history.component';
export * from './components/language-selector.component'; export * from './components/language-selector.component';
export * from './components/pipes'; export * from './components/pipes';

3
src/Squidex/app/shared/module.ts

@ -28,6 +28,7 @@ import {
ContentsService, ContentsService,
EventConsumersService, EventConsumersService,
FileIconPipe, FileIconPipe,
GeolocationEditorComponent,
GraphQlService, GraphQlService,
HelpComponent, HelpComponent,
HelpService, HelpService,
@ -73,6 +74,7 @@ import {
AssetPreviewUrlPipe, AssetPreviewUrlPipe,
AssetUrlPipe, AssetUrlPipe,
FileIconPipe, FileIconPipe,
GeolocationEditorComponent,
HelpComponent, HelpComponent,
HistoryComponent, HistoryComponent,
LanguageSelectorComponent, LanguageSelectorComponent,
@ -92,6 +94,7 @@ import {
AssetPreviewUrlPipe, AssetPreviewUrlPipe,
AssetUrlPipe, AssetUrlPipe,
FileIconPipe, FileIconPipe,
GeolocationEditorComponent,
HelpComponent, HelpComponent,
HistoryComponent, HistoryComponent,
LanguageSelectorComponent, LanguageSelectorComponent,

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

@ -29,13 +29,13 @@ describe('AppsStoreService', () => {
beforeEach(() => { beforeEach(() => {
appsService = Mock.ofType(AppsService); appsService = Mock.ofType(AppsService);
});
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());
});
it('should load automatically', () => {
const store = new AppsStoreService(appsService.object); const store = new AppsStoreService(appsService.object);
let result1: AppDto[] | null = null; let result1: AppDto[] | null = null;
@ -56,10 +56,6 @@ 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());
@ -86,10 +82,6 @@ describe('AppsStoreService', () => {
}); });
it('should select app', (done) => { 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);
store.selectApp('old-name').subscribe(isSelected => { store.selectApp('old-name').subscribe(isSelected => {

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: [], mapType: 'OSM', mapKey: '' };
const req = httpMock.expectOne('http://service/p/api/ui/settings'); const req = httpMock.expectOne('http://service/p/api/ui/settings');

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

@ -15,6 +15,8 @@ import { ApiUrlConfig } from 'framework';
export interface UISettingsDto { export interface UISettingsDto {
regexSuggestions: UIRegexSuggestionDto[]; regexSuggestions: UIRegexSuggestionDto[];
mapType: string;
mapKey: string;
} }
export interface UIRegexSuggestionDto { export interface UIRegexSuggestionDto {
@ -39,7 +41,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: [], mapType: 'OSM', mapKey: '' });
}) })
.do(settings => { .do(settings => {
this.settings = settings; this.settings = settings;

21
src/Squidex/appsettings.json

@ -19,14 +19,29 @@
"Slug": "^[a-z0-9]+(\\\\-[a-z0-9]+)*$", "Slug": "^[a-z0-9]+(\\\\-[a-z0-9]+)*$",
// Regex for urls. // Regex for urls.
"Url": "^(?:http(s)?:\\/\\/)?[\\w.-]+(?:\\.[\\w\\.-]+)+[\\w\\-\\._~:/?#[\\]@!\\$&'\\(\\)\\*\\+,;=.]+$" "Url": "^(?:http(s)?:\\/\\/)?[\\w.-]+(?:\\.[\\w\\.-]+)+[\\w\\-\\._~:/?#[\\]@!\\$&'\\(\\)\\*\\+,;=.]+$"
},
"map": {
/*
* Define the type of the geolocation service.
*
* Supported: GoogleMaps, OSM
*/
"type": "OSM",
"googleMaps": {
/*
* The optional google maps API key. CREATE YOUR OWN PLEASE.
*/
"key": "AIzaSyB_Z8l3nwUxZhMJykiDUJy6bSHXXlwcYMg"
}
} }
}, },
"logging": { "logging": {
/* /*
* Setting the flag to true, enables well formatteds json logs. * Setting the flag to true, enables well formatteds json logs.
*/ */
"human": true "human": true
}, },
/* /*

4
tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs

@ -32,7 +32,7 @@ namespace Squidex.Infrastructure.Migrations
return Task.FromResult(version); return Task.FromResult(version);
} }
public Task<bool> TryLockAsync() public Task<bool> TryLockAsync(int currentVersion)
{ {
var lockAcquired = false; var lockAcquired = false;
@ -65,7 +65,7 @@ namespace Squidex.Infrastructure.Migrations
public MigratorTests() public MigratorTests()
{ {
A.CallTo(() => status.GetVersionAsync()).Returns(0); A.CallTo(() => status.GetVersionAsync()).Returns(0);
A.CallTo(() => status.TryLockAsync()).Returns(true); A.CallTo(() => status.TryLockAsync(A<int>.Ignored)).Returns(true);
} }
[Fact] [Fact]

Loading…
Cancel
Save