diff --git a/backend/src/graphql/model/Contribution.ts b/backend/src/graphql/model/Contribution.ts index 301348ccc..21f47bf2d 100644 --- a/backend/src/graphql/model/Contribution.ts +++ b/backend/src/graphql/model/Contribution.ts @@ -1,13 +1,15 @@ import { Contribution as dbContribution } from '@entity/Contribution' -import { User as DbUser } from '@entity/User' +import { ContributionMessage as dbContributionMessage } from '@entity/ContributionMessage' import { Decimal } from 'decimal.js-light' import { Field, Int, ObjectType } from 'type-graphql' +import { ContributionMessage } from './ContributionMessage' import { User } from './User' @ObjectType() export class Contribution { - constructor(contribution: dbContribution, user?: DbUser | null) { + constructor(contribution: dbContribution) { + const user = contribution.user this.id = contribution.id this.firstName = user?.firstName ?? null this.lastName = user?.lastName ?? null @@ -18,7 +20,7 @@ export class Contribution { this.confirmedBy = contribution.confirmedBy this.contributionDate = contribution.contributionDate this.status = contribution.contributionStatus - this.messagesCount = contribution.messages ? contribution.messages.length : 0 + this.messagesCount = contribution.messages?.length ?? 0 this.deniedAt = contribution.deniedAt this.deniedBy = contribution.deniedBy this.deletedAt = contribution.deletedAt @@ -28,9 +30,12 @@ export class Contribution { this.moderatorId = contribution.moderatorId this.userId = contribution.userId this.resubmissionAt = contribution.resubmissionAt - if (user) { - this.user = new User(user) - } + this.user = user ? new User(user) : null + this.messages = contribution.messages + ? contribution.messages.map( + (message: dbContributionMessage) => new ContributionMessage(message), + ) + : null } @Field(() => Int) @@ -87,6 +92,9 @@ export class Contribution { @Field(() => Int) messagesCount: number + @Field(() => [ContributionMessage], { nullable: true }) + messages: ContributionMessage[] | null + @Field(() => String) status: string diff --git a/backend/src/graphql/model/ContributionMessage.ts b/backend/src/graphql/model/ContributionMessage.ts index 6f70d5024..998717c83 100644 --- a/backend/src/graphql/model/ContributionMessage.ts +++ b/backend/src/graphql/model/ContributionMessage.ts @@ -1,18 +1,18 @@ import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage' -import { User } from '@entity/User' import { Field, Int, ObjectType } from 'type-graphql' @ObjectType() export class ContributionMessage { - constructor(contributionMessage: DbContributionMessage, user: User) { + constructor(contributionMessage: DbContributionMessage) { + const user = contributionMessage.user this.id = contributionMessage.id this.message = contributionMessage.message this.createdAt = contributionMessage.createdAt this.updatedAt = contributionMessage.updatedAt this.type = contributionMessage.type - this.userFirstName = user.firstName - this.userLastName = user.lastName - this.userId = user.id + this.userFirstName = user?.firstName ?? null + this.userLastName = user?.lastName ?? null + this.userId = user?.id ?? null this.isModerator = contributionMessage.isModerator } diff --git a/backend/src/graphql/resolver/ContributionMessageResolver.ts b/backend/src/graphql/resolver/ContributionMessageResolver.ts index 88cd73bc7..3eae0b042 100644 --- a/backend/src/graphql/resolver/ContributionMessageResolver.ts +++ b/backend/src/graphql/resolver/ContributionMessageResolver.ts @@ -75,7 +75,7 @@ export class ContributionMessageResolver { { id: contributionId } as DbContribution, finalContributionMessage, ) - return new ContributionMessage(finalContributionMessage, user) + return new ContributionMessage(finalContributionMessage) } @Authorized([RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES]) @@ -87,16 +87,12 @@ export class ContributionMessageResolver { ): Promise { const [contributionMessages, count] = await findContributionMessages({ contributionId, - currentPage, - pageSize, - order, + pagination: { currentPage, pageSize, order }, }) return { count, - messages: contributionMessages.map( - (message) => new ContributionMessage(message, message.user), - ), + messages: contributionMessages.map((message) => new ContributionMessage(message)), } } @@ -109,17 +105,13 @@ export class ContributionMessageResolver { ): Promise { const [contributionMessages, count] = await findContributionMessages({ contributionId, - currentPage, - pageSize, - order, + pagination: { currentPage, pageSize, order }, showModeratorType: true, }) return { count, - messages: contributionMessages.map( - (message) => new ContributionMessage(message, message.user), - ), + messages: contributionMessages.map((message) => new ContributionMessage(message)), } } @@ -194,6 +186,6 @@ export class ContributionMessageResolver { finalContribution, finalContributionMessage, ) - return new ContributionMessage(finalContributionMessage, moderator) + return new ContributionMessage(finalContributionMessage) } } diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts index a137b0d79..365c52f5d 100644 --- a/backend/src/graphql/resolver/ContributionResolver.ts +++ b/backend/src/graphql/resolver/ContributionResolver.ts @@ -24,7 +24,6 @@ import { AdminUpdateContributionArgs } from '@arg/AdminUpdateContributionArgs' import { ContributionArgs } from '@arg/ContributionArgs' import { Paginated } from '@arg/Paginated' import { SearchContributionsFilterArgs } from '@arg/SearchContributionsFilterArgs' -import { ContributionMessageType } from '@enum/ContributionMessageType' import { ContributionStatus } from '@enum/ContributionStatus' import { ContributionType } from '@enum/ContributionType' import { TransactionTypeId } from '@enum/TransactionTypeId' @@ -60,11 +59,15 @@ import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { calculateDecay } from '@/util/decay' import { fullName } from '@/util/utilities' +import { ContributionMessage } from '@model/ContributionMessage' +import { ContributionMessageType } from '../enum/ContributionMessageType' import { findContribution } from './util/contributions' import { getOpenCreations, getUserCreation, validateContribution } from './util/creations' import { extractGraphQLFields, extractGraphQLFieldsForSelect } from './util/extractGraphQLFields' +import { findContributionMessages } from './util/findContributionMessages' import { findContributions } from './util/findContributions' import { getLastTransaction } from './util/getLastTransaction' +import { loadAllContributions, loadUserContributions } from './util/loadContributions' import { sendTransactionsToDltConnector } from './util/sendTransactionsToDltConnector' @Resolver(() => Contribution) @@ -143,46 +146,59 @@ export class ContributionResolver { @Ctx() context: Context, @Args() paginated: Paginated, - @Arg('statusFilter', () => [ContributionStatus], { nullable: true }) - statusFilter?: ContributionStatus[] | null, ): Promise { const user = getUser(context) - const filter = new SearchContributionsFilterArgs() - filter.statusFilter = statusFilter - filter.userId = user.id - const [dbContributions, count] = await findContributions(paginated, filter, true, { - messages: true, - }) + const [dbContributions, count] = await loadUserContributions(user.id, paginated) + + // show contributions in progress first + const inProgressContributions = dbContributions.filter( + (contribution) => contribution.contributionStatus === ContributionStatus.IN_PROGRESS, + ) + const notInProgressContributions = dbContributions.filter( + (contribution) => contribution.contributionStatus !== ContributionStatus.IN_PROGRESS, + ) return new ContributionListResult( count, - dbContributions.map((contribution) => { - // filter out moderator messages for this call + [...inProgressContributions, ...notInProgressContributions].map((contribution) => { + // we currently expect not much contribution messages for needing pagination + // but if we get more than expected, we should get warned + if ((contribution.messages?.length || 0) > 10) { + logger.warn('more contribution messages as expected, consider pagination', { + contributionId: contribution.id, + expected: 10, + actual: contribution.messages?.length || 0, + }) + } contribution.messages = contribution.messages?.filter( - (m) => (m.type as ContributionMessageType) !== ContributionMessageType.MODERATOR, + (message) => message.type !== ContributionMessageType.MODERATOR, ) - return new Contribution(contribution, user) + return new Contribution(contribution) }), ) } + @Authorized([RIGHTS.LIST_CONTRIBUTIONS]) + @Query(() => Int) + async countContributionsInProgress(@Ctx() context: Context): Promise { + const user = getUser(context) + const count = await DbContribution.count({ + where: { userId: user.id, contributionStatus: ContributionStatus.IN_PROGRESS }, + }) + return count + } + @Authorized([RIGHTS.LIST_ALL_CONTRIBUTIONS]) @Query(() => ContributionListResult) async listAllContributions( @Args() paginated: Paginated, - @Arg('statusFilter', () => [ContributionStatus], { nullable: true }) - statusFilter?: ContributionStatus[] | null, ): Promise { - const filter = new SearchContributionsFilterArgs() - filter.statusFilter = statusFilter - const [dbContributions, count] = await findContributions(paginated, filter, false, { - user: true, - }) + const [dbContributions, count] = await loadAllContributions(paginated) return new ContributionListResult( count, - dbContributions.map((contribution) => new Contribution(contribution, contribution.user)), + dbContributions.map((contribution) => new Contribution(contribution)), ) } @@ -370,7 +386,7 @@ export class ContributionResolver { return new ContributionListResult( count, - dbContributions.map((contribution) => new Contribution(contribution, contribution.user)), + dbContributions.map((contribution) => new Contribution(contribution)), ) } @@ -627,4 +643,30 @@ export class ContributionResolver { } return new User(user) } + + @Authorized([RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES]) + @FieldResolver(() => [ContributionMessage], { nullable: true }) + async messages( + @Root() contribution: Contribution, + @Arg('pagination', () => Paginated) pagination: Paginated, + ): Promise { + if (contribution.messagesCount === 0) { + return null + } + const [contributionMessages] = await findContributionMessages({ + contributionId: contribution.id, + pagination, + }) + // we currently expect not much contribution messages for needing pagination + // but if we get more than expected, we should get warned + if (contributionMessages.length > pagination.pageSize) { + logger.warn('more contribution messages as expected, consider pagination', { + contributionId: contribution.id, + expected: pagination.pageSize, + actual: contributionMessages.length, + }) + } + + return contributionMessages.map((message) => new ContributionMessage(message)) + } } diff --git a/backend/src/graphql/resolver/util/findContributionMessages.ts b/backend/src/graphql/resolver/util/findContributionMessages.ts index 460b62b38..9f9209e7c 100644 --- a/backend/src/graphql/resolver/util/findContributionMessages.ts +++ b/backend/src/graphql/resolver/util/findContributionMessages.ts @@ -1,28 +1,26 @@ import { In } from '@dbTools/typeorm' import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage' +import { Paginated } from '@arg/Paginated' import { ContributionMessageType } from '@enum/ContributionMessageType' -import { Order } from '@enum/Order' interface FindContributionMessagesOptions { contributionId: number - pageSize: number - currentPage: number - order: Order + pagination: Paginated showModeratorType?: boolean } export const findContributionMessages = async ( options: FindContributionMessagesOptions, ): Promise<[DbContributionMessage[], number]> => { - const { contributionId, pageSize, currentPage, order, showModeratorType } = options + const { contributionId, pagination, showModeratorType } = options const messageTypes = [ContributionMessageType.DIALOG, ContributionMessageType.HISTORY] if (showModeratorType) { messageTypes.push(ContributionMessageType.MODERATOR) } - + const { currentPage, pageSize, order } = pagination return DbContributionMessage.findAndCount({ where: { contributionId, diff --git a/backend/src/graphql/resolver/util/loadContributions.ts b/backend/src/graphql/resolver/util/loadContributions.ts new file mode 100644 index 000000000..18f4a4fe6 --- /dev/null +++ b/backend/src/graphql/resolver/util/loadContributions.ts @@ -0,0 +1,41 @@ +import { Paginated } from '@arg/Paginated' +import { FindManyOptions } from '@dbTools/typeorm' +import { Contribution as DbContribution } from '@entity/Contribution' + +function buildBaseOptions(paginated: Paginated): FindManyOptions { + const { order, currentPage, pageSize } = paginated + return { + order: { createdAt: order, id: order }, + skip: (currentPage - 1) * pageSize, + take: pageSize, + } +} + +/* + * Load user contributions with messages + * @param userId if userId is set, load all contributions of the user, with messages + * @param paginated pagination, see {@link Paginated} + */ +export const loadUserContributions = async ( + userId: number, + paginated: Paginated, +): Promise<[DbContribution[], number]> => { + return DbContribution.findAndCount({ + where: { userId }, + relations: { messages: { user: true } }, + ...buildBaseOptions(paginated), + }) +} + +/* + * Load all contributions + * @param paginated pagination, see {@link Paginated} + */ +export const loadAllContributions = async ( + paginated: Paginated, +): Promise<[DbContribution[], number]> => { + return DbContribution.findAndCount({ + relations: { user: true }, + ...buildBaseOptions(paginated), + }) +} diff --git a/frontend/src/components/ContributionMessages/ContributionMessagesFormular.vue b/frontend/src/components/ContributionMessages/ContributionMessagesFormular.vue index 85363665f..634823168 100644 --- a/frontend/src/components/ContributionMessages/ContributionMessagesFormular.vue +++ b/frontend/src/components/ContributionMessages/ContributionMessagesFormular.vue @@ -5,10 +5,10 @@ @@ -39,35 +39,37 @@ const props = defineProps({ }, }) -const emit = defineEmits(['get-list-contribution-messages', 'update-status']) +const emit = defineEmits([ + 'get-list-contribution-messages', + 'update-status', + 'add-contribution-message', +]) const { t } = useI18n() const { toastSuccess, toastError } = useAppToast() const { mutate: createContributionMessageMutation } = useMutation(createContributionMessage) -const form = ref({ - text: '', -}) +const formText = ref('') const isSubmitting = ref(false) const disabled = computed(() => { - return form.value.text === '' || isSubmitting.value + return formText.value === '' || isSubmitting.value }) async function onSubmit() { isSubmitting.value = true - try { - await createContributionMessageMutation({ + const result = await createContributionMessageMutation({ contributionId: props.contributionId, - message: form.value.text, + message: formText.value, }) - emit('get-list-contribution-messages', false) + // emit('get-list-contribution-messages', false) + formText.value = '' emit('update-status', props.contributionId) - form.value.text = '' + emit('add-contribution-message', result.data.createContributionMessage) toastSuccess(t('message.reply')) } catch (error) { toastError(error.message) @@ -77,6 +79,6 @@ async function onSubmit() { } function onReset() { - form.value.text = '' + formText.value = '' } diff --git a/frontend/src/components/ContributionMessages/ContributionMessagesList.vue b/frontend/src/components/ContributionMessages/ContributionMessagesList.vue index 56b7fea8b..1406ce744 100644 --- a/frontend/src/components/ContributionMessages/ContributionMessagesList.vue +++ b/frontend/src/components/ContributionMessages/ContributionMessagesList.vue @@ -10,7 +10,6 @@ v-if="['PENDING', 'IN_PROGRESS'].includes(status)" :contribution-id="contributionId" v-bind="$attrs" - @update-status="updateStatus" /> @@ -46,11 +45,6 @@ export default { required: true, }, }, - methods: { - updateStatus(id) { - this.$emit('update-status', id) - }, - }, }