mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge pull request #3323 from gradido/admin_add_location_alongside_gms_api_key
feat(admin): geo-coordinates for community
This commit is contained in:
commit
54c1bd4cdc
@ -113,7 +113,7 @@
|
||||
{{ $t('contributionLink.clear') }}
|
||||
</b-button>
|
||||
<b-button @click.prevent="$emit('closeContributionForm')">
|
||||
{{ $t('contributionLink.close') }}
|
||||
{{ $t('close') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-form>
|
||||
|
||||
@ -1,9 +1,19 @@
|
||||
import { createMockClient } from 'mock-apollo-client'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import VueApollo from 'vue-apollo'
|
||||
import Vuex from 'vuex'
|
||||
import CommunityVisualizeItem from './CommunityVisualizeItem.vue'
|
||||
import { updateHomeCommunity } from '../../graphql/updateHomeCommunity'
|
||||
import { toastSuccessSpy } from '../../../test/testSetup'
|
||||
|
||||
const mockClient = createMockClient()
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: mockClient,
|
||||
})
|
||||
|
||||
const localVue = global.localVue
|
||||
localVue.use(Vuex)
|
||||
localVue.use(VueApollo)
|
||||
const today = new Date()
|
||||
const createdDate = new Date()
|
||||
createdDate.setDate(createdDate.getDate() - 3)
|
||||
@ -19,7 +29,7 @@ const store = new Vuex.Store({
|
||||
|
||||
let propsData = {
|
||||
item: {
|
||||
id: 1,
|
||||
uuid: 1,
|
||||
foreign: false,
|
||||
url: 'http://localhost/api/',
|
||||
publicKey: '4007170edd8d33fb009cd99ee4e87f214e7cd21b668d45540a064deb42e243c2',
|
||||
@ -76,8 +86,18 @@ const mocks = {
|
||||
describe('CommunityVisualizeItem', () => {
|
||||
let wrapper
|
||||
|
||||
const updateHomeCommunityMock = jest.fn()
|
||||
mockClient.setRequestHandler(
|
||||
updateHomeCommunity,
|
||||
updateHomeCommunityMock.mockResolvedValue({
|
||||
data: {
|
||||
updateHomeCommunity: { id: 1 },
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(CommunityVisualizeItem, { localVue, mocks, propsData, store })
|
||||
return mount(CommunityVisualizeItem, { localVue, mocks, propsData, store, apolloProvider })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
@ -152,7 +172,7 @@ describe('CommunityVisualizeItem', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
item: {
|
||||
id: 7590,
|
||||
uuid: 7590,
|
||||
foreign: false,
|
||||
publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7',
|
||||
url: 'http://localhost/api/2_0',
|
||||
@ -195,7 +215,7 @@ describe('CommunityVisualizeItem', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
item: {
|
||||
id: 7590,
|
||||
uuid: 7590,
|
||||
foreign: false,
|
||||
publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7',
|
||||
url: 'http://localhost/api/',
|
||||
@ -219,7 +239,7 @@ describe('CommunityVisualizeItem', () => {
|
||||
beforeEach(() => {
|
||||
propsData = {
|
||||
item: {
|
||||
id: 7590,
|
||||
uuid: 7590,
|
||||
foreign: false,
|
||||
publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7',
|
||||
url: 'http://localhost/api/2_0',
|
||||
@ -237,6 +257,100 @@ describe('CommunityVisualizeItem', () => {
|
||||
expect(wrapper.vm.createdAt).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('test handleUpdateHomeCommunity', () => {
|
||||
describe('gms api key', () => {
|
||||
beforeEach(async () => {
|
||||
wrapper = Wrapper()
|
||||
wrapper.vm.originalGmsApiKey = 'original'
|
||||
wrapper.vm.gmsApiKey = 'changed key'
|
||||
|
||||
await wrapper.vm.handleUpdateHomeCommunity()
|
||||
// Wait for the next tick to allow async operations to complete
|
||||
await wrapper.vm.$nextTick()
|
||||
})
|
||||
|
||||
it('expect changed gms api key', () => {
|
||||
expect(updateHomeCommunityMock).toBeCalledWith({
|
||||
uuid: propsData.item.uuid,
|
||||
gmsApiKey: 'changed key',
|
||||
location: undefined,
|
||||
})
|
||||
expect(wrapper.vm.originalGmsApiKey).toBe('changed key')
|
||||
expect(toastSuccessSpy).toBeCalledWith('federation.toast_gmsApiKeyUpdated')
|
||||
})
|
||||
})
|
||||
|
||||
describe('location', () => {
|
||||
beforeEach(async () => {
|
||||
wrapper = Wrapper()
|
||||
wrapper.vm.originalLocation = { latitude: 15.121, longitude: 1.212 }
|
||||
wrapper.vm.location = { latitude: 1.121, longitude: 17.212 }
|
||||
|
||||
await wrapper.vm.handleUpdateHomeCommunity()
|
||||
// Wait for the next tick to allow async operations to complete
|
||||
await wrapper.vm.$nextTick()
|
||||
})
|
||||
|
||||
it('expect changed location', () => {
|
||||
expect(updateHomeCommunityMock).toBeCalledWith({
|
||||
uuid: propsData.item.uuid,
|
||||
location: { latitude: 1.121, longitude: 17.212 },
|
||||
gmsApiKey: undefined,
|
||||
})
|
||||
expect(wrapper.vm.originalLocation).toStrictEqual({
|
||||
latitude: 1.121,
|
||||
longitude: 17.212,
|
||||
})
|
||||
expect(toastSuccessSpy).toBeCalledWith('federation.toast_gmsLocationUpdated')
|
||||
})
|
||||
})
|
||||
|
||||
describe('gms api key and location', () => {
|
||||
beforeEach(async () => {
|
||||
wrapper = Wrapper()
|
||||
wrapper.vm.originalGmsApiKey = 'original'
|
||||
wrapper.vm.gmsApiKey = 'changed key'
|
||||
wrapper.vm.originalLocation = { latitude: 15.121, longitude: 1.212 }
|
||||
wrapper.vm.location = { latitude: 1.121, longitude: 17.212 }
|
||||
|
||||
await wrapper.vm.handleUpdateHomeCommunity()
|
||||
// Wait for the next tick to allow async operations to complete
|
||||
await wrapper.vm.$nextTick()
|
||||
})
|
||||
|
||||
it('expect changed gms api key and changed location', () => {
|
||||
expect(updateHomeCommunityMock).toBeCalledWith({
|
||||
uuid: propsData.item.uuid,
|
||||
gmsApiKey: 'changed key',
|
||||
location: undefined,
|
||||
})
|
||||
expect(wrapper.vm.originalGmsApiKey).toBe('changed key')
|
||||
expect(wrapper.vm.originalLocation).toStrictEqual({
|
||||
latitude: 1.121,
|
||||
longitude: 17.212,
|
||||
})
|
||||
expect(toastSuccessSpy).toBeCalledWith('federation.toast_gmsApiKeyAndLocationUpdated')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('test resetHomeCommunityEditable', () => {
|
||||
beforeEach(async () => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('test', () => {
|
||||
wrapper.vm.originalGmsApiKey = 'original'
|
||||
wrapper.vm.gmsApiKey = 'changed key'
|
||||
wrapper.vm.originalLocation = { latitude: 15.121, longitude: 1.212 }
|
||||
wrapper.vm.location = { latitude: 1.121, longitude: 17.212 }
|
||||
wrapper.vm.resetHomeCommunityEditable()
|
||||
|
||||
expect(wrapper.vm.location).toStrictEqual({ latitude: 15.121, longitude: 1.212 })
|
||||
expect(wrapper.vm.gmsApiKey).toBe('original')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -25,12 +25,34 @@
|
||||
{{ $t('federation.publicKey') }} {{ item.publicKey }}
|
||||
</b-list-group-item>
|
||||
<b-list-group-item v-if="!item.foreign">
|
||||
{{ $t('federation.gmsApiKey') }}
|
||||
<editable-label
|
||||
:value="gmsApiKey"
|
||||
<editable-group
|
||||
:allowEdit="$store.state.moderator.roles.includes('ADMIN')"
|
||||
@save="handleSaveGsmApiKey"
|
||||
/>
|
||||
@save="handleUpdateHomeCommunity"
|
||||
@reset="resetHomeCommunityEditable"
|
||||
>
|
||||
<template #view>
|
||||
<label>{{ $t('federation.gmsApiKey') }} {{ gmsApiKey }}</label>
|
||||
<b-form-group>
|
||||
{{ $t('federation.coordinates') }}
|
||||
<span v-if="isValidLocation">
|
||||
{{
|
||||
$t('geo-coordinates.format', {
|
||||
latitude: location.latitude,
|
||||
longitude: location.longitude,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</b-form-group>
|
||||
</template>
|
||||
<template #edit>
|
||||
<editable-groupable-label
|
||||
v-model="gmsApiKey"
|
||||
:label="$t('federation.gmsApiKey')"
|
||||
idName="home-community-api-key"
|
||||
/>
|
||||
<coordinates v-model="location" />
|
||||
</template>
|
||||
</editable-group>
|
||||
</b-list-group-item>
|
||||
<b-list-group-item>
|
||||
<b-list-group>
|
||||
@ -59,17 +81,21 @@
|
||||
<script>
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { de, enUS as en, fr, es, nl } from 'date-fns/locale'
|
||||
import EditableLabel from '@/components/input/EditableLabel'
|
||||
import EditableGroup from '@/components/input/EditableGroup'
|
||||
import FederationVisualizeItem from './FederationVisualizeItem.vue'
|
||||
import { updateHomeCommunity } from '../../graphql/updateHomeCommunity'
|
||||
import Coordinates from '../input/Coordinates.vue'
|
||||
import EditableGroupableLabel from '../input/EditableGroupableLabel.vue'
|
||||
|
||||
const locales = { en, de, es, fr, nl }
|
||||
|
||||
export default {
|
||||
name: 'CommunityVisualizeItem',
|
||||
components: {
|
||||
EditableLabel,
|
||||
Coordinates,
|
||||
EditableGroup,
|
||||
FederationVisualizeItem,
|
||||
EditableGroupableLabel,
|
||||
},
|
||||
props: {
|
||||
item: { type: Object },
|
||||
@ -79,12 +105,12 @@ export default {
|
||||
formatDistanceToNow,
|
||||
locale: this.$i18n.locale,
|
||||
details: false,
|
||||
gmsApiKey: '',
|
||||
gmsApiKey: this.item.gmsApiKey,
|
||||
location: this.item.location,
|
||||
originalGmsApiKey: this.item.gmsApiKey,
|
||||
originalLocation: this.item.location,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.gmsApiKey = this.item.gmsApiKey
|
||||
},
|
||||
computed: {
|
||||
verified() {
|
||||
if (!this.item.federatedCommunities || this.item.federatedCommunities.length === 0) {
|
||||
@ -133,28 +159,49 @@ export default {
|
||||
}
|
||||
return ''
|
||||
},
|
||||
isLocationChanged() {
|
||||
return this.originalLocation !== this.location
|
||||
},
|
||||
isGMSApiKeyChanged() {
|
||||
return this.originalGmsApiKey !== this.gmsApiKey
|
||||
},
|
||||
isValidLocation() {
|
||||
return this.location && this.location.latitude && this.location.longitude
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleDetails() {
|
||||
this.details = !this.details
|
||||
},
|
||||
handleSaveGsmApiKey(gmsApiKey) {
|
||||
this.gmsApiKey = gmsApiKey
|
||||
handleUpdateHomeCommunity() {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: updateHomeCommunity,
|
||||
variables: {
|
||||
uuid: this.item.uuid,
|
||||
gmsApiKey: gmsApiKey,
|
||||
gmsApiKey: this.gmsApiKey,
|
||||
location: this.location,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.toastSuccess(this.$t('federation.toast_gmsApiKeyUpdated'))
|
||||
if (this.isLocationChanged && this.isGMSApiKeyChanged) {
|
||||
this.toastSuccess(this.$t('federation.toast_gmsApiKeyAndLocationUpdated'))
|
||||
} else if (this.isGMSApiKeyChanged) {
|
||||
this.toastSuccess(this.$t('federation.toast_gmsApiKeyUpdated'))
|
||||
} else if (this.isLocationChanged) {
|
||||
this.toastSuccess(this.$t('federation.toast_gmsLocationUpdated'))
|
||||
}
|
||||
this.originalLocation = this.location
|
||||
this.originalGmsApiKey = this.gmsApiKey
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
resetHomeCommunityEditable() {
|
||||
this.location = this.originalLocation
|
||||
this.gmsApiKey = this.originalGmsApiKey
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
103
admin/src/components/input/Coordinates.spec.js
Normal file
103
admin/src/components/input/Coordinates.spec.js
Normal file
@ -0,0 +1,103 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Coordinates from './Coordinates.vue'
|
||||
import Vue from 'vue'
|
||||
import VueI18n from 'vue-i18n'
|
||||
|
||||
Vue.use(VueI18n)
|
||||
|
||||
const localVue = global.localVue
|
||||
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 = {
|
||||
latitude: 56.78,
|
||||
longitude: 12.34,
|
||||
}
|
||||
|
||||
const createWrapper = (propsData) => {
|
||||
return mount(Coordinates, {
|
||||
localVue,
|
||||
mocks,
|
||||
propsData,
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper({ value })
|
||||
})
|
||||
|
||||
it('renders the component with initial values', () => {
|
||||
expect(wrapper.find('#home-community-latitude').element.value).toBe('56.78')
|
||||
expect(wrapper.find('#home-community-longitude').element.value).toBe('12.34')
|
||||
expect(wrapper.find('#home-community-latitude-longitude-smart').element.value).toBe(
|
||||
'56.78, 12.34',
|
||||
)
|
||||
})
|
||||
|
||||
it('updates latitude and longitude when input 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.inputValue).toStrictEqual({
|
||||
latitude: 34.56,
|
||||
longitude: 78.9,
|
||||
})
|
||||
})
|
||||
|
||||
it('emits input event with updated values', async () => {
|
||||
const latitudeInput = wrapper.find('#home-community-latitude')
|
||||
const longitudeInput = wrapper.find('#home-community-longitude')
|
||||
|
||||
await latitudeInput.setValue('34.56')
|
||||
expect(wrapper.emitted().input).toBeTruthy()
|
||||
expect(wrapper.emitted().input[0][0]).toEqual({
|
||||
latitude: 34.56,
|
||||
longitude: 12.34,
|
||||
})
|
||||
|
||||
await longitudeInput.setValue('78.90')
|
||||
expect(wrapper.emitted().input).toBeTruthy()
|
||||
expect(wrapper.emitted().input[1][0]).toEqual({
|
||||
latitude: 34.56,
|
||||
longitude: 78.9,
|
||||
})
|
||||
})
|
||||
|
||||
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.inputValue).toStrictEqual({
|
||||
latitude: 34.56,
|
||||
longitude: 78.9,
|
||||
})
|
||||
})
|
||||
|
||||
it('validates coordinates correctly', 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.isValid).toBe(false)
|
||||
|
||||
await latitudeInput.setValue('34.56')
|
||||
await longitudeInput.setValue('78.90')
|
||||
|
||||
expect(wrapper.vm.isValid).toBe(true)
|
||||
})
|
||||
})
|
||||
114
admin/src/components/input/Coordinates.vue
Normal file
114
admin/src/components/input/Coordinates.vue
Normal file
@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-form-group
|
||||
:label="$t('geo-coordinates.label')"
|
||||
:invalid-feedback="$t('geo-coordinates.both-or-none')"
|
||||
:state="isValid"
|
||||
>
|
||||
<b-form-group
|
||||
:label="$t('latitude-longitude-smart')"
|
||||
label-for="home-community-latitude-longitude-smart"
|
||||
:description="$t('geo-coordinates.latitude-longitude-smart.describe')"
|
||||
>
|
||||
<b-form-input
|
||||
v-model="locationString"
|
||||
id="home-community-latitude-longitude-smart"
|
||||
type="text"
|
||||
@input="splitCoordinates"
|
||||
/>
|
||||
</b-form-group>
|
||||
<b-form-group :label="$t('latitude')" label-for="home-community-latitude">
|
||||
<b-form-input
|
||||
v-model="inputValue.latitude"
|
||||
id="home-community-latitude"
|
||||
type="text"
|
||||
@input="valueUpdated"
|
||||
/>
|
||||
</b-form-group>
|
||||
<b-form-group :label="$t('longitude')" label-for="home-community-longitude">
|
||||
<b-form-input
|
||||
v-model="inputValue.longitude"
|
||||
id="home-community-longitude"
|
||||
type="text"
|
||||
@input="valueUpdated"
|
||||
/>
|
||||
</b-form-group>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Coordinates',
|
||||
props: {
|
||||
value: Object,
|
||||
default: null,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
inputValue: this.value,
|
||||
originalValue: this.value,
|
||||
locationString: this.getLatitudeLongitudeString(this.value),
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isValid() {
|
||||
return (
|
||||
(!isNaN(parseFloat(this.inputValue.longitude)) &&
|
||||
!isNaN(parseFloat(this.inputValue.latitude))) ||
|
||||
(this.inputValue.longitude === '' && this.inputValue.latitude === '')
|
||||
)
|
||||
},
|
||||
isChanged() {
|
||||
return this.inputValue !== this.originalValue
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
splitCoordinates(value) {
|
||||
// default format for geo-coordinates: 'latitude, longitude'
|
||||
const parts = value.split(',').map((part) => part.trim())
|
||||
|
||||
if (parts.length === 2) {
|
||||
const [lat, lon] = parts
|
||||
if (!isNaN(parseFloat(lon) && !isNaN(parseFloat(lat)))) {
|
||||
this.inputValue.longitude = parseFloat(lon)
|
||||
this.inputValue.latitude = parseFloat(lat)
|
||||
}
|
||||
}
|
||||
this.valueUpdated()
|
||||
},
|
||||
sanitizeLocation(location) {
|
||||
if (!location) return { latitude: '', longitude: '' }
|
||||
|
||||
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()
|
||||
}
|
||||
} else {
|
||||
if (this.$parent.invalidValues) {
|
||||
this.$parent.invalidValues()
|
||||
}
|
||||
}
|
||||
|
||||
this.$emit('input', this.inputValue)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
92
admin/src/components/input/EditableGroup.spec.js
Normal file
92
admin/src/components/input/EditableGroup.spec.js
Normal file
@ -0,0 +1,92 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import EditableGroup from './EditableGroup.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
const viewValue = 'test label value'
|
||||
const editValue = 'test edit value'
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
}
|
||||
|
||||
describe('EditableGroup', () => {
|
||||
let wrapper
|
||||
|
||||
const createWrapper = (propsData) => {
|
||||
return mount(EditableGroup, {
|
||||
localVue,
|
||||
propsData,
|
||||
mocks,
|
||||
slots: {
|
||||
view: `<div>${viewValue}</div>`,
|
||||
edit: `<div class='test-edit'>${editValue}</div>`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
it('renders the view slot when not editing', () => {
|
||||
wrapper = createWrapper({ allowEdit: true })
|
||||
|
||||
expect(wrapper.find('div').text()).toBe(viewValue)
|
||||
})
|
||||
|
||||
it('renders the edit slot when editing', async () => {
|
||||
wrapper = createWrapper({ allowEdit: true })
|
||||
|
||||
await wrapper.find('button').trigger('click')
|
||||
|
||||
expect(wrapper.find('.test-edit').text()).toBe(editValue)
|
||||
})
|
||||
|
||||
it('emits save event when clicking save button', async () => {
|
||||
wrapper = createWrapper({ allowEdit: true })
|
||||
|
||||
await wrapper.find('button').trigger('click') // Click to enable editing
|
||||
await wrapper.vm.$emit('input', 'New Value') // Simulate input change
|
||||
await wrapper.setData({ isValueChanged: true }) // Set valueChanged to true
|
||||
await wrapper.find('button').trigger('click') // Click to save
|
||||
|
||||
expect(wrapper.emitted().save).toBeTruthy()
|
||||
})
|
||||
|
||||
it('disables save button when value is not changed', async () => {
|
||||
wrapper = createWrapper({ allowEdit: true })
|
||||
|
||||
await wrapper.find('button').trigger('click') // Click to enable editing
|
||||
|
||||
expect(wrapper.find('button').attributes('disabled')).toBe('disabled')
|
||||
})
|
||||
|
||||
it('enables save button when value is changed', async () => {
|
||||
wrapper = createWrapper({ allowEdit: true })
|
||||
|
||||
await wrapper.find('button').trigger('click') // Click to enable editing
|
||||
await wrapper.vm.$emit('input', 'New Value') // Simulate input change
|
||||
await wrapper.setData({ isValueChanged: true }) // Set valueChanged to true
|
||||
|
||||
expect(wrapper.find('button').attributes('disabled')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('updates variant to success when editing', async () => {
|
||||
wrapper = createWrapper({ allowEdit: true })
|
||||
|
||||
await wrapper.find('button').trigger('click') // Click to enable editing
|
||||
|
||||
expect(wrapper.vm.variant).toBe('success')
|
||||
})
|
||||
|
||||
it('updates variant to prime when not editing', async () => {
|
||||
wrapper = createWrapper({ allowEdit: true })
|
||||
|
||||
expect(wrapper.vm.variant).toBe('prime')
|
||||
})
|
||||
|
||||
it('emits reset event when clicking close button', async () => {
|
||||
wrapper = createWrapper({ allowEdit: true })
|
||||
|
||||
await wrapper.find('button').trigger('click') // Click to enable editing
|
||||
await wrapper.find('button.close-button').trigger('click') // Click close button
|
||||
|
||||
expect(wrapper.emitted().reset).toBeTruthy()
|
||||
})
|
||||
})
|
||||
62
admin/src/components/input/EditableGroup.vue
Normal file
62
admin/src/components/input/EditableGroup.vue
Normal file
@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div>
|
||||
<slot v-if="!isEditing" v-bind:isEditing="isEditing" name="view"></slot>
|
||||
<slot v-else v-bind:isEditing="isEditing" name="edit" @input="valueChanged"></slot>
|
||||
<b-form-group v-if="allowEdit && !isEditing">
|
||||
<b-button @click="enableEdit" :variant="variant">
|
||||
<b-icon icon="pencil-fill">{{ $t('edit') }}</b-icon>
|
||||
</b-button>
|
||||
</b-form-group>
|
||||
<b-form-group v-else-if="allowEdit && isEditing">
|
||||
<b-button @click="save" :variant="variant" :disabled="!isValueChanged" class="save-button">
|
||||
{{ $t('save') }}
|
||||
</b-button>
|
||||
<b-button @click="close" variant="secondary" class="close-button">
|
||||
{{ $t('close') }}
|
||||
</b-button>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'EditableGroup',
|
||||
props: {
|
||||
allowEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isEditing: false,
|
||||
isValueChanged: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
variant() {
|
||||
return this.isEditing ? 'success' : 'prime'
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
enableEdit() {
|
||||
this.isEditing = true
|
||||
},
|
||||
valueChanged() {
|
||||
this.isValueChanged = true
|
||||
},
|
||||
invalidValues() {
|
||||
this.isValueChanged = false
|
||||
},
|
||||
save() {
|
||||
this.$emit('save')
|
||||
this.isEditing = false
|
||||
this.isValueChanged = false
|
||||
},
|
||||
close() {
|
||||
this.$emit('reset')
|
||||
this.isEditing = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
79
admin/src/components/input/EditableGroupableLabel.spec.js
Normal file
79
admin/src/components/input/EditableGroupableLabel.spec.js
Normal file
@ -0,0 +1,79 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import EditableGroupableLabel from './EditableGroupableLabel.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
const value = 'test label value'
|
||||
const label = 'Test Label'
|
||||
const idName = 'test-id-name'
|
||||
|
||||
describe('EditableGroupableLabel', () => {
|
||||
let wrapper
|
||||
|
||||
const createWrapper = (propsData) => {
|
||||
return mount(EditableGroupableLabel, {
|
||||
localVue,
|
||||
propsData,
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createWrapper({ value, label, idName })
|
||||
})
|
||||
|
||||
it('renders the label correctly', () => {
|
||||
expect(wrapper.find('label').text()).toBe(label)
|
||||
})
|
||||
|
||||
it('renders the input with the correct id and value', () => {
|
||||
const input = wrapper.find('input')
|
||||
expect(input.attributes('id')).toBe(idName)
|
||||
expect(input.element.value).toBe(value)
|
||||
})
|
||||
|
||||
it('emits input event with the correct value when input changes', async () => {
|
||||
const newValue = 'new label value'
|
||||
const input = wrapper.find('input')
|
||||
input.element.value = newValue
|
||||
await input.trigger('input')
|
||||
|
||||
expect(wrapper.emitted().input).toBeTruthy()
|
||||
expect(wrapper.emitted().input[0][0]).toBe(newValue)
|
||||
})
|
||||
|
||||
it('calls valueChanged method on parent when value changes', async () => {
|
||||
const valueChangedMock = jest.fn()
|
||||
wrapper.vm.$parent = { valueChanged: valueChangedMock }
|
||||
|
||||
const newValue = 'new label value'
|
||||
const input = wrapper.find('input')
|
||||
input.element.value = newValue
|
||||
await input.trigger('input')
|
||||
|
||||
expect(valueChangedMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('calls invalidValues method on parent when value is reverted to original', async () => {
|
||||
const invalidValuesMock = jest.fn()
|
||||
wrapper.vm.$parent = { invalidValues: invalidValuesMock }
|
||||
|
||||
const input = wrapper.find('input')
|
||||
input.element.value = 'new label value'
|
||||
await input.trigger('input')
|
||||
|
||||
input.element.value = value
|
||||
await input.trigger('input')
|
||||
|
||||
expect(invalidValuesMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not call valueChanged method on parent when value is reverted to original', async () => {
|
||||
const valueChangedMock = jest.fn()
|
||||
wrapper.vm.$parent = { valueChanged: valueChangedMock }
|
||||
|
||||
const input = wrapper.find('input')
|
||||
input.element.value = value
|
||||
await input.trigger('input')
|
||||
|
||||
expect(valueChangedMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
47
admin/src/components/input/EditableGroupableLabel.vue
Normal file
47
admin/src/components/input/EditableGroupableLabel.vue
Normal file
@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<b-form-group :label="label" :label-for="idName">
|
||||
<b-form-input :id="idName" v-model="inputValue" @input="updateValue" />
|
||||
</b-form-group>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'EditableGroupableLabel',
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
idName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
inputValue: this.value,
|
||||
originalValue: this.value,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateValue(value) {
|
||||
this.inputValue = value
|
||||
if (this.inputValue !== this.originalValue) {
|
||||
if (this.$parent.valueChanged) {
|
||||
this.$parent.valueChanged()
|
||||
}
|
||||
} else {
|
||||
if (this.$parent.invalidValues) {
|
||||
this.$parent.invalidValues()
|
||||
}
|
||||
}
|
||||
this.$emit('input', this.inputValue)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -1,83 +0,0 @@
|
||||
// Test written by ChatGPT 3.5
|
||||
import { mount } from '@vue/test-utils'
|
||||
import EditableLabel from './EditableLabel.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
const value = 'test label value'
|
||||
|
||||
describe('EditableLabel', () => {
|
||||
let wrapper
|
||||
|
||||
const createWrapper = (propsData) => {
|
||||
return mount(EditableLabel, {
|
||||
localVue,
|
||||
propsData,
|
||||
})
|
||||
}
|
||||
|
||||
it('renders the label when not editing', () => {
|
||||
wrapper = createWrapper({ value, allowEdit: true })
|
||||
|
||||
expect(wrapper.find('label').text()).toBe(value)
|
||||
})
|
||||
|
||||
it('renders the input when editing', async () => {
|
||||
wrapper = createWrapper({ value, allowEdit: true })
|
||||
|
||||
await wrapper.find('button').trigger('click')
|
||||
|
||||
expect(wrapper.find('input').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('emits save event when clicking save button', async () => {
|
||||
wrapper = createWrapper({ value, allowEdit: true })
|
||||
|
||||
await wrapper.find('button').trigger('click')
|
||||
await wrapper.setData({ inputValue: 'New Value' })
|
||||
await wrapper.find('button').trigger('click')
|
||||
|
||||
expect(wrapper.emitted().save).toBeTruthy()
|
||||
expect(wrapper.emitted().save[0][0]).toBe('New Value')
|
||||
})
|
||||
|
||||
it('disables save button when value is not changed', async () => {
|
||||
wrapper = createWrapper({ value, allowEdit: true })
|
||||
|
||||
await wrapper.find('button').trigger('click')
|
||||
|
||||
expect(wrapper.find('button').attributes('disabled')).toBe('disabled')
|
||||
})
|
||||
|
||||
it('enables save button when value is changed', async () => {
|
||||
wrapper = createWrapper({ value, allowEdit: true })
|
||||
|
||||
await wrapper.find('button').trigger('click')
|
||||
await wrapper.setData({ inputValue: 'New Value' })
|
||||
|
||||
expect(wrapper.find('button').attributes('disabled')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('updates originalValue when saving changes', async () => {
|
||||
wrapper = createWrapper({ value, allowEdit: true })
|
||||
|
||||
await wrapper.find('button').trigger('click')
|
||||
await wrapper.setData({ inputValue: 'New Value' })
|
||||
await wrapper.find('button').trigger('click')
|
||||
|
||||
expect(wrapper.vm.originalValue).toBe('New Value')
|
||||
})
|
||||
|
||||
it('changes variant to success when editing', async () => {
|
||||
wrapper = createWrapper({ value, allowEdit: true })
|
||||
|
||||
await wrapper.find('button').trigger('click')
|
||||
|
||||
expect(wrapper.vm.variant).toBe('success')
|
||||
})
|
||||
|
||||
it('changes variant to prime when not editing', async () => {
|
||||
wrapper = createWrapper({ value, allowEdit: true })
|
||||
|
||||
expect(wrapper.vm.variant).toBe('prime')
|
||||
})
|
||||
})
|
||||
@ -1,64 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-form-group>
|
||||
<label v-if="!editing">{{ value }}</label>
|
||||
<b-form-input v-else v-model="inputValue" :placeholder="placeholder" />
|
||||
</b-form-group>
|
||||
<b-button
|
||||
v-if="allowEdit"
|
||||
@click="toggleEdit"
|
||||
:disabled="!isValueChanged && editing"
|
||||
:variant="variant"
|
||||
>
|
||||
<b-icon v-if="!editing" icon="pencil-fill" tooltip="$t('edit')"></b-icon>
|
||||
<b-icon v-else icon="check" tooltip="$t('save')"></b-icon>
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
// Code written from chatGPT 3.5
|
||||
name: 'EditableLabel',
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
allowEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editing: false,
|
||||
inputValue: this.value,
|
||||
originalValue: this.value,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
variant() {
|
||||
return this.editing ? 'success' : 'prime'
|
||||
},
|
||||
isValueChanged() {
|
||||
return this.inputValue !== this.originalValue
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleEdit() {
|
||||
if (this.editing) {
|
||||
this.$emit('save', this.inputValue)
|
||||
this.originalValue = this.inputValue
|
||||
}
|
||||
this.editing = !this.editing
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -11,6 +11,7 @@ export const allCommunities = gql`
|
||||
name
|
||||
description
|
||||
gmsApiKey
|
||||
location
|
||||
creationDate
|
||||
createdAt
|
||||
updatedAt
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const updateHomeCommunity = gql`
|
||||
mutation ($uuid: String!, $gmsApiKey: String!) {
|
||||
updateHomeCommunity(uuid: $uuid, gmsApiKey: $gmsApiKey) {
|
||||
mutation ($uuid: String!, $gmsApiKey: String, $location: Location) {
|
||||
updateHomeCommunity(uuid: $uuid, gmsApiKey: $gmsApiKey, location: $location) {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,11 +4,11 @@
|
||||
"back": "zurück",
|
||||
"change_user_role": "Nutzerrolle ändern",
|
||||
"chat": "Chat",
|
||||
"close": "Schließen",
|
||||
"contributionLink": {
|
||||
"amount": "Betrag",
|
||||
"changeSaved": "Änderungen gespeichert",
|
||||
"clear": "Löschen",
|
||||
"close": "Schließen",
|
||||
"contributionLinks": "Beitragslinks",
|
||||
"create": "Anlegen",
|
||||
"cycle": "Zyklus",
|
||||
@ -69,6 +69,7 @@
|
||||
"deleted_user": "Alle gelöschten Nutzer",
|
||||
"deny": "Ablehnen",
|
||||
"e_mail": "E-Mail",
|
||||
"edit": "bearbeiten",
|
||||
"enabled": "aktiviert",
|
||||
"error": "Fehler",
|
||||
"expired": "abgelaufen",
|
||||
@ -76,9 +77,12 @@
|
||||
"apiVersion": "API Version",
|
||||
"authenticatedAt": "Verifiziert am:",
|
||||
"communityUuid": "Community UUID:",
|
||||
"coordinates": "Koordinaten:",
|
||||
"createdAt": "Erstellt am",
|
||||
"gmsApiKey": "GMS API Key:",
|
||||
"toast_gmsApiKeyAndLocationUpdated": "Der GMS Api Key und die Location wurden erfolgreich aktualisiert!",
|
||||
"toast_gmsApiKeyUpdated": "Der GMS Api Key wurde erfolgreich aktualisiert!",
|
||||
"toast_gmsLocationUpdated": "Die GMS Location wurde erfolgreich aktualisiert!",
|
||||
"gradidoInstances": "Gradido Instanzen",
|
||||
"lastAnnouncedAt": "letzte Bekanntgabe",
|
||||
"lastErrorAt": "Letzer Fehler am",
|
||||
@ -100,6 +104,14 @@
|
||||
"form": {
|
||||
"cancel": "Abbrechen"
|
||||
},
|
||||
"geo-coordinates": {
|
||||
"both-or-none": "Bitte beide oder keine eingeben!",
|
||||
"format": "{latitude}, {longitude}",
|
||||
"label": "Geo-Koordinaten",
|
||||
"latitude-longitude-smart": {
|
||||
"describe": "Teilt Koordinaten im Format 'Breitengrad, Längengrad' automatisch auf. Fügen sie hier einfach z.B. ihre Koordinaten von Google Maps, zum Beispiel: 49.28187664243721, 9.740672183943639, ein."
|
||||
}
|
||||
},
|
||||
"help": {
|
||||
"help": "Hilfe",
|
||||
"transactionlist": {
|
||||
@ -113,6 +125,9 @@
|
||||
"hide_resubmission": "Wiedervorlage verbergen",
|
||||
"hide_resubmission_tooltip": "Verbirgt alle Schöpfungen für die ein Moderator ein Erinnerungsdatum festgelegt hat.",
|
||||
"lastname": "Nachname",
|
||||
"latitude": "Breitengrad:",
|
||||
"latitude-longitude-smart": "Breitengrad, Längengrad",
|
||||
"longitude": "Längengrad:",
|
||||
"math": {
|
||||
"equals": "=",
|
||||
"pipe": "|",
|
||||
|
||||
@ -4,11 +4,11 @@
|
||||
"back": "back",
|
||||
"change_user_role": "Change user role",
|
||||
"chat": "Chat",
|
||||
"close": "Close",
|
||||
"contributionLink": {
|
||||
"amount": "Amount",
|
||||
"changeSaved": "Changes saved",
|
||||
"clear": "Clear",
|
||||
"close": "Close",
|
||||
"contributionLinks": "Contribution Links",
|
||||
"create": "Create",
|
||||
"cycle": "Cycle",
|
||||
@ -69,6 +69,7 @@
|
||||
"deleted_user": "All deleted user",
|
||||
"deny": "Reject",
|
||||
"e_mail": "E-mail",
|
||||
"edit": "edit",
|
||||
"enabled": "enabled",
|
||||
"error": "Error",
|
||||
"expired": "expired",
|
||||
@ -76,9 +77,12 @@
|
||||
"apiVersion": "API Version",
|
||||
"authenticatedAt": "verified at:",
|
||||
"communityUuid": "Community UUID:",
|
||||
"coordinates": "Coordinates:",
|
||||
"createdAt": "Created At ",
|
||||
"gmsApiKey": "GMS API Key:",
|
||||
"toast_gmsApiKeyAndLocationUpdated": "The GMS Api Key and the location have been successfully updated!",
|
||||
"toast_gmsApiKeyUpdated": "The GMS Api Key has been successfully updated!",
|
||||
"toast_gmsLocationUpdated": "The GMS location has been successfully updated!",
|
||||
"gradidoInstances": "Gradido Instances",
|
||||
"lastAnnouncedAt": "Last Announced",
|
||||
"lastErrorAt": "last error at",
|
||||
@ -100,6 +104,14 @@
|
||||
"form": {
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"geo-coordinates": {
|
||||
"both-or-none": "Please enter both or none!",
|
||||
"label": "geo-coordinates",
|
||||
"format": "{latitude}, {longitude}",
|
||||
"latitude-longitude-smart": {
|
||||
"describe": "Automatically splits coordinates in the format 'latitude, longitude'. Simply enter your coordinates from Google Maps here, for example: 49.28187664243721, 9.740672183943639."
|
||||
}
|
||||
},
|
||||
"help": {
|
||||
"help": "Help",
|
||||
"transactionlist": {
|
||||
@ -113,6 +125,9 @@
|
||||
"hide_resubmission": "Hide resubmission",
|
||||
"hide_resubmission_tooltip": "Hides all creations for which a moderator has set a reminder date.",
|
||||
"lastname": "Lastname",
|
||||
"latitude": "Latitude:",
|
||||
"latitude-longitude-smart": "Latitude, Longitude",
|
||||
"longitude": "Longitude:",
|
||||
"math": {
|
||||
"equals": "=",
|
||||
"pipe": "|",
|
||||
|
||||
@ -36,6 +36,7 @@
|
||||
"gradido-database": "file:../database",
|
||||
"graphql": "^15.5.1",
|
||||
"graphql-request": "5.0.0",
|
||||
"graphql-type-json": "0.3.2",
|
||||
"helmet": "^5.1.1",
|
||||
"i18n": "^0.15.1",
|
||||
"jose": "^4.14.4",
|
||||
|
||||
@ -12,7 +12,7 @@ Decimal.set({
|
||||
})
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0085-add_index_transactions_user_id',
|
||||
DB_VERSION: '0086-add_community_location',
|
||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { IsString, IsUUID } from 'class-validator'
|
||||
import { ArgsType, Field, InputType } from 'type-graphql'
|
||||
|
||||
import { Location } from '@/graphql/model/Location'
|
||||
import { isValidLocation } from '@/graphql/validator/Location'
|
||||
|
||||
@ArgsType()
|
||||
@InputType()
|
||||
export class EditCommunityInput {
|
||||
@ -8,7 +11,11 @@ export class EditCommunityInput {
|
||||
@IsUUID('4')
|
||||
uuid: string
|
||||
|
||||
@Field(() => String)
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
gmsApiKey: string
|
||||
gmsApiKey?: string | null
|
||||
|
||||
@Field(() => Location, { nullable: true })
|
||||
@isValidLocation()
|
||||
location?: Location | null
|
||||
}
|
||||
|
||||
@ -1,8 +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 { Location } from './Location'
|
||||
|
||||
@ObjectType()
|
||||
export class AdminCommunityView {
|
||||
@ -36,6 +40,9 @@ export class AdminCommunityView {
|
||||
this.uuid = dbCom.communityUuid
|
||||
this.authenticatedAt = dbCom.authenticatedAt
|
||||
this.gmsApiKey = dbCom.gmsApiKey
|
||||
if (dbCom.location) {
|
||||
this.location = Point2Location(dbCom.location as Point)
|
||||
}
|
||||
}
|
||||
|
||||
@Field(() => Boolean)
|
||||
@ -62,6 +69,9 @@ export class AdminCommunityView {
|
||||
@Field(() => String, { nullable: true })
|
||||
gmsApiKey: string | null
|
||||
|
||||
@Field(() => Location, { nullable: true })
|
||||
location: Location | null
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
creationDate: Date | null
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
getCommunityByUuid,
|
||||
getHomeCommunity,
|
||||
} from './util/communities'
|
||||
import { Location2Point } from './util/Location2Point'
|
||||
|
||||
@Resolver()
|
||||
export class CommunityResolver {
|
||||
@ -78,7 +79,9 @@ export class CommunityResolver {
|
||||
|
||||
@Authorized([RIGHTS.COMMUNITY_UPDATE])
|
||||
@Mutation(() => Community)
|
||||
async updateHomeCommunity(@Args() { uuid, gmsApiKey }: EditCommunityInput): Promise<Community> {
|
||||
async updateHomeCommunity(
|
||||
@Args() { uuid, gmsApiKey, location }: EditCommunityInput,
|
||||
): Promise<Community> {
|
||||
const homeCom = await getCommunityByUuid(uuid)
|
||||
if (!homeCom) {
|
||||
throw new LogError('HomeCommunity with uuid not found: ', uuid)
|
||||
@ -86,8 +89,11 @@ export class CommunityResolver {
|
||||
if (homeCom.foreign) {
|
||||
throw new LogError('Error: Only the HomeCommunity could be modified!')
|
||||
}
|
||||
if (homeCom.gmsApiKey !== gmsApiKey) {
|
||||
homeCom.gmsApiKey = gmsApiKey
|
||||
if (homeCom.gmsApiKey !== gmsApiKey || homeCom.location !== location) {
|
||||
homeCom.gmsApiKey = gmsApiKey ?? null
|
||||
if (location) {
|
||||
homeCom.location = Location2Point(location)
|
||||
}
|
||||
await DbCommunity.save(homeCom)
|
||||
}
|
||||
return new Community(homeCom)
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import { Point as DbPoint } from '@dbTools/typeorm'
|
||||
import { GraphQLScalarType, Kind } from 'graphql'
|
||||
|
||||
export const PointScalar = new GraphQLScalarType({
|
||||
name: 'Point',
|
||||
description:
|
||||
'The `Point` scalar type to represent longitude and latitude values of a geo location',
|
||||
|
||||
serialize(value: DbPoint) {
|
||||
// Check type of value
|
||||
if (value.type !== 'Point') {
|
||||
throw new Error(`PointScalar can only serialize Geometry type 'Point' values`)
|
||||
}
|
||||
return value
|
||||
},
|
||||
|
||||
parseValue(value): DbPoint {
|
||||
const point = JSON.parse(value) as DbPoint
|
||||
return 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 DbPoint
|
||||
return point
|
||||
},
|
||||
})
|
||||
@ -3696,7 +3696,7 @@ graceful-fs@^4.1.6, graceful-fs@^4.2.0:
|
||||
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
|
||||
|
||||
"gradido-database@file:../database":
|
||||
version "2.2.1"
|
||||
version "2.3.1"
|
||||
dependencies:
|
||||
"@types/uuid" "^8.3.4"
|
||||
cross-env "^7.0.3"
|
||||
@ -3774,6 +3774,11 @@ graphql-tools@^4.0.8:
|
||||
iterall "^1.1.3"
|
||||
uuid "^3.1.0"
|
||||
|
||||
graphql-type-json@0.3.2:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/graphql-type-json/-/graphql-type-json-0.3.2.tgz#f53a851dbfe07bd1c8157d24150064baab41e115"
|
||||
integrity sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==
|
||||
|
||||
graphql@^15.5.1:
|
||||
version "15.6.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.6.1.tgz#9125bdf057553525da251e19e96dab3d3855ddfc"
|
||||
|
||||
89
database/entity/0086-add_community_location/Community.ts
Normal file
89
database/entity/0086-add_community_location/Community.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
OneToMany,
|
||||
JoinColumn,
|
||||
Geometry,
|
||||
} from 'typeorm'
|
||||
import { FederatedCommunity } from '../FederatedCommunity'
|
||||
import { GeometryTransformer } from '../../src/typeorm/GeometryTransformer'
|
||||
import { User } from '../User'
|
||||
|
||||
@Entity('communities')
|
||||
export class Community extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ name: 'foreign', type: 'bool', nullable: false, default: true })
|
||||
foreign: boolean
|
||||
|
||||
@Column({ name: 'url', length: 255, nullable: false })
|
||||
url: string
|
||||
|
||||
@Column({ name: 'public_key', type: 'binary', length: 32, nullable: false })
|
||||
publicKey: Buffer
|
||||
|
||||
@Column({ name: 'private_key', type: 'binary', length: 64, nullable: true })
|
||||
privateKey: Buffer | null
|
||||
|
||||
@Column({
|
||||
name: 'community_uuid',
|
||||
type: 'char',
|
||||
length: 36,
|
||||
nullable: true,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
communityUuid: string | null
|
||||
|
||||
@Column({ name: 'authenticated_at', type: 'datetime', nullable: true })
|
||||
authenticatedAt: Date | null
|
||||
|
||||
@Column({ name: 'name', type: 'varchar', length: 40, nullable: true })
|
||||
name: string | null
|
||||
|
||||
@Column({ name: 'description', type: 'varchar', length: 255, nullable: true })
|
||||
description: string | null
|
||||
|
||||
@CreateDateColumn({ name: 'creation_date', type: 'datetime', nullable: true })
|
||||
creationDate: Date | null
|
||||
|
||||
@Column({ name: 'gms_api_key', type: 'varchar', length: 512, nullable: true, default: null })
|
||||
gmsApiKey: string | null
|
||||
|
||||
@Column({
|
||||
name: 'location',
|
||||
type: 'geometry',
|
||||
default: null,
|
||||
nullable: true,
|
||||
transformer: GeometryTransformer,
|
||||
})
|
||||
location: Geometry | null
|
||||
|
||||
@CreateDateColumn({
|
||||
name: 'created_at',
|
||||
type: 'datetime',
|
||||
default: () => 'CURRENT_TIMESTAMP(3)',
|
||||
nullable: false,
|
||||
})
|
||||
createdAt: Date
|
||||
|
||||
@UpdateDateColumn({
|
||||
name: 'updated_at',
|
||||
type: 'datetime',
|
||||
onUpdate: 'CURRENT_TIMESTAMP(3)',
|
||||
nullable: true,
|
||||
})
|
||||
updatedAt: Date | null
|
||||
|
||||
@OneToMany(() => User, (user) => user.community)
|
||||
@JoinColumn({ name: 'community_uuid', referencedColumnName: 'communityUuid' })
|
||||
users: User[]
|
||||
|
||||
@OneToMany(() => FederatedCommunity, (federatedCommunity) => federatedCommunity.community)
|
||||
@JoinColumn({ name: 'public_key', referencedColumnName: 'publicKey' })
|
||||
federatedCommunities?: FederatedCommunity[]
|
||||
}
|
||||
@ -1 +1 @@
|
||||
export { Community } from './0083-join_community_federated_communities/Community'
|
||||
export { Community } from './0086-add_community_location/Community'
|
||||
|
||||
13
database/migrations/0086-add_community_location.ts
Normal file
13
database/migrations/0086-add_community_location.ts
Normal file
@ -0,0 +1,13 @@
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(
|
||||
'ALTER TABLE `communities` ADD COLUMN IF NOT EXISTS `location` geometry DEFAULT NULL NULL AFTER `gms_api_key`;',
|
||||
)
|
||||
}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn('ALTER TABLE `communities` DROP COLUMN IF EXISTS `location`;')
|
||||
}
|
||||
@ -4,7 +4,7 @@ import dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0085-add_index_transactions_user_id',
|
||||
DB_VERSION: '0086-add_community_location',
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',
|
||||
|
||||
@ -2260,6 +2260,11 @@ gensync@^1.0.0-beta.2:
|
||||
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
||||
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
|
||||
|
||||
geojson@^0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/geojson/-/geojson-0.5.0.tgz#3cd6c96399be65b56ee55596116fe9191ce701c0"
|
||||
integrity sha512-/Bx5lEn+qRF4TfQ5aLu6NH+UKtvIv7Lhc487y/c8BdludrCTpiWf9wyI0RTyqg49MFefIAvFDuEi5Dfd/zgNxQ==
|
||||
|
||||
get-caller-file@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
||||
@ -2389,18 +2394,20 @@ graceful-fs@^4.2.4:
|
||||
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
||||
|
||||
"gradido-database@file:../database":
|
||||
version "1.22.3"
|
||||
version "2.2.1"
|
||||
dependencies:
|
||||
"@types/uuid" "^8.3.4"
|
||||
cross-env "^7.0.3"
|
||||
crypto "^1.0.1"
|
||||
decimal.js-light "^2.5.1"
|
||||
dotenv "^10.0.0"
|
||||
geojson "^0.5.0"
|
||||
mysql2 "^2.3.0"
|
||||
reflect-metadata "^0.1.13"
|
||||
ts-mysql-migrate "^1.0.2"
|
||||
typeorm "^0.3.16"
|
||||
uuid "^8.3.2"
|
||||
wkx "^0.5.0"
|
||||
|
||||
grapheme-splitter@^1.0.4:
|
||||
version "1.0.4"
|
||||
@ -4888,6 +4895,13 @@ which@^2.0.1:
|
||||
dependencies:
|
||||
isexe "^2.0.0"
|
||||
|
||||
wkx@^0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/wkx/-/wkx-0.5.0.tgz#c6c37019acf40e517cc6b94657a25a3d4aa33e8c"
|
||||
integrity sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
word-wrap@^1.2.3, word-wrap@~1.2.3:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(`
|
||||
ALTER TABLE \`transactions\`
|
||||
RENAME COLUMN \`paring_transaction_id\` TO \`pairing_transaction_id\`,
|
||||
RENAME COLUMN \`paring_transaction_id\` TO \`pairing_transaction_id\`
|
||||
;
|
||||
`)
|
||||
}
|
||||
@ -9,7 +9,7 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(`
|
||||
ALTER TABLE \`transactions\`
|
||||
RENAME COLUMN \`pairing_transaction_id\` TO \`paring_transaction_id\`,
|
||||
RENAME COLUMN \`pairing_transaction_id\` TO \`paring_transaction_id\`
|
||||
;
|
||||
`)
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ Decimal.set({
|
||||
})
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0085-add_index_transactions_user_id',
|
||||
DB_VERSION: '0086-add_community_location',
|
||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user