diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs index 9da25e5e7..5d0f9eddb 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs @@ -54,7 +54,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas public async Task FindSchemaIdAsync(Guid appId, string name) { 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(); return schemaEntity != null ? Guid.Parse(schemaEntity["_id"].AsString) : Guid.Empty; diff --git a/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs b/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs index 7f96b8737..751a96f7f 100644 --- a/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs +++ b/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(); - return entity?.Version ?? 0; + return entity.Version; } - public async Task TryLockAsync() + public async Task TryLockAsync(int currentVersion) { var entity = await Collection.FindOneAndUpdateAsync(x => x.Id == DefaultId, - Update.Set(x => x.IsLocked, true), + Update + .Set(x => x.IsLocked, true) + .Set(x => x.Version, currentVersion), UpsertFind); - return entity?.IsLocked == false; + return entity == null || entity.IsLocked == false; } public Task UnlockAsync(int newVersion) diff --git a/src/Squidex.Infrastructure/Commands/CommandExtensions.cs b/src/Squidex.Infrastructure/Commands/CommandExtensions.cs index f6d174866..0c8a07c24 100644 --- a/src/Squidex.Infrastructure/Commands/CommandExtensions.cs +++ b/src/Squidex.Infrastructure/Commands/CommandExtensions.cs @@ -14,24 +14,24 @@ namespace Squidex.Infrastructure.Commands { public static class CommandExtensions { - public static Task CreateAsync(this IAggregateHandler handler, CommandContext context, Action creator) where T : class, IDomainObject + public static Task CreateAsync(this IAggregateHandler handler, CommandContext context, Action creator) where T : class, IDomainObject { - return handler.CreateAsync(context, creator.ToAsync()); + return handler.CreateAsync(context, creator.ToAsync()); } - public static Task UpdateAsync(this IAggregateHandler handler, CommandContext context, Action updater) where T : class, IDomainObject + public static Task UpdateAsync(this IAggregateHandler handler, CommandContext context, Action updater) where T : class, IDomainObject { - return handler.UpdateAsync(context, updater.ToAsync()); + return handler.UpdateAsync(context, updater.ToAsync()); } - public static Task CreateSyncedAsync(this IAggregateHandler handler, CommandContext context, Action creator) where T : class, IDomainObject + public static Task CreateSyncedAsync(this IAggregateHandler handler, CommandContext context, Action creator) where T : class, IDomainObject { - return handler.CreateSyncedAsync(context, creator.ToAsync()); + return handler.CreateSyncedAsync(context, creator.ToAsync()); } - public static Task UpdateSyncedAsync(this IAggregateHandler handler, CommandContext context, Action updater) where T : class, IDomainObject + public static Task UpdateSyncedAsync(this IAggregateHandler handler, CommandContext context, Action updater) where T : class, IDomainObject { - return handler.UpdateSyncedAsync(context, updater.ToAsync()); + return handler.UpdateSyncedAsync(context, updater.ToAsync()); } public static Task HandleAsync(this ICommandMiddleware commandMiddleware, CommandContext context) diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs index 840440bde..991f51438 100644 --- a/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs +++ b/src/Squidex.Infrastructure/Commands/DomainObjectBase.cs @@ -70,6 +70,8 @@ namespace Squidex.Infrastructure.Commands OnRaised(@event.To()); + state.Version++; + uncomittedEvents.Add(@event.To()); } @@ -95,8 +97,6 @@ namespace Squidex.Infrastructure.Commands if (events.Length > 0) { - state.Version += events.Length; - foreach (var @event in events) { @event.SetSnapshotVersion(state.Version); diff --git a/src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs b/src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs index 96f7e2043..3de9b5536 100644 --- a/src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs +++ b/src/Squidex.Infrastructure/Migrations/IMigrationStatus.cs @@ -14,7 +14,7 @@ namespace Squidex.Infrastructure.Migrations { Task GetVersionAsync(); - Task TryLockAsync(); + Task TryLockAsync(int currentVersion); Task UnlockAsync(int newVersion); } diff --git a/src/Squidex.Infrastructure/Migrations/Migrator.cs b/src/Squidex.Infrastructure/Migrations/Migrator.cs index 127bc7fcd..d5d015489 100644 --- a/src/Squidex.Infrastructure/Migrations/Migrator.cs +++ b/src/Squidex.Infrastructure/Migrations/Migrator.cs @@ -40,7 +40,9 @@ namespace Squidex.Infrastructure.Migrations try { - while (!await migrationStatus.TryLockAsync()) + var lastMigrator = migrations.FirstOrDefault(); + + while (!await migrationStatus.TryLockAsync(lastMigrator.ToVersion)) { log.LogInformation(w => w .WriteProperty("action", "Migrate") @@ -49,8 +51,6 @@ namespace Squidex.Infrastructure.Migrations await Task.Delay(LockWaitMs); } - var lastMigrator = migrations.FirstOrDefault(); - version = await migrationStatus.GetVersionAsync(); if (lastMigrator != null && lastMigrator.ToVersion != version) diff --git a/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs b/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs index 6c051df7f..c110d7517 100644 --- a/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs @@ -18,5 +18,17 @@ namespace Squidex.Areas.Api.Controllers.UI.Models /// [Required] public List RegexSuggestions { get; set; } + + /// + /// The type of the map control. + /// + [Required] + public string MapType { get; set; } + + /// + /// The key for the map control. + /// + [Required] + public string MapKey { get; set; } } } diff --git a/src/Squidex/Areas/Api/Controllers/UI/UIController.cs b/src/Squidex/Areas/Api/Controllers/UI/UIController.cs index 7d4e75afc..17a3dbc52 100644 --- a/src/Squidex/Areas/Api/Controllers/UI/UIController.cs +++ b/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.Value)) .Select(x => new UIRegexSuggestionDto { Name = x.Key, Pattern = x.Value }).ToList() - ?? new List() + ?? new List(), + MapType = uiOptions.Map?.Type ?? "OSM", + MapKey = uiOptions.Map?.GoogleMaps?.Key }; return Ok(dto); diff --git a/src/Squidex/Config/MyUIOptions.cs b/src/Squidex/Config/MyUIOptions.cs index f95e5c24b..37d32c449 100644 --- a/src/Squidex/Config/MyUIOptions.cs +++ b/src/Squidex/Config/MyUIOptions.cs @@ -13,5 +13,19 @@ namespace Squidex.Config public sealed class MyUIOptions { public Dictionary 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; } + } } } 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 a6ebdcd8c..b9da6ba1c 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 ad1a9b076..2f0579f5a 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, @@ -98,7 +97,6 @@ import { FileSizePipe, FocusOnInitDirective, FromNowPipe, - GeolocationEditorComponent, ImageSourceDirective, IndeterminateValueDirective, JscriptEditorComponent, @@ -148,7 +146,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..b84bfeef5 --- /dev/null +++ b/src/Squidex/app/shared/components/geolocation-editor.component.html @@ -0,0 +1,31 @@ +
+
+
+ +
+
+
+
+ +
+
+ + + +
+ +
+ +
+
+ + + +
+ +
+ +
+
+
+
\ 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..87fd5f1b7 --- /dev/null +++ b/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; +} \ 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..fc86a349d --- /dev/null +++ b/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: '© 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(); + }); + } + }); + } + + 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(); + } + } +} \ 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..181d2271c 100644 --- a/src/Squidex/app/shared/services/apps-store.service.spec.ts +++ b/src/Squidex/app/shared/services/apps-store.service.spec.ts @@ -29,13 +29,13 @@ describe('AppsStoreService', () => { beforeEach(() => { appsService = Mock.ofType(AppsService); - }); - it('should load automatically', () => { appsService.setup(x => x.getApps()) .returns(() => Observable.of(oldApps)) .verifiable(Times.once()); + }); + it('should load automatically', () => { const store = new AppsStoreService(appsService.object); let result1: AppDto[] | null = null; @@ -56,10 +56,6 @@ 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()); @@ -86,10 +82,6 @@ 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); store.selectApp('old-name').subscribe(isSelected => { diff --git a/src/Squidex/app/shared/services/ui.service.spec.ts b/src/Squidex/app/shared/services/ui.service.spec.ts index 129dc3598..182eb502a 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: [], mapType: 'OSM', mapKey: '' }; 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..633639291 100644 --- a/src/Squidex/app/shared/services/ui.service.ts +++ b/src/Squidex/app/shared/services/ui.service.ts @@ -15,6 +15,8 @@ import { ApiUrlConfig } from 'framework'; export interface UISettingsDto { regexSuggestions: UIRegexSuggestionDto[]; + mapType: string; + mapKey: string; } export interface UIRegexSuggestionDto { @@ -39,7 +41,7 @@ export class UIService { return this.http.get(url) .catch(error => { - return Observable.of({ regexSuggestions: [] }); + return Observable.of({ regexSuggestions: [], mapType: 'OSM', mapKey: '' }); }) .do(settings => { this.settings = settings; diff --git a/src/Squidex/appsettings.json b/src/Squidex/appsettings.json index 2709ed69d..ea9bf1082 100644 --- a/src/Squidex/appsettings.json +++ b/src/Squidex/appsettings.json @@ -19,14 +19,29 @@ "Slug": "^[a-z0-9]+(\\\\-[a-z0-9]+)*$", // Regex for urls. "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": { /* - * Setting the flag to true, enables well formatteds json logs. - */ - "human": true + * Setting the flag to true, enables well formatteds json logs. + */ + "human": true }, /* diff --git a/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs b/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs index c70e10290..2c479f9d7 100644 --- a/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs @@ -32,7 +32,7 @@ namespace Squidex.Infrastructure.Migrations return Task.FromResult(version); } - public Task TryLockAsync() + public Task TryLockAsync(int currentVersion) { var lockAcquired = false; @@ -65,7 +65,7 @@ namespace Squidex.Infrastructure.Migrations public MigratorTests() { A.CallTo(() => status.GetVersionAsync()).Returns(0); - A.CallTo(() => status.TryLockAsync()).Returns(true); + A.CallTo(() => status.TryLockAsync(A.Ignored)).Returns(true); } [Fact]