fix(webapp): fix lang query location (#9337)

This commit is contained in:
Ulf Gebhardt 2026-03-01 11:00:37 +01:00 committed by GitHub
parent 4b0470310e
commit d543c192a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 135 additions and 88 deletions

View File

@ -40,6 +40,7 @@ export const getContext =
req,
cypherParams: {
currentUserId: user ? user.id : null,
languageDefault: config.LANGUAGE_DEFAULT.toUpperCase(),
},
config,
}

View File

@ -13,6 +13,7 @@ export default {
nameNL: { type: 'string' },
namePL: { type: 'string' },
nameRU: { type: 'string' },
nameSQ: { type: 'string' },
isIn: {
type: 'relationship',
relationship: 'IS_IN',

View File

@ -14,6 +14,7 @@ export interface LocationDbProperties {
namePL: string
namePT: string
nameRU: string
nameSQ: string
type: string
}

View File

@ -42,8 +42,6 @@ mutation CreateGroup(
location {
id
name
nameDE
nameEN
}
myRole
}

View File

@ -25,8 +25,6 @@ query Group($isMember: Boolean, $id: ID, $slug: String) {
location {
id
name
nameDE
nameEN
}
myRole
inviteCodes {

View File

@ -43,8 +43,6 @@ mutation UpdateGroup(
location {
id
name
nameDE
nameEN
}
myRole
}

View File

@ -37,9 +37,6 @@ mutation UpdateUser(
location {
id
name
nameDE
nameEN
nameRU
}
emailNotificationSettings {
type

View File

@ -302,8 +302,6 @@ describe('in mode', () => {
locationName: 'Hamburg, Germany',
location: expect.objectContaining({
name: 'Hamburg',
nameDE: 'Hamburg',
nameEN: 'Hamburg',
}),
},
},
@ -551,8 +549,6 @@ describe('in mode', () => {
locationName: 'Hamburg, Germany',
location: expect.objectContaining({
name: 'Hamburg',
nameDE: 'Hamburg',
nameEN: 'Hamburg',
}),
myRole: 'owner',
}),
@ -2895,8 +2891,6 @@ describe('in mode', () => {
locationName: 'Berlin, Germany',
location: expect.objectContaining({
name: 'Berlin',
nameDE: 'Berlin',
nameEN: 'Berlin',
}),
myRole: 'owner',
},
@ -2947,8 +2941,6 @@ describe('in mode', () => {
locationName: 'Paris, France',
location: expect.objectContaining({
name: 'Paris',
nameDE: 'Paris',
nameEN: 'Paris',
}),
myRole: 'owner',
},
@ -2975,8 +2967,6 @@ describe('in mode', () => {
locationName: 'Hamburg, Germany',
location: expect.objectContaining({
name: 'Hamburg',
nameDE: 'Hamburg',
nameEN: 'Hamburg',
}),
myRole: 'owner',
},

View File

@ -1,17 +1,29 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { parse } from 'graphql'
import Factory, { cleanDatabase } from '@db/factories'
import UpdateUser from '@graphql/queries/users/UpdateUser.gql'
import User from '@graphql/queries/users/User.gql'
import { createApolloTestSetup } from '@root/test/helpers'
import type { ApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
const UserLocationName = parse(`
query User($id: ID, $lang: String) {
User(id: $id) {
id
location {
id
name(lang: $lang)
}
}
}
`)
let authenticatedUser: Context['user']
const context = () => ({ authenticatedUser })
let mutate: ApolloTestSetup['mutate']
let query: ApolloTestSetup['query']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
@ -19,7 +31,6 @@ let server: ApolloTestSetup['server']
beforeAll(async () => {
await cleanDatabase()
const apolloSetup = await createApolloTestSetup({ context })
mutate = apolloSetup.mutate
query = apolloSetup.query
database = apolloSetup.database
server = apolloSetup.server
@ -39,41 +50,84 @@ afterEach(async () => {
describe('resolvers', () => {
describe('Location', () => {
describe('custom mutation, not handled by neo4j-graphql-js', () => {
let variables
describe('name(lang)', () => {
beforeEach(async () => {
variables = {
id: 'u47',
name: 'John Doughnut',
}
const Paris = await Factory.build('location', {
id: 'region.9397217726497330',
name: 'Paris',
const Hamburg = await Factory.build('location', {
id: 'region.5127278006398860',
name: 'Hamburg',
type: 'region',
lng: 2.35183,
lat: 48.85658,
nameEN: 'Paris',
lng: 10.0,
lat: 53.55,
nameEN: 'Hamburg',
nameDE: 'Hamburg',
nameIT: 'Amburgo',
nameRU: 'Гамбург',
nameFR: 'Hambourg',
nameES: 'Hamburgo',
})
const user = await Factory.build('user', {
id: 'u47',
name: 'John Doe',
})
await user.relateTo(Paris, 'isIn')
await user.relateTo(Hamburg, 'isIn')
authenticatedUser = await user.toJson()
})
it('returns `null` if location translation is not available', async () => {
await expect(mutate({ mutation: UpdateUser, variables })).resolves.toMatchObject({
it('returns the name in the requested language', async () => {
await expect(
query({ query: UserLocationName, variables: { id: 'u47', lang: 'RU' } }),
).resolves.toMatchObject({
data: {
UpdateUser: {
name: 'John Doughnut',
location: {
nameRU: null,
nameEN: 'Paris',
},
},
User: [
expect.objectContaining({
location: expect.objectContaining({ name: 'Гамбург' }),
}),
],
},
errors: undefined,
})
})
it('returns a different name for a different language', async () => {
await expect(
query({ query: UserLocationName, variables: { id: 'u47', lang: 'IT' } }),
).resolves.toMatchObject({
data: {
User: [
expect.objectContaining({
location: expect.objectContaining({ name: 'Amburgo' }),
}),
],
},
errors: undefined,
})
})
it('returns the default name when no lang is provided', async () => {
await expect(
query({ query: UserLocationName, variables: { id: 'u47' } }),
).resolves.toMatchObject({
data: {
User: [
expect.objectContaining({
location: expect.objectContaining({ name: 'Hamburg' }),
}),
],
},
errors: undefined,
})
})
it('falls back to default when the requested translation does not exist', async () => {
await expect(
query({ query: UserLocationName, variables: { id: 'u47', lang: 'ZZ' } }),
).resolves.toMatchObject({
data: {
User: [
expect.objectContaining({
location: expect.objectContaining({ name: 'Hamburg' }),
}),
],
},
errors: undefined,
})

View File

@ -5,26 +5,12 @@
/* eslint-disable @typescript-eslint/return-await */
import { UserInputError } from '@graphql/errors'
import Resolver from './helpers/Resolver'
import { queryLocations } from './users/location'
import type { Context } from '@src/context'
export default {
Location: {
...Resolver('Location', {
undefinedToNull: [
'nameEN',
'nameDE',
'nameFR',
'nameNL',
'nameIT',
'nameES',
'namePT',
'namePL',
'nameRU',
],
}),
distanceToMe: async (parent, _params, context: Context, _resolveInfo) => {
if (!parent.id) {
throw new Error('Can not identify selected Location!')

View File

@ -245,8 +245,6 @@ describe('UpdateUser', () => {
locationName: 'Hamburg, New Jersey, United States',
location: expect.objectContaining({
name: 'Hamburg',
nameDE: 'Hamburg',
nameEN: 'Hamburg',
}),
},
},

View File

@ -34,6 +34,7 @@ const newlyCreatedNodesWithLocales = [
nameRU: 'Вельцхайм',
nameNL: 'Welzheim',
namePL: 'Welzheim',
nameSQ: 'Welzheim',
lng: 9.634301,
lat: 48.874393,
},
@ -50,6 +51,7 @@ const newlyCreatedNodesWithLocales = [
namePL: 'Badenia-Wirtembergia',
namePT: 'Baden-Württemberg',
nameRU: 'Баден-Вюртемберг',
nameSQ: 'Baden-Vyrtemberg',
},
country: {
id: expect.stringContaining('country'),
@ -64,6 +66,7 @@ const newlyCreatedNodesWithLocales = [
namePL: 'Niemcy',
namePT: 'Alemanha',
nameRU: 'Германия',
nameSQ: 'Gjermania',
},
},
]

View File

@ -13,7 +13,7 @@ import { UserInputError } from '@graphql/errors'
import type { Context } from '@src/context'
const locales = ['en', 'de', 'fr', 'nl', 'it', 'es', 'pt', 'pl', 'ru']
const locales = ['en', 'de', 'fr', 'nl', 'it', 'es', 'pt', 'pl', 'ru', 'sq']
const REQUEST_TIMEOUT = 3000
@ -29,6 +29,7 @@ const createLocation = async (session, mapboxData) => {
namePT: mapboxData.text_pt,
namePL: mapboxData.text_pl,
nameRU: mapboxData.text_ru,
nameSQ: mapboxData.text_sq,
type: mapboxData.id.split('.')[0].toLowerCase(),
address: mapboxData.address,
lng: mapboxData.center?.length ? mapboxData.center[0] : null,
@ -47,6 +48,7 @@ const createLocation = async (session, mapboxData) => {
'l.namePT = $namePT, ' +
'l.namePL = $namePL, ' +
'l.nameRU = $nameRU, ' +
'l.nameSQ = $nameSQ, ' +
'l.type = $type'
if (data.lat && data.lng) {

View File

@ -1,15 +1,17 @@
type Location {
id: ID!
name: String!
nameEN: String
nameDE: String
nameFR: String
nameNL: String
nameIT: String
nameES: String
namePT: String
namePL: String
nameRU: String
name(lang: String = ""): String!
@cypher(
statement: """
RETURN COALESCE(
CASE WHEN $lang <> '' THEN this['name' + toUpper($lang)] END,
this['name' + $cypherParams.languageDefault],
this.name,
this.nameEN,
this.id
)
"""
)
type: String!
lat: Float
lng: Float

View File

@ -59,6 +59,7 @@ describe('LocationSelect', () => {
place: 'nowhere',
lang: 'en',
},
fetchPolicy: 'network-only',
})
})
@ -69,8 +70,8 @@ describe('LocationSelect', () => {
it('emits an empty string', () => {
expect(wrapper.emitted().input).toBeTruthy()
expect(wrapper.emitted().input.length).toBe(1)
expect(wrapper.emitted().input[0]).toEqual([''])
const lastEmit = wrapper.emitted().input[wrapper.emitted().input.length - 1]
expect(lastEmit).toEqual([''])
})
})

View File

@ -40,6 +40,7 @@ export default {
components: { OsButton, OsIcon },
props: {
value: {
type: [String, Object],
required: true,
},
canBeCleared: {
@ -55,10 +56,7 @@ export default {
},
async created() {
this.icons = iconRegistry
const result = await this.requestGeoData(this.locationName)
this.$nextTick(() => {
this.currentValue = result || this.locationName
})
await this.resolveLocalizedLocation()
},
data() {
return {
@ -74,6 +72,9 @@ export default {
locationNameLabelAddOnOldName() {
return this.locationName !== '' && this.showPreviousLocation ? ' — ' + this.locationName : ''
},
currentLocale() {
return this.$store && this.$store.state.i18n && this.$store.state.i18n.locale
},
},
watch: {
currentValue() {
@ -81,10 +82,19 @@ export default {
this.$emit('input', this.currentValue)
}
},
value() {
if (this.value !== this.currentValue) {
this.currentValue = this.value
value(newVal, oldVal) {
if (newVal !== this.currentValue) {
this.currentValue = newVal
}
// resolve when value is set after initial mount (e.g. settings page)
const newName = typeof newVal === 'object' ? newVal.value : newVal
const oldName = typeof oldVal === 'object' ? oldVal.value : oldVal
if (newName && newName !== oldName) {
this.resolveLocalizedLocation()
}
},
currentLocale() {
this.resolveLocalizedLocation()
},
},
methods: {
@ -124,7 +134,11 @@ export default {
const {
data: { queryLocations: result },
} = await this.$apollo.query({ query: queryLocations(), variables: { place, lang } })
} = await this.$apollo.query({
query: queryLocations(),
variables: { place, lang },
fetchPolicy: 'network-only',
})
this.cities = this.processLocationsResult(result)
this.loadingGeo = false
@ -136,6 +150,13 @@ export default {
this.loadingGeo = false
}
},
async resolveLocalizedLocation() {
if (!this.locationName) return
const result = await this.requestGeoData(this.locationName)
this.$nextTick(() => {
this.currentValue = result || (this.cities.length ? this.cities[0] : this.locationName)
})
},
clearLocationName() {
this.currentValue = ''
},

View File

@ -5,7 +5,7 @@ export const location = (type, lang) => gql`
locationName
location {
id
name: name${lang}
name(lang: "${lang}")
lng
lat
distanceToMe

View File

@ -149,8 +149,6 @@ describe('GroupProfileSlug', () => {
locationName: 'France',
location: {
name: 'Paris',
nameDE: 'Paris',
nameEN: 'Paris',
},
isMutedByMe: true,
membersCount: 0,
@ -193,8 +191,6 @@ describe('GroupProfileSlug', () => {
locationName: 'Hamburg, Germany',
location: {
name: 'Hamburg',
nameDE: 'Hamburg',
nameEN: 'Hamburg',
},
isMutedByMe: false,
membersCount: 0,