feat(admin): automatic contributions updates (#3332)

* feat(admin): Automatic Contributions update

* feat(admin): Automatic Contributions fixed

* feat(admin): Automatic Contributions remove unused code

* feat(admin): Fix lint

* feat(admin): fix lock
This commit is contained in:
MateuszMichalowski 2024-07-25 16:15:04 +02:00 committed by GitHub
parent a8fdc5e94a
commit 4e11dd258c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 563 additions and 482 deletions

46
admin/components.d.ts vendored
View File

@ -7,6 +7,40 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
BAvatar: typeof import('bootstrap-vue-next')['BAvatar']
BBadge: typeof import('bootstrap-vue-next')['BBadge']
BButton: typeof import('bootstrap-vue-next')['BButton']
BCard: typeof import('bootstrap-vue-next')['BCard']
BCardText: typeof import('bootstrap-vue-next')['BCardText']
BCol: typeof import('bootstrap-vue-next')['BCol']
BCollapse: typeof import('bootstrap-vue-next')['BCollapse']
BContainer: typeof import('bootstrap-vue-next')['BContainer']
BForm: typeof import('bootstrap-vue-next')['BForm']
BFormCheckbox: typeof import('bootstrap-vue-next')['BFormCheckbox']
BFormGroup: typeof import('bootstrap-vue-next')['BFormGroup']
BFormInput: typeof import('bootstrap-vue-next')['BFormInput']
BFormRadioGroup: typeof import('bootstrap-vue-next')['BFormRadioGroup']
BFormSelect: typeof import('bootstrap-vue-next')['BFormSelect']
BFormTextarea: typeof import('bootstrap-vue-next')['BFormTextarea']
BInputGroup: typeof import('bootstrap-vue-next')['BInputGroup']
BLink: typeof import('bootstrap-vue-next')['BLink']
BListGroup: typeof import('bootstrap-vue-next')['BListGroup']
BListGroupItem: typeof import('bootstrap-vue-next')['BListGroupItem']
BModal: typeof import('bootstrap-vue-next')['BModal']
BPagination: typeof import('bootstrap-vue-next')['BPagination']
BRow: typeof import('bootstrap-vue-next')['BRow']
BTab: typeof import('bootstrap-vue-next')['BTab']
BTable: typeof import('bootstrap-vue-next')['BTable']
BTableLite: typeof import('bootstrap-vue-next')['BTableLite']
BTableSimple: typeof import('bootstrap-vue-next')['BTableSimple']
BTabs: typeof import('bootstrap-vue-next')['BTabs']
BTbody: typeof import('bootstrap-vue-next')['BTbody']
BTd: typeof import('bootstrap-vue-next')['BTd']
BTh: typeof import('bootstrap-vue-next')['BTh']
BThead: typeof import('bootstrap-vue-next')['BThead']
BToastOrchestrator: typeof import('bootstrap-vue-next')['BToastOrchestrator']
BTooltip: typeof import('bootstrap-vue-next')['BTooltip']
BTr: typeof import('bootstrap-vue-next')['BTr']
ChangeUserRoleFormular: typeof import('./src/components/ChangeUserRoleFormular.vue')['default']
CommunityVisualizeItem: typeof import('./src/components/Federation/CommunityVisualizeItem.vue')['default']
ConfirmRegisterMailFormular: typeof import('./src/components/ConfirmRegisterMailFormular.vue')['default']
@ -26,7 +60,15 @@ declare module 'vue' {
EditCreationFormular: typeof import('./src/components/EditCreationFormular.vue')['default']
FederationVisualizeItem: typeof import('./src/components/Federation/FederationVisualizeItem.vue')['default']
FigureQrCode: typeof import('./src/components/FigureQrCode.vue')['default']
IBiArrowClockwise: typeof import('~icons/bi/arrow-clockwise')['default']
IBiBellFill: typeof import('~icons/bi/bell-fill')['default']
IBiCheck: typeof import('~icons/bi/check')['default']
IBiEnvelope: typeof import('~icons/bi/envelope')['default']
IBiEye: typeof import('~icons/bi/eye')['default']
IBiList: typeof import('~icons/bi/list')['default']
IBiPencil: typeof import('~icons/bi/pencil')['default']
IBiPencilFill: typeof import('~icons/bi/pencil-fill')['default']
IBiTrash: typeof import('~icons/bi/trash')['default']
IBiXCircle: typeof import('~icons/bi/x-circle')['default']
IIcBaselineClose: typeof import('~icons/ic/baseline-close')['default']
IOcticonCircleSlash24: typeof import('~icons/octicon/circle-slash24')['default']
@ -48,4 +90,8 @@ declare module 'vue' {
TransactionLinkList: typeof import('./src/components/TransactionLinkList.vue')['default']
UserQuery: typeof import('./src/components/UserQuery.vue')['default']
}
export interface ComponentCustomProperties {
vBToggle: typeof import('bootstrap-vue-next')['vBToggle']
vBTooltip: typeof import('bootstrap-vue-next')['vBTooltip']
}
}

View File

@ -1,5 +1,6 @@
<template>
<div id="app">
<BToastOrchestrator />
<default-layout v-if="$store.state.token" />
<router-view v-else></router-view>
</div>

View File

@ -1,6 +1,6 @@
<template>
<div class="contribution-link">
<b-card
<BCard
border-variant="success"
:header="$t('contributionLink.contributionLinks')"
header-bg-variant="success"
@ -8,17 +8,17 @@
header-class="text-center"
class="mt-5"
>
<b-button
<BButton
v-if="!editContributionLink"
class="my-3 d-flex justify-content-left"
data-test="new-contribution-link-button"
@click="visible = !visible"
>
{{ $t('math.plus') }} {{ $t('contributionLink.newContributionLink') }}
</b-button>
</BButton>
<b-collapse id="newContribution" v-model="visible" class="mt-2">
<b-card>
<BCollapse id="newContribution" v-model="visible" class="mt-2">
<BCard>
<p class="h2 ml-5">{{ $t('contributionLink.contributionLinks') }}</p>
<contribution-link-form
:contribution-link-data="contributionLinkData"
@ -26,10 +26,10 @@
@get-contribution-links="$emit('get-contribution-links')"
@close-contribution-form="closeContributionForm"
/>
</b-card>
</b-collapse>
</BCard>
</BCollapse>
<b-card-text>
<BCardText>
<contribution-link-list
v-if="count > 0"
:items="items"
@ -38,10 +38,11 @@
@close-contribution-form="closeContributionForm"
/>
<div v-else>{{ $t('contributionLink.noContributionLinks') }}</div>
</b-card-text>
</b-card>
</BCardText>
</BCard>
</div>
</template>
<script>
import ContributionLinkForm from '../ContributionLink/ContributionLinkForm'
import ContributionLinkList from '../ContributionLink/ContributionLinkList'
@ -73,14 +74,14 @@ export default {
methods: {
closeContributionForm() {
if (this.visible) {
this.$root.$emit('bv::toggle::collapse', 'newContribution')
this.visible = false
this.editContributionLink = false
this.contributionLinkData = {}
}
},
editContributionLinkData(data) {
if (!this.visible) {
this.$root.$emit('bv::toggle::collapse', 'newContribution')
this.visible = true
}
this.contributionLinkData = data
this.editContributionLink = true

View File

@ -1,11 +1,11 @@
<template>
<div class="contribution-link-form">
<b-form ref="contributionLinkForm" class="m-5" @submit.prevent="onSubmit">
<BForm ref="contributionLinkForm" class="m-5" @submit.prevent="onSubmit" @reset="onReset">
<!-- Date -->
<b-row>
<b-col>
<b-form-group :label="$t('contributionLink.validFrom')">
<b-form-datepicker
<BRow>
<BCol>
<BFormGroup :label="$t('contributionLink.validFrom')">
<BFormInput
v-model="form.validFrom"
reset-button
size="lg"
@ -14,12 +14,13 @@
reset-value=""
:label-no-date-selected="$t('contributionLink.noDateSelected')"
required
></b-form-datepicker>
</b-form-group>
</b-col>
<b-col>
<b-form-group :label="$t('contributionLink.validTo')">
<b-form-datepicker
type="date"
/>
</BFormGroup>
</BCol>
<BCol>
<BFormGroup :label="$t('contributionLink.validTo')">
<BFormInput
v-model="form.validTo"
reset-button
size="lg"
@ -28,14 +29,15 @@
reset-value=""
:label-no-date-selected="$t('contributionLink.noDateSelected')"
required
></b-form-datepicker>
</b-form-group>
</b-col>
</b-row>
type="date"
/>
</BFormGroup>
</BCol>
</BRow>
<!-- Name -->
<b-form-group :label="$t('contributionLink.name')">
<b-form-input
<BFormGroup :label="$t('contributionLink.name')">
<BFormInput
v-model="form.name"
size="lg"
type="text"
@ -43,175 +45,173 @@
required
maxlength="100"
class="test-name"
></b-form-input>
</b-form-group>
></BFormInput>
</BFormGroup>
<!-- Desc -->
<b-form-group :label="$t('contributionLink.memo')">
<b-form-textarea
<BFormGroup :label="$t('contributionLink.memo')">
<BFormTextarea
v-model="form.memo"
size="lg"
:placeholder="$t('contributionLink.memo')"
required
maxlength="255"
class="test-memo"
></b-form-textarea>
</b-form-group>
></BFormTextarea>
</BFormGroup>
<!-- Amount -->
<b-form-group :label="$t('contributionLink.amount')">
<b-form-input
<BFormGroup :label="$t('contributionLink.amount')">
<BFormInput
v-model="form.amount"
size="lg"
type="number"
placeholder="0"
required
class="test-amount"
></b-form-input>
</b-form-group>
<b-row class="mb-4">
<b-col>
></BFormInput>
</BFormGroup>
<BRow class="mb-4">
<BCol>
<!-- Cycle -->
<label for="cycle">{{ $t('contributionLink.cycle') }}</label>
<b-form-select
v-model="form.cycle"
:options="cycle"
class="mb-3"
size="lg"
></b-form-select>
</b-col>
<b-col>
<BFormSelect v-model="form.cycle" :options="cycle" class="mb-3" size="lg"></BFormSelect>
</BCol>
<BCol>
<!-- maxPerCycle -->
<label for="maxPerCycle">{{ $t('contributionLink.maxPerCycle') }}</label>
<b-form-select
<BFormSelect
v-model="form.maxPerCycle"
:options="maxPerCycle"
:disabled="disabled"
class="mb-3"
size="lg"
></b-form-select>
</b-col>
</b-row>
></BFormSelect>
</BCol>
</BRow>
<!-- Max amount -->
<!--
<b-form-group :label="$t('contributionLink.maximumAmount')">
<b-form-input
<BFormGroup :label="$t('contributionLink.maximumAmount')">
<BFormInput
v-model="form.maxAmountPerMonth"
size="lg"
:disabled="disabled"
type="number"
placeholder="0"
></b-form-input>
</b-form-group>
></BFormInput>
</BFormGroup>
-->
<div class="mt-6">
<b-button type="submit" variant="primary">
<BButton type="submit" variant="primary">
{{
editContributionLink ? $t('contributionLink.saveChange') : $t('contributionLink.create')
}}
</b-button>
<b-button type="reset" variant="danger" @click.prevent="onReset">
</BButton>
<BButton type="reset" variant="danger">
{{ $t('contributionLink.clear') }}
</b-button>
<b-button @click.prevent="$emit('close-contribution-form')">
</BButton>
<BButton @click.prevent="emit('close-contribution-form')">
{{ $t('close') }}
</b-button>
</BButton>
</div>
</b-form>
</BForm>
</div>
</template>
<script>
<script setup>
import { ref, watch } from 'vue'
import { useMutation } from '@vue/apollo-composable'
import { createContributionLink } from '@/graphql/createContributionLink.js'
import { updateContributionLink } from '@/graphql/updateContributionLink.js'
import { useAppToast } from '@/composables/useToast'
import { useI18n } from 'vue-i18n'
export default {
name: 'ContributionLinkForm',
props: {
contributionLinkData: {
type: Object,
default() {
return {}
},
},
editContributionLink: { type: Boolean, required: true },
const props = defineProps({
contributionLinkData: {
type: Object,
default: () => ({}),
},
emits: ['close-contribution-form', 'close-contribution-link', 'get-contribution-links'],
data() {
return {
form: {
name: null,
memo: null,
amount: null,
validFrom: null,
validTo: null,
cycle: 'ONCE',
maxPerCycle: 1,
maxAmountPerMonth: '0',
},
min: new Date(),
cycle: [
{ value: 'ONCE', text: this.$t('contributionLink.options.cycle.once') },
// { value: 'hourly', text: this.$t('contributionLink.options.cycle.hourly') },
{ value: 'DAILY', text: this.$t('contributionLink.options.cycle.daily') },
// { value: 'weekly', text: this.$t('contributionLink.options.cycle.weekly') },
// { value: 'monthly', text: this.$t('contributionLink.options.cycle.monthly') },
// { value: 'yearly', text: this.$t('contributionLink.options.cycle.yearly') },
],
maxPerCycle: [
{ value: '1', text: '1 x' },
// { value: '2', text: '2 x' },
// { value: '3', text: '3 x' },
// { value: '4', text: '4 x' },
// { value: '5', text: '5 x' },
],
}
},
computed: {
disabled() {
return true
},
},
watch: {
contributionLinkData() {
this.form = this.contributionLinkData
},
},
methods: {
onSubmit() {
if (this.form.validFrom === null)
return this.toastError(this.$t('contributionLink.noStartDate'))
if (this.form.validTo === null) return this.toastError(this.$t('contributionLink.noEndDate'))
editContributionLink: { type: Boolean, required: true },
})
const variables = {
...this.form,
id: this.contributionLinkData.id ? this.contributionLinkData.id : null,
}
const emit = defineEmits(['get-contribution-links', 'close-contribution-form'])
this.$apollo
.mutate({
mutation: this.editContributionLink ? updateContributionLink : createContributionLink,
variables: variables,
})
.then((result) => {
const link = this.editContributionLink
? result.data.updateContributionLink.link
: result.data.createContributionLink.link
this.toastSuccess(
this.editContributionLink ? this.$t('contributionLink.changeSaved') : link,
)
this.onReset()
this.$root.$emit('bv::toggle::collapse', 'newContribution')
this.$emit('get-contribution-links')
})
.catch((error) => {
this.toastError(error.message)
})
},
onReset() {
this.$refs.contributionLinkForm.reset()
this.form = {}
this.form.validFrom = null
this.form.validTo = null
},
const { t } = useI18n()
const contributionLinkForm = ref(null)
const form = ref({
name: null,
memo: null,
amount: null,
validFrom: null,
validTo: null,
cycle: 'ONCE',
maxPerCycle: 1,
maxAmountPerMonth: '0',
})
const min = new Date().toLocaleDateString()
const { toastError, toastSuccess } = useAppToast()
const cycle = ref([
{ value: 'ONCE', text: t('contributionLink.options.cycle.once') },
{ value: 'DAILY', text: t('contributionLink.options.cycle.daily') },
])
const maxPerCycle = ref([{ value: '1', text: '1 x' }])
const { mutate: contributionLinkMutation } = useMutation(
props.editContributionLink ? updateContributionLink : createContributionLink,
)
watch(
() => props.contributionLinkData,
(newVal) => {
form.value = newVal
form.value.validFrom = formatDateFromDateTime(newVal.validFrom)
form.value.validTo = formatDateFromDateTime(newVal.validTo)
},
)
const onSubmit = async () => {
if (form.value.validFrom === null) return toastError(t('contributionLink.noStartDate'))
if (form.value.validTo === null) return toastError(t('contributionLink.noEndDate'))
const variables = {
...form.value,
maxAmountPerMonth: 1, // TODO this is added only for test puropuse during migration since max amount input is commented out but without it being a number bigger then 0 it doesn't work
id: props.contributionLinkData.id ? props.contributionLinkData.id : null,
}
try {
const result = await contributionLinkMutation({ ...variables })
const link = props.editContributionLink
? result.data.updateContributionLink.link
: result.data.createContributionLink.link
toastSuccess(props.editContributionLink ? t('contributionLink.changeSaved') : link)
onReset()
emit('close-contribution-form')
emit('get-contribution-links')
} catch (error) {
toastError(error.message)
}
}
const formatDateFromDateTime = (datetimeString) => {
if (!datetimeString || !datetimeString?.includes('T')) return datetimeString
return datetimeString.split('T')[0]
}
const onReset = () => {
form.value = { validFrom: null, validTo: null }
}
defineExpose({
form,
min,
cycle,
maxPerCycle,
onSubmit,
})
</script>

View File

@ -1,131 +1,144 @@
<template>
<div class="contribution-link-list">
<b-table :items="items" :fields="fields" striped hover stacked="lg">
<BTable :items="props.items" :fields="fields" striped hover stacked="lg">
<template #cell(delete)="data">
<b-button
<BButton
variant="danger"
size="md"
class="mr-2 test-delete-link"
@click="deleteContributionLink(data.item.id, data.item.name)"
@click="handleDelete(data)"
>
<b-icon icon="trash" variant="light"></b-icon>
</b-button>
<IBiTrash />
</BButton>
</template>
<template #cell(edit)="data">
<b-button variant="success" size="md" class="mr-2" @click="editContributionLink(data.item)">
<b-icon icon="pencil" variant="light"></b-icon>
</b-button>
<BButton variant="success" size="md" class="mr-2" @click="editContributionLink(data.item)">
<IBiPencil />
</BButton>
</template>
<template #cell(show)="data">
<b-button
<BButton
variant="info"
size="md"
class="mr-2 test-show"
@click="showContributionLink(data.item)"
>
<b-icon icon="eye" variant="light"></b-icon>
</b-button>
<IBiEye />
</BButton>
</template>
</b-table>
</BTable>
<b-modal ref="my-modal" ok-only hide-header-close>
<b-card header-tag="header" footer-tag="footer">
<BModal id="qr-link-modal" ref="my-modal" v-model="qrLinkModal" ok-only hide-header-close>
<BCard header-tag="header" footer-tag="footer">
<template #header>
<h6 class="mb-0">{{ modalData ? modalData.name : '' }}</h6>
</template>
<b-card-text>
<BCardText>
{{ modalData.memo ? modalData.memo : '' }}
<figure-qr-code :link="modalData ? modalData.link : ''" />
</b-card-text>
</BCardText>
<template #footer>
<em>{{ modalData ? modalData.link : '' }}</em>
</template>
</b-card>
</b-modal>
</BCard>
</BModal>
<BModal id="delete-link-modal" v-model="deleteLinkModal" @ok="executeDelete">
<template #default>
{{ t('contributionLink.deleteNow', { name: itemToBeDeleted.name }) }}
</template>
</BModal>
</div>
</template>
<script>
<script setup>
import { ref } from 'vue'
import { useMutation } from '@vue/apollo-composable'
import { deleteContributionLink } from '@/graphql/deleteContributionLink.js'
import FigureQrCode from '../FigureQrCode'
import { useModal } from 'bootstrap-vue-next'
import { useI18n } from 'vue-i18n'
import { useAppToast } from '@/composables/useToast'
export default {
name: 'ContributionLinkList',
components: {
FigureQrCode,
const props = defineProps({
items: {
type: Array,
required: true,
},
props: {
items: { type: Array, required: true },
},
emits: [
'close-contribution-form',
'edit-contribution-link-data',
'get-contribution-links',
'get-contribution-link',
],
data() {
return {
fields: [
'name',
'memo',
'amount',
{ key: 'cycle', label: this.$t('contributionLink.cycle') },
{ key: 'maxPerCycle', label: this.$t('contributionLink.maxPerCycle') },
{
key: 'validFrom',
label: this.$t('contributionLink.validFrom'),
formatter: (value, key, item) => {
if (value) {
return this.$d(new Date(value))
}
},
},
{
key: 'validTo',
label: this.$t('contributionLink.validTo'),
formatter: (value, key, item) => {
if (value) {
return this.$d(new Date(value))
}
},
},
'delete',
'edit',
'show',
],
modalData: {},
}
},
methods: {
deleteContributionLink(id, name) {
this.$bvModal
.msgBoxConfirm(this.$t('contributionLink.deleteNow', { name: name }))
.then(async (value) => {
if (value)
await this.$apollo
.mutate({
mutation: deleteContributionLink,
variables: {
id: id,
},
})
.then(() => {
this.toastSuccess(this.$t('contributionLink.deleted'))
this.$emit('close-contribution-form')
this.$emit('get-contribution-links')
})
.catch((err) => {
this.toastError(err.message)
})
})
},
editContributionLink(row) {
this.$emit('edit-contribution-link-data', row)
},
})
showContributionLink(row) {
this.modalData = row
this.$refs['my-modal'].show()
},
const qrLinkModal = ref(false)
const { show: showQrCodeModal } = useModal('qr-link-modal')
const deleteLinkModal = ref(false)
const { show: showDeleteLinkModal } = useModal('delete-link-modal')
const emit = defineEmits([
'close-contribution-form',
'get-contribution-links',
'edit-contribution-link-data',
])
const { t, d } = useI18n()
const { toastError, toastSuccess } = useAppToast()
const modalData = ref({})
const fields = ref([
'name',
'memo',
'amount',
{ key: 'cycle', label: t('contributionLink.cycle') },
{ key: 'maxPerCycle', label: t('contributionLink.maxPerCycle') },
{
key: 'validFrom',
label: t('contributionLink.validFrom'),
formatter: (value) => (value ? d(new Date(value)) : ''),
},
{
key: 'validTo',
label: t('contributionLink.validTo'),
formatter: (value) => (value ? d(new Date(value)) : ''),
},
'delete',
'edit',
'show',
])
const { mutate: deleteContributionLinkMutation } = useMutation(deleteContributionLink)
const itemToBeDeleted = ref({})
const handleDelete = async (dataPayload) => {
itemToBeDeleted.value = { ...dataPayload.item }
showDeleteLinkModal()
}
const executeDelete = async () => {
try {
await deleteContributionLinkMutation({ id: parseInt(itemToBeDeleted.value.id) })
toastSuccess(t('contributionLink.deleted'))
emit('close-contribution-form')
emit('get-contribution-links')
itemToBeDeleted.value = {}
} catch (err) {
toastError(err.message)
}
}
const editContributionLink = (row) => {
emit('edit-contribution-link-data', row)
}
const showContributionLink = (row) => {
modalData.value = row
showQrCodeModal()
}
defineExpose({
fields,
modalData,
deleteContributionLink,
editContributionLink,
showContributionLink,
})
</script>

View File

@ -1,30 +1,30 @@
<template>
<div class="community-visualize-item">
<b-row @click="toggleDetails">
<b-col cols="1"><b-icon :icon="icon" :variant="variant" class="mr-4"></b-icon></b-col>
<b-col>
<BRow @click="toggleDetails">
<BCol cols="1"><b-icon :icon="icon" :variant="variant" class="mr-4"></b-icon></BCol>
<BCol>
<div>
<a :href="item.url" target="_blank">{{ item.url }}</a>
</div>
<small>{{ `${item.publicKey.substring(0, 26)}` }}</small>
</b-col>
<b-col v-b-tooltip="item.description">{{ item.name }}</b-col>
<b-col cols="2">{{ lastAnnouncedAt }}</b-col>
<b-col cols="2">{{ createdAt }}</b-col>
</b-row>
<b-row v-if="details" class="details">
<b-col colspan="5">
<b-list-group>
<b-list-group-item v-if="item.uuid">
</BCol>
<BCol v-b-tooltip="item.description">{{ item.name }}</BCol>
<BCol cols="2">{{ lastAnnouncedAt }}</BCol>
<BCol cols="2">{{ createdAt }}</BCol>
</BRow>
<BRow v-if="details" class="details">
<BCol colspan="5">
<BListGroup>
<BListGroupItem v-if="item.uuid">
{{ $t('federation.communityUuid') }}&nbsp;{{ item.uuid }}
</b-list-group-item>
<b-list-group-item v-if="item.authenticatedAt">
</BListGroupItem>
<BListGroupItem v-if="item.authenticatedAt">
{{ $t('federation.authenticatedAt') }}&nbsp;{{ item.authenticatedAt }}
</b-list-group-item>
<b-list-group-item>
</BListGroupItem>
<BListGroupItem>
{{ $t('federation.publicKey') }}&nbsp;{{ item.publicKey }}
</b-list-group-item>
<b-list-group-item v-if="!item.foreign">
</BListGroupItem>
<BListGroupItem v-if="!item.foreign">
<editable-group
:allow-edit="$store.state.moderator.roles.includes('ADMIN')"
@save="handleUpdateHomeCommunity"
@ -32,7 +32,7 @@
>
<template #view>
<label>{{ $t('federation.gmsApiKey') }}&nbsp;{{ gmsApiKey }}</label>
<b-form-group>
<BFormGroup>
{{ $t('federation.coordinates') }}
<span v-if="isValidLocation">
{{
@ -42,7 +42,7 @@
})
}}
</span>
</b-form-group>
</BFormGroup>
</template>
<template #edit>
<editable-groupable-label
@ -53,29 +53,29 @@
<coordinates v-model="location" />
</template>
</editable-group>
</b-list-group-item>
<b-list-group-item>
<b-list-group>
<b-row>
<b-col class="ml-1">{{ $t('federation.verified') }}</b-col>
<b-col>{{ $t('federation.apiVersion') }}</b-col>
<b-col>{{ $t('federation.createdAt') }}</b-col>
<b-col>{{ $t('federation.lastAnnouncedAt') }}</b-col>
<b-col>{{ $t('federation.verifiedAt') }}</b-col>
<b-col>{{ $t('federation.lastErrorAt') }}</b-col>
</b-row>
<b-list-group-item
</BListGroupItem>
<BListGroup-item>
<BListGroup>
<BRow>
<BCol class="ml-1">{{ $t('federation.verified') }}</BCol>
<BCol>{{ $t('federation.apiVersion') }}</BCol>
<BCol>{{ $t('federation.createdAt') }}</BCol>
<BCol>{{ $t('federation.lastAnnouncedAt') }}</BCol>
<BCol>{{ $t('federation.verifiedAt') }}</BCol>
<BCol>{{ $t('federation.lastErrorAt') }}</BCol>
</BRow>
<BListGroup-item
v-for="federation in item.federatedCommunities"
:key="federation.id"
:variant="!item.foreign ? 'primary' : 'warning'"
>
<federation-visualize-item :item="federation" />
</b-list-group-item>
</b-list-group>
</b-list-group-item>
</b-list-group>
</b-col>
</b-row>
</BListGroup-item>
</BListGroup>
</BListGroup-item>
</BListGroup>
</BCol>
</BRow>
</div>
</template>
<script>

View File

@ -1,7 +1,7 @@
<template>
<div class="figure-qr-code">
<div class="qrbox">
<q-r-canvas :options="options" class="canvas" />
<q-r-canvas v-if="showQr" :options="qrOptions" class="canvas" />
</div>
</div>
</template>
@ -18,26 +18,26 @@ export default {
},
data() {
return {
options: {
image: null,
showQr: false,
}
},
computed: {
qrOptions() {
return {
cellSize: 8,
correctLevel: 'H',
data: this.link,
logo: {
image: null,
},
},
}
logo: { image: this.image },
}
},
},
created() {
const image = new Image()
image.src = 'img/gdd-coin.png'
image.src = '/img/gdd-coin.png'
image.onload = () => {
this.options = {
...this.options,
logo: {
image,
},
}
this.image = image
this.showQr = true
}
},
}

View File

@ -9,7 +9,7 @@
/>
</BNavbarBrand>
<BNavbarToggle target="navbar-toggle-collapse" />
<BNavbarToggle v-b-toggle.nav-collapse target="navbar-toggle-collapse" />
<BCollapse id="nav-collapse" is-nav>
<BNavbarNav>

View File

@ -15,7 +15,7 @@
<b>{{ $t('statistic.totalUsers') }}</b>
</BTd>
<BTd class="text-right">
{{ props.modelValue.totalUsers }}
{{ props.statistics.totalUsers }}
</BTd>
<BTd></BTd>
</BTr>
@ -24,7 +24,7 @@
<b>{{ $t('statistic.activeUsers') }}</b>
</BTd>
<BTd class="text-right">
{{ props.modelValue.activeUsers }}
{{ props.statistics.activeUsers }}
</BTd>
<BTd></BTd>
</BTr>
@ -33,7 +33,7 @@
<b>{{ $t('statistic.deletedUsers') }}</b>
</BTd>
<BTd class="text-right">
{{ props.modelValue.deletedUsers }}
{{ props.statistics.deletedUsers }}
</BTd>
<BTd></BTd>
</BTr>
@ -42,11 +42,11 @@
<b>{{ $t('statistic.totalGradidoCreated') }}</b>
</BTd>
<BTd class="text-right">
<!-- {{ $n(props.modelValue.totalGradidoCreated, 'decimal') }} {{ $t('GDD') }}-->
<!-- {{ $n(props.statistics.totalGradidoCreated, 'decimal') }} {{ $t('GDD') }}-->
4500
</BTd>
<BTd class="text-right">
{{ props.modelValue.totalGradidoCreated }}
{{ props.statistics.totalGradidoCreated }}
</BTd>
</BTr>
<BTr>
@ -54,21 +54,19 @@
<b>{{ $t('statistic.totalGradidoDecayed') }}</b>
</BTd>
<BTd class="text-right">
<!-- {{ $n(props.modelValue.totalGradidoDecayed, 'decimal') }} {{ $t('GDD') }}-->
100
{{ $n(parseFloat(props.statistics.totalGradidoDecayed), 'decimal') }} {{ $t('GDD') }}
</BTd>
<BTd class="text-right">{{ props.modelValue.totalGradidoDecayed }}</BTd>
<BTd class="text-right">{{ props.statistics.totalGradidoDecayed }}</BTd>
</BTr>
<BTr>
<BTd>
<b>{{ $t('statistic.totalGradidoAvailable') }}</b>
</BTd>
<BTd class="text-right">
<!-- {{ $n(props.modelValue.totalGradidoAvailable, 'decimal') }} {{ $t('GDD') }}-->
500
{{ $n(parseFloat(props.statistics.totalGradidoAvailable), 'decimal') }} {{ $t('GDD') }}
</BTd>
<BTd class="text-right">
{{ props.modelValue.totalGradidoAvailable }}
{{ props.statistics.totalGradidoAvailable }}
</BTd>
</BTr>
<BTr>
@ -76,12 +74,10 @@
<b>{{ $t('statistic.totalGradidoUnbookedDecayed') }}</b>
</BTd>
<BTd class="text-right">
<!-- {{ $n(props.modelValue.totalGradidoUnbookedDecayed, 'decimal') }} {{ $t('GDD') }}-->
600
</BTd>
<BTd class="text-right">
{{ props.modelValue.totalGradidoUnbookedDecayed }}
{{ $n(parseFloat(props.statistics.totalGradidoUnbookedDecayed), 'decimal') }}
{{ $t('GDD') }}
</BTd>
<BTd class="text-right">{{ props.statistics.totalGradidoUnbookedDecayed }}</BTd>
</BTr>
</BTbody>
</BTableSimple>
@ -89,10 +85,9 @@
</template>
<script setup>
import { defineProps } from 'vue'
import { BTableSimple, BThead, BTr, BTh, BTbody, BTd } from 'bootstrap-vue-next'
const props = defineProps({
modelValue: {
statistics: {
type: Object,
required: true,
},

View File

@ -2,19 +2,20 @@
<div>
<slot v-if="!isEditing" :is-editing="isEditing" name="view"></slot>
<slot v-else :is-editing="isEditing" name="edit" @input="valueChanged"></slot>
<b-form-group v-if="allowEdit && !isEditing">
<b-button :variant="variant" @click="enableEdit">
<b-icon icon="pencil-fill">{{ $t('edit') }}</b-icon>
</b-button>
</b-form-group>
<b-form-group v-else-if="allowEdit && isEditing">
<b-button :variant="variant" :disabled="!isValueChanged" class="save-button" @click="save">
<BFormGroup v-if="allowEdit && !isEditing">
<BButton :variant="variant" @click="enableEdit">
<IBiPencilFill />
{{ $t('edit') }}
</BButton>
</BFormGroup>
<BFormGroup v-else-if="allowEdit && isEditing">
<BButton :variant="variant" :disabled="!isValueChanged" class="save-button" @click="save">
{{ $t('save') }}
</b-button>
<b-button variant="secondary" class="close-button" @click="close">
</BButton>
<BButton variant="secondary" class="close-button" @click="close">
{{ $t('close') }}
</b-button>
</b-form-group>
</BButton>
</BFormGroup>
</div>
</template>

View File

@ -1,7 +1,7 @@
<template>
<b-form-group :label="label" :label-for="idName">
<b-form-input :id="idName" v-model="inputValue" @input="updateValue" />
</b-form-group>
<BFormGroup :label="label" :label-for="idName">
<BFormInput :id="idName" v-model="inputValue" @input="updateValue" />
</BFormGroup>
</template>
<script>

View File

@ -0,0 +1,41 @@
import { useI18n } from 'vue-i18n'
import { useToast } from 'bootstrap-vue-next'
export function useAppToast() {
const { t } = useI18n()
const { show } = useToast()
const toastSuccess = (message) => {
toast(message, {
title: t('success'),
variant: 'success',
})
}
const toastError = (message) => {
toast(message, {
title: t('error'),
variant: 'danger',
})
}
const toast = (message, options = {}) => {
if (message.replace) message = message.replace(/^GraphQL error: /, '')
options = {
solid: true,
toaster: 'b-toaster-top-right',
headerClass: 'gdd-toaster-title',
bodyClass: 'gdd-toaster-body',
toastClass: 'gdd-toaster',
...options,
body: message,
}
show({ props: { ...options } })
}
return {
toastSuccess,
toastError,
toast,
}
}

View File

@ -1,44 +1,43 @@
<template>
<div class="community-statistic">
<statistic-table v-model="statistics" />
<statistic-table v-if="!loading" :statistics="statistics" />
</div>
</template>
<script>
import { communityStatistics } from '@/graphql/communityStatistics.js'
import StatisticTable from '../components/Tables/StatisticTable'
export default {
name: 'CommunityStatistic',
components: {
StatisticTable,
<script setup>
import { ref, watch } from 'vue'
import { useQuery } from '@vue/apollo-composable'
import { communityStatistics } from '@/graphql/communityStatistics'
import StatisticTable from '../components/Tables/StatisticTable'
import { useAppToast } from '@/composables/useToast'
const statistics = ref({
totalUsers: null,
activeUsers: null,
deletedUsers: null,
totalGradidoCreated: null,
totalGradidoDecayed: null,
totalGradidoAvailable: null,
totalGradidoUnbookedDecayed: null,
})
const { result, loading, error } = useQuery(communityStatistics, () => ({}))
const { toastError } = useAppToast()
watch(
result,
() => {
if (!result.value) return
const totals = { ...result.value.communityStatistics.dynamicStatisticsFields }
statistics.value = { ...result.value.communityStatistics, ...totals }
delete statistics.value.dynamicStatisticsFields
},
data() {
return {
statistics: {
totalUsers: null,
activeUsers: null,
deletedUsers: null,
totalGradidoCreated: null,
totalGradidoDecayed: null,
totalGradidoAvailable: null,
totalGradidoUnbookedDecayed: null,
},
}
},
apollo: {
CommunityStatistics: {
query() {
return communityStatistics
},
update({ communityStatistics }) {
const totals = { ...communityStatistics.dynamicStatisticsFields }
this.statistics = { ...communityStatistics, ...totals }
delete this.statistics.dynamicStatisticsFields
},
error({ message }) {
this.toastError(message)
},
},
},
}
{ immediate: true },
)
watch(error, () => {
if (error.value) {
toastError(error.value.message)
}
})
</script>

View File

@ -1,45 +1,31 @@
<template>
<div class="contribution-link">
<contribution-link
:items="items"
:count="count"
@get-contribution-links="getContributionLinks"
/>
<contribution-link :items="items" :count="count" @get-contribution-links="refetch" />
</div>
</template>
<script>
<script setup>
import { computed, watch } from 'vue'
import { useQuery } from '@vue/apollo-composable'
import { listContributionLinks } from '@/graphql/listContributionLinks.js'
import ContributionLink from '../components/ContributionLink/ContributionLink'
import { useAppToast } from '@/composables/useToast'
export default {
name: 'ContributionLinks',
components: {
ContributionLink,
},
data() {
return {
items: [],
count: 0,
}
},
created() {
this.getContributionLinks()
},
methods: {
getContributionLinks() {
this.$apollo
.query({
query: listContributionLinks,
fetchPolicy: 'network-only',
})
.then((result) => {
this.count = result.data.listContributionLinks.count
this.items = result.data.listContributionLinks.links
})
.catch(() => {
this.toastError('listContributionLinks has no result, use default data')
})
},
},
}
const { toastError } = useAppToast()
const { result, error, refetch } = useQuery(listContributionLinks, null, {
fetchPolicy: 'network-only',
})
const items = computed(() => {
return result.value?.listContributionLinks?.links
})
const count = computed(() => {
return result.value?.listContributionLinks?.count
})
watch(error, () => {
toastError('listContributionLinks has no result, use default data')
})
</script>

View File

@ -13,41 +13,46 @@
</span>
</p>
<div>
<b-tabs v-model="tabIndex" content-class="mt-3" fill>
<b-tab active :title-link-attributes="{ 'data-test': 'open' }">
<BTabs v-model="tabIndex" content-class="mt-3" fill>
<BTab active :title-link-attributes="{ 'data-test': 'open' }">
<template #title>
<b-icon icon="bell-fill" variant="primary"></b-icon>
<IBiBellFill />
<!-- <b-icon icon="bell-fill" variant="primary"></b-icon>-->
{{ $t('contributions.open') }}
<b-badge v-if="$store.state.openCreations > 0" variant="danger">
<BBadge v-if="$store.state.openCreations > 0" variant="danger">
{{ $store.state.openCreations }}
</b-badge>
</BBadge>
</template>
</b-tab>
<b-tab :title-link-attributes="{ 'data-test': 'confirmed' }">
</BTab>
<BTab :title-link-attributes="{ 'data-test': 'confirmed' }">
<template #title>
<b-icon icon="check" variant="success"></b-icon>
<IBiCheck />
<!-- <b-icon icon="check" variant="success"></b-icon>-->
{{ $t('contributions.confirms') }}
</template>
</b-tab>
<b-tab :title-link-attributes="{ 'data-test': 'denied' }">
</BTab>
<BTab :title-link-attributes="{ 'data-test': 'denied' }">
<template #title>
<b-icon icon="x-circle" variant="warning"></b-icon>
<IBiXCircle />
<!-- <b-icon icon="x-circle" variant="warning"></b-icon>-->
{{ $t('contributions.denied') }}
</template>
</b-tab>
<b-tab :title-link-attributes="{ 'data-test': 'deleted' }">
</BTab>
<BTab :title-link-attributes="{ 'data-test': 'deleted' }">
<template #title>
<b-icon icon="trash" variant="danger"></b-icon>
<IBiTrash />
<!-- <b-icon icon="trash" variant="danger"></b-icon>-->
{{ $t('contributions.deleted') }}
</template>
</b-tab>
<b-tab :title-link-attributes="{ 'data-test': 'all' }">
</BTab>
<BTab :title-link-attributes="{ 'data-test': 'all' }">
<template #title>
<b-icon icon="list"></b-icon>
<IBiList />
<!-- <b-icon icon="list"></b-icon>-->
{{ $t('contributions.all') }}
</template>
</b-tab>
</b-tabs>
</BTab>
</BTabs>
</div>
<open-creations-table
class="mt-4"
@ -60,7 +65,7 @@
@update-contributions="$apollo.queries.ListAllContributions.refetch()"
/>
<b-pagination
<BPagination
v-model="currentPage"
pills
size="lg"
@ -68,7 +73,7 @@
:total-rows="rows"
align="center"
:hide-ellipsis="true"
></b-pagination>
/>
<div v-if="overlay" id="overlay" @dblclick="overlay = false">
<overlay :item="item" @overlay-cancel="overlay = false">

View File

@ -2,69 +2,57 @@
<div class="federation-visualize">
<div class="d-flex justify-content-between align-items-center mb-3">
<span class="h2">{{ $t('federation.gradidoInstances') }}</span>
<b-button>
<b-icon
icon="arrow-clockwise"
font-scale="2"
:animation="animation"
data-test="federation-communities-refresh-btn"
@click="$apollo.queries.allCommunities.refresh()"
></b-icon>
</b-button>
<BButton
:animation="animation"
data-test="federation-communities-refresh-btn"
font-scale="2"
@click="refetch"
>
<IBiArrowClockwise />
<!-- <b-icon-->
<!-- icon="arrow-clockwise"-->
<!-- font-scale="2"-->
<!-- :animation="animation"-->
<!-- data-test="federation-communities-refresh-btn"-->
<!-- @click="$apollo.queries.allCommunities.refresh()"-->
<!-- ></b-icon>-->
</BButton>
</div>
<b-list-group>
<b-row>
<b-col cols="1" class="ml-1">{{ $t('federation.verified') }}</b-col>
<b-col class="ml-3">{{ $t('federation.url') }}</b-col>
<b-col class="ml-3">{{ $t('federation.name') }}</b-col>
<b-col cols="2">{{ $t('federation.lastAnnouncedAt') }}</b-col>
<b-col cols="2">{{ $t('federation.createdAt') }}</b-col>
</b-row>
<b-list-group-item
<BListGroup>
<BRow>
<BCol cols="1" class="ml-1">{{ $t('federation.verified') }}</BCol>
<BCol class="ml-3">{{ $t('federation.url') }}</BCol>
<BCol class="ml-3">{{ $t('federation.name') }}</BCol>
<BCol cols="2">{{ $t('federation.lastAnnouncedAt') }}</BCol>
<BCol cols="2">{{ $t('federation.createdAt') }}</BCol>
</BRow>
<BListGroupItem
v-for="item in communities"
:key="item.publicKey"
:variant="!item.foreign ? 'primary' : 'warning'"
>
<community-visualize-item :item="item" />
</b-list-group-item>
</b-list-group>
</BListGroupItem>
</BListGroup>
</div>
</template>
<script>
<script setup>
import { ref, computed } from 'vue'
import { useQuery } from '@vue/apollo-composable'
import { allCommunities } from '@/graphql/allCommunities'
import { useAppToast } from '@/composables/useToast'
import CommunityVisualizeItem from '../components/Federation/CommunityVisualizeItem.vue'
const communities = ref([])
export default {
name: 'FederationVisualize',
components: {
CommunityVisualizeItem,
},
data() {
return {
oldPublicKey: '',
communities: [],
icon: '',
}
},
computed: {
animation() {
return this.$apollo.queries.allCommunities.loading ? 'spin' : ''
},
},
apollo: {
allCommunities: {
fetchPolicy: 'network-only',
query() {
return allCommunities
},
update({ allCommunities }) {
this.communities = allCommunities
},
error({ message }) {
this.toastError(message)
},
},
},
}
const { toastError } = useAppToast()
const { result, loading, refetch, error } = useQuery(allCommunities, () => ({}), {
fetchPolicy: 'network-only',
})
result.value = allCommunities
if (error) toastError(error.value.message)
const animation = computed(() => (loading.value ? 'spin' : ''))
</script>

View File

@ -51,6 +51,7 @@ import SearchUserTable from '../components/Tables/SearchUserTable'
import UserQuery from '../components/UserQuery'
import { BPagination, BButton } from 'bootstrap-vue-next'
import { useI18n } from 'vue-i18n'
import { useAppToast } from '@/composables/useToast'
const { t } = useI18n()
@ -66,6 +67,7 @@ const perPage = ref(25)
const response = ref()
const { creationLabel } = useCreationMonths()
const { toastSuccess } = useAppToast()
const { result, refetch } = useQuery(searchUsers, {
query: criteria.value,
@ -90,7 +92,7 @@ const updateRoles = (userId, roles) => {
const updateDeletedAt = (userId, deletedAt) => {
searchResult.value.find((obj) => obj.userId === userId).deletedAt = deletedAt
// toastSuccess(deletedAt ? $t('user_deleted') : $t('user_recovered'))
toastSuccess(deletedAt ? t('user_deleted') : t('user_recovered'))
}
const unconfirmedRegisterMails = () => {

View File

@ -4,11 +4,14 @@ import commonjs from 'vite-plugin-commonjs'
import Icons from 'unplugin-icons/vite'
import Components from 'unplugin-vue-components/vite'
import IconsResolve from 'unplugin-icons/resolver'
import { BootstrapVueNextResolver } from 'bootstrap-vue-next'
const path = require('path')
export default defineConfig({
base: '/admin/',
server: {
host: '0.0.0.0',
port: 8080,
},
resolve: {
@ -30,7 +33,7 @@ export default defineConfig({
},
}),
Components({
resolvers: [IconsResolve()],
resolvers: [IconsResolve(), BootstrapVueNextResolver()],
dts: true,
}),
Icons({