change from using geojson object fullstack to use Location model in admin frontend

This commit is contained in:
einhornimmond 2024-06-27 14:54:48 +02:00
parent 70f8b6d18b
commit aadd00f175
15 changed files with 77 additions and 182 deletions

View File

@ -37,8 +37,8 @@
<span v-if="isValidLocation">
{{
$t('geo-coordinates.format', {
latitude: location.coordinates[1],
longitude: location.coordinates[0],
latitude: location.latitude,
longitude: location.longitude,
})
}}
</span>
@ -166,7 +166,7 @@ export default {
return this.originalGmsApiKey !== this.gmsApiKey
},
isValidLocation() {
return this.location && this.location.coordinates.length === 2
return this.location && this.location.latitude && this.location.longitude
},
},
methods: {

View File

@ -6,26 +6,26 @@ import VueI18n from 'vue-i18n'
Vue.use(VueI18n)
const localVue = global.localVue
const i18n = new VueI18n({
locale: 'en',
messages: {
en: {
'geo-coordinates.format': '{latitude}, {longitude}',
},
},
})
const mocks = {
$t: jest.fn((t, v) => {
if (t === 'geo-coordinates.format') {
return `${v.latitude}, ${v.longitude}`
}
return t
}),
}
describe('Coordinates', () => {
let wrapper
const value = {
type: 'Point',
coordinates: [12.34, 56.78],
latitude: 56.78,
longitude: 12.34,
}
const createWrapper = (propsData) => {
return mount(Coordinates, {
localVue,
i18n,
mocks,
propsData,
})
}
@ -49,8 +49,10 @@ describe('Coordinates', () => {
await latitudeInput.setValue('34.56')
await longitudeInput.setValue('78.90')
expect(wrapper.vm.latitude).toBe('34.56')
expect(wrapper.vm.longitude).toBe('78.90')
expect(wrapper.vm.inputValue).toStrictEqual({
latitude: 34.56,
longitude: 78.9,
})
})
it('emits input event with updated values', async () => {
@ -60,46 +62,28 @@ describe('Coordinates', () => {
await latitudeInput.setValue('34.56')
expect(wrapper.emitted().input).toBeTruthy()
expect(wrapper.emitted().input[0][0]).toEqual({
type: 'Point',
coordinates: [12.34, 34.56],
latitude: 34.56,
longitude: 12.34,
})
await longitudeInput.setValue('78.90')
expect(wrapper.emitted().input).toBeTruthy()
expect(wrapper.emitted().input[1][0]).toEqual({
type: 'Point',
coordinates: [78.9, 34.56],
latitude: 34.56,
longitude: 78.9,
})
})
it('updates latitudeLongitude when latitude or longitude changes', async () => {
const latitudeInput = wrapper.find('#home-community-latitude')
const longitudeInput = wrapper.find('#home-community-longitude')
await latitudeInput.setValue('34.56')
await longitudeInput.setValue('78.90')
expect(wrapper.vm.latitudeLongitude).toBe('34.56, 78.90')
})
it('splits coordinates correctly when entering in latitudeLongitude input', async () => {
const latitudeLongitudeInput = wrapper.find('#home-community-latitude-longitude-smart')
await latitudeLongitudeInput.setValue('34.56, 78.90')
await latitudeLongitudeInput.trigger('input')
expect(wrapper.vm.latitude).toBe(34.56)
expect(wrapper.vm.longitude).toBe(78.9)
})
it('sets inputValue to null if coordinates are invalid', async () => {
const latitudeInput = wrapper.find('#home-community-latitude')
const longitudeInput = wrapper.find('#home-community-longitude')
await latitudeInput.setValue('invalid')
await longitudeInput.setValue('78.90')
expect(wrapper.vm.inputValue).toBeNull()
expect(wrapper.vm.inputValue).toStrictEqual({
latitude: 34.56,
longitude: 78.9,
})
})
it('validates coordinates correctly', async () => {

View File

@ -11,7 +11,7 @@
:description="$t('geo-coordinates.latitude-longitude-smart.describe')"
>
<b-form-input
v-model="latitudeLongitude"
v-model="locationString"
id="home-community-latitude-longitude-smart"
type="text"
@input="splitCoordinates"
@ -19,7 +19,7 @@
</b-form-group>
<b-form-group :label="$t('latitude')" label-for="home-community-latitude">
<b-form-input
v-model="latitude"
v-model="inputValue.latitude"
id="home-community-latitude"
type="text"
@input="valueUpdated"
@ -27,7 +27,7 @@
</b-form-group>
<b-form-group :label="$t('longitude')" label-for="home-community-longitude">
<b-form-input
v-model="longitude"
v-model="inputValue.longitude"
id="home-community-longitude"
type="text"
@input="valueUpdated"
@ -47,21 +47,20 @@ export default {
data() {
return {
inputValue: this.value,
originalValueString: this.getLatitudeLongitudeString(this.value),
longitude: this.value ? this.value.coordinates[0] : '',
latitude: this.value ? this.value.coordinates[1] : '',
latitudeLongitude: this.getLatitudeLongitudeString(this.value),
originalValue: this.value,
locationString: this.getLatitudeLongitudeString(this.value),
}
},
computed: {
isValid() {
return (
(!isNaN(parseFloat(this.longitude)) && !isNaN(parseFloat(this.latitude))) ||
(this.longitude === '' && this.latitude === '')
(!isNaN(parseFloat(this.inputValue.longitude)) &&
!isNaN(parseFloat(this.inputValue.latitude))) ||
(this.inputValue.longitude === '' && this.inputValue.latitude === '')
)
},
isChanged() {
return this.getLatitudeLongitudeString(this.inputValue) !== this.originalValueString
return this.inputValue !== this.originalValue
},
},
methods: {
@ -72,44 +71,32 @@ export default {
if (parts.length === 2) {
const [lat, lon] = parts
if (!isNaN(parseFloat(lon) && !isNaN(parseFloat(lat)))) {
this.longitude = parseFloat(lon)
this.latitude = parseFloat(lat)
this.inputValue.longitude = parseFloat(lon)
this.inputValue.latitude = parseFloat(lat)
}
}
this.valueUpdated()
},
getLatitudeLongitudeString(geoJSONPoint) {
if (!geoJSONPoint || geoJSONPoint.coordinates.length !== 2) {
return ''
}
return this.$t('geo-coordinates.format', {
latitude: geoJSONPoint.coordinates[1],
longitude: geoJSONPoint.coordinates[0],
})
},
valueUpdated() {
if (this.longitude && this.latitude) {
this.inputValue = {
type: 'Point',
// format in geojson Point: coordinates[longitude, latitude]
coordinates: [this.longitude, this.latitude],
}
} else {
this.inputValue = null
}
this.latitudeLongitude = this.getLatitudeLongitudeString(this.inputValue)
sanitizeLocation(location) {
if (!location) return { latitude: '', longitude: '' }
if (this.inputValue) {
// make sure all coordinates are numbers
this.inputValue.coordinates = this.inputValue.coordinates
.map((coord) => parseFloat(coord))
// Remove null and NaN values
.filter((coord) => coord !== null && !isNaN(coord))
if (this.inputValue.coordinates.length !== 2) {
this.inputValue = null
}
const parseNumber = (value) => {
const number = parseFloat(value)
return isNaN(number) ? '' : number
}
return {
latitude: parseNumber(location.latitude),
longitude: parseNumber(location.longitude),
}
},
getLatitudeLongitudeString({ latitude, longitude } = {}) {
return latitude && longitude ? this.$t('geo-coordinates.format', { latitude, longitude }) : ''
},
valueUpdated(value) {
this.locationString = this.getLatitudeLongitudeString(this.inputValue)
this.inputValue = this.sanitizeLocation(this.inputValue)
if (this.isValid && this.isChanged) {
if (this.$parent.valueChanged) {
this.$parent.valueChanged()

View File

@ -1,7 +1,7 @@
import gql from 'graphql-tag'
export const updateHomeCommunity = gql`
mutation ($uuid: String!, $gmsApiKey: String, $location: Point) {
mutation ($uuid: String!, $gmsApiKey: String, $location: Location) {
updateHomeCommunity(uuid: $uuid, gmsApiKey: $gmsApiKey, location: $location) {
id
}

View File

@ -33,7 +33,6 @@
"email-templates": "^10.0.1",
"express": "^4.17.1",
"express-slow-down": "^2.0.1",
"geojson": "^0.5.0",
"gradido-database": "file:../database",
"graphql": "^15.5.1",
"graphql-request": "5.0.0",
@ -58,7 +57,6 @@
"@types/email-templates": "^10.0.1",
"@types/express": "^4.17.12",
"@types/faker": "^5.5.9",
"@types/geojson": "^7946.0.13",
"@types/i18n": "^0.13.4",
"@types/jest": "^27.0.2",
"@types/lodash.clonedeep": "^4.5.6",

View File

@ -1,8 +1,8 @@
import { IsString, IsUUID } from 'class-validator'
import { ArgsType, Field, InputType } from 'type-graphql'
import { Point } from '@/graphql/model/Point'
import { isValidPoint } from '@/graphql/validator/Point'
import { Location } from '@/graphql/model/Location'
import { isValidLocation } from '@/graphql/validator/Location'
@ArgsType()
@InputType()
@ -15,7 +15,7 @@ export class EditCommunityInput {
@IsString()
gmsApiKey?: string | null
@Field(() => Point, { nullable: true })
@isValidPoint()
location?: Point | null
@Field(() => Location, { nullable: true })
@isValidLocation()
location?: Location | null
}

View File

@ -1,9 +1,12 @@
import { Point } from '@dbTools/typeorm'
import { Community as DbCommunity } from '@entity/Community'
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
import { ObjectType, Field } from 'type-graphql'
import { Point2Location } from '@/graphql/resolver/util/Location2Point'
import { FederatedCommunity } from './FederatedCommunity'
import { Point } from './Point'
import { Location } from './Location'
@ObjectType()
export class AdminCommunityView {
@ -37,7 +40,9 @@ export class AdminCommunityView {
this.uuid = dbCom.communityUuid
this.authenticatedAt = dbCom.authenticatedAt
this.gmsApiKey = dbCom.gmsApiKey
this.location = dbCom.location
if (dbCom.location) {
this.location = Point2Location(dbCom.location as Point)
}
}
@Field(() => Boolean)
@ -64,8 +69,8 @@ export class AdminCommunityView {
@Field(() => String, { nullable: true })
gmsApiKey: string | null
@Field(() => Point, { nullable: true })
location: Point | null
@Field(() => Location, { nullable: true })
location: Location | null
@Field(() => Date, { nullable: true })
creationDate: Date | null

View File

@ -1,11 +0,0 @@
import { Position, Point as geojsonPoint } from 'geojson'
export class Point implements geojsonPoint {
constructor() {
this.coordinates = []
this.type = 'Point'
}
type: 'Point'
coordinates: Position
}

View File

@ -18,6 +18,7 @@ import {
getCommunityByUuid,
getHomeCommunity,
} from './util/communities'
import { Location2Point } from './util/Location2Point'
@Resolver()
export class CommunityResolver {
@ -90,7 +91,9 @@ export class CommunityResolver {
}
if (homeCom.gmsApiKey !== gmsApiKey || homeCom.location !== location) {
homeCom.gmsApiKey = gmsApiKey ?? null
homeCom.location = location ?? null
if (location) {
homeCom.location = Location2Point(location)
}
await DbCommunity.save(homeCom)
}
return new Community(homeCom)

View File

@ -1,35 +0,0 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { GraphQLScalarType, Kind } from 'graphql'
import { Point } from '@/graphql/model/Point'
export const PointScalar = new GraphQLScalarType({
name: 'Point',
description:
'The `Point` scalar type to represent longitude and latitude values of a geo location',
serialize(value: Point) {
// Check type of value
if (value.type !== 'Point') {
throw new Error(`PointScalar can only serialize Geometry type 'Point' values`)
}
return value
},
parseValue(value): Point {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (value.type !== 'Point') {
throw new Error(`PointScalar can only deserialize Geometry type 'Point' values`)
}
return value as Point
},
parseLiteral(ast) {
if (ast.kind !== Kind.STRING) {
throw new TypeError(`${String(ast)} is not a valid Geometry value.`)
}
const point = JSON.parse(ast.value) as Point
return point
},
})

View File

@ -7,10 +7,8 @@ import { buildSchema } from 'type-graphql'
import { Location } from '@model/Location'
import { isAuthorized } from './directive/isAuthorized'
import { Point } from './model/Point'
import { DecimalScalar } from './scalar/Decimal'
import { LocationScalar } from './scalar/Location'
import { PointScalar } from './scalar/Point'
export const schema = async (): Promise<GraphQLSchema> => {
return buildSchema({
@ -19,7 +17,6 @@ export const schema = async (): Promise<GraphQLSchema> => {
scalarsMap: [
{ type: Decimal, scalar: DecimalScalar },
{ type: Location, scalar: LocationScalar },
{ type: Point, scalar: PointScalar },
],
validate: {
validationError: { target: false },

View File

@ -1,28 +0,0 @@
import { registerDecorator, ValidationOptions, ValidationArguments } from 'class-validator'
import { Point } from '@/graphql/model/Point'
export function isValidPoint(validationOptions?: ValidationOptions) {
// eslint-disable-next-line @typescript-eslint/ban-types
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'isValidPoint',
target: object.constructor,
propertyName,
options: validationOptions,
validator: {
validate(value: Point) {
if (value.type === 'Point') {
if (value.coordinates.length === 2) {
return value.coordinates.every((coord) => typeof coord === 'number')
}
}
return false
},
defaultMessage(args: ValidationArguments) {
return `${propertyName} must be a valid Point in geoJSON Format, ${args.property}`
},
},
})
}
}

View File

@ -994,11 +994,6 @@
dependencies:
"@types/node" "*"
"@types/geojson@^7946.0.13":
version "7946.0.14"
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.14.tgz#319b63ad6df705ee2a65a73ef042c8271e696613"
integrity sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==
"@types/glob@^7.1.3":
version "7.1.4"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.4.tgz#ea59e21d2ee5c517914cb4bc8e4153b99e566672"

View File

@ -7,7 +7,7 @@ import {
UpdateDateColumn,
OneToMany,
JoinColumn,
Point,
Geometry,
} from 'typeorm'
import { FederatedCommunity } from '../FederatedCommunity'
import { GeometryTransformer } from '../../src/typeorm/GeometryTransformer'
@ -56,12 +56,12 @@ export class Community extends BaseEntity {
@Column({
name: 'location',
type: 'point',
type: 'geometry',
default: null,
nullable: true,
transformer: GeometryTransformer,
})
location: Point | null
location: Geometry | null
@CreateDateColumn({
name: 'created_at',

View File

@ -4,7 +4,7 @@
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(
'ALTER TABLE `communities` ADD COLUMN IF NOT EXISTS `location` POINT DEFAULT NULL NULL AFTER `gms_api_key`;',
'ALTER TABLE `communities` ADD COLUMN IF NOT EXISTS `location` geometry DEFAULT NULL NULL AFTER `gms_api_key`;',
)
}