Merge branch 'master' into optimize_frontend_auth_css

This commit is contained in:
einhornimmond 2025-03-14 18:31:43 +01:00 committed by GitHub
commit 6401f50700
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 253 additions and 70 deletions

View File

@ -3,4 +3,5 @@ GRAPHQL_PATH=/graphql
WALLET_URL=http://localhost
WALLET_AUTH_PATH=/authenticate?token=
WALLET_LOGIN_PATH=/login
DEBUG_DISABLE_AUTH=false
DEBUG_DISABLE_AUTH=false
HUMHUB_ACTIVE=false

View File

@ -6,3 +6,6 @@ WALLET_AUTH_PATH=$WALLET_AUTH_PATH
WALLET_LOGIN_PATH=$WALLET_LOGIN_PATH
GRAPHQL_PATH=$GRAPHQL_PATH
DEBUG_DISABLE_AUTH=false
HUMHUB_ACTIVE=$HUMHUB_ACTIVE
HUMHUB_API_URL=$HUMHUB_API_URL

View File

@ -0,0 +1,17 @@
<template>
<div class="collapse-icon">
<IBiArrowUpCircle v-if="visible" class="text-black h2" />
<IBiArrowDownCircle v-else class="text-muted h2" />
</div>
</template>
<script>
export default {
name: 'CollapseIcon',
props: {
visible: {
type: Boolean,
required: true,
},
},
}
</script>

View File

