From d20904fbfe6a3769546b78a3d842ce30d6f39953 Mon Sep 17 00:00:00 2001
From: einhorn_b
Date: Mon, 20 Nov 2023 13:53:46 +0100
Subject: [PATCH 1/9] add new date field to contribution_message
---
.../ContributionMessage.ts | 59 +++++++++++++++++++
database/entity/ContributionMessage.ts | 2 +-
..._resubmission_date_contribution_message.ts | 12 ++++
3 files changed, 72 insertions(+), 1 deletion(-)
create mode 100644 database/entity/0077-add_resubmission_date_contribution_message/ContributionMessage.ts
create mode 100644 database/migrations/0077-add_resubmission_date_contribution_message.ts
diff --git a/database/entity/0077-add_resubmission_date_contribution_message/ContributionMessage.ts b/database/entity/0077-add_resubmission_date_contribution_message/ContributionMessage.ts
new file mode 100644
index 000000000..2ea886dd5
--- /dev/null
+++ b/database/entity/0077-add_resubmission_date_contribution_message/ContributionMessage.ts
@@ -0,0 +1,59 @@
+import {
+ BaseEntity,
+ Column,
+ DeleteDateColumn,
+ Entity,
+ Index,
+ JoinColumn,
+ ManyToOne,
+ PrimaryGeneratedColumn,
+} from 'typeorm'
+import { Contribution } from '../Contribution'
+import { User } from '../User'
+
+@Entity('contribution_messages', {
+ engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
+})
+export class ContributionMessage extends BaseEntity {
+ @PrimaryGeneratedColumn('increment', { unsigned: true })
+ id: number
+
+ @Index()
+ @Column({ name: 'contribution_id', unsigned: true, nullable: false })
+ contributionId: number
+
+ @ManyToOne(() => Contribution, (contribution) => contribution.messages)
+ @JoinColumn({ name: 'contribution_id' })
+ contribution: Contribution
+
+ @Column({ name: 'user_id', unsigned: true, nullable: false })
+ userId: number
+
+ @ManyToOne(() => User, (user) => user.messages)
+ @JoinColumn({ name: 'user_id' })
+ user: User
+
+ @Column({ length: 2000, nullable: false, collation: 'utf8mb4_unicode_ci' })
+ message: string
+
+ @Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP', name: 'created_at' })
+ createdAt: Date
+
+ @Column({ type: 'datetime', default: null, nullable: true, name: 'updated_at' })
+ updatedAt: Date
+
+ @DeleteDateColumn({ name: 'deleted_at' })
+ deletedAt: Date | null
+
+ @Column({ name: 'deleted_by', default: null, unsigned: true, nullable: true })
+ deletedBy: number
+
+ @Column({ type: 'datetime', name: 'resubmission_at', default: null, nullable: true })
+ resubmissionAt: Date | null
+
+ @Column({ length: 12, nullable: false, collation: 'utf8mb4_unicode_ci' })
+ type: string
+
+ @Column({ name: 'is_moderator', type: 'bool', nullable: false, default: false })
+ isModerator: boolean
+}
diff --git a/database/entity/ContributionMessage.ts b/database/entity/ContributionMessage.ts
index a5b370406..d1512863b 100644
--- a/database/entity/ContributionMessage.ts
+++ b/database/entity/ContributionMessage.ts
@@ -1 +1 @@
-export { ContributionMessage } from './0075-contribution_message_add_index/ContributionMessage'
+export { ContributionMessage } from './0077-add_resubmission_date_contribution_message/ContributionMessage'
diff --git a/database/migrations/0077-add_resubmission_date_contribution_message.ts b/database/migrations/0077-add_resubmission_date_contribution_message.ts
new file mode 100644
index 000000000..598068b2f
--- /dev/null
+++ b/database/migrations/0077-add_resubmission_date_contribution_message.ts
@@ -0,0 +1,12 @@
+/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) {
+ await queryFn(
+ `ALTER TABLE \`contribution_messages\` ADD COLUMN \`resubmission_at\` datetime NULL DEFAULT NULL AFTER \`deleted_by\`;`,
+ )
+}
+
+export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) {
+ await queryFn(`ALTER TABLE \`contribution_messages\` DROP COLUMN \`resubmission_at\`;`)
+}
From ac7c4bf6861e58ceca7d013408b8c98129ba9c6c Mon Sep 17 00:00:00 2001
From: einhorn_b
Date: Mon, 20 Nov 2023 18:48:46 +0100
Subject: [PATCH 2/9] first approach, filtering out resubmission work, but
expensive and query don't longer work
---
.../ContributionMessagesFormular.vue | 38 +++++++++++++++
admin/src/components/input/TimePicker.vue | 48 +++++++++++++++++++
.../graphql/adminCreateContributionMessage.js | 8 +++-
admin/src/graphql/adminListContributions.js | 2 +
admin/src/locales/de.json | 3 ++
admin/src/locales/en.json | 3 ++
admin/src/pages/CreationConfirm.vue | 12 ++++-
backend/src/config/index.ts | 2 +-
.../graphql/arg/ContributionMessageArgs.ts | 5 ++
.../arg/SearchContributionsFilterArgs.ts | 4 ++
.../resolver/ContributionMessageResolver.ts | 5 +-
.../resolver/util/findContributions.ts | 29 ++++++++++-
dht-node/src/config/index.ts | 2 +-
federation/src/config/index.ts | 2 +-
14 files changed, 154 insertions(+), 9 deletions(-)
create mode 100644 admin/src/components/input/TimePicker.vue
diff --git a/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue b/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue
index 1e395c183..2799b74ad 100644
--- a/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue
+++ b/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue
@@ -4,6 +4,15 @@
+
+
+ {{ $t('moderator.show-submission-form') }}
+
+
+
+
+
+
import { adminCreateContributionMessage } from '@/graphql/adminCreateContributionMessage'
import { adminUpdateContribution } from '@/graphql/adminUpdateContribution'
+import TimePicker from '@/components/input/TimePicker'
export default {
+ components: {
+ TimePicker,
+ },
name: 'ContributionMessagesFormular',
props: {
contributionId: {
@@ -85,6 +98,9 @@ export default {
memo: this.contributionMemo,
},
loading: false,
+ resubmissionDate: null,
+ resubmissionTime: '00:00',
+ showResubmissionDate: false,
chatOrMemo: 0, // 0 = Chat, 1 = Memo
messageType: {
DIALOG: 'DIALOG',
@@ -93,6 +109,22 @@ export default {
}
},
methods: {
+ combineResubmissionDateAndTime() {
+ if (this.resubmissionDate) {
+ const formattedDate = new Date(this.resubmissionDate)
+ console.log('resubmission time: %s', this.resubmissionTime)
+ const [hours, minutes] = this.resubmissionTime.split(':')
+ console.log('hours: %s, minutes: %s', hours, minutes)
+ formattedDate.setHours(parseInt(hours))
+ console.log('set hours: %d', formattedDate.getHours())
+ formattedDate.setMinutes(parseInt(minutes))
+ console.log('set minutes: %d', formattedDate.getMinutes())
+ console.log('IOS String: %s', formattedDate.toISOString())
+ return formattedDate.toString()
+ } else {
+ return null
+ }
+ },
onSubmit(mType) {
this.loading = true
if (this.chatOrMemo === 0) {
@@ -103,6 +135,9 @@ export default {
contributionId: this.contributionId,
message: this.form.text,
messageType: mType,
+ resubmissionAt: this.showResubmissionDate
+ ? this.combineResubmissionDateAndTime()
+ : null,
},
})
.then((result) => {
@@ -141,6 +176,9 @@ export default {
onReset(event) {
this.form.text = ''
this.form.memo = this.contributionMemo
+ this.showResubmissionDate = false
+ this.resubmissionDate = null
+ this.resubmissionTime = '00:00'
},
enableMemo() {
this.chatOrMemo = 1
diff --git a/admin/src/components/input/TimePicker.vue b/admin/src/components/input/TimePicker.vue
new file mode 100644
index 000000000..7a8111f1e
--- /dev/null
+++ b/admin/src/components/input/TimePicker.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
diff --git a/admin/src/graphql/adminCreateContributionMessage.js b/admin/src/graphql/adminCreateContributionMessage.js
index 66750b833..df7ca5458 100644
--- a/admin/src/graphql/adminCreateContributionMessage.js
+++ b/admin/src/graphql/adminCreateContributionMessage.js
@@ -1,11 +1,17 @@
import gql from 'graphql-tag'
export const adminCreateContributionMessage = gql`
- mutation ($contributionId: Int!, $message: String!, $messageType: ContributionMessageType) {
+ mutation (
+ $contributionId: Int!
+ $message: String!
+ $messageType: ContributionMessageType
+ $resubmissionAt: String
+ ) {
adminCreateContributionMessage(
contributionId: $contributionId
message: $message
messageType: $messageType
+ resubmissionAt: $resubmissionAt
) {
id
message
diff --git a/admin/src/graphql/adminListContributions.js b/admin/src/graphql/adminListContributions.js
index e11ebfa05..5daa742b5 100644
--- a/admin/src/graphql/adminListContributions.js
+++ b/admin/src/graphql/adminListContributions.js
@@ -9,6 +9,7 @@ export const adminListContributions = gql`
$userId: Int
$query: String
$noHashtag: Boolean
+ $hideResubmission: Boolean
) {
adminListContributions(
currentPage: $currentPage
@@ -18,6 +19,7 @@ export const adminListContributions = gql`
userId: $userId
query: $query
noHashtag: $noHashtag
+ hideResubmission: $hideResubmission
) {
contributionCount
contributionList {
diff --git a/admin/src/locales/de.json b/admin/src/locales/de.json
index 264029cc6..5b6fefc47 100644
--- a/admin/src/locales/de.json
+++ b/admin/src/locales/de.json
@@ -99,6 +99,8 @@
}
},
"hide_details": "Details verbergen",
+ "hide_resubmission": "Wiedervorlage verbergen",
+ "hide_resubmission_tooltip": "Verbirgt alle Schöpfungen für die ein Moderator ein Erinnerungsdatum festgelegt hat.",
"lastname": "Nachname",
"math": {
"equals": "=",
@@ -111,6 +113,7 @@
"moderator": {
"chat": "Chat",
"history": "Die Daten wurden geändert. Dies sind die alten Daten.",
+ "show-submission-form": "warten auf Erinnerung?",
"moderator": "Moderator",
"notice": "Moderator Notiz",
"memo": "Memo",
diff --git a/admin/src/locales/en.json b/admin/src/locales/en.json
index dbd831bb9..57d375c77 100644
--- a/admin/src/locales/en.json
+++ b/admin/src/locales/en.json
@@ -99,6 +99,8 @@
}
},
"hide_details": "Hide details",
+ "hide_resubmission": "Hide resubmission",
+ "hide_resubmission_tooltip": "Hides all creations for which a moderator has set a reminder date.",
"lastname": "Lastname",
"math": {
"equals": "=",
@@ -111,6 +113,7 @@
"moderator": {
"chat": "Chat",
"history": "The data has been changed. This is the old data.",
+ "show-submission-form": "wait for reminder?",
"moderator": "Moderator",
"notice": "Moderator note",
"memo": "Memo",
diff --git a/admin/src/pages/CreationConfirm.vue b/admin/src/pages/CreationConfirm.vue
index 3ca382c43..1e6ff8231 100644
--- a/admin/src/pages/CreationConfirm.vue
+++ b/admin/src/pages/CreationConfirm.vue
@@ -2,10 +2,16 @@
-
+
+
+
+
+ {{ $t('hide_resubmission') }}
+
+
@@ -125,6 +131,7 @@ export default {
pageSize: 25,
query: '',
noHashtag: null,
+ hideResubmission: true,
}
},
watch: {
@@ -438,6 +445,7 @@ export default {
statusFilter: this.statusFilter,
query: this.query,
noHashtag: this.noHashtag,
+ hideResubmission: this.hideResubmission,
}
},
fetchPolicy: 'no-cache',
diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts
index f96cb4fe4..4dc092c86 100644
--- a/backend/src/config/index.ts
+++ b/backend/src/config/index.ts
@@ -12,7 +12,7 @@ Decimal.set({
})
const constants = {
- DB_VERSION: '0076-add_updated_by_contribution',
+ DB_VERSION: '0077-add_resubmission_date_contribution_message',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info
diff --git a/backend/src/graphql/arg/ContributionMessageArgs.ts b/backend/src/graphql/arg/ContributionMessageArgs.ts
index 847cb5b33..f79a596c8 100644
--- a/backend/src/graphql/arg/ContributionMessageArgs.ts
+++ b/backend/src/graphql/arg/ContributionMessageArgs.ts
@@ -1,3 +1,4 @@
+import { isValidDateString } from '@/graphql/validator/DateString'
import { IsInt, IsString, IsEnum } from 'class-validator'
import { ArgsType, Field, Int, InputType } from 'type-graphql'
@@ -17,4 +18,8 @@ export class ContributionMessageArgs {
@Field(() => ContributionMessageType, { defaultValue: ContributionMessageType.DIALOG })
@IsEnum(ContributionMessageType)
messageType: ContributionMessageType
+
+ @Field(() => String, { nullable: true })
+ @isValidDateString()
+ resubmissionAt?: string | null
}
diff --git a/backend/src/graphql/arg/SearchContributionsFilterArgs.ts b/backend/src/graphql/arg/SearchContributionsFilterArgs.ts
index f1142d758..0d50457ca 100644
--- a/backend/src/graphql/arg/SearchContributionsFilterArgs.ts
+++ b/backend/src/graphql/arg/SearchContributionsFilterArgs.ts
@@ -22,4 +22,8 @@ export class SearchContributionsFilterArgs {
@Field(() => Boolean, { nullable: true })
@IsBoolean()
noHashtag?: boolean | null
+
+ @Field(() => Boolean, { nullable: true })
+ @IsBoolean()
+ hideResubmission?: boolean | null
}
diff --git a/backend/src/graphql/resolver/ContributionMessageResolver.ts b/backend/src/graphql/resolver/ContributionMessageResolver.ts
index 5910befa1..502d2821d 100644
--- a/backend/src/graphql/resolver/ContributionMessageResolver.ts
+++ b/backend/src/graphql/resolver/ContributionMessageResolver.ts
@@ -125,7 +125,7 @@ export class ContributionMessageResolver {
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTION_MESSAGE])
@Mutation(() => ContributionMessage)
async adminCreateContributionMessage(
- @Args() { contributionId, message, messageType }: ContributionMessageArgs,
+ @Args() { contributionId, message, messageType, resubmissionAt }: ContributionMessageArgs,
@Ctx() context: Context,
): Promise {
const moderator = getUser(context)
@@ -156,6 +156,9 @@ export class ContributionMessageResolver {
contributionMessage.userId = moderator.id
contributionMessage.type = messageType
contributionMessage.isModerator = true
+ if (resubmissionAt) {
+ contributionMessage.resubmissionAt = new Date(resubmissionAt)
+ }
await queryRunner.manager.insert(DbContributionMessage, contributionMessage)
if (messageType !== ContributionMessageType.MODERATOR) {
diff --git a/backend/src/graphql/resolver/util/findContributions.ts b/backend/src/graphql/resolver/util/findContributions.ts
index d02043ff6..5566a9853 100644
--- a/backend/src/graphql/resolver/util/findContributions.ts
+++ b/backend/src/graphql/resolver/util/findContributions.ts
@@ -1,6 +1,7 @@
/* eslint-disable security/detect-object-injection */
-import { Brackets, In, Like, Not, SelectQueryBuilder } from '@dbTools/typeorm'
-import { Contribution as DbContribution } from '@entity/Contribution'
+import { Brackets, In, Like, MoreThan, Not, SelectQueryBuilder } from '@dbTools/typeorm'
+import { Contribution, Contribution as DbContribution } from '@entity/Contribution'
+import { ContributionMessage } from '@entity/ContributionMessage'
import { Paginated } from '@arg/Paginated'
import { SearchContributionsFilterArgs } from '@arg/SearchContributionsFilterArgs'
@@ -46,6 +47,30 @@ export const findContributions = async (
...(filter.userId && { userId: filter.userId }),
...(filter.noHashtag && { memo: Not(Like(`%#%`)) }),
})
+ if (relations?.messages && filter.hideResubmission) {
+ queryBuilder.andWhere((qb: SelectQueryBuilder) => {
+ const newestContributionMessageResubmissionDateSubQuery = qb
+ .subQuery()
+ // .select(['contributionMessage.resubmission_at', 'MAX(contributionMessage.created_at)'])
+ .select('contributionMessage.resubmission_at')
+ .from(ContributionMessage, 'contributionMessage')
+ .where('contributionMessage.contribution_id = Contribution.id')
+ .orderBy('contributionMessage.resubmissionAt', 'DESC')
+ .limit(1)
+ // .andWhere({ resubmissionAt: MoreThan(currentDate) })
+ .getQuery()
+ return (
+ 'NOT EXISTS ' +
+ newestContributionMessageResubmissionDateSubQuery +
+ ' OR ' +
+ newestContributionMessageResubmissionDateSubQuery +
+ ' IS NULL ' +
+ ' OR ' +
+ newestContributionMessageResubmissionDateSubQuery +
+ ' <= NOW()'
+ )
+ })
+ }
queryBuilder.printSql()
if (filter.query) {
const queryString = '%' + filter.query + '%'
diff --git a/dht-node/src/config/index.ts b/dht-node/src/config/index.ts
index f3c2e0723..49660e856 100644
--- a/dht-node/src/config/index.ts
+++ b/dht-node/src/config/index.ts
@@ -4,7 +4,7 @@ import dotenv from 'dotenv'
dotenv.config()
const constants = {
- DB_VERSION: '0076-add_updated_by_contribution',
+ DB_VERSION: '0077-add_resubmission_date_contribution_message',
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
diff --git a/federation/src/config/index.ts b/federation/src/config/index.ts
index 9dec6d878..a4acbd1cf 100644
--- a/federation/src/config/index.ts
+++ b/federation/src/config/index.ts
@@ -10,7 +10,7 @@ Decimal.set({
})
const constants = {
- DB_VERSION: '0076-add_updated_by_contribution',
+ DB_VERSION: '0077-add_resubmission_date_contribution_message',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info
From 9dc7d08b3de5a4b0b0fe34733bcdb5bec3c6aa2b Mon Sep 17 00:00:00 2001
From: einhorn_b
Date: Thu, 23 Nov 2023 14:45:46 +0100
Subject: [PATCH 3/9] construct database query
---
.../src/data/ContributionMessage.builder.ts | 4 +-
.../resolver/util/findContributions.ts | 47 ++++++++++---------
.../ContributionMessage.ts | 4 ++
3 files changed, 29 insertions(+), 26 deletions(-)
diff --git a/backend/src/data/ContributionMessage.builder.ts b/backend/src/data/ContributionMessage.builder.ts
index 6d07bb81e..0f5ade7ff 100644
--- a/backend/src/data/ContributionMessage.builder.ts
+++ b/backend/src/data/ContributionMessage.builder.ts
@@ -43,9 +43,7 @@ export class ContributionMessageBuilder {
public setParentContribution(contribution: Contribution): this {
this.contributionMessage.contributionId = contribution.id
- this.contributionMessage.createdAt = contribution.updatedAt
- ? contribution.updatedAt
- : contribution.createdAt
+ this.contributionMessage.contribution = contribution
return this
}
diff --git a/backend/src/graphql/resolver/util/findContributions.ts b/backend/src/graphql/resolver/util/findContributions.ts
index 5566a9853..2853b7b96 100644
--- a/backend/src/graphql/resolver/util/findContributions.ts
+++ b/backend/src/graphql/resolver/util/findContributions.ts
@@ -1,5 +1,5 @@
/* eslint-disable security/detect-object-injection */
-import { Brackets, In, Like, MoreThan, Not, SelectQueryBuilder } from '@dbTools/typeorm'
+import { Brackets, In, IsNull, LessThanOrEqual, Like, MoreThan, Not, SelectQueryBuilder } from '@dbTools/typeorm'
import { Contribution, Contribution as DbContribution } from '@entity/Contribution'
import { ContributionMessage } from '@entity/ContributionMessage'
@@ -47,29 +47,30 @@ export const findContributions = async (
...(filter.userId && { userId: filter.userId }),
...(filter.noHashtag && { memo: Not(Like(`%#%`)) }),
})
- if (relations?.messages && filter.hideResubmission) {
- queryBuilder.andWhere((qb: SelectQueryBuilder) => {
- const newestContributionMessageResubmissionDateSubQuery = qb
- .subQuery()
- // .select(['contributionMessage.resubmission_at', 'MAX(contributionMessage.created_at)'])
- .select('contributionMessage.resubmission_at')
- .from(ContributionMessage, 'contributionMessage')
- .where('contributionMessage.contribution_id = Contribution.id')
- .orderBy('contributionMessage.resubmissionAt', 'DESC')
- .limit(1)
- // .andWhere({ resubmissionAt: MoreThan(currentDate) })
- .getQuery()
- return (
- 'NOT EXISTS ' +
- newestContributionMessageResubmissionDateSubQuery +
- ' OR ' +
- newestContributionMessageResubmissionDateSubQuery +
- ' IS NULL ' +
- ' OR ' +
- newestContributionMessageResubmissionDateSubQuery +
- ' <= NOW()'
+ if (filter.hideResubmission) {
+ queryBuilder
+ .leftJoinAndSelect(
+ (qb: SelectQueryBuilder) => {
+ return qb
+ .select('resubmission_at', 'resubmissionAt')
+ .addSelect('id', 'latestMessageId')
+ .addSelect('contribution_id', 'latestMessageContributionId')
+ .addSelect(
+ 'ROW_NUMBER() OVER (PARTITION BY latestMessageContributionId ORDER BY created_at DESC)',
+ 'rn',
+ )
+ .from(ContributionMessage, 'contributionMessage')
+ },
+ 'latestContributionMessage',
+ 'latestContributionMessage.latestMessageContributionId = Contribution.id AND latestContributionMessage.rn = 1',
+ )
+ .andWhere(
+ new Brackets((qb) => {
+ qb.where('latestContributionMessage.resubmissionAt IS NULL').orWhere(
+ 'latestContributionMessage.resubmissionAt <= NOW()',
+ )
+ }),
)
- })
}
queryBuilder.printSql()
if (filter.query) {
diff --git a/database/entity/0077-add_resubmission_date_contribution_message/ContributionMessage.ts b/database/entity/0077-add_resubmission_date_contribution_message/ContributionMessage.ts
index 2ea886dd5..8dd7af06a 100644
--- a/database/entity/0077-add_resubmission_date_contribution_message/ContributionMessage.ts
+++ b/database/entity/0077-add_resubmission_date_contribution_message/ContributionMessage.ts
@@ -1,12 +1,14 @@
import {
BaseEntity,
Column,
+ CreateDateColumn,
DeleteDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
+ UpdateDateColumn,
} from 'typeorm'
import { Contribution } from '../Contribution'
import { User } from '../User'
@@ -36,9 +38,11 @@ export class ContributionMessage extends BaseEntity {
@Column({ length: 2000, nullable: false, collation: 'utf8mb4_unicode_ci' })
message: string
+ @CreateDateColumn()
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP', name: 'created_at' })
createdAt: Date
+ @UpdateDateColumn()
@Column({ type: 'datetime', default: null, nullable: true, name: 'updated_at' })
updatedAt: Date
From 5d22b5912d5fe86e0d643e1d54f6c6034b67d0d1 Mon Sep 17 00:00:00 2001
From: einhorn_b
Date: Thu, 23 Nov 2023 14:46:15 +0100
Subject: [PATCH 4/9] update admin logic to be still intuitive
---
.../ContributionMessagesFormular.vue | 19 +++++++++----------
.../ContributionMessagesList.vue | 4 ++++
.../components/Tables/OpenCreationsTable.vue | 4 ++++
admin/src/pages/Overview.vue | 1 +
4 files changed, 18 insertions(+), 10 deletions(-)
diff --git a/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue b/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue
index 2799b74ad..e00684a53 100644
--- a/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue
+++ b/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue
@@ -112,15 +112,10 @@ export default {
combineResubmissionDateAndTime() {
if (this.resubmissionDate) {
const formattedDate = new Date(this.resubmissionDate)
- console.log('resubmission time: %s', this.resubmissionTime)
const [hours, minutes] = this.resubmissionTime.split(':')
- console.log('hours: %s, minutes: %s', hours, minutes)
formattedDate.setHours(parseInt(hours))
- console.log('set hours: %d', formattedDate.getHours())
formattedDate.setMinutes(parseInt(minutes))
- console.log('set minutes: %d', formattedDate.getMinutes())
- console.log('IOS String: %s', formattedDate.toISOString())
- return formattedDate.toString()
+ return formattedDate
} else {
return null
}
@@ -136,14 +131,18 @@ export default {
message: this.form.text,
messageType: mType,
resubmissionAt: this.showResubmissionDate
- ? this.combineResubmissionDateAndTime()
+ ? this.combineResubmissionDateAndTime().toString()
: null,
},
})
.then((result) => {
- this.$emit('get-list-contribution-messages', this.contributionId)
- this.$emit('update-status', this.contributionId)
- this.form.text = ''
+ if (this.showResubmissionDate && this.combineResubmissionDateAndTime() > new Date()) {
+ this.$emit('update-contributions')
+ } else {
+ this.$emit('get-list-contribution-messages', this.contributionId)
+ this.$emit('update-status', this.contributionId)
+ }
+ this.onReset()
this.toastSuccess(this.$t('message.request'))
this.loading = false
})
diff --git a/admin/src/components/ContributionMessages/ContributionMessagesList.vue b/admin/src/components/ContributionMessages/ContributionMessagesList.vue
index c6bed086d..2b9c063bf 100644
--- a/admin/src/components/ContributionMessages/ContributionMessagesList.vue
+++ b/admin/src/components/ContributionMessages/ContributionMessagesList.vue
@@ -15,6 +15,7 @@
@get-list-contribution-messages="$apollo.queries.Messages.refetch()"
@update-status="updateStatus"
@reload-contribution="reloadContribution"
+ @update-contributions="updateContributions"
/>
@@ -79,6 +80,9 @@ export default {
reloadContribution(id) {
this.$emit('reload-contribution', id)
},
+ updateContributions() {
+ this.$emit('update-contributions')
+ },
},
}
diff --git a/admin/src/components/Tables/OpenCreationsTable.vue b/admin/src/components/Tables/OpenCreationsTable.vue
index f351fe228..e43fe398a 100644
--- a/admin/src/components/Tables/OpenCreationsTable.vue
+++ b/admin/src/components/Tables/OpenCreationsTable.vue
@@ -114,6 +114,7 @@
:contributionMemo="row.item.memo"
@update-status="updateStatus"
@reload-contribution="reloadContribution"
+ @update-contributions="updateContributions"
/>
@@ -176,6 +177,9 @@ export default {
reloadContribution(id) {
this.$emit('reload-contribution', id)
},
+ updateContributions() {
+ this.$emit('update-contributions')
+ },
},
}
diff --git a/admin/src/pages/Overview.vue b/admin/src/pages/Overview.vue
index 90924c9a9..f921d2bc3 100644
--- a/admin/src/pages/Overview.vue
+++ b/admin/src/pages/Overview.vue
@@ -49,6 +49,7 @@ export default {
// may be at some point we need a pagination here
return {
statusFilter: this.statusFilter,
+ hideResubmission: true,
}
},
update({ adminListContributions }) {
From 5d7a75603c55844173393460de9f1b0038f314da Mon Sep 17 00:00:00 2001
From: einhorn_b
Date: Thu, 23 Nov 2023 14:58:56 +0100
Subject: [PATCH 5/9] pass the status of the hideResubmission checkbox through
to ContributionMessagesFormular
---
.../ContributionMessagesFormular.vue | 10 +++++++++-
.../ContributionMessages/ContributionMessagesList.vue | 5 +++++
admin/src/components/Tables/OpenCreationsTable.vue | 5 +++++
admin/src/pages/CreationConfirm.vue | 1 +
4 files changed, 20 insertions(+), 1 deletion(-)
diff --git a/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue b/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue
index e00684a53..41f88b1af 100644
--- a/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue
+++ b/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue
@@ -90,6 +90,10 @@ export default {
type: String,
required: true,
},
+ hideResubmission: {
+ type: Boolean,
+ required: true,
+ },
},
data() {
return {
@@ -136,7 +140,11 @@ export default {
},
})
.then((result) => {
- if (this.showResubmissionDate && this.combineResubmissionDateAndTime() > new Date()) {
+ if (
+ this.hideResubmission &&
+ this.showResubmissionDate &&
+ this.combineResubmissionDateAndTime() > new Date()
+ ) {
this.$emit('update-contributions')
} else {
this.$emit('get-list-contribution-messages', this.contributionId)
diff --git a/admin/src/components/ContributionMessages/ContributionMessagesList.vue b/admin/src/components/ContributionMessages/ContributionMessagesList.vue
index 2b9c063bf..229fe6e04 100644
--- a/admin/src/components/ContributionMessages/ContributionMessagesList.vue
+++ b/admin/src/components/ContributionMessages/ContributionMessagesList.vue
@@ -12,6 +12,7 @@
Date: Thu, 23 Nov 2023 15:13:02 +0100
Subject: [PATCH 6/9] show hideResubmissionCheckbox only on page with pending
contribution, else default false
---
admin/src/pages/CreationConfirm.vue | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/admin/src/pages/CreationConfirm.vue b/admin/src/pages/CreationConfirm.vue
index 6841579fc..a0cd9a8a3 100644
--- a/admin/src/pages/CreationConfirm.vue
+++ b/admin/src/pages/CreationConfirm.vue
@@ -6,8 +6,8 @@
{{ $t('no_hashtag') }}
-
-
+
+
{{ $t('hide_resubmission') }}
@@ -132,7 +132,7 @@ export default {
pageSize: 25,
query: '',
noHashtag: null,
- hideResubmission: true,
+ hideResubmissionModel: true,
}
},
watch: {
@@ -433,6 +433,12 @@ export default {
return 'info'
}
},
+ showResubmissionCheckbox() {
+ return this.tabIndex === 0
+ },
+ hideResubmission() {
+ return this.showResubmissionCheckbox ? this.hideResubmissionModel : false
+ },
},
apollo: {
ListAllContributions: {
From 224b453af69a48e45d7df4238e525b2640e15642 Mon Sep 17 00:00:00 2001
From: einhorn_b
Date: Thu, 23 Nov 2023 15:42:42 +0100
Subject: [PATCH 7/9] show updated from moderator for moderator history
messages
---
.../ContributionMessages/ContributionMessagesListItem.vue | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/frontend/src/components/ContributionMessages/ContributionMessagesListItem.vue b/frontend/src/components/ContributionMessages/ContributionMessagesListItem.vue
index 01f835197..60f0a0819 100644
--- a/frontend/src/components/ContributionMessages/ContributionMessagesListItem.vue
+++ b/frontend/src/components/ContributionMessages/ContributionMessagesListItem.vue
@@ -4,9 +4,12 @@
{{ $d(new Date(message.createdAt), 'short') }}
-
+
{{ storeName.username }} {{ $t('contribution.isEdited') }}
+
+ {{ $t('community.moderator') }} {{ $t('contribution.isEdited') }}
+
{{ $t('contribution.oldContribution') }}
From 2b77caa51e4ec1d6a0c491faf17ae7394a1b10c2 Mon Sep 17 00:00:00 2001
From: einhorn_b
Date: Thu, 23 Nov 2023 15:59:36 +0100
Subject: [PATCH 8/9] fix backend linting
---
backend/src/graphql/arg/ContributionMessageArgs.ts | 3 ++-
backend/src/graphql/resolver/util/findContributions.ts | 5 ++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/backend/src/graphql/arg/ContributionMessageArgs.ts b/backend/src/graphql/arg/ContributionMessageArgs.ts
index f79a596c8..e20686187 100644
--- a/backend/src/graphql/arg/ContributionMessageArgs.ts
+++ b/backend/src/graphql/arg/ContributionMessageArgs.ts
@@ -1,9 +1,10 @@
-import { isValidDateString } from '@/graphql/validator/DateString'
import { IsInt, IsString, IsEnum } from 'class-validator'
import { ArgsType, Field, Int, InputType } from 'type-graphql'
import { ContributionMessageType } from '@enum/ContributionMessageType'
+import { isValidDateString } from '@/graphql/validator/DateString'
+
@InputType()
@ArgsType()
export class ContributionMessageArgs {
diff --git a/backend/src/graphql/resolver/util/findContributions.ts b/backend/src/graphql/resolver/util/findContributions.ts
index 2853b7b96..9d26d212c 100644
--- a/backend/src/graphql/resolver/util/findContributions.ts
+++ b/backend/src/graphql/resolver/util/findContributions.ts
@@ -1,6 +1,6 @@
/* eslint-disable security/detect-object-injection */
-import { Brackets, In, IsNull, LessThanOrEqual, Like, MoreThan, Not, SelectQueryBuilder } from '@dbTools/typeorm'
-import { Contribution, Contribution as DbContribution } from '@entity/Contribution'
+import { Brackets, In, Like, Not, SelectQueryBuilder } from '@dbTools/typeorm'
+import { Contribution as DbContribution } from '@entity/Contribution'
import { ContributionMessage } from '@entity/ContributionMessage'
import { Paginated } from '@arg/Paginated'
@@ -20,7 +20,6 @@ function joinRelationsRecursive(
currentPath: string,
): void {
for (const key in relations) {
- // console.log('leftJoin: %s, %s', `${currentPath}.${key}`, key)
queryBuilder.leftJoinAndSelect(`${currentPath}.${key}`, key)
if (typeof relations[key] === 'object') {
// If it's a nested relation
From 4665d42405a1eb9a3650ecef1c27466021b7e0f3 Mon Sep 17 00:00:00 2001
From: einhorn_b
Date: Thu, 23 Nov 2023 19:56:38 +0100
Subject: [PATCH 9/9] fix add add tests
---
.../ContributionMessagesFormular.spec.js | 52 ++++++++++++++-
.../ContributionMessagesFormular.vue | 17 +++--
.../ContributionMessagesList.spec.js | 11 ++++
.../Tables/OpenCreationsTable.spec.js | 1 +
admin/src/components/input/TimePicker.spec.js | 63 +++++++++++++++++++
admin/src/components/input/TimePicker.vue | 4 +-
admin/src/pages/CreationConfirm.spec.js | 9 +++
admin/src/pages/Overview.spec.js | 1 +
8 files changed, 145 insertions(+), 13 deletions(-)
create mode 100644 admin/src/components/input/TimePicker.spec.js
diff --git a/admin/src/components/ContributionMessages/ContributionMessagesFormular.spec.js b/admin/src/components/ContributionMessages/ContributionMessagesFormular.spec.js
index f19459ce9..3638f5180 100644
--- a/admin/src/components/ContributionMessages/ContributionMessagesFormular.spec.js
+++ b/admin/src/components/ContributionMessages/ContributionMessagesFormular.spec.js
@@ -14,6 +14,7 @@ describe('ContributionMessagesFormular', () => {
const propsData = {
contributionId: 42,
contributionMemo: 'It is a test memo',
+ hideResubmission: true,
}
const mocks = {
@@ -95,13 +96,14 @@ describe('ContributionMessagesFormular', () => {
await wrapper.find('button[data-test="submit-dialog"]').trigger('click')
})
- it('moderatorMesage has `DIALOG`', () => {
+ it('moderatorMessage has `DIALOG`', () => {
expect(apolloMutateMock).toBeCalledWith({
mutation: adminCreateContributionMessage,
variables: {
contributionId: 42,
message: 'text form message',
messageType: 'DIALOG',
+ resubmissionAt: null,
},
})
})
@@ -128,6 +130,7 @@ describe('ContributionMessagesFormular', () => {
contributionId: 42,
message: 'text form message',
messageType: 'MODERATOR',
+ resubmissionAt: null,
},
})
})
@@ -137,6 +140,53 @@ describe('ContributionMessagesFormular', () => {
})
})
+ describe('send resubmission contribution message with success', () => {
+ const futureDate = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7 days in milliseconds
+
+ beforeEach(async () => {
+ await wrapper.setData({
+ form: {
+ text: 'text form message',
+ },
+ showResubmissionDate: true,
+ resubmissionDate: futureDate,
+ resubmissionTime: '08:46',
+ })
+ await wrapper.find('button[data-test="submit-moderator"]').trigger('click')
+ })
+
+ it('graphql payload contain resubmission date', () => {
+ const futureDateExactTime = futureDate
+ futureDateExactTime.setHours(8)
+ futureDateExactTime.setMinutes(46)
+ expect(apolloMutateMock).toBeCalledWith({
+ mutation: adminCreateContributionMessage,
+ variables: {
+ contributionId: 42,
+ message: 'text form message',
+ messageType: 'MODERATOR',
+ resubmissionAt: futureDateExactTime.toString(),
+ },
+ })
+ })
+
+ it('toasts an success message', () => {
+ expect(toastSuccessSpy).toBeCalledWith('message.request')
+ })
+ })
+
+ describe('set memo', () => {
+ beforeEach(async () => {
+ await wrapper.setData({
+ chatOrMemo: 0,
+ })
+ await wrapper.find('button[data-test="submit-memo"]').trigger('click')
+ })
+ it('check chatOrMemo value is 1', () => {
+ expect(wrapper.vm.chatOrMemo).toBe(1)
+ })
+ })
+
describe('update contribution memo from moderator for user created contributions', () => {
beforeEach(async () => {
await wrapper.setData({
diff --git a/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue b/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue
index 41f88b1af..9b27f34a8 100644
--- a/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue
+++ b/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue
@@ -114,15 +114,11 @@ export default {
},
methods: {
combineResubmissionDateAndTime() {
- if (this.resubmissionDate) {
- const formattedDate = new Date(this.resubmissionDate)
- const [hours, minutes] = this.resubmissionTime.split(':')
- formattedDate.setHours(parseInt(hours))
- formattedDate.setMinutes(parseInt(minutes))
- return formattedDate
- } else {
- return null
- }
+ const formattedDate = new Date(this.resubmissionDate)
+ const [hours, minutes] = this.resubmissionTime.split(':')
+ formattedDate.setHours(parseInt(hours))
+ formattedDate.setMinutes(parseInt(minutes))
+ return formattedDate
},
onSubmit(mType) {
this.loading = true
@@ -196,7 +192,8 @@ export default {
return (
(this.chatOrMemo === 0 && this.form.text === '') ||
this.loading ||
- (this.chatOrMemo === 1 && this.form.memo.length < 5)
+ (this.chatOrMemo === 1 && this.form.memo.length < 5) ||
+ (this.showResubmissionDate && !this.resubmissionDate)
)
},
moderatorDisabled() {
diff --git a/admin/src/components/ContributionMessages/ContributionMessagesList.spec.js b/admin/src/components/ContributionMessages/ContributionMessagesList.spec.js
index b38c4e7d4..fe91abe6c 100644
--- a/admin/src/components/ContributionMessages/ContributionMessagesList.spec.js
+++ b/admin/src/components/ContributionMessages/ContributionMessagesList.spec.js
@@ -89,6 +89,7 @@ describe('ContributionMessagesList', () => {
contributionMemo: 'test memo',
contributionUserId: 108,
contributionStatus: 'PENDING',
+ hideResubmission: true,
}
const mocks = {
@@ -155,5 +156,15 @@ describe('ContributionMessagesList', () => {
expect(wrapper.emitted('reload-contribution')[0]).toEqual([3])
})
})
+
+ describe('test update-contributions', () => {
+ beforeEach(() => {
+ wrapper.vm.updateContributions()
+ })
+
+ it('emits update-contributions', () => {
+ expect(wrapper.emitted('update-contributions')).toBeTruthy()
+ })
+ })
})
})
diff --git a/admin/src/components/Tables/OpenCreationsTable.spec.js b/admin/src/components/Tables/OpenCreationsTable.spec.js
index 054ef9067..6babe9956 100644
--- a/admin/src/components/Tables/OpenCreationsTable.spec.js
+++ b/admin/src/components/Tables/OpenCreationsTable.spec.js
@@ -70,6 +70,7 @@ const propsData = {
{ key: 'confirm', label: 'save' },
],
toggleDetails: false,
+ hideResubmission: true,
}
const mocks = {
diff --git a/admin/src/components/input/TimePicker.spec.js b/admin/src/components/input/TimePicker.spec.js
new file mode 100644
index 000000000..82f51a969
--- /dev/null
+++ b/admin/src/components/input/TimePicker.spec.js
@@ -0,0 +1,63 @@
+import { mount } from '@vue/test-utils'
+import TimePicker from './TimePicker.vue'
+
+describe('TimePicker', () => {
+ it('updates timeValue on input and emits input event', async () => {
+ const wrapper = mount(TimePicker, {
+ propsData: {
+ value: '12:34', // Set an initial value for testing
+ },
+ })
+
+ const input = wrapper.find('input[type="text"]')
+
+ // Simulate user input
+ await input.setValue('23:45')
+
+ // Check if timeValue is updated
+ expect(wrapper.vm.timeValue).toBe('23:45')
+
+ // Check if input event is emitted with updated value
+ expect(wrapper.emitted().input).toBeTruthy()
+ expect(wrapper.emitted().input[0]).toEqual(['23:45'])
+ })
+
+ it('validates and corrects time format on blur', async () => {
+ const wrapper = mount(TimePicker)
+
+ const input = wrapper.find('input[type="text"]')
+
+ // Simulate user input
+ await input.setValue('99:99')
+ expect(wrapper.emitted().input).toBeTruthy()
+ expect(wrapper.emitted().input[0]).toEqual(['99:99'])
+
+ // Trigger blur event
+ await input.trigger('blur')
+
+ // Check if timeValue is corrected to valid format
+ expect(wrapper.vm.timeValue).toBe('23:59') // Maximum allowed value for hours and minutes
+
+ // Check if input event is emitted with corrected value
+ expect(wrapper.emitted().input).toBeTruthy()
+ expect(wrapper.emitted().input[1]).toEqual(['23:59'])
+ })
+
+ it('check handling of empty input', async () => {
+ const wrapper = mount(TimePicker)
+ const input = wrapper.find('input[type="text"]')
+
+ // Simulate user input with non-numeric characters
+ await input.setValue('')
+
+ // Trigger blur event
+ await input.trigger('blur')
+
+ // Check if non-numeric characters are filtered out
+ expect(wrapper.vm.timeValue).toBe('00:00')
+
+ // Check if input event is emitted with filtered value
+ expect(wrapper.emitted().input).toBeTruthy()
+ expect(wrapper.emitted().input[1]).toEqual(['00:00'])
+ })
+})
diff --git a/admin/src/components/input/TimePicker.vue b/admin/src/components/input/TimePicker.vue
index 7a8111f1e..e8fb416ad 100644
--- a/admin/src/components/input/TimePicker.vue
+++ b/admin/src/components/input/TimePicker.vue
@@ -36,8 +36,8 @@ export default {
let [hours, minutes] = this.timeValue.split(':')
// Validate hours and minutes
- hours = Math.min(Math.max(parseInt(hours) || 0, 0), 23)
- minutes = Math.min(Math.max(parseInt(minutes) || 0, 0), 59)
+ hours = Math.min(parseInt(hours) || 0, 23)
+ minutes = Math.min(parseInt(minutes) || 0, 59)
// Update the value with correct format
this.timeValue = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`
diff --git a/admin/src/pages/CreationConfirm.spec.js b/admin/src/pages/CreationConfirm.spec.js
index 4842c8b3b..cba169655 100644
--- a/admin/src/pages/CreationConfirm.spec.js
+++ b/admin/src/pages/CreationConfirm.spec.js
@@ -347,6 +347,7 @@ describe('CreationConfirm', () => {
it('refetches contributions with proper filter', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
+ hideResubmission: false,
noHashtag: null,
order: 'DESC',
pageSize: 25,
@@ -364,6 +365,7 @@ describe('CreationConfirm', () => {
it('refetches contributions with proper filter', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
+ hideResubmission: true,
noHashtag: null,
order: 'DESC',
pageSize: 25,
@@ -382,6 +384,7 @@ describe('CreationConfirm', () => {
it('refetches contributions with proper filter', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
+ hideResubmission: false,
noHashtag: null,
order: 'DESC',
pageSize: 25,
@@ -400,6 +403,7 @@ describe('CreationConfirm', () => {
it('refetches contributions with proper filter', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
+ hideResubmission: false,
noHashtag: null,
order: 'DESC',
pageSize: 25,
@@ -418,6 +422,7 @@ describe('CreationConfirm', () => {
it('refetches contributions with proper filter', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
+ hideResubmission: false,
noHashtag: null,
order: 'DESC',
pageSize: 25,
@@ -440,6 +445,7 @@ describe('CreationConfirm', () => {
it('calls the API again', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 2,
+ hideResubmission: false,
noHashtag: null,
order: 'DESC',
pageSize: 25,
@@ -457,6 +463,7 @@ describe('CreationConfirm', () => {
it('refetches contributions with proper filter and current page = 1', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
+ hideResubmission: true,
noHashtag: null,
order: 'DESC',
pageSize: 25,
@@ -480,6 +487,7 @@ describe('CreationConfirm', () => {
it('calls the API with query', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
+ hideResubmission: true,
noHashtag: null,
order: 'DESC',
pageSize: 25,
@@ -496,6 +504,7 @@ describe('CreationConfirm', () => {
it('calls the API with empty query', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
+ hideResubmission: true,
noHashtag: null,
order: 'DESC',
pageSize: 25,
diff --git a/admin/src/pages/Overview.spec.js b/admin/src/pages/Overview.spec.js
index d5265f0e2..c0e358478 100644
--- a/admin/src/pages/Overview.spec.js
+++ b/admin/src/pages/Overview.spec.js
@@ -116,6 +116,7 @@ describe('Overview', () => {
it('calls the adminListContributions query', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
+ hideResubmission: true,
order: 'DESC',
pageSize: 25,
statusFilter: ['IN_PROGRESS', 'PENDING'],