mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-12 23:35:58 +00:00
refactor(webapp): make group form's location select available as a separate component (#6245)
* WIP make location select a separate component * trying ... * fix update of geolocale from component * clean up location select code * refactor code * refactor code * clean up * linting * add first unit tests for location select * reuse location setting component in user settings * get it working * fix unit test * Update webapp/components/Select/LocationSelect.vue Co-authored-by: Wolfgang Huß <wolle.huss@pjannto.com> * Update webapp/components/Select/LocationSelect.spec.js Co-authored-by: Wolfgang Huß <wolle.huss@pjannto.com> * Update webapp/components/Select/LocationSelect.spec.js Co-authored-by: Wolfgang Huß <wolle.huss@pjannto.com> --------- Co-authored-by: Moriz Wahl <moriz.wahl@gmx.de> Co-authored-by: ogerly <fridolin@tutanota.com> Co-authored-by: Wolfgang Huß <wolle.huss@pjannto.com>
This commit is contained in:
parent
ba2e086b4b
commit
51564e5d9b
@ -110,29 +110,7 @@
|
||||
</ds-chip>
|
||||
|
||||
<!-- location -->
|
||||
<!-- TODO: move 'ds-select' from styleguide to main code and implement missing translation etc. functionality -->
|
||||
<ds-select
|
||||
id="city"
|
||||
:label="$t('settings.data.labelCity') + locationNameLabelAddOnOldName"
|
||||
v-model="formData.locationName"
|
||||
:options="cities"
|
||||
icon="map-marker"
|
||||
:icon-right="null"
|
||||
:placeholder="$t('settings.data.labelCity') + ' …'"
|
||||
:loading="loadingGeo"
|
||||
@input.native="handleCityInput"
|
||||
/>
|
||||
<base-button
|
||||
v-if="formLocationName !== ''"
|
||||
icon="close"
|
||||
ghost
|
||||
size="small"
|
||||
style="position: relative; display: inline-block; right: -96%; top: -33px; width: 26px"
|
||||
@click="formData.locationName = ''"
|
||||
></base-button>
|
||||
<ds-text class="location-hint" color="softer">
|
||||
{{ $t('settings.data.labelCityHint') }}
|
||||
</ds-text>
|
||||
<location-select v-model="formData.locationName" />
|
||||
|
||||
<ds-space margin-top="small" />
|
||||
|
||||
@ -176,11 +154,9 @@ import {
|
||||
} from '~/constants/groups.js'
|
||||
import Editor from '~/components/Editor/Editor'
|
||||
import ActionRadiusSelect from '~/components/Select/ActionRadiusSelect'
|
||||
import { queryLocations } from '~/graphql/location'
|
||||
import LocationSelect from '~/components/Select/LocationSelect'
|
||||
import GetCategories from '~/mixins/getCategoriesMixin.js'
|
||||
|
||||
let timeout
|
||||
|
||||
export default {
|
||||
name: 'GroupForm',
|
||||
mixins: [GetCategories],
|
||||
@ -188,6 +164,7 @@ export default {
|
||||
CategoriesSelect,
|
||||
Editor,
|
||||
ActionRadiusSelect,
|
||||
LocationSelect,
|
||||
},
|
||||
props: {
|
||||
update: {
|
||||
@ -215,12 +192,6 @@ export default {
|
||||
groupType: groupType || '',
|
||||
about: about || '',
|
||||
description: description || '',
|
||||
// from database 'locationName' comes as "string | null"
|
||||
// 'formData.locationName':
|
||||
// see 'created': tries to set it to a "requestGeoData" object and fills the menu if possible
|
||||
// if user selects one from menu we get a "requestGeoData" object here
|
||||
// "requestGeoData" object: "{ id: String, label: String, value: String }"
|
||||
// otherwise it's a string: empty or none empty
|
||||
locationName: locationName || '',
|
||||
actionRadius: actionRadius || '',
|
||||
categoryIds: categories ? categories.map((category) => category.id) : [],
|
||||
@ -259,11 +230,6 @@ export default {
|
||||
},
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
// set to "requestGeoData" object and fill select menu if possible
|
||||
this.formData.locationName =
|
||||
(await this.requestGeoData(this.formLocationName)) || this.formLocationName
|
||||
},
|
||||
computed: {
|
||||
formLocationName() {
|
||||
const isNestedValue =
|
||||
@ -276,9 +242,6 @@ export default {
|
||||
? this.formData.locationName
|
||||
: ''
|
||||
},
|
||||
locationNameLabelAddOnOldName() {
|
||||
return this.formLocationName !== '' ? ' — ' + this.formLocationName : ''
|
||||
},
|
||||
descriptionLength() {
|
||||
return this.$filters.removeHtml(this.formData.description).length
|
||||
},
|
||||
@ -322,6 +285,9 @@ export default {
|
||||
changeActionRadius(event) {
|
||||
this.$refs.groupForm.update('actionRadius', event.target.value)
|
||||
},
|
||||
changeLocation(event) {
|
||||
this.formData.locationName = event.target.value
|
||||
},
|
||||
updateEditorDescription(value) {
|
||||
this.$refs.groupForm.update('description', value)
|
||||
},
|
||||
@ -344,47 +310,6 @@ export default {
|
||||
})
|
||||
: this.$emit('createGroup', variables)
|
||||
},
|
||||
handleCityInput(event) {
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(
|
||||
() => this.requestGeoData(event.target ? event.target.value.trim() : ''),
|
||||
500,
|
||||
)
|
||||
},
|
||||
processLocationsResult(places) {
|
||||
if (!places.length) {
|
||||
return []
|
||||
}
|
||||
const result = []
|
||||
places.forEach((place) => {
|
||||
result.push({
|
||||
label: place.place_name,
|
||||
value: place.place_name,
|
||||
id: place.id,
|
||||
})
|
||||
})
|
||||
|
||||
return result
|
||||
},
|
||||
async requestGeoData(value) {
|
||||
if (value === '') {
|
||||
this.cities = []
|
||||
return
|
||||
}
|
||||
this.loadingGeo = true
|
||||
|
||||
const place = encodeURIComponent(value)
|
||||
const lang = this.$i18n.locale()
|
||||
|
||||
const {
|
||||
data: { queryLocations: result },
|
||||
} = await this.$apollo.query({ query: queryLocations(), variables: { place, lang } })
|
||||
|
||||
this.cities = this.processLocationsResult(result)
|
||||
this.loadingGeo = false
|
||||
|
||||
return this.cities.find((city) => city.value === value)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
52
webapp/components/Select/LocationSelect.spec.js
Normal file
52
webapp/components/Select/LocationSelect.spec.js
Normal file
@ -0,0 +1,52 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import LocationSelect from './LocationSelect'
|
||||
|
||||
const localVue = global.localVue
|
||||
const propsData = { value: 'nowhere' }
|
||||
|
||||
let wrapper
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((string) => string),
|
||||
$i18n: {
|
||||
locale: () => 'en',
|
||||
},
|
||||
}
|
||||
|
||||
describe('LocationSelect', () => {
|
||||
beforeEach(() => {})
|
||||
|
||||
describe('mount', () => {
|
||||
const Wrapper = () => {
|
||||
return mount(LocationSelect, { mocks, localVue, propsData })
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders the label', () => {
|
||||
expect(wrapper.find('label.ds-input-label').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders the select', () => {
|
||||
expect(wrapper.find('.ds-select').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders the clearLocationName button', () => {
|
||||
expect(wrapper.find('.base-button').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('clearLocationName button click', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.find('.base-button').trigger('click')
|
||||
})
|
||||
|
||||
it('emits an empty string', () => {
|
||||
expect(wrapper.emitted().input).toBeTruthy()
|
||||
expect(wrapper.emitted().input.length).toBe(1)
|
||||
expect(wrapper.emitted().input[0]).toEqual([''])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
118
webapp/components/Select/LocationSelect.vue
Normal file
118
webapp/components/Select/LocationSelect.vue
Normal file
@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div>
|
||||
<label class="ds-input-label">
|
||||
{{ `${$t('settings.data.labelCity')}` }}
|
||||
<span v-if="locationName">{{ `— ${locationName}` }}</span>
|
||||
</label>
|
||||
<ds-select
|
||||
id="city"
|
||||
v-model="currentValue"
|
||||
:options="cities"
|
||||
icon="map-marker"
|
||||
:icon-right="null"
|
||||
:placeholder="$t('settings.data.labelCity') + ' …'"
|
||||
:loading="loadingGeo"
|
||||
@input.native="handleCityInput"
|
||||
/>
|
||||
<base-button
|
||||
v-if="locationName !== ''"
|
||||
icon="close"
|
||||
ghost
|
||||
size="small"
|
||||
style="position: relative; display: inline-block; right: -94%; top: -48px; width: 29px"
|
||||
@click.native="clearLocationName"
|
||||
></base-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { queryLocations } from '~/graphql/location'
|
||||
|
||||
let timeout
|
||||
|
||||
export default {
|
||||
name: 'LocationSelect',
|
||||
props: {
|
||||
value: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
const result = await this.requestGeoData(this.locationName)
|
||||
this.$nextTick(() => {
|
||||
this.currentValue = result || this.locationName
|
||||
})
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentValue: this.value,
|
||||
loadingGeo: false,
|
||||
cities: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
locationName() {
|
||||
return typeof this.value === 'object' ? this.value.value : this.value
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentValue() {
|
||||
if (this.currentValue !== this.value) {
|
||||
this.$emit('input', this.currentValue)
|
||||
}
|
||||
},
|
||||
value() {
|
||||
if (this.value !== this.currentValue) {
|
||||
this.currentValue = this.value
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleCityInput(event) {
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(
|
||||
() => this.requestGeoData(event.target ? event.target.value.trim() : ''),
|
||||
500,
|
||||
)
|
||||
},
|
||||
processLocationsResult(places) {
|
||||
if (!places.length) {
|
||||
return []
|
||||
}
|
||||
const result = []
|
||||
places.forEach((place) => {
|
||||
result.push({
|
||||
label: place.place_name,
|
||||
value: place.place_name,
|
||||
id: place.id,
|
||||
})
|
||||
})
|
||||
|
||||
return result
|
||||
},
|
||||
async requestGeoData(value) {
|
||||
if (value === '') {
|
||||
this.cities = []
|
||||
return
|
||||
}
|
||||
this.loadingGeo = true
|
||||
|
||||
const place = encodeURIComponent(value)
|
||||
const lang = this.$i18n.locale()
|
||||
|
||||
const {
|
||||
data: { queryLocations: result },
|
||||
} = await this.$apollo.query({ query: queryLocations(), variables: { place, lang } })
|
||||
|
||||
this.cities = this.processLocationsResult(result)
|
||||
this.loadingGeo = false
|
||||
|
||||
return this.cities.find((city) => city.value === value)
|
||||
},
|
||||
clearLocationName(event) {
|
||||
event.target.value = ''
|
||||
this.$emit('input', event.target.value)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -1,6 +1,7 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import index from './index.vue'
|
||||
import Vuex from 'vuex'
|
||||
import LocationSelect from '~/components/Select/LocationSelect'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
@ -161,17 +162,7 @@ describe('index.vue', () => {
|
||||
describe('given a new location and hitting submit', () => {
|
||||
it('calls updateUser mutation', async () => {
|
||||
const wrapper = Wrapper()
|
||||
wrapper.setData({
|
||||
cities: [
|
||||
{
|
||||
label: 'Berlin, Germany',
|
||||
value: 'Berlin, Germany',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
wrapper.find('.ds-select-option').trigger('click')
|
||||
wrapper.findComponent(LocationSelect).vm.$emit('input', 'Berlin, Germany')
|
||||
wrapper.find('.ds-form').trigger('submit')
|
||||
|
||||
await expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
|
||||
@ -204,25 +195,9 @@ describe('index.vue', () => {
|
||||
describe('given new username, slug, location and about then hitting submit', () => {
|
||||
it('calls updateUser mutation', async () => {
|
||||
const wrapper = Wrapper()
|
||||
|
||||
wrapper.setData({
|
||||
cities: [
|
||||
{
|
||||
label: 'Berlin, Germany',
|
||||
value: 'Berlin, Germany',
|
||||
id: '1',
|
||||
},
|
||||
{
|
||||
label: 'Hamburg, Germany',
|
||||
value: 'Hamburg, Germany',
|
||||
id: '2',
|
||||
},
|
||||
],
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
wrapper.find('#name').setValue('Peter')
|
||||
wrapper.find('#slug').setValue('peter-der-lustige')
|
||||
wrapper.findAll('.ds-select-option').at(1).trigger('click')
|
||||
await wrapper.findComponent(LocationSelect).vm.$emit('input', 'Hamburg, Germany')
|
||||
wrapper.find('#about').setValue('I am Peter!111elf')
|
||||
wrapper.find('.ds-form').trigger('submit')
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ds-form class="settings-form" v-model="form" :schema="formSchema" @submit="submit">
|
||||
<ds-form class="settings-form" v-model="formData" :schema="formSchema" @submit="submit">
|
||||
<template #default="{ errors }">
|
||||
<base-card>
|
||||
<h2 class="title">{{ $t('settings.data.name') }}</h2>
|
||||
@ -15,20 +15,7 @@
|
||||
:placeholder="$t('settings.data.namePlaceholder')"
|
||||
/>
|
||||
<ds-input id="slug" model="slug" icon="at" :label="$t('settings.data.labelSlug')" />
|
||||
<!-- eslint-disable vue/use-v-on-exact -->
|
||||
<ds-select
|
||||
id="city"
|
||||
model="locationName"
|
||||
icon="map-marker"
|
||||
:options="cities"
|
||||
:label="$t('settings.data.labelCity')"
|
||||
:placeholder="$t('settings.data.labelCity')"
|
||||
:loading="loadingGeo"
|
||||
@input.native="handleCityInput"
|
||||
/>
|
||||
<ds-text class="location-hint" color="softer">
|
||||
{{ $t('settings.data.labelCityHint') }}
|
||||
</ds-text>
|
||||
<location-select v-model="formData.locationName" />
|
||||
<!-- eslint-enable vue/use-v-on-exact -->
|
||||
<ds-input
|
||||
id="about"
|
||||
@ -49,23 +36,35 @@
|
||||
<script>
|
||||
import { mapGetters, mapMutations } from 'vuex'
|
||||
import UniqueSlugForm from '~/components/utils/UniqueSlugForm'
|
||||
import LocationSelect from '~/components/Select/LocationSelect'
|
||||
import { updateUserMutation } from '~/graphql/User'
|
||||
import { queryLocations } from '~/graphql/location'
|
||||
import scrollToContent from './scroll-to-content.js'
|
||||
|
||||
let timeout
|
||||
|
||||
export default {
|
||||
mixins: [scrollToContent],
|
||||
name: 'NewsFeed',
|
||||
components: {
|
||||
LocationSelect,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
cities: [],
|
||||
loadingData: false,
|
||||
loadingGeo: false,
|
||||
formData: {},
|
||||
formData: {
|
||||
name: '',
|
||||
slug: '',
|
||||
about: '',
|
||||
locationName: '',
|
||||
},
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.formData.name = this.currentUser.name
|
||||
this.formData.slug = this.currentUser.slug
|
||||
this.formData.about = this.currentUser.about
|
||||
this.formData.locationName = this.currentUser.locationName || ''
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'auth/user',
|
||||
@ -77,18 +76,10 @@ export default {
|
||||
translate: this.$t,
|
||||
})
|
||||
return {
|
||||
name: { required: true, min: 3 },
|
||||
...uniqueSlugForm.formSchema,
|
||||
}
|
||||
},
|
||||
form: {
|
||||
get: function () {
|
||||
const { name, slug, locationName, about } = this.currentUser
|
||||
return { name, slug, locationName, about }
|
||||
},
|
||||
set: function (formData) {
|
||||
this.formData = formData
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapMutations({
|
||||
@ -123,52 +114,12 @@ export default {
|
||||
this.loadingData = false
|
||||
}
|
||||
},
|
||||
handleCityInput(value) {
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(() => this.requestGeoData(value), 500)
|
||||
},
|
||||
processLocationsResult(places) {
|
||||
if (!places.length) {
|
||||
return []
|
||||
}
|
||||
const result = []
|
||||
places.forEach((place) => {
|
||||
result.push({
|
||||
label: place.place_name,
|
||||
value: place.place_name,
|
||||
id: place.id,
|
||||
})
|
||||
})
|
||||
|
||||
return result
|
||||
},
|
||||
async requestGeoData(e) {
|
||||
const value = e.target ? e.target.value.trim() : ''
|
||||
if (value === '') {
|
||||
this.cities = []
|
||||
return
|
||||
}
|
||||
this.loadingGeo = true
|
||||
|
||||
const place = encodeURIComponent(value)
|
||||
const lang = this.$i18n.locale()
|
||||
|
||||
const {
|
||||
data: { queryLocations: res },
|
||||
} = await this.$apollo.query({ query: queryLocations(), variables: { place, lang } })
|
||||
|
||||
this.cities = this.processLocationsResult(res)
|
||||
this.loadingGeo = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
// .settings-form {
|
||||
// >
|
||||
.location-hint {
|
||||
margin-top: -$space-x-small - $space-xxx-small - $space-xxx-small;
|
||||
}
|
||||
// }
|
||||
</style>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user