@ -222,6 +222,10 @@ const onSubmit = () => {
}
}
toastSuccess(t('message.request'))
form.value = {
text: '',
memo: props.contributionMemo,
}
loading.value = false
})
.catch((error) => {

View File

@ -93,10 +93,12 @@ describe('ContributionMessagesList', () => {
wrapper = mount(ContributionMessagesList, {
props: {
contributionId: 42,
contributionMemo: 'test memo',
contributionUserId: 108,
contributionStatus: 'PENDING',
contribution: {
id: 42,
memo: 'test memo',
userId: 108,
status: 'PENDING',
},
hideResubmission: true,
},
global: {
@ -137,7 +139,7 @@ describe('ContributionMessagesList', () => {
})
it('does not render the ContributionMessagesFormular when status is not PENDING or IN_PROGRESS', async () => {
await wrapper.setProps({ contributionStatus: 'COMPLETED' })
await wrapper.setProps({ contribution: { status: 'COMPLETED' } })
expect(wrapper.find('contribution-messages-formular-stub').exists()).toBe(false)
})

View File

@ -1,17 +1,49 @@
<template>
<div class="contribution-messages-list">
<BListGroup>
<BListGroupItem>
<routerLink :to="searchLink" :title="$t('goTo.userSearch')">
{{ contribution.firstName }} {{ contribution.lastName }}
</routerLink>
&nbsp;
<a :href="mailtoLink">{{ contribution.email }}</a>
<IBiFilter id="filter-by-email" class="ms-1 pointer" @click="searchForEmail" />
<BTooltip target="filter-by-email" triggers="hover">
{{ $t('filter.byEmail') }}
</BTooltip>
&nbsp;
{{ contribution.username }}
&nbsp;
<span>
<a
v-if="humhubProfileLink"
id="humhub-username"
:href="humhubProfileLink"
target="_blank"
>
<i-arcticons-circles class="svg-icon" />
</a>
<BTooltip target="humhub-username" triggers="hover">
{{ $t('goTo.humhubProfile') }}
</BTooltip>
</span>
</BListGroupItem>
<BListGroupItem>
{{ $t('registered') }}: {{ new Date(contribution.createdAt).toLocaleString() }}
</BListGroupItem>
</BListGroup>
<BContainer>
<div v-for="message in messages" :key="message.id">
<contribution-messages-list-item
:message="message"
:contribution-user-id="contributionUserId"
:contribution-user-id="contribution.userId"
/>
</div>
</BContainer>
<div v-if="contributionStatus === 'PENDING' || contributionStatus === 'IN_PROGRESS'">
<div v-if="contribution.status === 'PENDING' || contribution.status === 'IN_PROGRESS'">
<contribution-messages-formular
:contribution-id="contributionId"
:contribution-memo="contributionMemo"
:contribution-id="contribution.id"
:contribution-memo="contribution.memo"
:hide-resubmission="hideResubmission"
:input-resubmission-date="resubmissionAt"
@get-list-contribution-messages="refetch"
@ -24,27 +56,16 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed } from 'vue'
import { useQuery } from '@vue/apollo-composable'
import { adminListContributionMessages } from '../../graphql/adminListContributionMessages.js'
import { useAppToast } from '@/composables/useToast'
import { BListGroupItem } from 'bootstrap-vue-next'
import CONFIG from '@/config'
const props = defineProps({
contributionId: {
type: Number,
required: true,
},
contributionMemo: {
type: String,
required: true,
},
contributionStatus: {
type: String,
required: true,
},
contributionUserId: {
type: Number,
contribution: {
type: Object,
required: true,
},
hideResubmission: {
@ -57,15 +78,36 @@ const props = defineProps({
},
})
const emit = defineEmits(['update-status', 'reload-contribution', 'update-contributions'])
const emit = defineEmits([
'update-status',
'reload-contribution',
'update-contributions',
'search-for-email',
])
const { toastError } = useAppToast()
const mailtoLink = computed(() => {
return `mailto:${props.contribution.email}`
})
const searchLink = computed(() => {
return `/user?search=${props.contribution.email}`
})
const humhubProfileLink = computed(() => {
if (CONFIG.HUMHUB_ACTIVE !== true) {
return undefined
}
let url = CONFIG.HUMHUB_API_URL
if (url.endsWith('/')) {
url = url.slice(0, -1)
}
return `${url}/u/${props.contribution.humhubUsername}`
})
const messages = ref([])
const { onResult, onError, result, refetch } = useQuery(
adminListContributionMessages,
{
contributionId: props.contributionId,
contributionId: props.contribution.id,
},
{
fetchPolicy: 'no-cache',
@ -91,6 +133,10 @@ const reloadContribution = (id) => {
const updateContributions = () => {
emit('update-contributions')
}
const searchForEmail = () => {
emit('search-for-email', props.contribution.email)
}
</script>
<style scoped>
.temp-message {

View File

@ -43,6 +43,13 @@
{{ $t('navbar.statistic') }}
</BNavItem>
<BNavItem @click="handleWallet">{{ $t('navbar.my-account') }}</BNavItem>
<BLink
href="https://gradido.net/coin/moderators-tutorial/"
class="nav-link"
target="_blank"
>
{{ $t('help.help') }}
</BLink>
<BNavItem @click="handleLogout">{{ $t('navbar.logout') }}</BNavItem>
</BNavbarNav>
</BCollapse>
@ -65,6 +72,7 @@ import {
BNavbarToggle,
vBToggle,
vBColorMode,
BLink,
} from 'bootstrap-vue-next'
import { useRoute } from 'vue-router'

View File

@ -67,6 +67,7 @@
<BButton v-if="row.item.messagesCount > 0" @click="rowToggleDetails(row, 0)">
<IBiChatDots />
</BButton>
<collapse-icon v-else :visible="row.detailsShowing" @click="rowToggleDetails(row, 0)" />
</template>
<template #cell(deny)="row">
<div v-if="!myself(row.item)">
@ -92,6 +93,12 @@
</BButton>
</div>
</template>
<template #cell(firstName)="row">
<div class="no-select">{{ row.item.firstName }}</div>
</template>
<template #cell(lastName)="row">
<div class="no-select">{{ row.item.lastName }}</div>
</template>
<template #row-details="row">
<row-details
:row="row"
@ -103,6 +110,7 @@
<template #show-creation>
<div v-if="row.item.moderatorId">
<edit-creation-formular
v-if="row.item.confirmedAt === null"
type="singleCreation"
:item="row.item"
:row="row"
@ -112,15 +120,13 @@
</div>
<div v-else>
<contribution-messages-list
:contribution-id="row.item.id"
:contribution-status="row.item.status"
:contribution-user-id="row.item.userId"
:contribution-memo="row.item.memo"
:contribution="row.item"
:resubmission-at="row.item.resubmissionAt"
:hide-resubmission="hideResubmission"
@update-status="updateStatus"
@reload-contribution="reloadContribution"
@update-contributions="updateContributions"
@search-for-email="$emit('search-for-email', $event)"
/>
</div>
</template>
@ -168,7 +174,13 @@ export default {
required: false,
},
},
emits: ['update-contributions', 'reload-contribution', 'update-status', 'show-overlay'],
emits: [
'update-contributions',
'reload-contribution',
'update-status',
'show-overlay',
'search-for-email',
],
data() {
return {
slotIndex: 0,
@ -176,6 +188,12 @@ export default {
creationUserData: {},
}
},
mounted() {
this.addClipboardListener()
},
beforeUnmount() {
this.removeClipboardListener()
},
methods: {
myself(item) {
return item.userId === this.$store.state.moderator.id
@ -201,28 +219,39 @@ export default {
this.$emit('update-contributions')
},
rowToggleDetails(row, index) {
if (this.openRow) {
if (this.openRow.index === row.index) {
if (index === this.slotIndex) {
row.toggleDetails()
this.openRow = null
} else {
this.slotIndex = index
}
} else {
this.openRow.toggleDetails()
row.toggleDetails()
this.slotIndex = index
this.openRow = row
this.creationUserData = row.item
}
const isSameRow = this.openRow && this.openRow.index === row.index
const isSameSlot = index === this.slotIndex
if (isSameRow && isSameSlot) {
row.toggleDetails()
this.openRow = null
} else {
if (this.openRow) {
this.openRow.toggleDetails()
}
row.toggleDetails()
this.slotIndex = index
this.openRow = row
this.creationUserData = row.item
}
},
addClipboardListener() {
document.addEventListener('copy', this.handleCopy)
},
removeClipboardListener() {
document.removeEventListener('copy', this.handleCopy)
},
handleCopy(event) {
// get from user selected text
const selectedText = window.getSelection().toString()
if (selectedText) {
// remove hashtags
const cleanedText = selectedText.replace(/#[a-zA-Z0-9_-]*/g, '')
event.clipboardData.setData('text/plain', cleanedText)
event.preventDefault()
}
},
},
}
</script>
@ -231,4 +260,8 @@ export default {
background-color: #e1a908;
border-color: #e1a908;
}
.no-select {
user-select: none;
}
</style>

View File

@ -50,12 +50,17 @@ const endpoints = {
const debug = {
DEBUG_DISABLE_AUTH: process.env.DEBUG_DISABLE_AUTH === 'true' ?? false,
}
const humhub = {
HUMHUB_ACTIVE: process.env.HUMHUB_ACTIVE === 'true' || false,
HUMHUB_API_URL: process.env.HUMHUB_API_URL ?? COMMUNITY_URL + '/community/',
}
const CONFIG = {
...version,
...environment,
...endpoints,
...debug,
...humhub,
ADMIN_MODULE_URL,
COMMUNITY_URL,
}

View File

@ -5,6 +5,8 @@ const {
COMMUNITY_URL,
DEBUG,
GRAPHQL_URI,
HUMHUB_ACTIVE,
HUMHUB_API_URL,
NODE_ENV,
PRODUCTION,
} = require('gradido-config/build/src/commonSchema.js')
@ -17,6 +19,8 @@ module.exports = Joi.object({
COMMUNITY_URL,
DEBUG,
GRAPHQL_URI,
HUMHUB_ACTIVE,
HUMHUB_API_URL,
NODE_ENV,
PRODUCTION,

View File

@ -26,6 +26,9 @@ export const adminListContributions = gql`
id
firstName
lastName
email
username
humhubUsername
amount
memo
createdAt

View File

@ -35,7 +35,8 @@
"validTo": "Enddatum"
},
"contributionMessagesForm": {
"resubmissionDateInPast": "Wiedervorlage Datum befindet sich in der Vergangenheit!"
"resubmissionDateInPast": "Wiedervorlage Datum befindet sich in der Vergangenheit!",
"hasRegisteredAt": "hat sich am {createdAt} registriert."
},
"contributions": {
"all": "Alle",
@ -73,6 +74,7 @@
"deleted_user": "Alle gelöschten Nutzer",
"deny": "Ablehnen",
"description": "Beschreibung",
"details": "Details",
"e_mail": "E-Mail",
"edit": "bearbeiten",
"enabled": "aktiviert",
@ -97,6 +99,9 @@
"verified": "Verifiziert",
"verifiedAt": "Verifiziert am"
},
"filter": {
"byEmail": "Nach E-Mail filtern"
},
"firstname": "Vorname",
"footer": {
"app_version": "App version {version}",
@ -117,6 +122,10 @@
"describe": "Teilt Koordinaten im Format 'Breitengrad, Längengrad' automatisch auf. Fügen sie hier einfach z.B. ihre Koordinaten von Google Maps, zum Beispiel: 49.28187664243721, 9.740672183943639, ein."
}
},
"goTo": {
"userSearch": "Zur Nutzersuche gehen",
"humhubProfile": "Zum Humhub Profil gehen"
},
"help": {
"help": "Hilfe",
"transactionlist": {
@ -141,7 +150,7 @@
"plus": "+"
},
"message": {
"request": "Die Anfrage wurde gesendet."
"request": "Die Eingabe wurde gespeichert."
},
"moderator": {
"history": "Die Daten wurden geändert. Dies sind die alten Daten.",
@ -223,6 +232,7 @@
"newUserToSpaceTooltip": "Neue Benutzer automatisch zum Space hinzufügen, falls Space vorhanden"
},
"redeemed": "eingelöst",
"registered": "Registriert",
"removeNotSelf": "Als Admin/Moderator kannst du dich nicht selber löschen.",
"reset": "Zurücksetzen",
"save": "Speichern",

View File

@ -35,7 +35,8 @@
"validTo": "End-Date"
},
"contributionMessagesForm": {
"resubmissionDateInPast": "Resubmission date is in the past!"
"resubmissionDateInPast": "Resubmission date is in the past!",
"hasRegisteredAt": "registered on {createdAt}."
},
"contributions": {
"all": "All",
@ -73,6 +74,7 @@
"deleted_user": "All deleted user",
"deny": "Reject",
"description": "Description",
"details": "Details",
"e_mail": "E-mail",
"edit": "edit",
"enabled": "enabled",
@ -97,6 +99,9 @@
"verified": "Verified",
"verifiedAt": "Verified at"
},
"filter": {
"byEmail": "Filter by email"
},
"firstname": "Firstname",
"footer": {
"app_version": "App version {version}",
@ -117,6 +122,10 @@
"describe": "Automatically splits coordinates in the format 'latitude, longitude'. Simply enter your coordinates from Google Maps here, for example: 49.28187664243721, 9.740672183943639."
}
},
"goTo": {
"userSearch": "Go to user search",
"humhubProfile": "Go to Humhub profile"
},
"help": {
"help": "Help",
"transactionlist": {
@ -141,7 +150,7 @@
"plus": "+"
},
"message": {
"request": "Request has been sent."
"request": "The entry has been saved."
},
"moderator": {
"history": "The data has been changed. This is the old data.",
@ -223,6 +232,7 @@
"newUserToSpaceTooltip": "The hours should contain a maximum of two decimal places"
},
"redeemed": "redeemed",
"registered": "Registered",
"removeNotSelf": "As an admin/moderator, you cannot delete yourself.",
"reset": "Reset",
"save": "Save",

View File

@ -58,6 +58,7 @@
@update-status="updateStatus"
@reload-contribution="reloadContribution"
@update-contributions="refetch"
@search-for-email="query = $event"
/>
<BPagination
@ -152,7 +153,7 @@ const fields = computed(
formatter: (value) => formatDateOrDash(value),
},
{ key: 'moderatorId', label: t('moderator.moderator') },
{ key: 'editCreation', label: t('chat') },
{ key: 'editCreation', label: t('details') },
{ key: 'confirm', label: t('save') },
],
// confirmed contributions
@ -181,7 +182,7 @@ const fields = computed(
formatter: (value) => formatDateOrDash(value),
},
{ key: 'confirmedBy', label: t('moderator.moderator') },
{ key: 'chatCreation', label: t('chat') },
{ key: 'chatCreation', label: t('details') },
],
// denied contributions
[
@ -209,7 +210,7 @@ const fields = computed(
formatter: (value) => formatDateOrDash(value),
},
{ key: 'deniedBy', label: t('moderator.moderator') },
{ key: 'chatCreation', label: t('chat') },
{ key: 'chatCreation', label: t('details') },
],
// deleted contributions
[
@ -237,7 +238,7 @@ const fields = computed(
formatter: (value) => formatDateOrDash(value),
},
{ key: 'deletedBy', label: t('moderator.moderator') },
{ key: 'chatCreation', label: t('chat') },
{ key: 'chatCreation', label: t('details') },
],
// all contributions
[
@ -266,7 +267,7 @@ const fields = computed(
formatter: (value) => formatDateOrDash(value),
},
{ key: 'confirmedBy', label: t('moderator.moderator') },
{ key: 'chatCreation', label: t('chat') },
{ key: 'chatCreation', label: t('details') },
],
][tabIndex.value],
)

View File

@ -15,6 +15,11 @@ vi.mock('@/composables/useToast', () => ({
toastSuccess: vi.fn(),
}),
}))
vi.mock('vue-router', () => ({
useRoute: () => ({
query: {},
}),
}))
// Mock icon components
const mockIconComponent = {

View File

@ -43,8 +43,9 @@
</div>
</template>
<script setup>
import { ref, reactive, computed, watch, watchEffect } from 'vue'
import { ref, reactive, computed, watch, watchEffect, onMounted } from 'vue'
import { useQuery } from '@vue/apollo-composable'
import { useRoute } from 'vue-router'
import { searchUsers } from '../graphql/searchUsers.js'
import useCreationMonths from '../composables/useCreationMonths'
import SearchUserTable from '../components/Tables/SearchUserTable'
@ -68,6 +69,7 @@ const response = ref()
const { creationLabel } = useCreationMonths()
const { toastSuccess } = useAppToast()
const route = useRoute()
const { result, refetch } = useQuery(searchUsers, {
query: criteria.value,
@ -105,6 +107,13 @@ const deletedUserSearch = () => {
refetch()
}
onMounted(() => {
const searchQuery = route.query.search
if (searchQuery) {
criteria.value = searchQuery
}
})
const fields = computed(() => [
{ key: 'email', label: t('e_mail') },
{ key: 'firstName', label: t('firstname') },

View File

@ -24,7 +24,6 @@ export default defineConfig(async ({ command }) => {
} else {
CONFIG.ADMIN_HOSTING = 'nginx'
}
// Check config
validate(schema, CONFIG)
// make sure that all urls used in browser have the same protocol to prevent mixed content errors
validate(browserUrls, [
@ -78,6 +77,8 @@ export default defineConfig(async ({ command }) => {
WALLET_AUTH_PATH: CONFIG.WALLET_AUTH_PATH ?? null,
WALLET_LOGIN_PATH: CONFIG.WALLET_LOGIN_URL ?? null, // null,
DEBUG_DISABLE_AUTH: CONFIG.DEBUG_DISABLE_AUTH ?? null, // null,
HUMHUB_ACTIVE: CONFIG.HUMHUB_ACTIVE ?? null, // null,
HUMHUB_API_URL: CONFIG.HUMHUB_API_URL ?? null, // null,
// CONFIG_VERSION: CONFIG.CONFIG_VERSION, // null,
}),
vitePluginGraphqlLoader(),

View File

@ -17,6 +17,7 @@ import {
GMS_ACTIVE,
GRAPHIQL,
HUMHUB_ACTIVE,
HUMHUB_API_URL,
LOG4JS_CONFIG,
LOGIN_APP_SECRET,
LOGIN_SERVER_KEY,
@ -44,6 +45,7 @@ export const schema = Joi.object({
GMS_ACTIVE,
GRAPHIQL,
HUMHUB_ACTIVE,
HUMHUB_API_URL,
LOG4JS_CONFIG,
LOGIN_APP_SECRET,
LOGIN_SERVER_KEY,
@ -281,11 +283,6 @@ export const schema = Joi.object({
.when('GMS_ACTIVE', { is: true, then: Joi.required(), otherwise: Joi.optional() })
.description('The secret postfix for the GMS webhook endpoint'),
HUMHUB_API_URL: Joi.string()
.uri({ scheme: ['http', 'https'] })
.when('HUMHUB_ACTIVE', { is: true, then: Joi.required(), otherwise: Joi.optional() })
.description('The API URL for HumHub integration'),
HUMHUB_JWT_KEY: Joi.string()
.min(1)
.when('HUMHUB_ACTIVE', {

View File

@ -3,12 +3,22 @@ import { User } from '@entity/User'
import { Decimal } from 'decimal.js-light'
import { ObjectType, Field, Int } from 'type-graphql'
import { PublishNameType } from '@enum/PublishNameType'
import { PublishNameLogic } from '@/data/PublishName.logic'
@ObjectType()
export class Contribution {
constructor(contribution: dbContribution, user?: User | null) {
this.id = contribution.id
this.firstName = user ? user.firstName : null
this.lastName = user ? user.lastName : null
this.firstName = user?.firstName ?? null
this.lastName = user?.lastName ?? null
this.email = user?.emailContact?.email ?? null
this.username = user?.alias ?? null
if (user) {
const publishNameLogic = new PublishNameLogic(user)
this.humhubUsername = publishNameLogic.getUsername(user.humhubPublishName as PublishNameType)
}
this.amount = contribution.amount
this.memo = contribution.memo
this.createdAt = contribution.createdAt
@ -37,6 +47,15 @@ export class Contribution {
@Field(() => String, { nullable: true })
lastName: string | null
@Field(() => String, { nullable: true })
email: string | null
@Field(() => String, { nullable: true })
username: string | null
@Field(() => String, { nullable: true })
humhubUsername: string | null
@Field(() => Decimal)
amount: Decimal

View File

@ -117,6 +117,11 @@ export const HUMHUB_ACTIVE = Joi.boolean()
.default(false)
.required()
export const HUMHUB_API_URL = Joi.string()
.uri({ scheme: ['http', 'https'] })
.when('HUMHUB_ACTIVE', { is: true, then: Joi.required(), otherwise: Joi.optional() })
.description('The API URL for HumHub integration')
export const LOG_LEVEL = Joi.string()
.valid('all', 'mark', 'trace', 'debug', 'info', 'warn', 'error', 'fatal', 'off')
.description('set log level')

View File

@ -51,8 +51,8 @@
"contribution": {
"activity": "Tätigkeit",
"alert": {
"answerQuestion": "Bitte beantworte diese Rückfrage.",
"answerQuestionToast": "Du hast eine Rückfrage auf einen Beitrag. Bitte antworte auf diese.",
"answerQuestion": "Zu diesem Beitrag liegt eine neue Nachricht vor.",
"answerQuestionToast": "Du hast neue Nachrichten.",
"communityNoteList": "Hier findest du alle eingereichten und bestätigten Beiträge von allen Mitgliedern aus dieser Gemeinschaft.",
"confirm": "bestätigt",
"deleted": "gelöscht",

View File

@ -51,8 +51,8 @@
"contribution": {
"activity": "Activity",
"alert": {
"answerQuestion": "Please answer the question.",
"answerQuestionToast": "You have a question about a post. Please reply to it.",
"answerQuestion": "There is a new message for this article.",
"answerQuestionToast": "You have new messages.",
"communityNoteList": "Here you will find all submitted and confirmed contributions from all members of this community.",
"confirm": "confirmed",
"deleted": "deleted",