mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge pull request #3383 from gradido/monterail_vue3_migration
feat(frontend): monterail vue3 migration
This commit is contained in:
commit
1f9d21a6e7
@ -35,7 +35,10 @@
|
||||
@reset="resetHomeCommunityEditable"
|
||||
>
|
||||
<template #view>
|
||||
<label>{{ $t('federation.gmsApiKey') }} {{ gmsApiKey }}</label>
|
||||
<div class="d-flex">
|
||||
<p style="text-wrap: nowrap">{{ $t('federation.gmsApiKey') }} </p>
|
||||
<span class="d-block" style="overflow-x: auto">{{ gmsApiKey }}</span>
|
||||
</div>
|
||||
<BFormGroup>
|
||||
{{ $t('federation.coordinates') }}
|
||||
<span v-if="isValidLocation">
|
||||
@ -198,131 +201,3 @@ const resetHomeCommunityEditable = () => {
|
||||
gmsApiKey.value = originalGmsApiKey.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<!--<script>-->
|
||||
<!--import { formatDistanceToNow } from 'date-fns'-->
|
||||
<!--import { de, enUS as en, fr, es, nl } from 'date-fns/locale'-->
|
||||
<!--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: {-->
|
||||
<!-- Coordinates,-->
|
||||
<!-- EditableGroup,-->
|
||||
<!-- FederationVisualizeItem,-->
|
||||
<!-- EditableGroupableLabel,-->
|
||||
<!-- },-->
|
||||
<!-- props: {-->
|
||||
<!-- item: { type: Object },-->
|
||||
<!-- },-->
|
||||
<!-- data() {-->
|
||||
<!-- return {-->
|
||||
<!-- formatDistanceToNow,-->
|
||||
<!-- locale: this.$i18n.locale,-->
|
||||
<!-- details: false,-->
|
||||
<!-- gmsApiKey: this.item.gmsApiKey,-->
|
||||
<!-- location: this.item.location,-->
|
||||
<!-- originalGmsApiKey: this.item.gmsApiKey,-->
|
||||
<!-- originalLocation: this.item.location,-->
|
||||
<!-- }-->
|
||||
<!-- },-->
|
||||
<!-- computed: {-->
|
||||
<!-- verified() {-->
|
||||
<!-- if (!this.item.federatedCommunities || this.item.federatedCommunities.length === 0) {-->
|
||||
<!-- return false-->
|
||||
<!-- }-->
|
||||
<!-- return (-->
|
||||
<!-- this.item.federatedCommunities.filter(-->
|
||||
<!-- (federatedCommunity) =>-->
|
||||
<!-- new Date(federatedCommunity.verifiedAt) >= new Date(federatedCommunity.lastAnnouncedAt),-->
|
||||
<!-- ).length > 0-->
|
||||
<!-- )-->
|
||||
<!-- },-->
|
||||
<!-- icon() {-->
|
||||
<!-- return this.verified ? 'check' : 'x-circle'-->
|
||||
<!-- },-->
|
||||
<!-- variant() {-->
|
||||
<!-- return this.verified ? 'success' : 'danger'-->
|
||||
<!-- },-->
|
||||
<!-- lastAnnouncedAt() {-->
|
||||
<!-- if (!this.item.federatedCommunities || this.item.federatedCommunities.length === 0) return ''-->
|
||||
<!-- const minDate = new Date(0)-->
|
||||
<!-- const lastAnnouncedAt = this.item.federatedCommunities.reduce(-->
|
||||
<!-- (lastAnnouncedAt, federateCommunity) => {-->
|
||||
<!-- if (!federateCommunity.lastAnnouncedAt) return lastAnnouncedAt-->
|
||||
<!-- const date = new Date(federateCommunity.lastAnnouncedAt)-->
|
||||
<!-- return date > lastAnnouncedAt ? date : lastAnnouncedAt-->
|
||||
<!-- },-->
|
||||
<!-- minDate,-->
|
||||
<!-- )-->
|
||||
<!-- if (lastAnnouncedAt !== minDate) {-->
|
||||
<!-- return formatDistanceToNow(lastAnnouncedAt, {-->
|
||||
<!-- includeSecond: true,-->
|
||||
<!-- addSuffix: true,-->
|
||||
<!-- locale: locales[this.locale],-->
|
||||
<!-- })-->
|
||||
<!-- }-->
|
||||
<!-- return ''-->
|
||||
<!-- },-->
|
||||
<!-- createdAt() {-->
|
||||
<!-- if (this.item.createdAt) {-->
|
||||
<!-- return formatDistanceToNow(new Date(this.item.createdAt), {-->
|
||||
<!-- includeSecond: true,-->
|
||||
<!-- addSuffix: true,-->
|
||||
<!-- locale: locales[this.locale],-->
|
||||
<!-- })-->
|
||||
<!-- }-->
|
||||
<!-- 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-->
|
||||
<!-- },-->
|
||||
<!-- handleUpdateHomeCommunity() {-->
|
||||
<!-- this.$apollo-->
|
||||
<!-- .mutate({-->
|
||||
<!-- mutation: updateHomeCommunity,-->
|
||||
<!-- variables: {-->
|
||||
<!-- uuid: this.item.uuid,-->
|
||||
<!-- gmsApiKey: this.gmsApiKey,-->
|
||||
<!-- location: this.location,-->
|
||||
<!-- },-->
|
||||
<!-- })-->
|
||||
<!-- .then(() => {-->
|
||||
<!-- 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>-->
|
||||
|
||||
@ -3,33 +3,43 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import Coordinates from './Coordinates.vue'
|
||||
import { BFormGroup, BFormInput } from 'bootstrap-vue-next'
|
||||
|
||||
const value = {
|
||||
const modelValue = {
|
||||
latitude: 56.78,
|
||||
longitude: 12.34,
|
||||
}
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key, v) => (key === 'geo-coordinates.format' ? `${v.latitude}, ${v.longitude}` : key),
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockEditableGroup = {
|
||||
valueChanged: vi.fn(function () {
|
||||
this.isValueChanged = true
|
||||
}),
|
||||
invalidValues: vi.fn(function () {
|
||||
this.isValueChanged = false
|
||||
}),
|
||||
}
|
||||
|
||||
describe('Coordinates', () => {
|
||||
let wrapper
|
||||
|
||||
const createWrapper = (props = {}) => {
|
||||
return mount(Coordinates, {
|
||||
props: {
|
||||
value,
|
||||
modelValue,
|
||||
...props,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: vi.fn((t, v) => {
|
||||
if (t === 'geo-coordinates.format') {
|
||||
return `${v.latitude}, ${v.longitude}`
|
||||
}
|
||||
return t
|
||||
}),
|
||||
},
|
||||
stubs: {
|
||||
BFormGroup,
|
||||
BFormInput,
|
||||
},
|
||||
provide: {
|
||||
editableGroup: mockEditableGroup,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -63,19 +73,13 @@ describe('Coordinates', () => {
|
||||
const latitudeInput = wrapper.find('#home-community-latitude')
|
||||
const longitudeInput = wrapper.find('#home-community-longitude')
|
||||
|
||||
await latitudeInput.setValue(34.56)
|
||||
await latitudeInput.setValue('34.56')
|
||||
expect(wrapper.emitted('input')).toBeTruthy()
|
||||
expect(wrapper.emitted('input')[0][0]).toEqual({
|
||||
latitude: 34.56,
|
||||
longitude: 12.34,
|
||||
})
|
||||
expect(wrapper.vm.inputValue.latitude).toBe('34.56')
|
||||
|
||||
await longitudeInput.setValue('78.90')
|
||||
await longitudeInput.setValue('78.9')
|
||||
expect(wrapper.emitted('input')).toBeTruthy()
|
||||
expect(wrapper.emitted('input')[1][0]).toEqual({
|
||||
latitude: 34.56,
|
||||
longitude: '78.90',
|
||||
})
|
||||
expect(wrapper.vm.inputValue.longitude).toBe('78.9')
|
||||
})
|
||||
|
||||
it('splits coordinates correctly when entering in latitudeLongitude input', async () => {
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<BFormGroup
|
||||
:label="$t('geo-coordinates.label')"
|
||||
:invalid-feedback="$t('geo-coordinates.both-or-none')"
|
||||
:label="t('geo-coordinates.label')"
|
||||
:invalid-feedback="t('geo-coordinates.both-or-none')"
|
||||
:state="isValid"
|
||||
>
|
||||
<BFormGroup
|
||||
:label="$t('latitude-longitude-smart')"
|
||||
:label="t('latitude-longitude-smart')"
|
||||
label-for="home-community-latitude-longitude-smart"
|
||||
:description="$t('geo-coordinates.latitude-longitude-smart.describe')"
|
||||
:description="t('geo-coordinates.latitude-longitude-smart.describe')"
|
||||
>
|
||||
<BFormInput
|
||||
id="home-community-latitude-longitude-smart"
|
||||
@ -17,7 +17,7 @@
|
||||
@input="splitCoordinates"
|
||||
/>
|
||||
</BFormGroup>
|
||||
<BFormGroup :label="$t('latitude')" label-for="home-community-latitude">
|
||||
<BFormGroup :label="t('latitude')" label-for="home-community-latitude">
|
||||
<BFormInput
|
||||
id="home-community-latitude"
|
||||
v-model="inputValue.latitude"
|
||||
@ -25,7 +25,7 @@
|
||||
@input="valueUpdated"
|
||||
/>
|
||||
</BFormGroup>
|
||||
<BFormGroup :label="$t('longitude')" label-for="home-community-longitude">
|
||||
<BFormGroup :label="t('longitude')" label-for="home-community-longitude">
|
||||
<BFormInput
|
||||
id="home-community-longitude"
|
||||
v-model="inputValue.longitude"
|
||||
@ -37,81 +37,94 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Coordinates',
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
<script setup>
|
||||
import { ref, computed, watch, inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { BFormGroup, BFormInput } from 'bootstrap-vue-next'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
emits: ['input'],
|
||||
data() {
|
||||
return {
|
||||
inputValue: this.value,
|
||||
originalValue: this.value,
|
||||
locationString: this.getLatitudeLongitudeString(this.value),
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const { t } = useI18n()
|
||||
|
||||
const editableGroup = inject('editableGroup')
|
||||
|
||||
const inputValue = ref(sanitizeLocation(props.modelValue))
|
||||
const originalValue = ref(props.modelValue)
|
||||
const locationString = ref(getLatitudeLongitudeString(props.modelValue))
|
||||
|
||||
const isValid = computed(() => {
|
||||
return (
|
||||
(!isNaN(parseFloat(inputValue.value.longitude)) &&
|
||||
!isNaN(parseFloat(inputValue.value.latitude))) ||
|
||||
(inputValue.value.longitude === '' && inputValue.value.latitude === '')
|
||||
)
|
||||
})
|
||||
|
||||
const isChanged = computed(() => {
|
||||
return inputValue.value !== originalValue.value
|
||||
})
|
||||
|
||||
function splitCoordinates() {
|
||||
const parts = locationString.value.split(',').map((part) => part.trim())
|
||||
|
||||
if (parts.length === 2) {
|
||||
const [lat, lon] = parts
|
||||
if (!isNaN(parseFloat(lon)) && !isNaN(parseFloat(lat))) {
|
||||
inputValue.value.longitude = parseFloat(lon)
|
||||
inputValue.value.latitude = parseFloat(lat)
|
||||
}
|
||||
},
|
||||
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 = this.locationString.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() {
|
||||
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)
|
||||
},
|
||||
},
|
||||
}
|
||||
valueUpdated()
|
||||
}
|
||||
|
||||
function 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),
|
||||
}
|
||||
}
|
||||
|
||||
function getLatitudeLongitudeString(locationData) {
|
||||
return locationData?.latitude && locationData?.longitude
|
||||
? t('geo-coordinates.format', {
|
||||
latitude: locationData.latitude,
|
||||
longitude: locationData.longitude,
|
||||
})
|
||||
: ''
|
||||
}
|
||||
|
||||
function valueUpdated() {
|
||||
locationString.value = getLatitudeLongitudeString(inputValue.value)
|
||||
inputValue.value = sanitizeLocation(inputValue.value)
|
||||
|
||||
if (isValid.value && isChanged.value) {
|
||||
editableGroup.valueChanged()
|
||||
} else {
|
||||
editableGroup.invalidValues()
|
||||
}
|
||||
|
||||
emit('update:modelValue', inputValue.value)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
inputValue.value = sanitizeLocation(newValue)
|
||||
originalValue.value = newValue
|
||||
locationString.value = getLatitudeLongitudeString(newValue)
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<slot v-if="!isEditing" :is-editing="isEditing" name="view"></slot>
|
||||
<slot v-else :is-editing="isEditing" name="edit" @input="valueChanged"></slot>
|
||||
<slot v-if="!isEditing" :is-editing="isEditing" name="view" />
|
||||
<slot v-else :is-editing="isEditing" name="edit" @update:model-value="valueChanged" />
|
||||
<BFormGroup v-if="allowEdit && !isEditing">
|
||||
<BButton :variant="variant" @click="enableEdit">
|
||||
<IBiPencilFill />
|
||||
@ -12,7 +12,7 @@
|
||||
<BButton :variant="variant" :disabled="!isValueChanged" class="save-button" @click="save">
|
||||
{{ $t('save') }}
|
||||
</BButton>
|
||||
<BButton variant="secondary" class="close-button" @click="close">
|
||||
<BButton variant="secondary" class="close-button ms-2" @click="close">
|
||||
{{ $t('close') }}
|
||||
</BButton>
|
||||
</BFormGroup>
|
||||
@ -22,6 +22,14 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'EditableGroup',
|
||||
provide() {
|
||||
return {
|
||||
editableGroup: {
|
||||
valueChanged: this.valueChanged,
|
||||
invalidValues: this.invalidValues,
|
||||
},
|
||||
}
|
||||
},
|
||||
props: {
|
||||
allowEdit: {
|
||||
type: Boolean,
|
||||
@ -58,6 +66,7 @@ export default {
|
||||
close() {
|
||||
this.$emit('reset')
|
||||
this.isEditing = false
|
||||
this.isValueChanged = false
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import EditableGroupableLabel from './EditableGroupableLabel.vue'
|
||||
import { BFormGroup, BFormInput } from 'bootstrap-vue-next'
|
||||
|
||||
const value = 'test label value'
|
||||
const modelValue = 'test label value'
|
||||
const label = 'Test Label'
|
||||
const idName = 'test-id-name'
|
||||
|
||||
@ -16,7 +16,7 @@ describe('EditableGroupableLabel', () => {
|
||||
components: {
|
||||
EditableGroupableLabel,
|
||||
},
|
||||
props: ['value', 'label', 'idName'],
|
||||
props: ['modelValue', 'label', 'idName'],
|
||||
methods: {
|
||||
onInput: vi.fn(),
|
||||
...parentMethods,
|
||||
@ -24,7 +24,7 @@ describe('EditableGroupableLabel', () => {
|
||||
}
|
||||
return mount(Parent, {
|
||||
props: {
|
||||
value,
|
||||
modelValue,
|
||||
label,
|
||||
idName,
|
||||
...props,
|
||||
@ -55,7 +55,7 @@ describe('EditableGroupableLabel', () => {
|
||||
it('renders BFormInput with correct props', () => {
|
||||
const formInput = wrapper.findComponent({ name: 'BFormInput' })
|
||||
expect(formInput.props('id')).toBe(idName)
|
||||
expect(formInput.props('modelValue')).toBe(value)
|
||||
expect(formInput.props('modelValue')).toBe(modelValue)
|
||||
})
|
||||
|
||||
// it('emits input event with the correct value when input changes', async () => {
|
||||
@ -76,7 +76,7 @@ describe('EditableGroupableLabel', () => {
|
||||
|
||||
const newValue = 'new label value'
|
||||
const input = wrapper.findComponent({ name: 'BFormInput' })
|
||||
await input.vm.$emit('input', newValue)
|
||||
await input.vm.$emit('update:model-value', newValue)
|
||||
|
||||
expect(valueChangedMock).toHaveBeenCalled()
|
||||
})
|
||||
@ -86,8 +86,8 @@ describe('EditableGroupableLabel', () => {
|
||||
wrapper = createWrapper({}, { invalidValues: invalidValuesMock })
|
||||
|
||||
const input = wrapper.findComponent({ name: 'BFormInput' })
|
||||
await input.vm.$emit('input', 'new label value')
|
||||
await input.vm.$emit('input', value)
|
||||
await input.vm.$emit('update:model-value', 'new label value')
|
||||
await input.vm.$emit('update:model-value', modelValue)
|
||||
|
||||
expect(invalidValuesMock).toHaveBeenCalled()
|
||||
})
|
||||
@ -97,7 +97,7 @@ describe('EditableGroupableLabel', () => {
|
||||
wrapper = createWrapper({}, { valueChanged: valueChangedMock })
|
||||
|
||||
const input = wrapper.findComponent({ name: 'BFormInput' })
|
||||
await input.vm.$emit('input', value)
|
||||
await input.vm.$emit('input', modelValue)
|
||||
|
||||
expect(valueChangedMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<BFormGroup :label="label" :label-for="idName">
|
||||
<BFormInput :id="idName" v-model="inputValue" @input="updateValue" />
|
||||
<BFormInput :id="idName" :model-value="modelValue" @update:model-value="inputValue = $event" />
|
||||
</BFormGroup>
|
||||
</template>
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
export default {
|
||||
name: 'EditableGroupableLabel',
|
||||
props: {
|
||||
value: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
@ -22,16 +22,20 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['input'],
|
||||
emits: ['update:model-value'],
|
||||
data() {
|
||||
return {
|
||||
inputValue: this.value,
|
||||
originalValue: this.value,
|
||||
inputValue: this.modelValue,
|
||||
originalValue: this.modelValue,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
inputValue() {
|
||||
this.updateValue()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateValue(value) {
|
||||
this.inputValue = value
|
||||
updateValue() {
|
||||
if (this.inputValue !== this.originalValue) {
|
||||
if (this.$parent.valueChanged) {
|
||||
this.$parent.valueChanged()
|
||||
@ -41,7 +45,7 @@ export default {
|
||||
this.$parent.invalidValues()
|
||||
}
|
||||
}
|
||||
this.$emit('input', this.inputValue)
|
||||
this.$emit('update:model-value', this.inputValue)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# This file defines the production settings. It is overwritten by docker-compose.override.yml,
|
||||
# which defines the development settings. The override.yml is loaded by default. Therefore it
|
||||
# is required to explicitly define if you want an production build:
|
||||
# > docker-compose -f docker-compose.yml up
|
||||
# > docker-compose -f docker-compose.yml up
|
||||
|
||||
services:
|
||||
|
||||
@ -67,13 +67,13 @@ services:
|
||||
environment:
|
||||
- MARIADB_ALLOW_EMPTY_PASSWORD=1
|
||||
- MARIADB_USER=root
|
||||
networks:
|
||||
networks:
|
||||
- internal-net
|
||||
ports:
|
||||
ports:
|
||||
- 3306:3306
|
||||
volumes:
|
||||
volumes:
|
||||
- db_vol:/var/lib/mysql
|
||||
|
||||
|
||||
########################################################
|
||||
# BACKEND ##############################################
|
||||
########################################################
|
||||
@ -264,12 +264,12 @@ services:
|
||||
# Application only envs
|
||||
#env_file:
|
||||
# - ./frontend/.env
|
||||
|
||||
|
||||
#########################################################
|
||||
## NGINX ################################################
|
||||
#########################################################
|
||||
nginx:
|
||||
build:
|
||||
build:
|
||||
context: ./nginx/
|
||||
networks:
|
||||
- external-net
|
||||
@ -277,17 +277,17 @@ services:
|
||||
depends_on:
|
||||
- frontend
|
||||
- backend
|
||||
- admin
|
||||
- admin
|
||||
ports:
|
||||
- 80:80
|
||||
volumes:
|
||||
- ./logs/nginx:/var/log/nginx
|
||||
|
||||
volumes:
|
||||
- ./logs/nginx:/var/log/nginx
|
||||
|
||||
networks:
|
||||
external-net:
|
||||
internal-net:
|
||||
internal: true
|
||||
|
||||
volumes:
|
||||
db_vol:
|
||||
|
||||
db_vol:
|
||||
|
||||
|
||||
@ -50,7 +50,6 @@ module.exports = {
|
||||
'vue/v-on-event-hyphenation': 0, // TODO remove at the end of migration and fix
|
||||
'vue/require-default-prop': 0, // TODO remove at the end of migration and fix
|
||||
'vue/no-computed-properties-in-data': 0, // TODO remove at the end of migration and fix
|
||||
'@intlify/vue-i18n/no-dynamic-keys': 'error',
|
||||
'@intlify/vue-i18n/no-missing-keys': 0, // TODO remove at the end of migration and fix
|
||||
'@intlify/vue-i18n/no-unused-keys': [
|
||||
'error',
|
||||
|
||||
@ -22,13 +22,14 @@
|
||||
"@babel/node": "^7.13.13",
|
||||
"@babel/preset-env": "^7.13.12",
|
||||
"@morev/vue-transitions": "^3.0.2",
|
||||
"@types/leaflet": "^1.9.12",
|
||||
"@vee-validate/i18n": "^4.13.2",
|
||||
"@vee-validate/rules": "^4.13.2",
|
||||
"@vee-validate/yup": "^4.13.2",
|
||||
"@vitejs/plugin-vue": "3.2.0",
|
||||
"@vitejs/plugin-vue": "5.1.4",
|
||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||
"@vue/apollo-composable": "^4.0.2",
|
||||
"@vue/apollo-option": "^4.0.0",
|
||||
"@vue/compat": "^3.4.31",
|
||||
"apollo-boost": "^0.4.9",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"babel-core": "^7.0.0-bridge.0",
|
||||
@ -45,22 +46,22 @@
|
||||
"graphql-tag": "^2.12.6",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet-geosearch": "^4.0.0",
|
||||
"portal-vue": "^3.0.0",
|
||||
"qrcanvas-vue": "3",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"tua-body-scroll-lock": "^1.5.1",
|
||||
"uuid": "^9.0.0",
|
||||
"vee-validate": "^4.13.2",
|
||||
"vite": "3.2.10",
|
||||
"vite-plugin-commonjs": "^0.10.1",
|
||||
"vue": "3.4.31",
|
||||
"vue-apollo": "^3.1.2",
|
||||
"vue-avatar": "^2.3.3",
|
||||
"vue-flatpickr-component": "^8.1.2",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-loading-overlay": "^3.4.2",
|
||||
"vue-router": "^4.4.0",
|
||||
"vue-timer-hook": "^1.0.84",
|
||||
"vue-timers": "^2.0.4",
|
||||
"vuex": "^4.1.0",
|
||||
"vuex-persistedstate": "^4.1.0",
|
||||
"yup": "^1.4.0"
|
||||
@ -70,7 +71,6 @@
|
||||
"@iconify-json/bi": "^1.1.23",
|
||||
"@intlify/eslint-plugin-vue-i18n": "^1.4.0",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
"@vue/compiler-sfc": "^3.4.35",
|
||||
"@vue/eslint-config-prettier": "^4.0.1",
|
||||
"@vue/test-utils": "^2.4.5",
|
||||
"babel-plugin-component": "^1.1.0",
|
||||
|
||||
@ -1,68 +1,5 @@
|
||||
// import { shallowMount, RouterLinkStub } from '@vue/test-utils'
|
||||
// import App from './App'
|
||||
//
|
||||
// const localVue = global.localVue
|
||||
// const mockStoreCommit = jest.fn()
|
||||
//
|
||||
// const stubs = {
|
||||
// RouterLink: RouterLinkStub,
|
||||
// RouterView: true,
|
||||
// }
|
||||
//
|
||||
// describe('App', () => {
|
||||
// const mocks = {
|
||||
// $i18n: {
|
||||
// locale: 'en',
|
||||
// },
|
||||
// $t: jest.fn((t) => t),
|
||||
// $store: {
|
||||
// commit: mockStoreCommit,
|
||||
// state: {
|
||||
// token: null,
|
||||
// },
|
||||
// },
|
||||
// $route: {
|
||||
// meta: {
|
||||
// requiresAuth: false,
|
||||
// },
|
||||
// params: {},
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// let wrapper
|
||||
//
|
||||
// const Wrapper = () => {
|
||||
// return shallowMount(App, { localVue, mocks, stubs })
|
||||
// }
|
||||
//
|
||||
// describe('mount', () => {
|
||||
// beforeEach(() => {
|
||||
// wrapper = Wrapper()
|
||||
// })
|
||||
//
|
||||
// it('renders the App', () => {
|
||||
// expect(wrapper.find('#app').exists()).toBe(true)
|
||||
// })
|
||||
//
|
||||
// it('has a component AuthLayout', () => {
|
||||
// expect(wrapper.findComponent({ name: 'AuthLayout' }).exists()).toBe(true)
|
||||
// })
|
||||
//
|
||||
// describe('route requires authorization', () => {
|
||||
// beforeEach(async () => {
|
||||
// mocks.$route.meta.requiresAuth = true
|
||||
// wrapper = Wrapper()
|
||||
// })
|
||||
//
|
||||
// it('has a component DashboardLayout', () => {
|
||||
// expect(wrapper.findComponent({ name: 'DashboardLayout' }).exists()).toBe(true)
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import App from './App'
|
||||
import DashboardLayout from '@/layouts/DashboardLayout'
|
||||
import AuthLayout from '@/layouts/AuthLayout'
|
||||
|
||||
@ -189,8 +189,10 @@ a:hover,
|
||||
|
||||
// .btn-primary pim {
|
||||
.btn-primary {
|
||||
background-color: #5a7b02;
|
||||
border-color: #5e72e4;
|
||||
background-color: #5a7b02 !important;
|
||||
border-color: #5e72e4 !important;
|
||||
border-radius: 25px !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.gradido-font-large {
|
||||
@ -345,3 +347,24 @@ a:hover,
|
||||
.gdd-toaster-body {
|
||||
color: rgb(255 255 255);
|
||||
}
|
||||
|
||||
.gdd-toaster-body-darken {
|
||||
color: #2c2c2c;
|
||||
}
|
||||
|
||||
input.rounded-input {
|
||||
border-radius: 17px;
|
||||
}
|
||||
|
||||
.fs-7 {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.vue3-avatar.container {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
font-family: inherit !important;
|
||||
font-weight: normal !important;
|
||||
}
|
||||
|
||||
135
frontend/src/components/AppAvatar.vue
Normal file
135
frontend/src/components/AppAvatar.vue
Normal file
@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div
|
||||
class="app-avatar d-flex justify-content-center align-items-center rounded-circle"
|
||||
:style="{
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
backgroundColor,
|
||||
textTransform: 'uppercase',
|
||||
}"
|
||||
>
|
||||
<span
|
||||
:style="{
|
||||
fontSize: `${size * 0.4}px`,
|
||||
lineHeight: '1',
|
||||
color: props.color,
|
||||
}"
|
||||
class="font-medium"
|
||||
>
|
||||
{{ computedInitials }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
size: {
|
||||
type: Number,
|
||||
default: 50,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
initials: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
// Enhanced color palette with better contrast ratios
|
||||
const colorPalette = [
|
||||
{ bg: '#4A5568', text: '#FFFFFF' }, // Slate Blue
|
||||
{ bg: '#2C7A7B', text: '#FFFFFF' }, // Teal
|
||||
{ bg: '#805AD5', text: '#FFFFFF' }, // Purple
|
||||
{ bg: '#DD6B20', text: '#FFFFFF' }, // Orange
|
||||
{ bg: '#3182CE', text: '#FFFFFF' }, // Blue
|
||||
{ bg: '#38A169', text: '#FFFFFF' }, // Green
|
||||
{ bg: '#E53E3E', text: '#FFFFFF' }, // Red
|
||||
{ bg: '#6B46C1', text: '#FFFFFF' }, // Indigo
|
||||
{ bg: '#2B6CB0', text: '#FFFFFF' }, // Dark Blue
|
||||
{ bg: '#9C4221', text: '#FFFFFF' }, // Brown
|
||||
]
|
||||
|
||||
// Generate consistent index based on string
|
||||
function stringToIndex(str) {
|
||||
let hash = 0
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash)
|
||||
}
|
||||
return Math.abs(hash % colorPalette.length)
|
||||
}
|
||||
|
||||
// Parse any color format to RGB
|
||||
function parseColor(color) {
|
||||
const div = document.createElement('div')
|
||||
div.style.color = color
|
||||
document.body.appendChild(div)
|
||||
const computed = window.getComputedStyle(div).color
|
||||
document.body.removeChild(div)
|
||||
|
||||
const match = computed.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/)
|
||||
if (!match) return [0, 0, 0]
|
||||
|
||||
return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])]
|
||||
}
|
||||
|
||||
// Calculate relative luminance using WCAG formula
|
||||
function getLuminance(r, g, b) {
|
||||
const [rs, gs, bs] = [r, g, b].map((c) => {
|
||||
c = c / 255
|
||||
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
|
||||
})
|
||||
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs
|
||||
}
|
||||
|
||||
// Calculate contrast ratio using WCAG formula
|
||||
function getContrastRatio(l1, l2) {
|
||||
const lighter = Math.max(l1, l2)
|
||||
const darker = Math.min(l1, l2)
|
||||
return (lighter + 0.05) / (darker + 0.05)
|
||||
}
|
||||
|
||||
// Get text color based on background ensuring WCAG AA compliance (4.5:1 minimum)
|
||||
function getTextColor(backgroundColor) {
|
||||
const [r, g, b] = parseColor(backgroundColor)
|
||||
const bgLuminance = getLuminance(r, g, b)
|
||||
const whiteLuminance = getLuminance(255, 255, 255)
|
||||
const blackLuminance = getLuminance(0, 0, 0)
|
||||
|
||||
const whiteContrast = getContrastRatio(whiteLuminance, bgLuminance)
|
||||
const blackContrast = getContrastRatio(blackLuminance, bgLuminance)
|
||||
|
||||
// Return the color with better contrast (minimum 4.5:1 for WCAG AA)
|
||||
return whiteContrast >= blackContrast ? '#FFFFFF' : '#000000'
|
||||
}
|
||||
|
||||
const computedInitials = computed(() => {
|
||||
if (props.initials) return props.initials
|
||||
return props.name
|
||||
.split(' ')
|
||||
.map((word) => word[0])
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
.slice(0, 2)
|
||||
})
|
||||
|
||||
const backgroundColor = computed(() => {
|
||||
const colorIndex = stringToIndex(computedInitials.value || props.name)
|
||||
return colorPalette[colorIndex].bg
|
||||
})
|
||||
|
||||
const textColor = computed(() => {
|
||||
if (props.color) {
|
||||
return getTextColor(props.color)
|
||||
}
|
||||
const colorIndex = stringToIndex(computedInitials.value || props.name)
|
||||
return colorPalette[colorIndex].text
|
||||
})
|
||||
</script>
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="breadcrumb bg-transparent">
|
||||
<div class="page-breadcrumb breadcrumb bg-transparent">
|
||||
<h1>{{ pageTitle }}</h1>
|
||||
</div>
|
||||
</template>
|
||||
@ -17,3 +17,10 @@ export default {
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-breadcrumb {
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -90,5 +90,4 @@ function setDefaultCommunity() {
|
||||
}
|
||||
|
||||
onMounted(setDefaultCommunity)
|
||||
onUpdated(setDefaultCommunity)
|
||||
</script>
|
||||
|
||||
@ -16,7 +16,17 @@
|
||||
<parse-message v-bind="message" data-test="message" class="p-2"></parse-message>
|
||||
</BCol>
|
||||
<BCol cols="2">
|
||||
<avatar :username="storeName.username" :initials="storeName.initials"></avatar>
|
||||
<!-- <avatar-->
|
||||
<!-- class="vue3-avatar"-->
|
||||
<!-- :name="storeName.username"-->
|
||||
<!-- :initials="storeName.initials"-->
|
||||
<!-- :border="false"-->
|
||||
<!-- />-->
|
||||
<app-avatar
|
||||
class="vue3-avatar"
|
||||
:name="storeName.username"
|
||||
:initials="storeName.initials"
|
||||
/>
|
||||
</BCol>
|
||||
</BRow>
|
||||
</div>
|
||||
@ -28,14 +38,30 @@
|
||||
<parse-message v-bind="message" data-test="message"></parse-message>
|
||||
</BCol>
|
||||
<BCol cols="2">
|
||||
<avatar :username="storeName.username" :initials="storeName.initials"></avatar>
|
||||
<!-- <avatar-->
|
||||
<!-- class="vue3-avatar"-->
|
||||
<!-- :name="storeName.username"-->
|
||||
<!-- :initials="storeName.initials"-->
|
||||
<!-- :border="false"-->
|
||||
<!-- />-->
|
||||
<app-avatar
|
||||
class="vue3-avatar"
|
||||
:name="storeName.username"
|
||||
:initials="storeName.initials"
|
||||
/>
|
||||
</BCol>
|
||||
</BRow>
|
||||
</div>
|
||||
<div v-else>
|
||||
<BRow class="mb-3 p-2 is-moderator">
|
||||
<BCol cols="2">
|
||||
<avatar :username="moderationName.username" :initials="moderationName.initials"></avatar>
|
||||
<!-- <avatar-->
|
||||
<!-- class="vue3-avatar"-->
|
||||
<!-- :name="moderationName.username"-->
|
||||
<!-- :initials="moderationName.initials"-->
|
||||
<!-- :border="false"-->
|
||||
<!-- />-->
|
||||
<app-avatar :name="moderationName.username" :initials="moderationName.initials" />
|
||||
</BCol>
|
||||
<BCol cols="10">
|
||||
<div class="font-weight-bold">
|
||||
@ -54,13 +80,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Avatar from 'vue-avatar'
|
||||
import ParseMessage from '@/components/ContributionMessages/ParseMessage'
|
||||
import AppAvatar from '@/components/AppAvatar.vue'
|
||||
|
||||
export default {
|
||||
name: 'ContributionMessagesListItem',
|
||||
components: {
|
||||
Avatar,
|
||||
AppAvatar,
|
||||
ParseMessage,
|
||||
},
|
||||
props: {
|
||||
|
||||
@ -6,13 +6,21 @@
|
||||
>
|
||||
<BRow>
|
||||
<BCol cols="3" lg="2" md="2">
|
||||
<avatar
|
||||
<!-- <avatar-->
|
||||
<!-- v-if="firstName"-->
|
||||
<!-- :name="username.username"-->
|
||||
<!-- :initials="username.initials"-->
|
||||
<!-- :border="false"-->
|
||||
<!-- color="#fff"-->
|
||||
<!-- class="vue3-avatar fw-bold"-->
|
||||
<!-- />-->
|
||||
<app-avatar
|
||||
v-if="firstName"
|
||||
:username="username.username"
|
||||
:name="username.username"
|
||||
:initials="username.initials"
|
||||
color="#fff"
|
||||
class="fw-bold"
|
||||
></avatar>
|
||||
class="vue3-avatar fw-bold"
|
||||
/>
|
||||
<BAvatar v-else rounded="lg" :variant="variant" size="4.55em">
|
||||
<variant-icon :icon="icon" variant="white" />
|
||||
</BAvatar>
|
||||
@ -112,13 +120,13 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import Avatar from 'vue-avatar'
|
||||
import CollapseIcon from '../TransactionRows/CollapseIcon'
|
||||
import ContributionMessagesList from '@/components/ContributionMessages/ContributionMessagesList'
|
||||
import { listContributionMessages } from '@/graphql/queries'
|
||||
import { useAppToast } from '@/composables/useToast'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useLazyQuery, useQuery } from '@vue/apollo-composable'
|
||||
import AppAvatar from '@/components/AppAvatar.vue'
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
|
||||
@ -162,10 +162,11 @@ describe('CollapseLinksList', () => {
|
||||
transactionLinks: [{ id: 1 }, { id: 2 }],
|
||||
pageSize: 5,
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
})
|
||||
|
||||
it('renders text in plural with page size links to load', () => {
|
||||
expect(mockT).toHaveBeenCalledWith('link-load', 2, { n: 5 })
|
||||
it('renders text with pageSize as number of links to load', () => {
|
||||
expect(mockT).toHaveBeenCalledWith('link-load-more', { n: 5 })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
<div class="mb-3">
|
||||
<BButton
|
||||
v-if="!pending && transactionLinks.length < transactionLinkCount"
|
||||
class="test-button-load-more"
|
||||
class="test-button-load-more w-100 rounded-5"
|
||||
block
|
||||
variant="outline-primary"
|
||||
@click.stop="loadMoreLinks"
|
||||
@ -50,8 +50,8 @@ export default {
|
||||
buttonText() {
|
||||
const i = this.transactionLinkCount - this.transactionLinks.length
|
||||
if (i === 1) return this.$t('link-load', 0)
|
||||
if (i <= this.pageSize) return this.$t('link-load', 1, { n: i })
|
||||
return this.$t('link-load', 2, { n: this.pageSize })
|
||||
if (i <= this.pageSize) return this.$t('link-load', { n: i })
|
||||
return this.$t('link-load-more', { n: this.pageSize })
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
@ -2,21 +2,34 @@
|
||||
<div class="decayinformation-startblock">
|
||||
<div class="my-4">
|
||||
<div class="fw-bold pb-2">{{ $t('form.memo') }}</div>
|
||||
<div>{{ memo }}</div>
|
||||
<div v-html="displayData" />
|
||||
</div>
|
||||
<div class="mt-3 mb-3 text-center">
|
||||
<b>{{ $t('decay.before_startblock_transaction') }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'DecayInformationStartBlock',
|
||||
props: {
|
||||
memo: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
memo: String,
|
||||
})
|
||||
|
||||
const formatLinks = (text) => {
|
||||
// URL regex pattern
|
||||
const urlPattern = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim
|
||||
// Email regex pattern
|
||||
const emailPattern = /(\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b)/g
|
||||
|
||||
// Replace URLs with clickable links
|
||||
text = text.replace(urlPattern, '<a href="$1" target="_blank">$1</a>')
|
||||
|
||||
// Replace email addresses with mailto links
|
||||
text = text.replace(emailPattern, '<a href="mailto:$1">$1</a>')
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
const displayData = computed(() => formatLinks(props.memo))
|
||||
</script>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<div class="decayinformation-long px-1">
|
||||
<div class="word-break mb-5 mt-lg-3">
|
||||
<div class="fw-bold pb-2">{{ $t('form.memo') }}</div>
|
||||
<div class="">{{ memo }}</div>
|
||||
<div @click.stop v-html="displayData" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<IBiDropletHalf class="me-2" />
|
||||
@ -73,6 +73,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import DurationRow from '@/components/TransactionRows/DurationRow'
|
||||
import { computed } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'DecayInformationLong',
|
||||
@ -89,5 +90,27 @@ export default {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
displayData() {
|
||||
return this.formatLinks(this.memo)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatLinks(text) {
|
||||
const urlPattern = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim
|
||||
const emailPattern = /(\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b)/g
|
||||
|
||||
// Replace URLs with clickable links
|
||||
text = text.replace(
|
||||
urlPattern,
|
||||
'<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>',
|
||||
)
|
||||
|
||||
// Replace email addresses with mailto links
|
||||
text = text.replace(emailPattern, '<a href="mailto:$1">$1</a>')
|
||||
|
||||
return text
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -11,27 +11,19 @@
|
||||
>
|
||||
<BRow class="mb-4 gap-5">
|
||||
<BCol>
|
||||
<BRow class="bg-248 gradido-border-radius position-relative">
|
||||
<BFormRadio
|
||||
name="shipping"
|
||||
size="md"
|
||||
reverse
|
||||
:value="SEND_TYPES.send"
|
||||
class="transaction-form-radio"
|
||||
>
|
||||
<BRow
|
||||
class="bg-248 gradido-border-radius position-relative transaction-form-radio"
|
||||
>
|
||||
<BFormRadio name="shipping" size="md" reverse :value="SEND_TYPES.send">
|
||||
{{ $t('send_gdd') }}
|
||||
</BFormRadio>
|
||||
</BRow>
|
||||
</BCol>
|
||||
<BCol>
|
||||
<BRow class="bg-248 gradido-border-radius position-relative">
|
||||
<BFormRadio
|
||||
name="shipping"
|
||||
:value="SEND_TYPES.link"
|
||||
size="md"
|
||||
reverse
|
||||
class="transaction-form-radio"
|
||||
>
|
||||
<BRow
|
||||
class="bg-248 gradido-border-radius position-relative transaction-form-radio"
|
||||
>
|
||||
<BFormRadio name="shipping" :value="SEND_TYPES.link" size="md" reverse>
|
||||
{{ $t('send_per_link') }}
|
||||
</BFormRadio>
|
||||
</BRow>
|
||||
@ -287,7 +279,7 @@ label {
|
||||
display: flex !important;
|
||||
justify-content: space-between;
|
||||
|
||||
> input {
|
||||
> div > input {
|
||||
position: absolute;
|
||||
right: 35px;
|
||||
top: 50%;
|
||||
|
||||
@ -6,7 +6,9 @@
|
||||
{{ $t('form.send_transaction_success') }}
|
||||
</div>
|
||||
<div class="text-center mt-5">
|
||||
<b-button variant="primary" @click="$emit('on-back')">{{ $t('form.close') }}</b-button>
|
||||
<BButton variant="primary" @click="$emit('on-back')">
|
||||
{{ $t('form.close') }}
|
||||
</BButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -24,27 +24,18 @@
|
||||
</transaction-list-item>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<div v-for="({ id, typeId }, index) in transactions" :key="`l2-` + id">
|
||||
<div v-for="transaction in transactions" :key="`l2-` + transaction.id">
|
||||
<transaction-list-item
|
||||
v-if="typeId !== 'DECAY'"
|
||||
:type-id="typeId"
|
||||
v-if="transaction.typeId !== 'DECAY'"
|
||||
:type-id="transaction.typeId"
|
||||
class="pointer mb-3 bg-white app-box-shadow gradido-border-radius p-3 test-list-group-item"
|
||||
>
|
||||
<template #SEND>
|
||||
<transaction-send v-bind="transactions[index]" />
|
||||
<template v-if="transaction.typeId !== 'LINK_SUMMARY'" #item>
|
||||
<gdd-transaction :transaction="transaction" />
|
||||
</template>
|
||||
|
||||
<template #RECEIVE>
|
||||
<transaction-receive v-bind="transactions[index]" />
|
||||
</template>
|
||||
|
||||
<template #CREATION>
|
||||
<transaction-creation v-bind="transactions[index]" />
|
||||
</template>
|
||||
|
||||
<template #LINK_SUMMARY>
|
||||
<template v-else #LINK_SUMMARY>
|
||||
<transaction-link-summary
|
||||
v-bind="transactions[index]"
|
||||
v-bind="transaction"
|
||||
:transaction-link-count="transactionLinkCount"
|
||||
@update-transactions="updateTransactions"
|
||||
/>
|
||||
@ -75,19 +66,15 @@
|
||||
<script>
|
||||
import TransactionListItem from '@/components/TransactionListItem'
|
||||
import TransactionDecay from '@/components/Transactions/TransactionDecay'
|
||||
import TransactionSend from '@/components/Transactions/TransactionSend'
|
||||
import TransactionReceive from '@/components/Transactions/TransactionReceive'
|
||||
import TransactionCreation from '@/components/Transactions/TransactionCreation'
|
||||
import TransactionLinkSummary from '@/components/Transactions/TransactionLinkSummary'
|
||||
import GddTransaction from '@/components/Transactions/GddTransaction.vue'
|
||||
|
||||
export default {
|
||||
name: 'GddTransactionList',
|
||||
components: {
|
||||
GddTransaction,
|
||||
TransactionListItem,
|
||||
TransactionDecay,
|
||||
TransactionSend,
|
||||
TransactionReceive,
|
||||
TransactionCreation,
|
||||
TransactionLinkSummary,
|
||||
},
|
||||
props: {
|
||||
|
||||
@ -64,11 +64,11 @@ describe('InputEmail', () => {
|
||||
})
|
||||
|
||||
it('has the placeholder "input-field-placeholder"', () => {
|
||||
expect(wrapper.find('input').attributes('placeholder')).toBe('input-field-placeholder')
|
||||
expect(wrapper.find('input').attributes('placeholder')).toBe('form.email')
|
||||
})
|
||||
|
||||
it('has the label "input-field-label"', () => {
|
||||
expect(wrapper.find('label').text()).toBe('input-field-label')
|
||||
expect(wrapper.find('label').text()).toBe('form.email')
|
||||
})
|
||||
|
||||
it('has the label for "input-field-name-input-field"', () => {
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
:placeholder="defaultTranslations.placeholder"
|
||||
type="email"
|
||||
trim
|
||||
class="rounded-input"
|
||||
:class="$route.path === '/send' ? 'bg-248' : ''"
|
||||
:disabled="disabled"
|
||||
autocomplete="off"
|
||||
@ -33,14 +34,6 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: 'email',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: 'Email',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Email',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@ -54,8 +47,8 @@ const { value, errorMessage, validate, meta } = useField(() => props.name, 'requ
|
||||
const { t } = useI18n()
|
||||
|
||||
const defaultTranslations = computed(() => ({
|
||||
label: props.label ?? t('form.email'),
|
||||
placeholder: props.placeholder ?? t('form.email'),
|
||||
label: t('form.email'),
|
||||
placeholder: t('form.email'),
|
||||
}))
|
||||
|
||||
const normalizeEmail = (emailAddress) => {
|
||||
|
||||
@ -80,7 +80,7 @@ describe('InputPassword', () => {
|
||||
})
|
||||
|
||||
it('has the placeholder "input-field-placeholder"', () => {
|
||||
expect(wrapper.find('input').attributes('placeholder')).toEqual('input-field-placeholder')
|
||||
expect(wrapper.find('input').attributes('placeholder')).toEqual('form.password')
|
||||
})
|
||||
|
||||
it('has the value ""', () => {
|
||||
@ -88,7 +88,7 @@ describe('InputPassword', () => {
|
||||
})
|
||||
|
||||
it('has the label "input-field-label"', () => {
|
||||
expect(wrapper.find('label').text()).toEqual('input-field-label')
|
||||
expect(wrapper.find('label').text()).toEqual('form.password')
|
||||
})
|
||||
|
||||
it('has the label for "input-field-name-input-field"', () => {
|
||||
|
||||
@ -50,14 +50,6 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: 'password',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: 'Password',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Password',
|
||||
},
|
||||
immediate: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@ -78,18 +70,11 @@ const { value, errorMessage, meta, errors, validate } = useField(name, props.rul
|
||||
validateOnMount: props.immediate,
|
||||
})
|
||||
|
||||
// onMounted(async () => {
|
||||
// await nextTick()
|
||||
// if (props.immediate) {
|
||||
// await validate()
|
||||
// }
|
||||
// })
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const defaultTranslations = computed(() => ({
|
||||
label: props.label ?? t('form.password'),
|
||||
placeholder: props.placeholder ?? t('form.password'),
|
||||
label: t('form.password'),
|
||||
placeholder: t('form.password'),
|
||||
}))
|
||||
|
||||
const showPassword = ref(false)
|
||||
@ -109,3 +94,9 @@ const ariaMsg = computed(() => ({
|
||||
|
||||
const labelFor = computed(() => `${props.name}-input-field`)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
input {
|
||||
border-radius: 17px 0 0 17px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -4,6 +4,7 @@ import { createRouter, createWebHistory, RouterLink } from 'vue-router'
|
||||
import { createStore } from 'vuex'
|
||||
import Navbar from './Navbar.vue'
|
||||
import { BImg, BNavbar, BNavbarBrand, BNavbarNav } from 'bootstrap-vue-next'
|
||||
import AppAvatar from '@/components/AppAvatar.vue'
|
||||
|
||||
// Mock vue-avatar
|
||||
vi.mock('vue-avatar', () => ({
|
||||
@ -50,6 +51,7 @@ describe('Navbar', () => {
|
||||
BNavbarBrand,
|
||||
BImg,
|
||||
RouterLink,
|
||||
AppAvatar,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
@ -72,11 +74,11 @@ describe('Navbar', () => {
|
||||
|
||||
describe('.avatar element', () => {
|
||||
it('is rendered', () => {
|
||||
expect(wrapper.findComponent({ name: 'Avatar' }).exists()).toBe(true)
|
||||
expect(wrapper.findComponent({ name: 'AppAvatar' }).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it("has the user's initials", () => {
|
||||
const avatar = wrapper.findComponent({ name: 'Avatar' })
|
||||
const avatar = wrapper.findComponent({ name: 'AppAvatar' })
|
||||
expect(avatar.props('initials')).toBe('TU')
|
||||
})
|
||||
})
|
||||
|
||||
@ -20,12 +20,14 @@
|
||||
<router-link to="/settings">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="me-3">
|
||||
<avatar
|
||||
:username="username.username"
|
||||
<app-avatar
|
||||
class="vue3-avatar"
|
||||
:name="username.username"
|
||||
:initials="username.initials"
|
||||
:border="false"
|
||||
:color="'#fff'"
|
||||
:size="61"
|
||||
></avatar>
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div data-test="navbar-item-username">{{ username.username }}</div>
|
||||
@ -45,13 +47,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Avatar from 'vue-avatar'
|
||||
|
||||
export default {
|
||||
name: 'Navbar',
|
||||
components: {
|
||||
Avatar,
|
||||
},
|
||||
props: {
|
||||
balance: { type: Number, required: true },
|
||||
},
|
||||
@ -129,8 +126,9 @@ button.navbar-toggler > span.navbar-toggler-icon {
|
||||
.navbar-element {
|
||||
z-index: 1000;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
background-color: #f5f5f5e6;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.sheet-img {
|
||||
@ -139,3 +137,9 @@ button.navbar-toggler > span.navbar-toggler-icon {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
:deep(.container-fluid) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -107,6 +107,8 @@ const props = defineProps({
|
||||
shadow: { type: Boolean, default: true },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['closeSidebar'])
|
||||
|
||||
const route = useRoute()
|
||||
const communityLink = ref(null)
|
||||
|
||||
@ -134,6 +136,7 @@ watch(
|
||||
link.classList.remove('active-route')
|
||||
link.classList.remove('router-link-exact-active')
|
||||
}
|
||||
emit('closeSidebar')
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
@ -7,24 +7,55 @@
|
||||
no-header-close
|
||||
horizontal
|
||||
skip-animation
|
||||
:model-value="isMobileMenuOpen"
|
||||
@update:model-value="isMobileMenuOpen = $event"
|
||||
>
|
||||
<div class="mobile-sidebar-wrapper py-2">
|
||||
<BImg src="img/svg/lines.png" />
|
||||
<sidebar :shadow="false" @admin="emit('admin')" @logout="emit('logout')" />
|
||||
<sidebar
|
||||
:shadow="false"
|
||||
@admin="emit('admin')"
|
||||
@close-sidebar="closeMenu"
|
||||
@logout="emit('logout')"
|
||||
/>
|
||||
</div>
|
||||
<div v-b-toggle.sidebar-mobile class="simple-overlay" />
|
||||
</BCollapse>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onUnmounted, ref, watch } from 'vue'
|
||||
import { lock, unlock } from 'tua-body-scroll-lock'
|
||||
|
||||
const isMobileMenuOpen = ref(false)
|
||||
|
||||
const emit = defineEmits(['admin', 'logout'])
|
||||
|
||||
const closeMenu = () => {
|
||||
isMobileMenuOpen.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => isMobileMenuOpen.value,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
lock()
|
||||
} else {
|
||||
unlock()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
unlock()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.mobile-sidebar-wrapper {
|
||||
width: 220px;
|
||||
background-color: #fff;
|
||||
z-index: 10;
|
||||
z-index: 1001;
|
||||
position: absolute;
|
||||
border-bottom-right-radius: 26px;
|
||||
border-top-right-radius: 26px;
|
||||
@ -38,6 +69,7 @@ const emit = defineEmits(['admin', 'logout'])
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.simple-overlay {
|
||||
@ -46,7 +78,7 @@ const emit = defineEmits(['admin', 'logout'])
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-color: #212529;
|
||||
z-index: 9;
|
||||
z-index: 99;
|
||||
opacity: 0.6;
|
||||
width: calc(100vw - 200px);
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { nextTick } from 'vue'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import SessionLogoutTimeout from './SessionLogoutTimeout.vue'
|
||||
import { useLazyQuery } from '@vue/apollo-composable'
|
||||
import { useStore } from 'vuex'
|
||||
@ -13,25 +13,28 @@ vi.mock('vuex', () => ({
|
||||
vi.mock('@vue/apollo-composable', () => ({
|
||||
useLazyQuery: vi.fn(() => ({
|
||||
load: vi.fn(),
|
||||
loading: false,
|
||||
error: { value: null },
|
||||
loading: ref(false),
|
||||
error: ref(null),
|
||||
})),
|
||||
}))
|
||||
|
||||
// Mock bootstrap-vue-next
|
||||
const mockShow = vi.fn()
|
||||
const mockHide = vi.fn()
|
||||
|
||||
vi.mock('bootstrap-vue-next', () => ({
|
||||
useModal: vi.fn(() => ({
|
||||
show: mockShow,
|
||||
hide: mockHide,
|
||||
})),
|
||||
BModal: { template: '<div><slot></slot><slot name="modal-footer"></slot></div>' },
|
||||
BCard: { template: '<div><slot></slot></div>' },
|
||||
BCardText: { template: '<div><slot></slot></div>' },
|
||||
BRow: { template: '<div><slot></slot></div>' },
|
||||
BCol: { template: '<div><slot></slot></div>' },
|
||||
BButton: { template: '<button><slot></slot></button>' },
|
||||
BModal: {
|
||||
name: 'BModal',
|
||||
props: ['modelValue'],
|
||||
template: '<div><slot></slot><slot name="modal-footer"></slot></div>',
|
||||
},
|
||||
BCard: { name: 'BCard', template: '<div><slot></slot></div>' },
|
||||
BCardText: { name: 'BCardText', template: '<div><slot></slot></div>' },
|
||||
BRow: { name: 'BRow', template: '<div><slot></slot></div>' },
|
||||
BCol: { name: 'BCol', template: '<div><slot></slot></div>' },
|
||||
BButton: { name: 'BButton', template: '<button><slot></slot></button>' },
|
||||
}))
|
||||
|
||||
const setTokenTime = (seconds) => {
|
||||
@ -59,12 +62,12 @@ describe('SessionLogoutTimeout', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
mockShow.mockClear()
|
||||
mockHide.mockClear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('renders the component div.session-logout-timeout', () => {
|
||||
@ -72,69 +75,115 @@ describe('SessionLogoutTimeout', () => {
|
||||
expect(wrapper.find('div.session-logout-timeout').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('tokenExpiresInSeconds computed property', () => {
|
||||
it('returns 0 when token is expired', async () => {
|
||||
wrapper = createWrapper(setTokenTime(-60))
|
||||
await nextTick()
|
||||
expect(wrapper.vm.tokenExpiresInSeconds).toBe(0)
|
||||
})
|
||||
|
||||
it('returns remaining seconds when token is not expired', async () => {
|
||||
wrapper = createWrapper(setTokenTime(120))
|
||||
await nextTick()
|
||||
expect(wrapper.vm.tokenExpiresInSeconds).toBeGreaterThan(0)
|
||||
expect(wrapper.vm.tokenExpiresInSeconds).toBeLessThanOrEqual(120)
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkExpiration', () => {
|
||||
it('shows modal when token expires in less than 75 seconds', async () => {
|
||||
describe('token expiration', () => {
|
||||
it('shows modal when remaining time is 75 seconds or less', async () => {
|
||||
wrapper = createWrapper(setTokenTime(74))
|
||||
await nextTick()
|
||||
|
||||
vi.runAllTimers()
|
||||
|
||||
vi.runOnlyPendingTimers()
|
||||
await nextTick()
|
||||
expect(mockShow).toHaveBeenCalled()
|
||||
|
||||
const modal = wrapper.findComponent({ name: 'BModal' })
|
||||
expect(modal.props('modelValue')).toBe(true)
|
||||
})
|
||||
|
||||
it('emits logout when token is expired', async () => {
|
||||
wrapper = createWrapper(setTokenTime(-1))
|
||||
it('does not show modal when remaining time is more than 75 seconds', async () => {
|
||||
// 76 don't work, because value will be rounded down with floor, so with running the code time were passing,
|
||||
// and 76 will be rounded down to 75
|
||||
wrapper = createWrapper(setTokenTime(77))
|
||||
await nextTick()
|
||||
|
||||
const modal = wrapper.findComponent({ name: 'BModal' })
|
||||
expect(modal.props('modelValue')).toBe(false)
|
||||
})
|
||||
|
||||
it('emits logout when time expires', async () => {
|
||||
wrapper = createWrapper(setTokenTime(2))
|
||||
await nextTick()
|
||||
|
||||
vi.runAllTimers()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.emitted('logout')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleOk', () => {
|
||||
it('hides modal and does not emit logout on successful verification', async () => {
|
||||
it('hides modal and continues session on successful verification', async () => {
|
||||
const mockLoad = vi.fn().mockResolvedValue({})
|
||||
vi.mocked(useLazyQuery).mockReturnValue({
|
||||
load: mockLoad,
|
||||
loading: false,
|
||||
error: { value: null },
|
||||
loading: ref(false),
|
||||
error: ref(null),
|
||||
})
|
||||
|
||||
wrapper = createWrapper()
|
||||
await wrapper.vm.handleOk({ preventDefault: vi.fn() })
|
||||
await wrapper.findComponent({ name: 'BButton' }).trigger('click')
|
||||
await nextTick()
|
||||
|
||||
expect(mockLoad).toHaveBeenCalled()
|
||||
expect(mockHide).toHaveBeenCalledWith('modalSessionTimeOut')
|
||||
expect(wrapper.emitted('logout')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('emits logout on failed verification', async () => {
|
||||
const mockLoad = vi.fn().mockRejectedValue(new Error('Verification failed'))
|
||||
it('emits logout on verification failure', async () => {
|
||||
const mockLoad = vi.fn().mockResolvedValue({})
|
||||
vi.mocked(useLazyQuery).mockReturnValue({
|
||||
load: mockLoad,
|
||||
loading: false,
|
||||
error: { value: new Error('Verification failed') },
|
||||
loading: ref(false),
|
||||
error: ref(new Error('Verification failed')),
|
||||
})
|
||||
|
||||
wrapper = createWrapper()
|
||||
await wrapper.vm.handleOk({ preventDefault: vi.fn() })
|
||||
await wrapper.findComponent({ name: 'BButton' }).trigger('click')
|
||||
await nextTick()
|
||||
|
||||
expect(mockLoad).toHaveBeenCalled()
|
||||
expect(wrapper.emitted('logout')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('emits logout when verification throws an error', async () => {
|
||||
const mockLoad = vi.fn().mockRejectedValue(new Error('Network error'))
|
||||
vi.mocked(useLazyQuery).mockReturnValue({
|
||||
load: mockLoad,
|
||||
loading: ref(false),
|
||||
error: ref(null),
|
||||
})
|
||||
|
||||
wrapper = createWrapper()
|
||||
await wrapper.findComponent({ name: 'BButton' }).trigger('click')
|
||||
await nextTick()
|
||||
|
||||
expect(mockLoad).toHaveBeenCalled()
|
||||
expect(wrapper.emitted('logout')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('time formatting', () => {
|
||||
it('formats remaining time correctly', async () => {
|
||||
wrapper = createWrapper(setTokenTime(65))
|
||||
await nextTick()
|
||||
|
||||
const warningText = wrapper.find('.text-warning')
|
||||
// second will be rounded with floor
|
||||
expect(warningText.text()).toContain('64')
|
||||
})
|
||||
|
||||
it('shows 00 when time is expired', async () => {
|
||||
wrapper = createWrapper(setTokenTime(-1))
|
||||
await nextTick()
|
||||
|
||||
const warningText = wrapper.find('.text-warning')
|
||||
expect(warningText.text()).toContain('00')
|
||||
})
|
||||
})
|
||||
|
||||
describe('cleanup', () => {
|
||||
it('clears interval on unmount', () => {
|
||||
const clearIntervalSpy = vi.spyOn(window, 'clearInterval')
|
||||
wrapper = createWrapper()
|
||||
wrapper.unmount()
|
||||
expect(clearIntervalSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
</div>
|
||||
<div class="p-3 h2 text-warning">
|
||||
{{ $t('session.logoutIn') }}
|
||||
<b>{{ tokenExpiresInSeconds }}</b>
|
||||
<b>{{ formatTime(remainingTime) }}</b>
|
||||
{{ $t('time.seconds') }}
|
||||
</div>
|
||||
</BCardText>
|
||||
@ -43,91 +43,79 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onBeforeUnmount, watch } from 'vue'
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { useLazyQuery } from '@vue/apollo-composable'
|
||||
import { verifyLogin } from '@/graphql/queries'
|
||||
import { useModal } from 'bootstrap-vue-next'
|
||||
import { BButton, BCard, BCardText, BCol, BModal, BRow, useModal } from 'bootstrap-vue-next'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const emit = defineEmits(['logout'])
|
||||
|
||||
const sessionModalModel = ref(false)
|
||||
|
||||
const { show: showModal, hide: hideModal } = useModal('modalSessionTimeOut')
|
||||
const { hide: hideModal } = useModal('modalSessionTimeOut')
|
||||
|
||||
const { load: verifyLoginQuery, loading, error } = useLazyQuery(verifyLogin)
|
||||
|
||||
const timerInterval = ref(null)
|
||||
const remainingTime = ref(0)
|
||||
let intervalId = null
|
||||
|
||||
const now = ref(new Date().getTime())
|
||||
const tokenExpirationTime = computed(() => new Date(store.state.tokenTime * 1000))
|
||||
|
||||
const tokenExpiresInSeconds = computed(() => {
|
||||
const remainingSecs = Math.floor(
|
||||
(new Date(store.state.tokenTime * 1000).getTime() - now.value) / 1000,
|
||||
)
|
||||
return remainingSecs <= 0 ? 0 : remainingSecs
|
||||
const isTokenValid = computed(() => {
|
||||
return remainingTime.value > 0
|
||||
})
|
||||
|
||||
const updateNow = () => {
|
||||
now.value = new Date().getTime()
|
||||
const calculateRemainingTime = () => {
|
||||
const now = new Date()
|
||||
const diff = tokenExpirationTime.value - now
|
||||
remainingTime.value = Math.max(0, Math.floor(diff / 1000))
|
||||
// Show modal if remaining time is 75 seconds or less
|
||||
if (remainingTime.value <= 75) {
|
||||
sessionModalModel.value = true
|
||||
}
|
||||
// Clear interval if time expired
|
||||
if (remainingTime.value <= 0) {
|
||||
clearInterval(intervalId)
|
||||
}
|
||||
}
|
||||
|
||||
const checkExpiration = () => {
|
||||
if (tokenExpiresInSeconds.value < 75 && timerInterval.value && !sessionModalModel.value) {
|
||||
showModal()
|
||||
const formatTime = (seconds) => {
|
||||
if (seconds <= 0) return '00'
|
||||
return `${seconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
calculateRemainingTime()
|
||||
|
||||
if (isTokenValid.value) {
|
||||
intervalId = setInterval(calculateRemainingTime, 1000)
|
||||
}
|
||||
if (tokenExpiresInSeconds.value === 0) {
|
||||
stopTimer()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId)
|
||||
}
|
||||
})
|
||||
|
||||
watch(remainingTime, (newTime) => {
|
||||
if (newTime <= 0) {
|
||||
emit('logout')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const handleOk = async (bvModalEvent) => {
|
||||
bvModalEvent.preventDefault()
|
||||
try {
|
||||
await verifyLoginQuery()
|
||||
if (error.value) {
|
||||
emit('logout')
|
||||
throw new Error('Login verification failed')
|
||||
}
|
||||
hideModal('modalSessionTimeOut')
|
||||
} catch {
|
||||
stopTimer()
|
||||
emit('logout')
|
||||
}
|
||||
}
|
||||
|
||||
const startTimer = () => {
|
||||
stopTimer()
|
||||
timerInterval.value = setInterval(() => {
|
||||
updateNow()
|
||||
checkExpiration()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const stopTimer = () => {
|
||||
if (timerInterval.value) {
|
||||
clearInterval(timerInterval.value)
|
||||
timerInterval.value = null
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
tokenExpiresInSeconds,
|
||||
(newValue) => {
|
||||
if (newValue < 75 && !timerInterval.value) {
|
||||
startTimer()
|
||||
} else if (newValue >= 75 && timerInterval.value) {
|
||||
stopTimer()
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopTimer()
|
||||
})
|
||||
|
||||
checkExpiration()
|
||||
</script>
|
||||
|
||||
84
frontend/src/components/SkeletonLoaderElement.vue
Normal file
84
frontend/src/components/SkeletonLoaderElement.vue
Normal file
@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div
|
||||
class="skeleton-loader my-2"
|
||||
:class="{ 'with-animation': !disableAnimation }"
|
||||
:style="{
|
||||
width: computedWidth,
|
||||
height: computedHeight,
|
||||
borderRadius: computedRadius,
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: '100%',
|
||||
},
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: '1rem',
|
||||
},
|
||||
disableAnimation: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
fullRadius: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
useGradidoRadius: {
|
||||
type: Boolean,
|
||||
},
|
||||
})
|
||||
|
||||
const computedWidth = computed(() => {
|
||||
if (typeof props.width === 'number') return `${props.width}px`
|
||||
return props.width
|
||||
})
|
||||
|
||||
const computedHeight = computed(() => {
|
||||
if (typeof props.height === 'number') return `${props.height}px`
|
||||
return props.height
|
||||
})
|
||||
|
||||
const computedRadius = computed(() => {
|
||||
return props.fullRadius ? '100%' : props.useGradidoRadius ? '26px' : '0'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.skeleton-loader {
|
||||
background: #e9ecef; /* Bootstrap 5 gray-200 color */
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.with-animation {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.with-animation::after {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transform: translateX(-100%);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgb(255 255 255 / 0%) 0%,
|
||||
rgb(255 255 255 / 20%) 20%,
|
||||
rgb(255 255 255 / 50%) 60%,
|
||||
rgb(255 255 255 / 0%) 100%
|
||||
);
|
||||
animation: shimmer 2s infinite;
|
||||
content: '';
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -54,6 +54,7 @@ export default {
|
||||
font-size: 14px;
|
||||
text-wrap: nowrap;
|
||||
color: black !important;
|
||||
border-radius: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,25 +4,24 @@
|
||||
<BCol class="h3">{{ $t('transaction.lastTransactions') }}</BCol>
|
||||
</BRow>
|
||||
|
||||
<div v-for="(transaction, index) in transactions" :key="transaction.id">
|
||||
<BRow
|
||||
v-if="
|
||||
index <= 8 &&
|
||||
transaction.typeId !== 'DECAY' &&
|
||||
transaction.typeId !== 'LINK_SUMMARY' &&
|
||||
transaction.typeId !== 'CREATION'
|
||||
"
|
||||
align-v="center"
|
||||
class="mb-4"
|
||||
>
|
||||
<div v-for="transaction in filteredTransactions" :key="transaction.id">
|
||||
<BRow align-v="center" class="mb-4">
|
||||
<BCol cols="auto">
|
||||
<div class="align-items-center">
|
||||
<avatar
|
||||
<!-- <avatar-->
|
||||
<!-- class="vue3-avatar"-->
|
||||
<!-- :size="72"-->
|
||||
<!-- :color="'#fff'"-->
|
||||
<!-- :name="`${transaction.linkedUser.firstName} ${transaction.linkedUser.lastName}`"-->
|
||||
<!-- :initials="`${transaction.linkedUser.firstName[0]}${transaction.linkedUser.lastName[0]}`"-->
|
||||
<!-- :border="false"-->
|
||||
<!-- />-->
|
||||
<app-avatar
|
||||
:size="72"
|
||||
:color="'#fff'"
|
||||
:username="`${transaction.linkedUser.firstName} ${transaction.linkedUser.lastName}`"
|
||||
:name="`${transaction.linkedUser.firstName} ${transaction.linkedUser.lastName}`"
|
||||
:initials="`${transaction.linkedUser.firstName[0]}${transaction.linkedUser.lastName[0]}`"
|
||||
></avatar>
|
||||
/>
|
||||
</div>
|
||||
</BCol>
|
||||
<BCol class="p-1">
|
||||
@ -31,14 +30,19 @@
|
||||
<div class="fw-bold">
|
||||
<name :linked-user="transaction.linkedUser" font-color="text-dark" />
|
||||
</div>
|
||||
<div class="d-flex mt-3">
|
||||
<div class="small">
|
||||
<button
|
||||
class="transaction-details-link d-flex mt-3"
|
||||
role="link"
|
||||
:data-href="`/transactions#transaction-${transaction.id}`"
|
||||
@click="handleRedirect(transaction.id)"
|
||||
>
|
||||
<span class="small">
|
||||
{{ $filters.GDD(transaction.amount) }}
|
||||
</div>
|
||||
<div class="small ms-3 text-end">
|
||||
</span>
|
||||
<span class="small ms-3 text-end">
|
||||
{{ $d(new Date(transaction.balanceDate), 'short') }}
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</button>
|
||||
</BCol>
|
||||
</BRow>
|
||||
</BCol>
|
||||
@ -46,23 +50,50 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Avatar from 'vue-avatar'
|
||||
<script setup>
|
||||
import Name from '@/components/TransactionRows/Name'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
import { computed } from 'vue'
|
||||
import AppAvatar from '@/components/AppAvatar.vue'
|
||||
const props = defineProps({
|
||||
transactions: {
|
||||
default: () => [],
|
||||
type: Array,
|
||||
},
|
||||
})
|
||||
|
||||
export default {
|
||||
name: 'LastTransactions',
|
||||
components: {
|
||||
Avatar,
|
||||
Name,
|
||||
},
|
||||
props: {
|
||||
transactions: {
|
||||
default: () => [],
|
||||
type: Array,
|
||||
},
|
||||
transactionCount: { type: Number, default: 0 },
|
||||
transactionLinkCount: { type: Number, default: 0 },
|
||||
},
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const store = useStore()
|
||||
|
||||
const handleRedirect = (id) => {
|
||||
store.dispatch('changeTransactionToHighlightId', id)
|
||||
if (route.name !== 'Transactions') router.replace({ name: 'Transactions' })
|
||||
}
|
||||
|
||||
const filteredTransactions = computed(() => {
|
||||
return props.transactions
|
||||
.filter(
|
||||
(transaction) =>
|
||||
transaction.typeId !== 'DECAY' &&
|
||||
transaction.typeId !== 'LINK_SUMMARY' &&
|
||||
transaction.typeId !== 'CREATION',
|
||||
)
|
||||
.slice(0, 8)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.transaction-details-link {
|
||||
color: var(--bs-body-color) !important;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border-bottom-color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.transaction-details-link:hover {
|
||||
border-color: #383838;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -245,6 +245,7 @@ import { BAvatar, BCol, BCollapse, BRow } from 'bootstrap-vue-next'
|
||||
import TransactionCollapse from '@/components/TransactionCollapse.vue'
|
||||
import { GdtEntryType } from '@/graphql/enums'
|
||||
import VariantIcon from '@/components/VariantIcon.vue'
|
||||
import { createStore } from 'vuex'
|
||||
|
||||
const mockToastError = vi.fn()
|
||||
vi.mock('@/composables/useToast', () => ({
|
||||
@ -268,6 +269,13 @@ describe('Transaction', () => {
|
||||
const Wrapper = () => {
|
||||
return mount(Transaction, {
|
||||
global: {
|
||||
plugins: [
|
||||
createStore({
|
||||
state: {
|
||||
transactionToHighlightId: '',
|
||||
},
|
||||
}),
|
||||
],
|
||||
mocks: {
|
||||
$d: (value) => value?.toString() ?? '',
|
||||
$n: (value) => value?.toString() ?? '',
|
||||
|
||||
@ -49,11 +49,12 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, getCurrentInstance } from 'vue'
|
||||
import { ref, computed, onMounted, getCurrentInstance, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import CollapseIcon from './TransactionRows/CollapseIcon'
|
||||
import TransactionCollapse from './TransactionCollapse'
|
||||
import { GdtEntryType } from '../graphql/enums'
|
||||
import { useStore } from 'vuex'
|
||||
|
||||
const props = defineProps({
|
||||
amount: Number,
|
||||
@ -71,10 +72,14 @@ const props = defineProps({
|
||||
const collapseStatus = ref([])
|
||||
const visible = ref(false)
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const { t, n } = useI18n()
|
||||
|
||||
const collapseId = computed(() => 'gdt-collapse-' + String(props.id))
|
||||
|
||||
const transactionToHighlightId = computed(() => store.state.transactionToHighlightId)
|
||||
|
||||
const getLinesByType = computed(() => {
|
||||
switch (props.gdtEntryType) {
|
||||
case GdtEntryType.FORM:
|
||||
@ -116,6 +121,12 @@ const getLinesByType = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(transactionToHighlightId, () => {
|
||||
if (parseInt(transactionToHighlightId.value) === props.id) {
|
||||
visible.value = true
|
||||
}
|
||||
})
|
||||
|
||||
// onMounted(() => {
|
||||
// // Note: This event listener setup might need to be adjusted for Vue 3
|
||||
// const root = getCurrentInstance().appContext.config.globalProperties
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="transaction-link gradido-custom-background">
|
||||
<BRow :class="validLink ? '' : 'bg-muted text-dark'" class="mb-2 pt-2 pb-2">
|
||||
<BRow :class="{ 'light-gray-text': !validLink }" class="mb-2 pt-2 pb-2">
|
||||
<BCol cols="1">
|
||||
<variant-icon icon="link45deg" variant="danger" />
|
||||
</BCol>
|
||||
@ -153,4 +153,8 @@ const toggleQrModal = () => {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.light-gray-text {
|
||||
color: #adb5bd !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<slot name="item" />
|
||||
<slot :name="typeId"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<div class="text-end">{{ $t('form.memo') }}</div>
|
||||
</BCol>
|
||||
<BCol cols="7">
|
||||
<div class="gdd-transaction-list-message">{{ memo }}</div>
|
||||
<div class="gdd-transaction-list-message" v-html="displayData" />
|
||||
</BCol>
|
||||
</BRow>
|
||||
</div>
|
||||
@ -19,5 +19,27 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
displayData() {
|
||||
return this.formatLinks(this.memo)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatLinks(text) {
|
||||
const urlPattern = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim
|
||||
const emailPattern = /(\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b)/g
|
||||
|
||||
// Replace URLs with clickable links
|
||||
text = text.replace(
|
||||
urlPattern,
|
||||
'<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>',
|
||||
)
|
||||
|
||||
// Replace email addresses with mailto links
|
||||
text = text.replace(emailPattern, '<a href="mailto:$1">$1</a>')
|
||||
|
||||
return text
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { mount, RouterLinkStub } from '@vue/test-utils'
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import Name from './Name'
|
||||
import { BLink } from 'bootstrap-vue-next'
|
||||
@ -44,7 +44,7 @@ describe('Name', () => {
|
||||
global: {
|
||||
mocks,
|
||||
stubs: {
|
||||
BLink,
|
||||
RouterLink: RouterLinkStub,
|
||||
},
|
||||
},
|
||||
props: propsData,
|
||||
@ -88,31 +88,18 @@ describe('Name', () => {
|
||||
|
||||
it('has a link', () => {
|
||||
expect(
|
||||
wrapper
|
||||
.find('div.gdd-transaction-list-item-name')
|
||||
.findComponent({ name: 'BLink' })
|
||||
.exists(),
|
||||
wrapper.find('div.gdd-transaction-list-item-name').findComponent(RouterLinkStub).exists(),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
describe('click link', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findComponent({ name: 'BLink' }).trigger('click')
|
||||
})
|
||||
|
||||
it('pushes router to send', () => {
|
||||
expect(routerPushMock).toHaveBeenCalledWith({
|
||||
path: '/send',
|
||||
})
|
||||
})
|
||||
|
||||
it('pushes params for gradidoID and community UUID', () => {
|
||||
expect(routerPushMock).toHaveBeenCalledWith({
|
||||
params: {
|
||||
communityIdentifier: 'community UUID',
|
||||
userIdentifier: 'gradido-ID',
|
||||
},
|
||||
})
|
||||
it('RouterLink has correct to prop', () => {
|
||||
const routerLink = wrapper.findComponent(RouterLinkStub)
|
||||
expect(routerLink.props().to).toEqual({
|
||||
name: 'Send',
|
||||
params: {
|
||||
communityIdentifier: 'community UUID',
|
||||
userIdentifier: 'gradido-ID',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -2,9 +2,9 @@
|
||||
<div class="name">
|
||||
<div class="gdd-transaction-list-item-name">
|
||||
<div v-if="linkedUser && linkedUser.gradidoID">
|
||||
<BLink :class="fontColor" @click.stop="tunnelEmail">
|
||||
<router-link :class="fontColor" :to="pushTo">
|
||||
{{ itemText }}
|
||||
</BLink>
|
||||
</router-link>
|
||||
</div>
|
||||
<span v-else>{{ itemText }}</span>
|
||||
</div>
|
||||
@ -45,6 +45,15 @@ export default {
|
||||
(this.linkedUser.communityName ? ' / ' + this.linkedUser.communityName : '')
|
||||
: this.text
|
||||
},
|
||||
pushTo() {
|
||||
return {
|
||||
name: 'Send',
|
||||
params: {
|
||||
userIdentifier: this.linkedUser.gradidoID,
|
||||
communityIdentifier: this.linkedUser.communityUuid,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async tunnelEmail() {
|
||||
|
||||
167
frontend/src/components/Transactions/GddTransaction.vue
Normal file
167
frontend/src/components/Transactions/GddTransaction.vue
Normal file
@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<div
|
||||
:id="`transaction-${props.transaction.id}`"
|
||||
ref="gddTransaction"
|
||||
:class="`transaction-slot-${props.transaction.type}`"
|
||||
:data-transaction-id="`transaction-${props.transaction.id}`"
|
||||
@click="toggleVisible"
|
||||
>
|
||||
<BRow class="align-items-center">
|
||||
<BCol cols="3" lg="2" md="2">
|
||||
<component :is="avatarComponent" v-bind="avatarProps">
|
||||
<variant-icon v-if="isCreationType" icon="gift" variant="white" />
|
||||
</component>
|
||||
</BCol>
|
||||
<BCol>
|
||||
<div>
|
||||
<component :is="nameComponent" v-bind="nameProps" />
|
||||
</div>
|
||||
<span class="small">{{ $d(new Date(props.transaction.balanceDate), 'short') }}</span>
|
||||
<span class="ms-4 small">{{ $d(new Date(props.transaction.balanceDate), 'time') }}</span>
|
||||
</BCol>
|
||||
<BCol cols="8" lg="3" md="3" sm="8" offset="3" offset-md="0" offset-lg="0">
|
||||
<div class="small mb-2">
|
||||
{{ $t(`decay.types.${props.transaction.typeId.toLowerCase()}`) }}
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
'fw-bold',
|
||||
{
|
||||
'gradido-global-color-accent': props.transaction.typeId === 'RECEIVE',
|
||||
'text-140': props.transaction.typeId === 'SEND',
|
||||
},
|
||||
]"
|
||||
data-test="transaction-amount"
|
||||
>
|
||||
{{ $filters.GDD(props.transaction.amount) }}
|
||||
</div>
|
||||
<div v-if="props.transaction.linkId" class="small">
|
||||
{{ $t('via_link') }}
|
||||
<variant-icon icon="link45deg" variant="muted" class="m-mb-1" />
|
||||
</div>
|
||||
</BCol>
|
||||
<BCol cols="12" md="1" lg="1" class="text-end">
|
||||
<collapse-icon class="text-end" :visible="visible" />
|
||||
</BCol>
|
||||
</BRow>
|
||||
<BCollapse :model-value="visible" class="pb-4 pt-lg-3">
|
||||
<decay-information
|
||||
:type-id="props.transaction.typeId"
|
||||
:decay="props.transaction.decay"
|
||||
:amount="props.transaction.amount"
|
||||
:memo="props.transaction.memo"
|
||||
:balance="props.transaction.balance"
|
||||
:previous-balance="props.transaction.previousBalance"
|
||||
/>
|
||||
</BCollapse>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import CollapseIcon from '../TransactionRows/CollapseIcon'
|
||||
import Name from '../TransactionRows/Name'
|
||||
import DecayInformation from '../DecayInformations/DecayInformation'
|
||||
import { BAvatar, BRow } from 'bootstrap-vue-next'
|
||||
import AppAvatar from '@/components/AppAvatar.vue'
|
||||
|
||||
const props = defineProps({
|
||||
transaction: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const gddTransaction = ref(null)
|
||||
|
||||
const store = useStore()
|
||||
const visible = ref(false)
|
||||
|
||||
const toggleVisible = () => {
|
||||
visible.value = !visible.value
|
||||
}
|
||||
|
||||
const username = computed(() => ({
|
||||
username: `${props.transaction?.linkedUser?.firstName} ${props.transaction?.linkedUser?.lastName}`,
|
||||
initials: `${props.transaction?.linkedUser?.firstName[0]}${props.transaction.linkedUser?.lastName[0]}`,
|
||||
}))
|
||||
|
||||
const isCreationType = computed(() => {
|
||||
return props.transaction.typeId === 'CREATION'
|
||||
})
|
||||
|
||||
const avatarComponent = computed(() => {
|
||||
return isCreationType.value ? BAvatar : AppAvatar
|
||||
})
|
||||
|
||||
const avatarProps = computed(() => {
|
||||
if (isCreationType.value) {
|
||||
return {
|
||||
size: 42,
|
||||
rounded: 'lg',
|
||||
variant: 'success',
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
username: username.value.username,
|
||||
initials: username.value.initials,
|
||||
color: '#fff',
|
||||
size: 42,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const nameComponent = computed(() => {
|
||||
return isCreationType.value ? 'div' : Name
|
||||
})
|
||||
|
||||
const nameProps = computed(() => {
|
||||
if (isCreationType.value) {
|
||||
return {
|
||||
class: 'fw-bold',
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
class: 'fw-bold',
|
||||
amount: props.transaction.amount,
|
||||
linkedUser: props.transaction.linkedUser,
|
||||
linkId: props.transaction.linkId,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const handleOpenAfterScroll = (scrollY) => {
|
||||
const handleScrollEnd = () => {
|
||||
window.removeEventListener('scrollend', handleScrollEnd)
|
||||
}
|
||||
|
||||
window.addEventListener('scrollend', handleScrollEnd)
|
||||
window.scrollTo(0, scrollY)
|
||||
}
|
||||
|
||||
const transactionToHighlightId = computed(() => store.state.transactionToHighlightId)
|
||||
|
||||
watch(
|
||||
transactionToHighlightId,
|
||||
async (newValue) => {
|
||||
if (parseInt(newValue) === props.transaction.id) {
|
||||
visible.value = true
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById(`transaction-${props.transaction.id}`)
|
||||
const yVal = element.getBoundingClientRect().top + window.pageYOffset - 16
|
||||
handleOpenAfterScroll(yVal)
|
||||
}, 300)
|
||||
await store.dispatch('changeTransactionToHighlightId', '')
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.b-avatar-custom > svg) {
|
||||
height: 2em;
|
||||
width: 2em;
|
||||
}
|
||||
</style>
|
||||
@ -1,16 +1,16 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import TransactionReceive from './TransactionReceive'
|
||||
import Avatar from 'vue-avatar'
|
||||
import CollapseIcon from '../TransactionRows/CollapseIcon'
|
||||
import Name from '../TransactionRows/Name'
|
||||
import DecayInformation from '../DecayInformations/DecayInformation'
|
||||
import { BCol, BCollapse, BRow } from 'bootstrap-vue-next'
|
||||
import AppAvatar from '@/components/AppAvatar.vue'
|
||||
|
||||
// Mock child components
|
||||
vi.mock('vue-avatar', () => ({
|
||||
vi.mock('app-avatar', () => ({
|
||||
default: {
|
||||
name: 'Avatar',
|
||||
name: 'AppAvatar',
|
||||
render: () => null,
|
||||
},
|
||||
}))
|
||||
@ -86,7 +86,7 @@ describe('TransactionReceive', () => {
|
||||
$d: (date, format) => `Mocked ${format} date for ${date}`,
|
||||
},
|
||||
components: {
|
||||
Avatar,
|
||||
AppAvatar,
|
||||
CollapseIcon,
|
||||
Name,
|
||||
DecayInformation,
|
||||
|
||||
@ -3,12 +3,14 @@
|
||||
<BRow class="align-items-center">
|
||||
<BCol cols="3" lg="2" md="2">
|
||||
<!-- <b-avatar :text="avatarText" variant="success" size="3em"></b-avatar> -->
|
||||
<avatar
|
||||
:username="username.username"
|
||||
<app-avatar
|
||||
class="vue3-avatar"
|
||||
:name="username.username"
|
||||
:initials="username.initials"
|
||||
:color="'#fff'"
|
||||
:size="42"
|
||||
></avatar>
|
||||
:border="false"
|
||||
/>
|
||||
</BCol>
|
||||
<BCol>
|
||||
<div>
|
||||
@ -46,7 +48,6 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Avatar from 'vue-avatar'
|
||||
import CollapseIcon from '../TransactionRows/CollapseIcon'
|
||||
import Name from '../TransactionRows/Name'
|
||||
import DecayInformation from '../DecayInformations/DecayInformation'
|
||||
@ -54,7 +55,6 @@ import DecayInformation from '../DecayInformations/DecayInformation'
|
||||
export default {
|
||||
name: 'TransactionReceive',
|
||||
components: {
|
||||
Avatar,
|
||||
CollapseIcon,
|
||||
Name,
|
||||
DecayInformation,
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import TransactionSend from './TransactionSend'
|
||||
import Avatar from 'vue-avatar'
|
||||
import CollapseIcon from '../TransactionRows/CollapseIcon'
|
||||
import Name from '../TransactionRows/Name'
|
||||
import DecayInformation from '../DecayInformations/DecayInformation'
|
||||
import { BCol, BCollapse, BRow } from 'bootstrap-vue-next'
|
||||
import AppAvatar from '@/components/AppAvatar.vue'
|
||||
|
||||
vi.mock('vue-avatar', () => ({
|
||||
vi.mock('app-avatar', () => ({
|
||||
default: {
|
||||
name: 'Avatar',
|
||||
name: 'AppAvatar',
|
||||
render: () => null,
|
||||
},
|
||||
}))
|
||||
@ -84,7 +84,7 @@ describe('TransactionSend', () => {
|
||||
$d: (date, format) => `Mocked ${format} date for ${date}`,
|
||||
},
|
||||
components: {
|
||||
Avatar,
|
||||
AppAvatar,
|
||||
CollapseIcon,
|
||||
Name,
|
||||
DecayInformation,
|
||||
|
||||
@ -2,11 +2,13 @@
|
||||
<div class="transaction-slot-send" @click="visible = !visible">
|
||||
<BRow class="align-items-center">
|
||||
<BCol cols="3" lg="2" md="2">
|
||||
<avatar
|
||||
:username="username.username"
|
||||
<app-avatar
|
||||
class="vue3-avatar"
|
||||
:name="username.username"
|
||||
:initials="username.initials"
|
||||
:color="'#fff'"
|
||||
:size="42"
|
||||
:border="false"
|
||||
/>
|
||||
</BCol>
|
||||
<BCol>
|
||||
@ -45,7 +47,6 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Avatar from 'vue-avatar'
|
||||
import CollapseIcon from '../TransactionRows/CollapseIcon'
|
||||
import Name from '../TransactionRows/Name'
|
||||
import DecayInformation from '../DecayInformations/DecayInformation'
|
||||
@ -53,7 +54,6 @@ import DecayInformation from '../DecayInformations/DecayInformation'
|
||||
export default {
|
||||
name: 'TransactionSend',
|
||||
components: {
|
||||
Avatar,
|
||||
CollapseIcon,
|
||||
Name,
|
||||
DecayInformation,
|
||||
|
||||
46
frontend/src/components/UserSettings/CoordinatesDisplay.vue
Normal file
46
frontend/src/components/UserSettings/CoordinatesDisplay.vue
Normal file
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="coordinates-display">
|
||||
<div class="p-2">
|
||||
<BButton class="me-1" size="sm" @click="emit('centerMap', 'USER')"><IBiPinMapFill /></BButton>
|
||||
<span>
|
||||
{{
|
||||
$t('settings.GMS.map.userCoords', {
|
||||
lat: userPosition.lat.toFixed(6),
|
||||
lng: userPosition.lng.toFixed(6),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<BButton class="me-1" size="sm" @click="emit('centerMap', 'COMMUNITY')">
|
||||
<IBiPinMap />
|
||||
</BButton>
|
||||
<span>
|
||||
{{
|
||||
$t('settings.GMS.map.communityCoords', {
|
||||
lat: communityPosition.lat.toFixed(6),
|
||||
lng: communityPosition.lng.toFixed(6),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { BButton } from 'bootstrap-vue-next'
|
||||
|
||||
const emit = defineEmits(['centerMap'])
|
||||
|
||||
const props = defineProps({
|
||||
userPosition: Object,
|
||||
communityPosition: Object,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.coordinates-display {
|
||||
margin-top: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils'
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import UserCard from './UserCard.vue'
|
||||
import { BCol, BRow } from 'bootstrap-vue-next'
|
||||
import AppAvatar from '@/components/AppAvatar.vue'
|
||||
|
||||
vi.mock('vue-avatar', () => ({
|
||||
default: {
|
||||
@ -39,6 +40,7 @@ describe('UserCard', () => {
|
||||
components: {
|
||||
BRow,
|
||||
BCol,
|
||||
AppAvatar,
|
||||
},
|
||||
mocks,
|
||||
},
|
||||
@ -53,12 +55,12 @@ describe('UserCard', () => {
|
||||
expect(wrapper.find('.userdata-card').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders the SPAN Element ".vue-avatar--wrapper"', () => {
|
||||
expect(wrapper.find('.vue-avatar--wrapper').exists()).toBe(true)
|
||||
it('renders the DIV Element ".app-avatar"', () => {
|
||||
expect(wrapper.find('.app-avatar').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('displays the first letters of the firstName and lastName', () => {
|
||||
expect(wrapper.find('.vue-avatar--wrapper span').text()).toBe('BB')
|
||||
expect(wrapper.find('.app-avatar').text()).toBe('BB')
|
||||
})
|
||||
|
||||
it('displays the correct balance', () => {
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
<template>
|
||||
<div class="userdata-card">
|
||||
<div class="center-per-margin">
|
||||
<avatar
|
||||
:username="username.username"
|
||||
<app-avatar
|
||||
class="vue3-avatar"
|
||||
:name="username.username"
|
||||
:initials="username.initials"
|
||||
:color="'#fff'"
|
||||
:size="90"
|
||||
></avatar>
|
||||
:border="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="justify-content-center mt-5 mb-5">
|
||||
@ -44,14 +46,10 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Avatar from 'vue-avatar'
|
||||
import CONFIG from '@/config'
|
||||
|
||||
export default {
|
||||
name: 'UserCard',
|
||||
components: {
|
||||
Avatar,
|
||||
},
|
||||
props: {
|
||||
balance: { type: Number, default: 0 },
|
||||
transactionCount: { type: Number, default: 0 },
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
<template>
|
||||
<BButton>{{ $t('settings.GMS.location.button') }}</BButton>
|
||||
</template>
|
||||
<script setup>
|
||||
import { BButton } from 'bootstrap-vue-next'
|
||||
</script>
|
||||
@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<BButton @click="isModalOpen = !isModalOpen">{{ $t('settings.GMS.location.button') }}</BButton>
|
||||
<BModal :model-value="isModalOpen" fullscreen @update:modelValue="isModalOpen = !isModalOpen">
|
||||
<template #title>
|
||||
<h3>{{ $t('settings.GMS.map.headline') }}</h3>
|
||||
</template>
|
||||
<template #default>
|
||||
<BContainer class="bg-white appBoxShadow gradido-border-radius p-4 mt--3">
|
||||
<user-location-map
|
||||
v-if="isModalOpen"
|
||||
:user-marker-coords="userLocation"
|
||||
:community-marker-coords="communityLocation"
|
||||
@update:userPosition="updateUserLocation"
|
||||
/>
|
||||
</BContainer>
|
||||
</template>
|
||||
<template #footer>
|
||||
<BButton variant="gradido" @click="saveUserLocation">
|
||||
{{ $t('settings.GMS.location.saveLocation') }}
|
||||
</BButton>
|
||||
</template>
|
||||
</BModal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useMutation, useQuery } from '@vue/apollo-composable'
|
||||
import 'leaflet/dist/leaflet.css'
|
||||
import { updateUserInfos } from '@/graphql/mutations'
|
||||
import { useAppToast } from '@/composables/useToast'
|
||||
import UserLocationMap from '@/components/UserSettings/UserLocationMap'
|
||||
import { BButton, BModal } from 'bootstrap-vue-next'
|
||||
import { userLocationQuery } from '@/graphql/queries'
|
||||
import CONFIG from '@/config'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { mutate: updateUserInfo } = useMutation(updateUserInfos)
|
||||
const { onResult, onError } = useQuery(userLocationQuery, {}, { fetchPolicy: 'network-only' })
|
||||
const { toastSuccess, toastError } = useAppToast()
|
||||
|
||||
const capturedLocation = ref(null)
|
||||
const isModalOpen = ref(false)
|
||||
const userLocation = ref({ lat: 0, lng: 0 })
|
||||
const communityLocation = ref({ lat: 0, lng: 0 })
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
onResult(({ data }) => {
|
||||
communityLocation.value.lng = data.userLocation.longitude
|
||||
communityLocation.value.lat = data.userLocation.latitude
|
||||
|
||||
userLocation.value.lng = data?.userLocation?.longitude ?? communityLocation.value.lng
|
||||
userLocation.value.lat = data?.userLocation?.latitude ?? communityLocation.value.lat
|
||||
})
|
||||
|
||||
onError((err) => {
|
||||
userLocation.value = defaultLocation.value
|
||||
communityLocation.value = defaultLocation.value
|
||||
toastError(err.message)
|
||||
})
|
||||
|
||||
const defaultLocation = computed(() => {
|
||||
const defaultCommunityCoords = CONFIG.COMMUNITY_LOCATION.split(',')
|
||||
return {
|
||||
lat: parseFloat(defaultCommunityCoords[0]),
|
||||
lng: parseFloat(defaultCommunityCoords[1]),
|
||||
}
|
||||
})
|
||||
|
||||
const saveUserLocation = async () => {
|
||||
try {
|
||||
const loc = { longitude: capturedLocation.value.lng, latitude: capturedLocation.value.lat }
|
||||
|
||||
await updateUserInfo({
|
||||
gmsLocation: {
|
||||
longitude: capturedLocation.value.lng,
|
||||
latitude: capturedLocation.value.lat,
|
||||
},
|
||||
})
|
||||
toastSuccess(t('settings.GMS.location.updateSuccess'))
|
||||
isModalOpen.value = false
|
||||
} catch (error) {
|
||||
toastError(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const updateUserLocation = (currentUserLocation) => {
|
||||
capturedLocation.value = currentUserLocation
|
||||
}
|
||||
</script>
|
||||
218
frontend/src/components/UserSettings/UserLocationMap.vue
Normal file
218
frontend/src/components/UserSettings/UserLocationMap.vue
Normal file
@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<div>
|
||||
<coordinates-display
|
||||
:community-position="communityPosition"
|
||||
:user-position="userPosition"
|
||||
@centerMap="handleMapCenter"
|
||||
/>
|
||||
<div ref="mapContainer" class="map-container" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import L from 'leaflet'
|
||||
import { GeoSearchControl, OpenStreetMapProvider } from 'leaflet-geosearch'
|
||||
import 'leaflet-geosearch/dist/geosearch.css'
|
||||
import CoordinatesDisplay from '@/components/UserSettings/CoordinatesDisplay.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const mapContainer = ref(null)
|
||||
const map = ref(null)
|
||||
const userMarker = ref(null)
|
||||
const communityMarker = ref(null)
|
||||
const searchQuery = ref('')
|
||||
const userPosition = ref({ lat: 0, lng: 0 })
|
||||
const communityPosition = ref({ lat: 0, lng: 0 })
|
||||
const defaultZoom = 13
|
||||
|
||||
const emit = defineEmits(['update:userPosition'])
|
||||
|
||||
const props = defineProps({
|
||||
userMarkerCoords: Object,
|
||||
communityMarkerCoords: Object,
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.userMarkerCoords) {
|
||||
userPosition.value = props.userMarkerCoords
|
||||
}
|
||||
if (props.communityMarkerCoords) {
|
||||
communityPosition.value = props.communityMarkerCoords
|
||||
}
|
||||
await nextTick()
|
||||
initMap()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (map.value) {
|
||||
map.value.remove()
|
||||
}
|
||||
window.removeEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
function initMap() {
|
||||
if (mapContainer.value && !map.value) {
|
||||
map.value = L.map(mapContainer.value, {
|
||||
center: [userPosition.value.lat, userPosition.value.lng],
|
||||
zoom: defaultZoom,
|
||||
zoomControl: false,
|
||||
})
|
||||
|
||||
L.control.zoom({ position: 'topleft' }).addTo(map.value)
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors',
|
||||
maxZoom: 19,
|
||||
}).addTo(map.value)
|
||||
|
||||
// User marker (movable)
|
||||
userMarker.value = L.marker([userPosition.value.lat, userPosition.value.lng], {
|
||||
draggable: true,
|
||||
icon: L.icon({
|
||||
iconUrl:
|
||||
'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-blue.png',
|
||||
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41],
|
||||
}),
|
||||
}).addTo(map.value)
|
||||
|
||||
userMarker.value.bindPopup(t('settings.GMS.map.userLocationLabel')).openPopup()
|
||||
|
||||
// Community marker (fixed)
|
||||
communityMarker.value = L.marker([communityPosition.value.lat, communityPosition.value.lng], {
|
||||
draggable: false,
|
||||
icon: L.icon({
|
||||
iconUrl:
|
||||
'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
|
||||
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41],
|
||||
}),
|
||||
}).addTo(map.value)
|
||||
|
||||
communityMarker.value.bindPopup(t('settings.GMS.map.communityLocationLabel'))
|
||||
|
||||
map.value.on('click', onMapClick)
|
||||
userMarker.value.on('dragend', onMarkerDragEnd)
|
||||
|
||||
// GeoSearch control
|
||||
const provider = new OpenStreetMapProvider()
|
||||
const searchControl = new GeoSearchControl({
|
||||
provider: provider,
|
||||
style: 'button',
|
||||
showMarker: false,
|
||||
showPopup: false,
|
||||
autoClose: true,
|
||||
retainZoomLevel: false,
|
||||
animateZoom: true,
|
||||
keepResult: false,
|
||||
searchLabel: t('settings.GMS.map.search'),
|
||||
})
|
||||
map.value.addControl(searchControl)
|
||||
|
||||
map.value.on('geosearch/showlocation', (result) => {
|
||||
const { x, y, label } = result.location
|
||||
updateUserPosition({ lat: y, lng: x })
|
||||
})
|
||||
|
||||
// Center map on user position
|
||||
centerMapOnUser()
|
||||
}
|
||||
}
|
||||
|
||||
function handleResize() {
|
||||
if (map.value) {
|
||||
map.value.invalidateSize()
|
||||
centerMapOnUser()
|
||||
}
|
||||
}
|
||||
|
||||
function onMapClick(e) {
|
||||
updateUserPosition(e.latlng)
|
||||
}
|
||||
|
||||
function onMarkerDragEnd() {
|
||||
if (userMarker.value) {
|
||||
updateUserPosition(userMarker.value.getLatLng())
|
||||
}
|
||||
}
|
||||
|
||||
function updateUserPosition(latlng) {
|
||||
userPosition.value = { lat: latlng.lat, lng: latlng.lng }
|
||||
if (userMarker.value) {
|
||||
userMarker.value.setLatLng(latlng)
|
||||
userMarker.value.openPopup()
|
||||
}
|
||||
centerMapOnUser()
|
||||
emit('update:userPosition', userPosition.value)
|
||||
}
|
||||
|
||||
function centerMapOnUser() {
|
||||
if (map.value && userPosition.value) {
|
||||
map.value.setView([userPosition.value.lat, userPosition.value.lng], map.value.getZoom(), {
|
||||
animate: true,
|
||||
pan: {
|
||||
duration: 0.5,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function centerMapOnCommunity() {
|
||||
if (map.value && communityPosition.value) {
|
||||
map.value.setView(
|
||||
[communityPosition.value.lat, communityPosition.value.lng],
|
||||
map.value.getZoom(),
|
||||
{
|
||||
animate: true,
|
||||
pan: {
|
||||
duration: 0.5,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function handleMapCenter(centerMode) {
|
||||
if (centerMode === 'USER') centerMapOnUser()
|
||||
else centerMapOnCommunity()
|
||||
}
|
||||
|
||||
watch(userPosition, (newPosition) => {
|
||||
emit('update:userPosition', newPosition)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.map-container {
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.leaflet-control-custom a {
|
||||
background-color: #fff;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.leaflet-control-custom a:hover {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
|
||||
:deep(.leaflet-control-zoom > a) {
|
||||
color: #555 !important;
|
||||
}
|
||||
</style>
|
||||
@ -79,7 +79,7 @@ describe('UserNamingFormat', () => {
|
||||
await dropdownItems[3].trigger('click')
|
||||
|
||||
expect(mockUpdateUserData).toHaveBeenCalledWith({
|
||||
variables: { gmsPublishName: 'PUBLISH_NAME_FIRST_INITIAL' },
|
||||
gmsPublishName: 'PUBLISH_NAME_FIRST_INITIAL',
|
||||
})
|
||||
expect(mockToastSuccess).toHaveBeenCalledWith('success message')
|
||||
expect(mockStore.commit).toHaveBeenCalledWith('gmsPublishName', 'PUBLISH_NAME_FIRST_INITIAL')
|
||||
|
||||
@ -79,7 +79,7 @@ const update = async (option) => {
|
||||
try {
|
||||
const variables = {}
|
||||
variables[props.attrName] = option.value
|
||||
await updateUserData({ variables })
|
||||
await updateUserData({ ...variables })
|
||||
toastSuccess(props.successMessage)
|
||||
selectedOption.value = option.value
|
||||
store.commit(props.attrName, option.value)
|
||||
|
||||
@ -4,8 +4,10 @@ import UserNewsletter from './UserNewsletter'
|
||||
import { unsubscribeNewsletter, subscribeNewsletter } from '@/graphql/mutations'
|
||||
import { createStore } from 'vuex'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import { BFormCheckbox } from 'bootstrap-vue-next'
|
||||
import * as bootstrapVueNext from 'bootstrap-vue-next'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
// Mock composables and dependencies
|
||||
const mockToastError = vi.fn()
|
||||
const mockToastSuccess = vi.fn()
|
||||
|
||||
@ -26,6 +28,7 @@ vi.mock('@vue/apollo-composable', () => ({
|
||||
} else if (mutation === unsubscribeNewsletter) {
|
||||
return { mutate: mockUnsubscribeMutate }
|
||||
}
|
||||
throw new Error(`Unrecognized mutation: ${mutation}`)
|
||||
}),
|
||||
}))
|
||||
|
||||
@ -34,7 +37,7 @@ describe('UserNewsletter', () => {
|
||||
let store
|
||||
let i18n
|
||||
|
||||
const createVuexStore = (initialState) =>
|
||||
const createVuexStore = (initialState = {}) =>
|
||||
createStore({
|
||||
state: {
|
||||
language: 'de',
|
||||
@ -42,7 +45,7 @@ describe('UserNewsletter', () => {
|
||||
...initialState,
|
||||
},
|
||||
mutations: {
|
||||
setNewsletterState(state, value) {
|
||||
newsletterState(state, value) {
|
||||
state.newsletterState = value
|
||||
},
|
||||
},
|
||||
@ -63,11 +66,14 @@ describe('UserNewsletter', () => {
|
||||
const createWrapper = (storeState = {}) => {
|
||||
store = createVuexStore(storeState)
|
||||
i18n = createI18nInstance()
|
||||
|
||||
return mount(UserNewsletter, {
|
||||
global: {
|
||||
plugins: [store, i18n],
|
||||
stubs: {
|
||||
BFormCheckbox: true,
|
||||
plugins: [store, i18n, bootstrapVueNext],
|
||||
config: {
|
||||
compilerOptions: {
|
||||
isCustomElement: (tag) => tag.startsWith('b-'),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -82,8 +88,9 @@ describe('UserNewsletter', () => {
|
||||
expect(wrapper.find('div.formusernewsletter').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has an edit BFormCheckbox switch', () => {
|
||||
expect(wrapper.find('[test="BFormCheckbox"]').exists()).toBe(true)
|
||||
it('has a BFormCheckbox switch', () => {
|
||||
const checkbox = wrapper.findComponent({ name: 'BFormCheckbox' })
|
||||
expect(checkbox.exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('unsubscribe with success', () => {
|
||||
@ -94,7 +101,12 @@ describe('UserNewsletter', () => {
|
||||
unsubscribeNewsletter: true,
|
||||
},
|
||||
})
|
||||
wrapper.vm.localNewsletterState = false
|
||||
|
||||
const checkbox = wrapper.findComponent({ name: 'BFormCheckbox' })
|
||||
await checkbox.setValue(false)
|
||||
await nextTick()
|
||||
// Ensure all promises are resolved
|
||||
await Promise.resolve()
|
||||
})
|
||||
|
||||
it('calls the unsubscribe mutation', () => {
|
||||
@ -118,7 +130,12 @@ describe('UserNewsletter', () => {
|
||||
subscribeNewsletter: true,
|
||||
},
|
||||
})
|
||||
wrapper.vm.localNewsletterState = true
|
||||
|
||||
const checkbox = wrapper.findComponent({ name: 'BFormCheckbox' })
|
||||
await checkbox.setValue(true)
|
||||
await nextTick()
|
||||
// Ensure all promises are resolved
|
||||
await Promise.resolve()
|
||||
})
|
||||
|
||||
it('calls the subscribe mutation', () => {
|
||||
@ -138,7 +155,12 @@ describe('UserNewsletter', () => {
|
||||
beforeEach(async () => {
|
||||
wrapper = createWrapper({ newsletterState: true })
|
||||
mockUnsubscribeMutate.mockRejectedValue(new Error('Ouch'))
|
||||
wrapper.vm.localNewsletterState = false
|
||||
|
||||
const checkbox = wrapper.findComponent({ name: 'BFormCheckbox' })
|
||||
await checkbox.setValue(false)
|
||||
await nextTick()
|
||||
// Ensure all promises are resolved
|
||||
await Promise.resolve()
|
||||
})
|
||||
|
||||
it('resets the newsletterState', () => {
|
||||
|
||||
@ -29,20 +29,16 @@ const { mutate: newsletterSubscribe } = useMutation(subscribeNewsletter)
|
||||
const { mutate: newsletterUnsubscribe } = useMutation(unsubscribeNewsletter)
|
||||
|
||||
watch(localNewsletterState, async (newValue, oldValue) => {
|
||||
if (newValue !== oldValue) {
|
||||
if (newValue !== undefined && newValue !== null && newValue !== oldValue) {
|
||||
await onSubmit()
|
||||
}
|
||||
})
|
||||
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
if (localNewsletterState.value) {
|
||||
await newsletterSubscribe()
|
||||
} else {
|
||||
await newsletterUnsubscribe()
|
||||
}
|
||||
localNewsletterState.value ? await newsletterSubscribe() : await newsletterUnsubscribe()
|
||||
|
||||
store.commit('setNewsletterState', localNewsletterState.value)
|
||||
store.commit('newsletterState', localNewsletterState.value)
|
||||
|
||||
toastSuccess(
|
||||
localNewsletterState.value
|
||||
|
||||
@ -2,18 +2,18 @@
|
||||
<div class="skeleton-overview h-100">
|
||||
<BRow class="text-center">
|
||||
<BCol>
|
||||
<b-skeleton-img no-aspect animation="wave" height="118px"></b-skeleton-img>
|
||||
</BCol>
|
||||
<BCol cols="6">
|
||||
<b-skeleton animation="wave" class="mt-4 pt-5"></b-skeleton>
|
||||
<skeleton-loader-element :height="118" />
|
||||
</BCol>
|
||||
<BCol cols="6" />
|
||||
<BCol>
|
||||
<div class="b-right m-4">
|
||||
<BRow>
|
||||
<BCol><b-skeleton type="avatar"></b-skeleton></BCol>
|
||||
<BCol>
|
||||
<b-skeleton></b-skeleton>
|
||||
<b-skeleton></b-skeleton>
|
||||
<skeleton-loader-element full-radius :width="61" :height="61" />
|
||||
</BCol>
|
||||
<BCol>
|
||||
<skeleton-loader-element />
|
||||
<skeleton-loader-element />
|
||||
</BCol>
|
||||
</BRow>
|
||||
</div>
|
||||
@ -21,28 +21,53 @@
|
||||
</BRow>
|
||||
<BRow class="text-center mt-5 pt-5">
|
||||
<BCol cols="12" lg="">
|
||||
<b-skeleton animation="wave" width="85%"></b-skeleton>
|
||||
<b-skeleton animation="wave" width="55%"></b-skeleton>
|
||||
<b-skeleton animation="wave" width="70%"></b-skeleton>
|
||||
<skeleton-loader-element width="85%" :height="466" use-gradido-radius />
|
||||
</BCol>
|
||||
<BCol cols="12" lg="6">
|
||||
<b-skeleton animation="wave" width="85%"></b-skeleton>
|
||||
<b-skeleton animation="wave" width="55%"></b-skeleton>
|
||||
<b-skeleton animation="wave" width="70%"></b-skeleton>
|
||||
<b-skeleton animation="wave" width="85%"></b-skeleton>
|
||||
<b-skeleton animation="wave" width="55%"></b-skeleton>
|
||||
<b-skeleton animation="wave" width="70%"></b-skeleton>
|
||||
<BRow class="d-flex justify-content-between">
|
||||
<skeleton-loader-element width="45%" :height="105" use-gradido-radius />
|
||||
<skeleton-loader-element width="45%" :height="105" use-gradido-radius />
|
||||
</BRow>
|
||||
<skeleton-loader-element :height="450" use-gradido-radius />
|
||||
</BCol>
|
||||
<BCol cols="12" lg="">
|
||||
<b-skeleton animation="wave" width="85%"></b-skeleton>
|
||||
<b-skeleton animation="wave" width="55%"></b-skeleton>
|
||||
<b-skeleton animation="wave" width="70%"></b-skeleton>
|
||||
<skeleton-loader-element width="85%" />
|
||||
<BRow>
|
||||
<BCol style="max-width: min-content">
|
||||
<skeleton-loader-element full-radius :width="61" :height="61" />
|
||||
</BCol>
|
||||
<BCol>
|
||||
<skeleton-loader-element />
|
||||
<skeleton-loader-element />
|
||||
</BCol>
|
||||
</BRow>
|
||||
<BRow>
|
||||
<BCol style="max-width: min-content">
|
||||
<skeleton-loader-element full-radius :width="61" :height="61" />
|
||||
</BCol>
|
||||
<BCol>
|
||||
<skeleton-loader-element />
|
||||
<skeleton-loader-element />
|
||||
</BCol>
|
||||
</BRow>
|
||||
<BRow>
|
||||
<BCol style="max-width: min-content">
|
||||
<skeleton-loader-element full-radius :width="61" :height="61" />
|
||||
</BCol>
|
||||
<BCol>
|
||||
<skeleton-loader-element />
|
||||
<skeleton-loader-element />
|
||||
</BCol>
|
||||
</BRow>
|
||||
</BCol>
|
||||
</BRow>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import SkeletonLoaderElement from '@/components/SkeletonLoaderElement.vue'
|
||||
|
||||
export default {
|
||||
name: 'SkeletonOverview',
|
||||
components: { SkeletonLoaderElement },
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -22,6 +22,7 @@ export function useAppToast() {
|
||||
toast(message, {
|
||||
title: t('navigation.info'),
|
||||
variant: 'warning',
|
||||
bodyClass: 'gdd-toaster-body-darken',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -50,6 +50,7 @@ const community = {
|
||||
COMMUNITY_DESCRIPTION:
|
||||
process.env.COMMUNITY_DESCRIPTION ?? 'Die lokale Entwicklungsumgebung von Gradido.',
|
||||
COMMUNITY_SUPPORT_MAIL: process.env.COMMUNITY_SUPPORT_MAIL ?? 'support@supportmail.com',
|
||||
COMMUNITY_LOCATION: process.env.COMMUNITY_LOCATION ?? '49.280377, 9.690151',
|
||||
}
|
||||
|
||||
const meta = {
|
||||
|
||||
@ -28,6 +28,15 @@ export const authenticateGmsUserSearch = gql`
|
||||
}
|
||||
`
|
||||
|
||||
export const userLocationQuery = gql`
|
||||
query {
|
||||
userLocation {
|
||||
userLocation
|
||||
communityLocation
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const authenticateHumhubAutoLogin = gql`
|
||||
query {
|
||||
authenticateHumhubAutoLogin
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import DashboardLayout from './DashboardLayout'
|
||||
import { createStore } from 'vuex'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
@ -15,11 +15,15 @@ vi.mock('@/composables/useToast', () => ({
|
||||
}))
|
||||
|
||||
const mockQueryFn = vi.fn()
|
||||
const mockRefetchFn = vi.fn()
|
||||
const mockMutateFn = vi.fn()
|
||||
const mockQueryResult = ref(null)
|
||||
|
||||
vi.mock('@vue/apollo-composable', () => ({
|
||||
useLazyQuery: vi.fn(() => ({
|
||||
load: mockQueryFn,
|
||||
refetch: mockRefetchFn,
|
||||
result: mockQueryResult,
|
||||
onResult: vi.fn(),
|
||||
onError: vi.fn(),
|
||||
})),
|
||||
@ -136,25 +140,25 @@ describe('DashboardLayout', () => {
|
||||
|
||||
describe('update transactions', () => {
|
||||
beforeEach(async () => {
|
||||
mockQueryFn.mockResolvedValue({
|
||||
mockQueryResult.value = {
|
||||
transactionList: {
|
||||
balance: {
|
||||
balanceGDT: 100,
|
||||
balanceGDT: '100',
|
||||
count: 4,
|
||||
linkCount: 8,
|
||||
balance: 1450,
|
||||
decay: 1250,
|
||||
balance: '1450',
|
||||
},
|
||||
transactions: ['transaction', 'transaction', 'transaction', 'transaction'],
|
||||
transactions: ['transaction1', 'transaction2', 'transaction3', 'transaction4'],
|
||||
},
|
||||
})
|
||||
await wrapper
|
||||
.findComponent({ ref: 'router-view' })
|
||||
.vm.$emit('update-transactions', { currentPage: 2, pageSize: 5 })
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
mockQueryFn.mockResolvedValue(mockQueryResult.value)
|
||||
|
||||
await wrapper.vm.updateTransactions({ currentPage: 2, pageSize: 5 })
|
||||
await nextTick() // Ensure all promises are resolved
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
it('load call to the API', () => {
|
||||
expect(mockQueryFn).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@ -164,10 +168,10 @@ describe('DashboardLayout', () => {
|
||||
|
||||
it('updates transactions', () => {
|
||||
expect(wrapper.vm.transactions).toEqual([
|
||||
'transaction',
|
||||
'transaction',
|
||||
'transaction',
|
||||
'transaction',
|
||||
'transaction1',
|
||||
'transaction2',
|
||||
'transaction3',
|
||||
'transaction4',
|
||||
])
|
||||
})
|
||||
|
||||
|
||||
@ -181,7 +181,7 @@
|
||||
<content-footer v-if="!$route.meta.hideFooter" />
|
||||
</BCol>
|
||||
</BRow>
|
||||
<!-- <session-logout-timeout @logout="logoutUser" ref="sessionModal" />-->
|
||||
<session-logout-timeout @logout="logoutUser" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -215,7 +215,11 @@ import { useAppToast } from '@/composables/useToast'
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
const { load: useCommunityStatsQuery } = useLazyQuery(communityStatistics)
|
||||
const { load: useTransactionsQuery } = useLazyQuery(transactionsQuery)
|
||||
const {
|
||||
load: useTransactionsQuery,
|
||||
refetch: useRefetchTransactionsQuery,
|
||||
result: transactionQueryResult,
|
||||
} = useLazyQuery(transactionsQuery, {}, { fetchPolicy: 'network-only' })
|
||||
const { mutate: useLogoutMutation } = useMutation(logout)
|
||||
const { t } = useI18n()
|
||||
const { toastError } = useAppToast()
|
||||
@ -232,14 +236,8 @@ const darkMode = ref(false)
|
||||
const skeleton = ref(true)
|
||||
const totalUsers = ref(null)
|
||||
|
||||
const sessionModal = ref(null)
|
||||
|
||||
const testModal = () => {
|
||||
sessionModal.value.showTimeoutModalForTesting()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateTransactions({ currentPage: 0, pageSize: 10 })
|
||||
updateTransactions({ currentPage: 1, pageSize: 10 })
|
||||
getCommunityStatistics()
|
||||
setTimeout(() => {
|
||||
skeleton.value = false
|
||||
@ -251,7 +249,7 @@ const logoutUser = async () => {
|
||||
await useLogoutMutation()
|
||||
await store.dispatch('logout')
|
||||
await router.push('/login')
|
||||
} catch {
|
||||
} catch (err) {
|
||||
await store.dispatch('logout')
|
||||
if (router.currentRoute.value.path !== '/login') await router.push('/login')
|
||||
}
|
||||
@ -260,9 +258,9 @@ const logoutUser = async () => {
|
||||
const updateTransactions = async ({ currentPage, pageSize }) => {
|
||||
pending.value = true
|
||||
try {
|
||||
const result = await useTransactionsQuery()
|
||||
if (!result) return // TODO this return mitigate an error when this method is called second time but without actual request
|
||||
const { transactionList } = result
|
||||
await loadOrFetchTransactionQuery({ currentPage, pageSize })
|
||||
if (!transactionQueryResult) return
|
||||
const { transactionList } = transactionQueryResult.value
|
||||
GdtBalance.value =
|
||||
transactionList.balance.balanceGDT === null ? 0 : Number(transactionList.balance.balanceGDT)
|
||||
transactions.value = transactionList.transactions
|
||||
@ -277,6 +275,13 @@ const updateTransactions = async ({ currentPage, pageSize }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const loadOrFetchTransactionQuery = async (queryVariables = { currentPage: 1, pageSize: 25 }) => {
|
||||
return (
|
||||
(await useTransactionsQuery(transactionsQuery, queryVariables)) ||
|
||||
(await useRefetchTransactionsQuery(queryVariables))
|
||||
)
|
||||
}
|
||||
|
||||
const getCommunityStatistics = async () => {
|
||||
try {
|
||||
const result = await useCommunityStatsQuery()
|
||||
@ -298,6 +303,7 @@ const setVisible = (bool) => {
|
||||
<style>
|
||||
.breadcrumb {
|
||||
background-color: transparent;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.main-page {
|
||||
@ -342,7 +348,7 @@ const setVisible = (bool) => {
|
||||
|
||||
@media screen and (width <= 450px) {
|
||||
.breadcrumb {
|
||||
padding-top: 60px;
|
||||
padding-top: 55px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -248,7 +248,8 @@
|
||||
},
|
||||
"h": "h",
|
||||
"language": "Sprache",
|
||||
"link-load": "den letzten Link nachladen | die letzten {n} Links nachladen | weitere {n} Links nachladen",
|
||||
"link-load": "den letzten Link nachladen | die letzten {n} Links nachladen",
|
||||
"link-load-more": "weitere {n} Links nachladen",
|
||||
"login": "Anmelden",
|
||||
"math": {
|
||||
"asterisk": "*",
|
||||
@ -312,10 +313,20 @@
|
||||
"disabled": "Daten werden nicht nach GMS exportiert",
|
||||
"enabled": "Daten werden nach GMS exportiert",
|
||||
"location": {
|
||||
"button": "Klick mich!",
|
||||
"label": "Positionsbestimmung",
|
||||
"button": "Klick mich!"
|
||||
"saveLocation": "Standort speichern",
|
||||
"updateSuccess": "Standort erfolgreich gespeichert"
|
||||
},
|
||||
"location-format": "Position auf Karte anzeigen:",
|
||||
"map": {
|
||||
"communityCoords": "Ihr Gemeinschafts-Standort: Breitengrad {lat}, Längengrad {lng}",
|
||||
"communityLocationLabel": "Ihr Gemeinschafts-Standort",
|
||||
"headline": "Geografische Standorterfassung des Benutzers",
|
||||
"userCoords": "Ihr Standort: Breitengrad {lat}, Längengrad {lng}",
|
||||
"userLocationLabel": "Ihr Standort",
|
||||
"search": "Nach einem Standort suchen"
|
||||
},
|
||||
"naming-format": "Namen anzeigen:",
|
||||
"publish-location": {
|
||||
"exact": "Genaue Position",
|
||||
|
||||
@ -248,7 +248,8 @@
|
||||
},
|
||||
"h": "h",
|
||||
"language": "Language",
|
||||
"link-load": "Load the last link | Load the last {n} links | Load more {n} links",
|
||||
"link-load": "Load the last link | Load the last {n} links",
|
||||
"link-load-more": "Load more {n} links",
|
||||
"login": "Sign in",
|
||||
"math": {
|
||||
"asterisk": "*",
|
||||
@ -312,10 +313,20 @@
|
||||
"disabled": "Data not exported to GMS",
|
||||
"enabled": "Data exported to GMS",
|
||||
"location": {
|
||||
"label": "pinpoint location",
|
||||
"button": "click me!"
|
||||
"button": "Click me!",
|
||||
"label": "Pinpoint location",
|
||||
"saveLocation": "Save Location",
|
||||
"updateSuccess": "Location successfully saved"
|
||||
},
|
||||
"location-format": "Show position on map:",
|
||||
"map": {
|
||||
"communityCoords": "Your Community Location: Lat {lat}, Lng {lng}",
|
||||
"communityLocationLabel": "Your Community-Location",
|
||||
"headline": "Geographic Location-Capturing of the User",
|
||||
"userCoords": "Your Location: Lat {lat}, Lng {lng}",
|
||||
"userLocationLabel": "Your Location",
|
||||
"search": "Search for a location"
|
||||
},
|
||||
"naming-format": "Show Name:",
|
||||
"publish-location": {
|
||||
"exact": "exact position",
|
||||
|
||||
@ -218,7 +218,8 @@
|
||||
"recruited-member": "Miembro invitado"
|
||||
},
|
||||
"language": "Idioma",
|
||||
"link-load": "recargar el último enlace |recargar los últimos {n} enlaces | descargar más {n} enlaces",
|
||||
"link-load": "recargar el último enlace | recargar los últimos {n} enlaces",
|
||||
"link-load-more": "descargar más {n} enlaces",
|
||||
"login": "iniciar sesión",
|
||||
"math": {
|
||||
"aprox": "~",
|
||||
@ -275,6 +276,23 @@
|
||||
"warningText": "Aún estas?"
|
||||
},
|
||||
"settings": {
|
||||
"GMS": {
|
||||
"location": {
|
||||
"button": "¡Haz clic aquí!",
|
||||
"label": "Ubicación exacta",
|
||||
"saveLocation": "Guardar ubicación",
|
||||
"updateSuccess": "Ubicación guardada exitosamente"
|
||||
},
|
||||
"map": {
|
||||
"communityCoords": "Ubicación de tu comunidad: Lat {lat}, Lng {lng}",
|
||||
"communityLocationLabel": "Ubicación de tu comunidad",
|
||||
"headline": "Captura de ubicación geográfica del usuario",
|
||||
"userCoords": "Tu ubicación: Lat {lat}, Lng {lng}",
|
||||
"userLocationLabel": "Tu ubicación",
|
||||
"search": "Buscar una ubicación",
|
||||
"success": "Ubicación guardada exitosamente"
|
||||
}
|
||||
},
|
||||
"language": {
|
||||
"changeLanguage": "Cambiar idioma",
|
||||
"de": "Deutsch",
|
||||
|
||||
@ -226,7 +226,8 @@
|
||||
},
|
||||
"h": "h",
|
||||
"language": "Langage",
|
||||
"link-load": "Enregistrer le dernier lien | Enregistrer les derniers {n} liens | Enregistrer plus de {n} liens",
|
||||
"link-load": "Enregistrer le dernier lien | Enregistrer les derniers {n} liens",
|
||||
"link-load-more": "Enregistrer plus de {n} liens",
|
||||
"login": "Connexion",
|
||||
"math": {
|
||||
"asterisk": "*",
|
||||
@ -283,6 +284,23 @@
|
||||
"warningText": "Êtes-vous toujours connecté?"
|
||||
},
|
||||
"settings": {
|
||||
"GMS": {
|
||||
"location": {
|
||||
"button": "Cliquez ici !",
|
||||
"label": "Emplacement précis",
|
||||
"saveLocation": "Enregistrer l'emplacement",
|
||||
"updateSuccess": "Emplacement enregistré avec succès"
|
||||
},
|
||||
"map": {
|
||||
"communityCoords": "Emplacement de votre communauté : Lat {lat}, Long {lng}",
|
||||
"communityLocationLabel": "Emplacement de votre communauté",
|
||||
"headline": "Capture de la localisation géographique de l'utilisateur",
|
||||
"userCoords": "Votre emplacement : Lat {lat}, Long {lng}",
|
||||
"userLocationLabel": "Votre emplacement",
|
||||
"search": "Rechercher un emplacement",
|
||||
"success": "Emplacement enregistré avec succès"
|
||||
}
|
||||
},
|
||||
"hideAmountGDD": "Votre montant GDD est caché.",
|
||||
"hideAmountGDT": "Votre montant GDT est caché.",
|
||||
"language": {
|
||||
|
||||
@ -218,7 +218,8 @@
|
||||
"recruited-member": "Uitgenodigd lid"
|
||||
},
|
||||
"language": "Taal",
|
||||
"link-load": "de laatste link herladen | de laatste links herladen | verdere {n} links herladen",
|
||||
"link-load": "de laatste link herladen | de laatste links herladen",
|
||||
"link-load-more": "verdere {n} links herladen",
|
||||
"login": "Aanmelding",
|
||||
"math": {
|
||||
"aprox": "~",
|
||||
@ -275,6 +276,23 @@
|
||||
"warningText": "Ben je er nog?"
|
||||
},
|
||||
"settings": {
|
||||
"GMS": {
|
||||
"location": {
|
||||
"button": "Klik hier!",
|
||||
"label": "Exacte locatie",
|
||||
"saveLocation": "Locatie opslaan",
|
||||
"updateSuccess": "Locatie succesvol opgeslagen"
|
||||
},
|
||||
"map": {
|
||||
"communityCoords": "Locatie van je gemeenschap: Lat {lat}, Lng {lng}",
|
||||
"communityLocationLabel": "Locatie van je gemeenschap",
|
||||
"headline": "Geografische locatiebepaling van de gebruiker",
|
||||
"userCoords": "Jouw locatie: Lat {lat}, Lng {lng}",
|
||||
"userLocationLabel": "Jouw locatie",
|
||||
"search": "Zoek een locatie",
|
||||
"success": "Locatie succesvol opgeslagen"
|
||||
}
|
||||
},
|
||||
"language": {
|
||||
"changeLanguage": "Taal veranderen",
|
||||
"de": "Deutsch",
|
||||
|
||||
@ -204,7 +204,8 @@
|
||||
"recruited-member": "Davetli üye"
|
||||
},
|
||||
"language": "Dil",
|
||||
"link-load": "Son linki yükle| Son {n} linki yükle | {n} link daha yükle",
|
||||
"link-load": "Son linki yükle| Son {n} linki yükle",
|
||||
"link-load-more": "{n} link daha yükle",
|
||||
"login": "Giriş",
|
||||
"math": {
|
||||
"aprox": "~",
|
||||
@ -245,6 +246,23 @@
|
||||
"warningText": "Hala orada mısın?"
|
||||
},
|
||||
"settings": {
|
||||
"GMS": {
|
||||
"location": {
|
||||
"button": "Buraya tıklayın!",
|
||||
"label": "Tam konum",
|
||||
"saveLocation": "Konumu Kaydet",
|
||||
"updateSuccess": "Konum başarıyla kaydedildi"
|
||||
},
|
||||
"map": {
|
||||
"communityCoords": "Topluluk Konumunuz: Enlem {lat}, Boylam {lng}",
|
||||
"communityLocationLabel": "Topluluk Konumunuz",
|
||||
"headline": "Kullanıcının Coğrafi Konum Tespiti",
|
||||
"userCoords": "Konumunuz: Enlem {lat}, Boylam {lng}",
|
||||
"userLocationLabel": "Konumunuz",
|
||||
"search": "Konum ara",
|
||||
"success": "Konum başarıyla kaydedildi"
|
||||
}
|
||||
},
|
||||
"language": {
|
||||
"changeLanguage": "Dili değiştir",
|
||||
"de": "Deutsch",
|
||||
|
||||
@ -76,7 +76,7 @@ describe('Circles', () => {
|
||||
null,
|
||||
expect.objectContaining({
|
||||
fetchPolicy: 'network-only',
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
}),
|
||||
)
|
||||
expect(mockRefetch).toHaveBeenCalled()
|
||||
|
||||
@ -51,7 +51,7 @@ const {
|
||||
onError,
|
||||
} = useQuery(authenticateHumhubAutoLogin, null, {
|
||||
fetchPolicy: 'network-only',
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
})
|
||||
|
||||
onResult(({ data }) => {
|
||||
|
||||
@ -19,10 +19,11 @@
|
||||
</BCol>
|
||||
</BRow>
|
||||
<BRow>
|
||||
<BCol cols="12" lg="6">
|
||||
<BCol class="col-lg-6 col-12">
|
||||
<BButton
|
||||
ref="submitBtn"
|
||||
type="submit"
|
||||
class="w-100 fs-7"
|
||||
:variant="meta.valid ? 'gradido' : 'gradido-disable'"
|
||||
block
|
||||
:disabled="!meta.valid"
|
||||
|
||||
@ -81,100 +81,102 @@
|
||||
</BCol>
|
||||
</BRow>
|
||||
</BTab>
|
||||
<div v-if="isCommunityService">
|
||||
<BTab class="community-service-tabs" :title="$t('settings.community')">
|
||||
<div class="h2">{{ $t('settings.allow-community-services') }}</div>
|
||||
<div v-if="isHumhub" class="mt-3">
|
||||
<BRow>
|
||||
<BTab
|
||||
v-if="isCommunityService"
|
||||
class="community-service-tabs"
|
||||
:title="$t('settings.community')"
|
||||
>
|
||||
<div class="h2">{{ $t('settings.allow-community-services') }}</div>
|
||||
<div v-if="isHumhub" class="mt-3">
|
||||
<BRow>
|
||||
<BCol cols="12" md="6" lg="6">
|
||||
<div class="h3">{{ $t('Humhub.title') }}</div>
|
||||
</BCol>
|
||||
<BCol cols="12" md="6" lg="6" class="text-end">
|
||||
<user-settings-switch
|
||||
:initial-value="$store.state.humhubAllowed"
|
||||
:attr-name="'humhubAllowed'"
|
||||
:disabled="isHumhubActivated"
|
||||
:enabled-text="$t('settings.humhub.enabled')"
|
||||
:disabled-text="$t('settings.humhub.disabled')"
|
||||
:not-allowed-text="$t('settings.humhub.delete-disabled')"
|
||||
@value-changed="humhubStateSwitch"
|
||||
/>
|
||||
</BCol>
|
||||
</BRow>
|
||||
<div class="h4">{{ $t('Humhub.desc') }}</div>
|
||||
<BRow v-if="humhubAllowed" class="mb-4 humhub-publish-name-row">
|
||||
<BCol cols="12" md="6" lg="6">
|
||||
{{ $t('settings.humhub.naming-format') }}
|
||||
</BCol>
|
||||
<BCol cols="12" md="6" lg="6">
|
||||
<user-naming-format
|
||||
:initial-value="$store.state.humhubPublishName"
|
||||
:attr-name="'humhubPublishName'"
|
||||
:success-message="$t('settings.humhub.publish-name.updated')"
|
||||
/>
|
||||
</BCol>
|
||||
</BRow>
|
||||
</div>
|
||||
<div v-if="isGMS" class="mt-3">
|
||||
<BRow>
|
||||
<BCol cols="12" md="6" lg="6">
|
||||
<div class="h3">{{ $t('GMS.title') }}</div>
|
||||
</BCol>
|
||||
<BCol cols="12" md="6" lg="6" class="text-end">
|
||||
<user-settings-switch
|
||||
:initial-value="$store.state.gmsAllowed"
|
||||
:attr-name="'gmsAllowed'"
|
||||
:enabled-text="$t('settings.GMS.enabled')"
|
||||
:disabled-text="$t('settings.GMS.disabled')"
|
||||
@value-changed="gmsStateSwitch"
|
||||
/>
|
||||
</BCol>
|
||||
</BRow>
|
||||
<div class="h4 mt-3">{{ $t('GMS.desc') }}</div>
|
||||
<div v-if="gmsAllowed">
|
||||
<BRow class="mb-4">
|
||||
<BCol cols="12" md="6" lg="6">
|
||||
<div class="h3">{{ $t('Humhub.title') }}</div>
|
||||
</BCol>
|
||||
<BCol cols="12" md="6" lg="6" class="text-end">
|
||||
<user-settings-switch
|
||||
:initial-value="$store.state.humhubAllowed"
|
||||
:attr-name="'humhubAllowed'"
|
||||
:disabled="isHumhubActivated"
|
||||
:enabled-text="$t('settings.humhub.enabled')"
|
||||
:disabled-text="$t('settings.humhub.disabled')"
|
||||
:not-allowed-text="$t('settings.humhub.delete-disabled')"
|
||||
@value-changed="humhubStateSwitch"
|
||||
/>
|
||||
</BCol>
|
||||
</BRow>
|
||||
<div class="h4">{{ $t('Humhub.desc') }}</div>
|
||||
<BRow v-if="humhubAllowed" class="mb-4 humhub-publish-name-row">
|
||||
<BCol cols="12" md="6" lg="6">
|
||||
{{ $t('settings.humhub.naming-format') }}
|
||||
{{ $t('settings.GMS.naming-format') }}
|
||||
</BCol>
|
||||
<BCol cols="12" md="6" lg="6">
|
||||
<user-naming-format
|
||||
:initial-value="$store.state.humhubPublishName"
|
||||
:attr-name="'humhubPublishName'"
|
||||
:success-message="$t('settings.humhub.publish-name.updated')"
|
||||
:initial-value="$store.state.gmsPublishName"
|
||||
:attr-name="'gmsPublishName'"
|
||||
:success-message="$t('settings.GMS.publish-name.updated')"
|
||||
/>
|
||||
</BCol>
|
||||
</BRow>
|
||||
</div>
|
||||
<div v-if="isGMS" class="mt-3">
|
||||
<BRow>
|
||||
<BRow class="mb-4">
|
||||
<BCol cols="12" md="6" lg="6">
|
||||
<div class="h3">{{ $t('GMS.title') }}</div>
|
||||
{{ $t('settings.GMS.location-format') }}
|
||||
</BCol>
|
||||
<BCol cols="12" md="6" lg="6" class="text-end">
|
||||
<user-settings-switch
|
||||
:initial-value="$store.state.gmsAllowed"
|
||||
:attr-name="'gmsAllowed'"
|
||||
:enabled-text="$t('settings.GMS.enabled')"
|
||||
:disabled-text="$t('settings.GMS.disabled')"
|
||||
@value-changed="gmsStateSwitch"
|
||||
/>
|
||||
<BCol cols="12" md="6" lg="6">
|
||||
<user-g-m-s-location-format />
|
||||
</BCol>
|
||||
</BRow>
|
||||
<div class="h4 mt-3">{{ $t('GMS.desc') }}</div>
|
||||
<div v-if="gmsAllowed">
|
||||
<BRow class="mb-4">
|
||||
<BCol cols="12" md="6" lg="6">
|
||||
{{ $t('settings.GMS.naming-format') }}
|
||||
</BCol>
|
||||
<BCol cols="12" md="6" lg="6">
|
||||
<user-naming-format
|
||||
:initial-value="$store.state.gmsPublishName"
|
||||
:attr-name="'gmsPublishName'"
|
||||
:success-message="$t('settings.GMS.publish-name.updated')"
|
||||
/>
|
||||
</BCol>
|
||||
</BRow>
|
||||
<BRow class="mb-4">
|
||||
<BCol cols="12" md="6" lg="6">
|
||||
{{ $t('settings.GMS.location-format') }}
|
||||
</BCol>
|
||||
<BCol cols="12" md="6" lg="6">
|
||||
<user-g-m-s-location-format />
|
||||
</BCol>
|
||||
</BRow>
|
||||
<BRow class="mb-5">
|
||||
<BCol cols="12" md="6" lg="6">
|
||||
{{ $t('settings.GMS.location.label') }}
|
||||
</BCol>
|
||||
<BCol cols="12" md="6" lg="6">
|
||||
<user-g-m-s-location />
|
||||
</BCol>
|
||||
</BRow>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<BRow>
|
||||
<BRow class="mb-5">
|
||||
<BCol cols="12" md="6" lg="6">
|
||||
<div class="h3 text-muted">{{ $t('GMS.title') }}</div>
|
||||
{{ $t('settings.GMS.location.label') }}
|
||||
</BCol>
|
||||
<BCol cols="12" md="6" lg="6" class="text-end">
|
||||
<user-settings-switch :disabled="true" />
|
||||
<BCol cols="12" md="6" lg="6">
|
||||
<user-gms-location-capturing />
|
||||
</BCol>
|
||||
</BRow>
|
||||
<div class="h4 mt-3 text-muted">{{ $t('GMS.desc') }}</div>
|
||||
</div>
|
||||
</BTab>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<BRow>
|
||||
<BCol cols="12" md="6" lg="6">
|
||||
<div class="h3 text-muted">{{ $t('GMS.title') }}</div>
|
||||
</BCol>
|
||||
<BCol cols="12" md="6" lg="6" class="text-end">
|
||||
<user-settings-switch :disabled="true" />
|
||||
</BCol>
|
||||
</BRow>
|
||||
<div class="h4 mt-3 text-muted">{{ $t('GMS.desc') }}</div>
|
||||
</div>
|
||||
</BTab>
|
||||
</BTabs>
|
||||
|
||||
<!-- TODO<BRow>
|
||||
@ -200,7 +202,7 @@ import UserPassword from '@/components/UserSettings/UserPassword'
|
||||
import UserSettingsSwitch from '../components/UserSettings/UserSettingsSwitch.vue'
|
||||
import UserNamingFormat from '@/components/UserSettings/UserNamingFormat'
|
||||
import UserGMSLocationFormat from '@/components/UserSettings/UserGMSLocationFormat'
|
||||
import UserGMSLocation from '@/components/UserSettings/UserGMSLocation'
|
||||
import UserGmsLocationCapturing from '@/components/UserSettings/UserGmsLocationCapturing'
|
||||
import UserNewsletter from '@/components/UserSettings/UserNewsletter.vue'
|
||||
import { BTabs, BTab, BRow, BCol, BFormInput, BFormGroup, BForm, BButton } from 'bootstrap-vue-next'
|
||||
|
||||
@ -305,4 +307,12 @@ const humhubStateSwitch = (eventData) => {
|
||||
:deep(.form-label) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.nav-link) {
|
||||
color: #383838 !important;
|
||||
}
|
||||
|
||||
:deep(.nav-link.active) {
|
||||
color: #525f7f !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -48,7 +48,6 @@ const transactionsGdt = ref([])
|
||||
const transactionGdtCount = ref(0)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(25)
|
||||
// const tabIndex = ref(0)
|
||||
|
||||
const { toastError } = useAppToast()
|
||||
|
||||
|
||||
@ -23,6 +23,8 @@ const routes = [
|
||||
// communityIdentifier can be community name or community UUID
|
||||
path: '/send/:communityIdentifier?/:userIdentifier?',
|
||||
component: () => import('@/pages/Send'),
|
||||
name: 'Send',
|
||||
props: true,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
pageTitle: 'send',
|
||||
@ -36,6 +38,7 @@ const routes = [
|
||||
// },
|
||||
// },
|
||||
{
|
||||
name: 'Transactions',
|
||||
path: '/transactions',
|
||||
component: () => import('@/pages/Transactions'),
|
||||
props: { gdt: false },
|
||||
|
||||
@ -75,6 +75,9 @@ export const mutations = {
|
||||
redirectPath: (state, redirectPath) => {
|
||||
state.redirectPath = redirectPath || '/overview'
|
||||
},
|
||||
setTransactionToHighlightId: (state, id) => {
|
||||
state.transactionToHighlightId = id
|
||||
},
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
@ -119,6 +122,9 @@ export const actions = {
|
||||
commit('redirectPath', '/overview')
|
||||
localStorage.clear()
|
||||
},
|
||||
changeTransactionToHighlightId({ commit }, id) {
|
||||
commit('setTransactionToHighlightId', id)
|
||||
},
|
||||
}
|
||||
|
||||
let store
|
||||
@ -153,6 +159,7 @@ try {
|
||||
email: '',
|
||||
darkMode: false,
|
||||
redirectPath: '/overview',
|
||||
transactionToHighlightId: '',
|
||||
},
|
||||
getters: {},
|
||||
// Synchronous mutation of the state
|
||||
|
||||
@ -19,13 +19,13 @@ export default defineConfig({
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
vue: '@vue/compat',
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
assets: path.join(__dirname, 'src/assets'),
|
||||
},
|
||||
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
createHtmlPlugin({
|
||||
minify: true,
|
||||
inject: {
|
||||
@ -41,15 +41,6 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
}),
|
||||
vue({
|
||||
template: {
|
||||
compilerOptions: {
|
||||
compatConfig: {
|
||||
MODE: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
Components({
|
||||
resolvers: [BootstrapVueNextResolver(), IconsResolve()],
|
||||
dts: true,
|
||||
|
||||
@ -22,8 +22,10 @@ export default defineConfig({
|
||||
transformMode: {
|
||||
web: [/\.[jt]sx$/],
|
||||
},
|
||||
deps: {
|
||||
inline: [/vee-validate/, 'vitest-canvas-mock'],
|
||||
server: {
|
||||
deps: {
|
||||
inline: [/vee-validate/, 'vitest-canvas-mock'],
|
||||
},
|
||||
},
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
|
||||
@ -1278,6 +1278,11 @@
|
||||
"@floating-ui/utils" "^0.2.7"
|
||||
vue-demi ">=0.13.0"
|
||||
|
||||
"@googlemaps/js-api-loader@^1.16.6":
|
||||
version "1.16.8"
|
||||
resolved "https://registry.yarnpkg.com/@googlemaps/js-api-loader/-/js-api-loader-1.16.8.tgz#1595a2af80ca07e551fc961d921a2437d1cb3643"
|
||||
integrity sha512-CROqqwfKotdO6EBjZO/gQGVTbeDps5V7Mt9+8+5Q+jTg5CRMi3Ii/L9PmV3USROrt2uWxtGzJHORmByxyo9pSQ==
|
||||
|
||||
"@graphql-typed-document-node/core@^3.1.1":
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861"
|
||||
@ -1641,6 +1646,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
|
||||
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
|
||||
|
||||
"@types/geojson@*":
|
||||
version "7946.0.14"
|
||||
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.14.tgz#319b63ad6df705ee2a65a73ef042c8271e696613"
|
||||
integrity sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==
|
||||
|
||||
"@types/json-schema@^7.0.5":
|
||||
version "7.0.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
|
||||
@ -1651,6 +1661,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
|
||||
|
||||
"@types/leaflet@^1.9.12":
|
||||
version "1.9.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.9.12.tgz#a6626a0b3fba36fd34723d6e95b22e8024781ad6"
|
||||
integrity sha512-BK7XS+NyRI291HIo0HCfE18Lp8oA30H1gpi1tf0mF3TgiCEzanQjOqNZ4x126SXzzi2oNSZhZ5axJp1k0iM6jg==
|
||||
dependencies:
|
||||
"@types/geojson" "*"
|
||||
|
||||
"@types/node@>=6":
|
||||
version "22.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.1.0.tgz#6d6adc648b5e03f0e83c78dc788c2b037d0ad94b"
|
||||
@ -1739,10 +1756,10 @@
|
||||
vee-validate "4.13.2"
|
||||
yup "^1.3.2"
|
||||
|
||||
"@vitejs/plugin-vue@3.2.0":
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-3.2.0.tgz#a1484089dd85d6528f435743f84cdd0d215bbb54"
|
||||
integrity sha512-E0tnaL4fr+qkdCNxJ+Xd0yM31UwMkQje76fsDVBBUCoGOUPexu2VDUYHL8P4CwV+zMvWw6nlRw19OnRKmYAJpw==
|
||||
"@vitejs/plugin-vue@5.1.4":
|
||||
version "5.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.1.4.tgz#72b8b705cfce36b00b59af196195146e356500c4"
|
||||
integrity sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==
|
||||
|
||||
"@vitest/coverage-v8@^2.0.5":
|
||||
version "2.0.5"
|
||||
@ -1813,6 +1830,13 @@
|
||||
loupe "^3.1.1"
|
||||
tinyrainbow "^1.2.0"
|
||||
|
||||
"@vue-leaflet/vue-leaflet@^0.10.1":
|
||||
version "0.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@vue-leaflet/vue-leaflet/-/vue-leaflet-0.10.1.tgz#17330515629d500ac113009a49f8cf98d140e8cc"
|
||||
integrity sha512-RNEDk8TbnwrJl8ujdbKgZRFygLCxd0aBcWLQ05q/pGv4+d0jamE3KXQgQBqGAteE1mbQsk3xoNcqqUgaIGfWVg==
|
||||
dependencies:
|
||||
vue "^3.2.25"
|
||||
|
||||
"@vue/apollo-composable@^4.0.2":
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@vue/apollo-composable/-/apollo-composable-4.0.2.tgz#ea3c001d25b3bf659aad5ae7a849fed3928aaa35"
|
||||
@ -1829,15 +1853,6 @@
|
||||
dependencies:
|
||||
throttle-debounce "^5.0.0"
|
||||
|
||||
"@vue/compat@^3.4.31":
|
||||
version "3.4.36"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compat/-/compat-3.4.36.tgz#9af3b95adf614354f2595548f162e7af4891aba9"
|
||||
integrity sha512-41qHnPjRAjhNGCR77BQeaNKYNyZNqtZDNgg/3LQ3zIfOA8+cqFFPzZ/Ym1K4Viin0RS7bN4C8/fQFaRrvy7lnA==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.24.7"
|
||||
estree-walker "^2.0.2"
|
||||
source-map-js "^1.2.0"
|
||||
|
||||
"@vue/compiler-core@3.4.31":
|
||||
version "3.4.31"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.31.tgz#b51a76f1b30e9b5eba0553264dff0f171aedb7c6"
|
||||
@ -1849,14 +1864,14 @@
|
||||
estree-walker "^2.0.2"
|
||||
source-map-js "^1.2.0"
|
||||
|
||||
"@vue/compiler-core@3.4.36":
|
||||
version "3.4.36"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.36.tgz#4e28dfcbaa8a85e135f7a94c44372b6d52329e42"
|
||||
integrity sha512-qBkndgpwFKdupmOPoiS10i7oFdN7a+4UNDlezD0GlQ1kuA1pNrscg9g12HnB5E8hrWSuEftRsbJhL1HI2zpJhg==
|
||||
"@vue/compiler-core@3.5.12":
|
||||
version "3.5.12"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.12.tgz#bd70b7dabd12b0b6f31bc53418ba3da77994c437"
|
||||
integrity sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.24.7"
|
||||
"@vue/shared" "3.4.36"
|
||||
entities "^5.0.0"
|
||||
"@babel/parser" "^7.25.3"
|
||||
"@vue/shared" "3.5.12"
|
||||
entities "^4.5.0"
|
||||
estree-walker "^2.0.2"
|
||||
source-map-js "^1.2.0"
|
||||
|
||||
@ -1868,13 +1883,13 @@
|
||||
"@vue/compiler-core" "3.4.31"
|
||||
"@vue/shared" "3.4.31"
|
||||
|
||||
"@vue/compiler-dom@3.4.36":
|
||||
version "3.4.36"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.36.tgz#32f5f65d1fb242211df2ddc65a336779cd8b974c"
|
||||
integrity sha512-eEIjy4GwwZTFon/Y+WO8tRRNGqylaRlA79T1RLhUpkOzJ7EtZkkb8MurNfkqY6x6Qiu0R7ESspEF7GkPR/4yYg==
|
||||
"@vue/compiler-dom@3.5.12":
|
||||
version "3.5.12"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.12.tgz#456d631d11102535b7ee6fd954cf2c93158d0354"
|
||||
integrity sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==
|
||||
dependencies:
|
||||
"@vue/compiler-core" "3.4.36"
|
||||
"@vue/shared" "3.4.36"
|
||||
"@vue/compiler-core" "3.5.12"
|
||||
"@vue/shared" "3.5.12"
|
||||
|
||||
"@vue/compiler-sfc@3.4.31":
|
||||
version "3.4.31"
|
||||
@ -1891,19 +1906,19 @@
|
||||
postcss "^8.4.38"
|
||||
source-map-js "^1.2.0"
|
||||
|
||||
"@vue/compiler-sfc@^3.4.35":
|
||||
version "3.4.36"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.36.tgz#887809183a273dc0ef8337d5e84ef6a781727ccc"
|
||||
integrity sha512-rhuHu7qztt/rNH90dXPTzhB7hLQT2OC4s4GrPVqmzVgPY4XBlfWmcWzn4bIPEWNImt0CjO7kfHAf/1UXOtx3vw==
|
||||
"@vue/compiler-sfc@3.5.12":
|
||||
version "3.5.12"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.12.tgz#6688120d905fcf22f7e44d3cb90f8dabc4dd3cc8"
|
||||
integrity sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.24.7"
|
||||
"@vue/compiler-core" "3.4.36"
|
||||
"@vue/compiler-dom" "3.4.36"
|
||||
"@vue/compiler-ssr" "3.4.36"
|
||||
"@vue/shared" "3.4.36"
|
||||
"@babel/parser" "^7.25.3"
|
||||
"@vue/compiler-core" "3.5.12"
|
||||
"@vue/compiler-dom" "3.5.12"
|
||||
"@vue/compiler-ssr" "3.5.12"
|
||||
"@vue/shared" "3.5.12"
|
||||
estree-walker "^2.0.2"
|
||||
magic-string "^0.30.10"
|
||||
postcss "^8.4.40"
|
||||
magic-string "^0.30.11"
|
||||
postcss "^8.4.47"
|
||||
source-map-js "^1.2.0"
|
||||
|
||||
"@vue/compiler-ssr@3.4.31":
|
||||
@ -1914,13 +1929,13 @@
|
||||
"@vue/compiler-dom" "3.4.31"
|
||||
"@vue/shared" "3.4.31"
|
||||
|
||||
"@vue/compiler-ssr@3.4.36":
|
||||
version "3.4.36"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.36.tgz#5881f9303ad6a4fdf04fb4238ebb483caf040707"
|
||||
integrity sha512-Wt1zyheF0zVvRJyhY74uxQbnkXV2Le/JPOrAxooR4rFYKC7cFr+cRqW6RU3cM/bsTy7sdZ83IDuy/gLPSfPGng==
|
||||
"@vue/compiler-ssr@3.5.12":
|
||||
version "3.5.12"
|
||||
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.12.tgz#5f1a3fbd5c44b79a6dbe88729f7801d9c9218bde"
|
||||
integrity sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==
|
||||
dependencies:
|
||||
"@vue/compiler-dom" "3.4.36"
|
||||
"@vue/shared" "3.4.36"
|
||||
"@vue/compiler-dom" "3.5.12"
|
||||
"@vue/shared" "3.5.12"
|
||||
|
||||
"@vue/devtools-api@^6.0.0-beta.11", "@vue/devtools-api@^6.5.0", "@vue/devtools-api@^6.6.1", "@vue/devtools-api@^6.6.3":
|
||||
version "6.6.3"
|
||||
@ -1943,6 +1958,13 @@
|
||||
dependencies:
|
||||
"@vue/shared" "3.4.31"
|
||||
|
||||
"@vue/reactivity@3.5.12":
|
||||
version "3.5.12"
|
||||
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.12.tgz#a2815d91842ed7b9e7e7936c851923caf6b6e603"
|
||||
integrity sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==
|
||||
dependencies:
|
||||
"@vue/shared" "3.5.12"
|
||||
|
||||
"@vue/runtime-core@3.4.31":
|
||||
version "3.4.31"
|
||||
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.4.31.tgz#ad3a41ad76385c0429e3e4dbefb81918494e10cf"
|
||||
@ -1951,6 +1973,14 @@
|
||||
"@vue/reactivity" "3.4.31"
|
||||
"@vue/shared" "3.4.31"
|
||||
|
||||
"@vue/runtime-core@3.5.12":
|
||||
version "3.5.12"
|
||||
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.5.12.tgz#849207f203d0fd82971f19574d30dbe7134c78c7"
|
||||
integrity sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==
|
||||
dependencies:
|
||||
"@vue/reactivity" "3.5.12"
|
||||
"@vue/shared" "3.5.12"
|
||||
|
||||
"@vue/runtime-dom@3.4.31":
|
||||
version "3.4.31"
|
||||
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.4.31.tgz#bae7ad844f944af33699c73581bc36125bab96ce"
|
||||
@ -1961,6 +1991,16 @@
|
||||
"@vue/shared" "3.4.31"
|
||||
csstype "^3.1.3"
|
||||
|
||||
"@vue/runtime-dom@3.5.12":
|
||||
version "3.5.12"
|
||||
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.5.12.tgz#6d4de3df49a90a460b311b1100baa5e2d0d1c8c9"
|
||||
integrity sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==
|
||||
dependencies:
|
||||
"@vue/reactivity" "3.5.12"
|
||||
"@vue/runtime-core" "3.5.12"
|
||||
"@vue/shared" "3.5.12"
|
||||
csstype "^3.1.3"
|
||||
|
||||
"@vue/server-renderer@3.4.31":
|
||||
version "3.4.31"
|
||||
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.4.31.tgz#bbe990f793c36d62d05bdbbaf142511d53e159fd"
|
||||
@ -1969,15 +2009,23 @@
|
||||
"@vue/compiler-ssr" "3.4.31"
|
||||
"@vue/shared" "3.4.31"
|
||||
|
||||
"@vue/server-renderer@3.5.12":
|
||||
version "3.5.12"
|
||||
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.5.12.tgz#79c6bc3860e4e4ef80d85653c5d03fd94b26574e"
|
||||
integrity sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==
|
||||
dependencies:
|
||||
"@vue/compiler-ssr" "3.5.12"
|
||||
"@vue/shared" "3.5.12"
|
||||
|
||||
"@vue/shared@3.4.31":
|
||||
version "3.4.31"
|
||||
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.31.tgz#af9981f57def2c3f080c14bf219314fc0dc808a0"
|
||||
integrity sha512-Yp3wtJk//8cO4NItOPpi3QkLExAr/aLBGZMmTtW9WpdwBCJpRM6zj9WgWktXAl8IDIozwNMByT45JP3tO3ACWA==
|
||||
|
||||
"@vue/shared@3.4.36":
|
||||
version "3.4.36"
|
||||
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.36.tgz#7551f41684966acb6a307152b49a8308e7f69203"
|
||||
integrity sha512-fdPLStwl1sDfYuUftBaUVn2pIrVFDASYerZSrlBvVBfylObPA1gtcWJHy5Ox8jLEJ524zBibss488Q3SZtU1uA==
|
||||
"@vue/shared@3.5.12":
|
||||
version "3.5.12"
|
||||
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.12.tgz#f9e45b7f63f2c3f40d84237b1194b7f67de192e3"
|
||||
integrity sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==
|
||||
|
||||
"@vue/test-utils@^2.4.5":
|
||||
version "2.4.6"
|
||||
@ -3208,11 +3256,6 @@ entities@^4.2.0, entities@^4.4.0, entities@^4.5.0:
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
|
||||
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
|
||||
|
||||
entities@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/entities/-/entities-5.0.0.tgz#b2ab51fe40d995817979ec79dd621154c3c0f62b"
|
||||
integrity sha512-BeJFvFRJddxobhvEdm5GqHzRV/X+ACeuw0/BuuxsCh1EUZcAIz8+kYmBp/LrQuloy6K1f3a0M7+IhmZ7QnkISA==
|
||||
|
||||
env-paths@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
|
||||
@ -4962,6 +5005,19 @@ kolorist@^1.8.0:
|
||||
resolved "https://registry.yarnpkg.com/kolorist/-/kolorist-1.8.0.tgz#edddbbbc7894bc13302cdf740af6374d4a04743c"
|
||||
integrity sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==
|
||||
|
||||
leaflet-geosearch@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/leaflet-geosearch/-/leaflet-geosearch-4.0.0.tgz#d7488830004515452368d333f7a49d06d59ea81b"
|
||||
integrity sha512-a92VNY9gxyv3oyEDqIWoCNoBllajWRYejztzOSNmpLRtzpA6JtGgy/wwl9tsB8+6Eek1fe+L6+W0MDEOaidbXA==
|
||||
optionalDependencies:
|
||||
"@googlemaps/js-api-loader" "^1.16.6"
|
||||
leaflet "^1.6.0"
|
||||
|
||||
leaflet@^1.6.0, leaflet@^1.9.4:
|
||||
version "1.9.4"
|
||||
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.9.4.tgz#23fae724e282fa25745aff82ca4d394748db7d8d"
|
||||
integrity sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==
|
||||
|
||||
levn@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
|
||||
@ -5700,6 +5756,11 @@ picocolors@^1.0.0, picocolors@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1"
|
||||
integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==
|
||||
|
||||
picocolors@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59"
|
||||
integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==
|
||||
|
||||
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||
@ -5805,7 +5866,7 @@ postcss-value-parser@^4.2.0:
|
||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||
|
||||
postcss@^8.4.0, postcss@^8.4.18, postcss@^8.4.38, postcss@^8.4.39, postcss@^8.4.40, postcss@^8.4.8:
|
||||
postcss@^8.4.0, postcss@^8.4.18, postcss@^8.4.38, postcss@^8.4.39, postcss@^8.4.8:
|
||||
version "8.4.41"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.41.tgz#d6104d3ba272d882fe18fc07d15dc2da62fa2681"
|
||||
integrity sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==
|
||||
@ -5823,6 +5884,15 @@ postcss@^8.4.43:
|
||||
picocolors "^1.0.1"
|
||||
source-map-js "^1.2.0"
|
||||
|
||||
postcss@^8.4.47:
|
||||
version "8.4.47"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365"
|
||||
integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==
|
||||
dependencies:
|
||||
nanoid "^3.3.7"
|
||||
picocolors "^1.1.0"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
prelude-ls@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||
@ -6333,6 +6403,11 @@ source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
|
||||
integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==
|
||||
|
||||
source-map-js@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||
|
||||
source-map-support@^0.5.16, source-map-support@~0.5.20:
|
||||
version "0.5.21"
|
||||
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
|
||||
@ -6412,7 +6487,14 @@ string.prototype.trimstart@^1.0.8:
|
||||
define-properties "^1.2.1"
|
||||
es-object-atoms "^1.0.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1, strip-ansi@^7.1.0:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1, strip-ansi@^7.1.0:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@ -6772,6 +6854,11 @@ tslib@^2.1.0, tslib@^2.3.0, tslib@^2.6.2:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0"
|
||||
integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==
|
||||
|
||||
tua-body-scroll-lock@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/tua-body-scroll-lock/-/tua-body-scroll-lock-1.5.1.tgz#1b8b7316dff55a821d5bec3fef045f995e7627a5"
|
||||
integrity sha512-AOjusG9EjTGxqqL1xqg6JeMauJ+IQoX9ITW1qP7UugySUdH6lzi2CqJRmU+oYqOv7vCQjOs5CQrjIakGlbOenQ==
|
||||
|
||||
type-check@^0.4.0, type-check@~0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
|
||||
@ -7194,11 +7281,6 @@ vue-apollo@^3.1.2:
|
||||
serialize-javascript "^4.0.0"
|
||||
throttle-debounce "^2.1.0"
|
||||
|
||||
vue-avatar@^2.3.3:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/vue-avatar/-/vue-avatar-2.3.3.tgz#e125bf4f4a6f4f9480da0c522020266a8609d2a8"
|
||||
integrity sha512-Z57ILRTkFIAuCH9JiFBxX74C5zua5ub/jRDM/KZ+QKXNfscvmUOgWBs3kA2+wrpZMowIvfLHIT0gvQu1z+zpLg==
|
||||
|
||||
vue-component-type-helpers@^2.0.0:
|
||||
version "2.1.6"
|
||||
resolved "https://registry.yarnpkg.com/vue-component-type-helpers/-/vue-component-type-helpers-2.1.6.tgz#f350515b252ed9e76960ac51f135636f8baef3fe"
|
||||
@ -7238,11 +7320,6 @@ vue-i18n@^9.13.1:
|
||||
"@intlify/shared" "9.13.1"
|
||||
"@vue/devtools-api" "^6.5.0"
|
||||
|
||||
vue-loading-overlay@^3.4.2:
|
||||
version "3.4.3"
|
||||
resolved "https://registry.yarnpkg.com/vue-loading-overlay/-/vue-loading-overlay-3.4.3.tgz#ea14a0cb0d94ac5c91e45f90c21da6233b3cf1b5"
|
||||
integrity sha512-Q4+RNnI6+szylJ98Abnp9CUDagKphZMt7okznGu1m7tidZX5b9u+a+De6uktWa5WULu/as+IsrWVR8lpmbDDOA==
|
||||
|
||||
vue-router@^4.4.0:
|
||||
version "4.4.3"
|
||||
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.4.3.tgz#58a39dc804632bfb6d26f052aa8f6718bd130299"
|
||||
@ -7255,11 +7332,6 @@ vue-timer-hook@^1.0.84:
|
||||
resolved "https://registry.yarnpkg.com/vue-timer-hook/-/vue-timer-hook-1.0.84.tgz#96acd9a74297bd003ffb6983a5c660bc8260cee4"
|
||||
integrity sha512-OcHWYO8WD/XcGHdEqMkBI1Vj8m+OnXONrcmZvLcL3Gc6Js5LEs3UJ0eg1akVjNB0cXHishEmrmrhK7PZ+iULww==
|
||||
|
||||
vue-timers@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/vue-timers/-/vue-timers-2.0.4.tgz#7e1c443abf2109db5eeab6e62b0f5a47e94cf70b"
|
||||
integrity sha512-QOEVdO4V4o9WjFG6C0Kn9tfdTeeECjqvEQozcQlfL1Tn8v0qx4uUPhTYoc1+s6qoJnSbu8f68x8+nm1ZEir0kw==
|
||||
|
||||
vue@3.4.31:
|
||||
version "3.4.31"
|
||||
resolved "https://registry.yarnpkg.com/vue/-/vue-3.4.31.tgz#83a3c4dab8302b0e974b0d4b92a2f6a6378ae797"
|
||||
@ -7271,6 +7343,17 @@ vue@3.4.31:
|
||||
"@vue/server-renderer" "3.4.31"
|
||||
"@vue/shared" "3.4.31"
|
||||
|
||||
vue@^3.2.25:
|
||||
version "3.5.12"
|
||||
resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.12.tgz#e08421c601b3617ea2c9ef0413afcc747130b36c"
|
||||
integrity sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==
|
||||
dependencies:
|
||||
"@vue/compiler-dom" "3.5.12"
|
||||
"@vue/compiler-sfc" "3.5.12"
|
||||
"@vue/runtime-dom" "3.5.12"
|
||||
"@vue/server-renderer" "3.5.12"
|
||||
"@vue/shared" "3.5.12"
|
||||
|
||||
vuex-persistedstate@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/vuex-persistedstate/-/vuex-persistedstate-4.1.0.tgz#127165f85f5b4534fb3170a5d3a8be9811bd2a53"
|
||||
@ -7377,7 +7460,16 @@ word-wrap@^1.2.5:
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@7.0.0, wrap-ansi@^8.1.0:
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@7.0.0, wrap-ansi@^8.1.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user