feat(admin): Automatic Contributions update

This commit is contained in:
Mateusz Michałowski 2024-07-25 14:15:03 +02:00
parent d488d4b15f
commit 0be9f452f6
17 changed files with 609 additions and 390 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,61 @@
@close-contribution-form="closeContributionForm"
/>
<div v-else>{{ $t('contributionLink.noContributionLinks') }}</div>
</b-card-text>
</b-card>
</BCardText>
</BCard>
</div>
</template>
<!--<script setup>-->
<!--import { ref, watchEffect } from 'vue'-->
<!--import { useEmit } from '@vueuse/core'-->
<!--import ContributionLinkForm from '../ContributionLink/ContributionLinkForm'-->
<!--import ContributionLinkList from '../ContributionLink/ContributionLinkList'-->
<!--import { useToggle } from 'vueuse'-->
<!--// extracting props-->
<!--const props = defineProps({-->
<!-- items: {-->
<!-- type: Array,-->
<!-- required: true,-->
<!-- },-->
<!-- count: {-->
<!-- type: Number,-->
<!-- required: true,-->
<!-- },-->
<!--})-->
<!--// extracting emits-->
<!--const emit = useEmit()-->
<!--// setting up refs-->
<!--const visible = ref(false)-->
<!--const contributionLinkData = ref({})-->
<!--const editContributionLink = ref(false)-->
<!--// setting up methods-->
<!--const closeContributionForm = () => {-->
<!-- if (visible.value) {-->
<!-- emit('bv::toggle::collapse', 'newContribution')-->
<!-- editContributionLink.value = false-->
<!-- contributionLinkData.value = {}-->
<!-- }-->
<!--}-->
<!--const editContributionLinkData = (data) => {-->
<!-- if (!visible.value) {-->
<!-- emit('bv::toggle::collapse', 'newContribution')-->
<!-- }-->
<!-- contributionLinkData.value = data-->
<!-- editContributionLink.value = true-->
<!--}-->
<!--defineExpose({-->
<!-- closeContributionForm,-->
<!-- editContributionLinkData,-->
<!--})-->
<!--</script>-->
<script>
import ContributionLinkForm from '../ContributionLink/ContributionLinkForm'
import ContributionLinkList from '../ContributionLink/ContributionLinkList'
@ -73,14 +124,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,274 @@
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>-->
<!--import { createContributionLink } from '@/graphql/createContributionLink.js'-->
<!--import { updateContributionLink } from '@/graphql/updateContributionLink.js'-->
<!--export default {-->
<!-- name: 'ContributionLinkForm',-->
<!-- props: {-->
<!-- contributionLinkData: {-->
<!-- type: Object,-->
<!-- default() {-->
<!-- return {}-->
<!-- },-->
<!-- },-->
<!-- editContributionLink: { type: Boolean, required: true },-->
<!-- },-->
<!-- 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'))-->
<!-- const variables = {-->
<!-- ...this.form,-->
<!-- id: this.contributionLinkData.id ? this.contributionLinkData.id : null,-->
<!-- }-->
<!-- 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-->
<!-- },-->
<!-- },-->
<!--}-->
<!--</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([
'bv::toggle::collapse',
'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' }])
// Set client
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,47 +1,47 @@
<template>
<div class="contribution-link-list">
<b-table :items="items" :fields="fields" striped hover stacked="lg">
<BTable :items="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)"
>
<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 ref="my-modal" 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>
</div>
</template>
<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

@ -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"
@click="refetch"
font-scale="2"
>
<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({