Merge pull request #3252 from gradido/wiedervorlage

feat(admin): resubmission
This commit is contained in:
einhornimmond 2023-11-24 23:26:14 +01:00 committed by GitHub
commit e1b40166e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 405 additions and 18 deletions

View File

@ -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({

View File

@ -4,6 +4,15 @@
<b-form @reset.prevent="onReset" @submit="onSubmit(messageType.DIALOG)">
<b-tabs content-class="mt-3" v-model="chatOrMemo">
<b-tab :title="$t('moderator.chat')" active>
<b-form-group>
<b-form-checkbox v-model="showResubmissionDate">
{{ $t('moderator.show-submission-form') }}
</b-form-checkbox>
</b-form-group>
<b-form-group v-if="showResubmissionDate">
<b-form-datepicker v-model="resubmissionDate"></b-form-datepicker>
<time-picker v-model="resubmissionTime"></time-picker>
</b-form-group>
<b-form-textarea
id="textarea"
v-model="form.text"
@ -65,8 +74,12 @@
<script>
import { adminCreateContributionMessage } from '@/graphql/adminCreateContributionMessage'
import { adminUpdateContribution } from '@/graphql/adminUpdateContribution'
import TimePicker from '@/components/input/TimePicker'
export default {
components: {
TimePicker,
},
name: 'ContributionMessagesFormular',
props: {
contributionId: {
@ -77,6 +90,10 @@ export default {
type: String,
required: true,
},
hideResubmission: {
type: Boolean,
required: true,
},
},
data() {
return {
@ -85,6 +102,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 +113,13 @@ export default {
}
},
methods: {
combineResubmissionDateAndTime() {
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
if (this.chatOrMemo === 0) {
@ -103,12 +130,23 @@ export default {
contributionId: this.contributionId,
message: this.form.text,
messageType: mType,
resubmissionAt: this.showResubmissionDate
? 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.hideResubmission &&
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
})
@ -141,6 +179,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
@ -151,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() {

View File

@ -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()
})
})
})
})

View File

@ -12,9 +12,11 @@
<contribution-messages-formular
:contributionId="contributionId"
:contributionMemo="contributionMemo"
:hideResubmission="hideResubmission"
@get-list-contribution-messages="$apollo.queries.Messages.refetch()"
@update-status="updateStatus"
@reload-contribution="reloadContribution"
@update-contributions="updateContributions"
/>
</div>
</div>
@ -47,6 +49,10 @@ export default {
type: Number,
required: true,
},
hideResubmission: {
type: Boolean,
required: true,
},
},
data() {
return {
@ -79,6 +85,9 @@ export default {
reloadContribution(id) {
this.$emit('reload-contribution', id)
},
updateContributions() {
this.$emit('update-contributions')
},
},
}
</script>

View File

@ -70,6 +70,7 @@ const propsData = {
{ key: 'confirm', label: 'save' },
],
toggleDetails: false,
hideResubmission: true,
}
const mocks = {

View File

@ -112,8 +112,10 @@
:contributionStatus="row.item.status"
:contributionUserId="row.item.userId"
:contributionMemo="row.item.memo"
:hideResubmission="hideResubmission"
@update-status="updateStatus"
@reload-contribution="reloadContribution"
@update-contributions="updateContributions"
/>
</div>
</template>
@ -154,6 +156,10 @@ export default {
type: Array,
required: true,
},
hideResubmission: {
type: Boolean,
required: true,
},
},
methods: {
myself(item) {
@ -176,6 +182,9 @@ export default {
reloadContribution(id) {
this.$emit('reload-contribution', id)
},
updateContributions() {
this.$emit('update-contributions')
},
},
}
</script>

View File

@ -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'])
})
})

View File

@ -0,0 +1,48 @@
<template>
<div>
<input
type="text"
v-model="timeValue"
@input="updateValues"
@blur="validateAndCorrect"
placeholder="hh:mm"
/>
</div>
</template>
<script>
export default {
// Code written from chatGPT 3.5
name: 'TimePicker',
props: {
value: {
type: String,
default: '00:00',
},
},
data() {
return {
timeValue: this.value,
}
},
methods: {
updateValues(event) {
// Allow only numbers and ":"
const inputValue = event.target.value.replace(/[^0-9:]/g, '')
this.timeValue = inputValue
this.$emit('input', inputValue)
},
validateAndCorrect() {
let [hours, minutes] = this.timeValue.split(':')
// Validate hours and minutes
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')}`
this.$emit('input', this.timeValue)
},
},
}
</script>

View File

@ -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

View File

@ -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 {

View File

@ -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",

View File

@ -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",

View File

@ -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,

View File

@ -2,10 +2,16 @@
<template>
<div class="creation-confirm">
<user-query class="mb-2 mt-2" v-model="query" :placeholder="$t('user_memo_search')" />
<label class="mb-4">
<p class="mb-2">
<input type="checkbox" class="noHashtag" v-model="noHashtag" />
<span class="ml-2" v-b-tooltip="$t('no_hashtag_tooltip')">{{ $t('no_hashtag') }}</span>
</label>
</p>
<p class="mb-4" v-if="showResubmissionCheckbox">
<input type="checkbox" class="hideResubmission" v-model="hideResubmissionModel" />
<span class="ml-2" v-b-tooltip="$t('hide_resubmission_tooltip')">
{{ $t('hide_resubmission') }}
</span>
</p>
<div>
<b-tabs v-model="tabIndex" content-class="mt-3" fill>
<b-tab active :title-link-attributes="{ 'data-test': 'open' }">
@ -47,6 +53,7 @@
class="mt-4"
:items="items"
:fields="fields"
:hideResubmission="hideResubmission"
@show-overlay="showOverlay"
@update-status="updateStatus"
@reload-contribution="reloadContribution"
@ -125,6 +132,7 @@ export default {
pageSize: 25,
query: '',
noHashtag: null,
hideResubmissionModel: true,
}
},
watch: {
@ -425,6 +433,12 @@ export default {
return 'info'
}
},
showResubmissionCheckbox() {
return this.tabIndex === 0
},
hideResubmission() {
return this.showResubmissionCheckbox ? this.hideResubmissionModel : false
},
},
apollo: {
ListAllContributions: {
@ -438,6 +452,7 @@ export default {
statusFilter: this.statusFilter,
query: this.query,
noHashtag: this.noHashtag,
hideResubmission: this.hideResubmission,
}
},
fetchPolicy: 'no-cache',

View File

@ -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'],

View File

@ -49,6 +49,7 @@ export default {
// may be at some point we need a pagination here
return {
statusFilter: this.statusFilter,
hideResubmission: true,
}
},
update({ adminListContributions }) {

View File

@ -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

View File

@ -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
}

View File

@ -3,6 +3,8 @@ import { ArgsType, Field, Int, InputType } from 'type-graphql'
import { ContributionMessageType } from '@enum/ContributionMessageType'
import { isValidDateString } from '@/graphql/validator/DateString'
@InputType()
@ArgsType()
export class ContributionMessageArgs {
@ -17,4 +19,8 @@ export class ContributionMessageArgs {
@Field(() => ContributionMessageType, { defaultValue: ContributionMessageType.DIALOG })
@IsEnum(ContributionMessageType)
messageType: ContributionMessageType
@Field(() => String, { nullable: true })
@isValidDateString()
resubmissionAt?: string | null
}

View File

@ -22,4 +22,8 @@ export class SearchContributionsFilterArgs {
@Field(() => Boolean, { nullable: true })
@IsBoolean()
noHashtag?: boolean | null
@Field(() => Boolean, { nullable: true })
@IsBoolean()
hideResubmission?: boolean | null
}

View File

@ -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<ContributionMessage> {
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) {

View File

@ -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 { ContributionMessage } from '@entity/ContributionMessage'
import { Paginated } from '@arg/Paginated'
import { SearchContributionsFilterArgs } from '@arg/SearchContributionsFilterArgs'
@ -19,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
@ -46,6 +46,31 @@ export const findContributions = async (
...(filter.userId && { userId: filter.userId }),
...(filter.noHashtag && { memo: Not(Like(`%#%`)) }),
})
if (filter.hideResubmission) {
queryBuilder
.leftJoinAndSelect(
(qb: SelectQueryBuilder<ContributionMessage>) => {
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) {
const queryString = '%' + filter.query + '%'

View File

@ -0,0 +1,63 @@
import {
BaseEntity,
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} 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
@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
@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
}

View File

@ -1 +1 @@
export { ContributionMessage } from './0075-contribution_message_add_index/ContributionMessage'
export { ContributionMessage } from './0077-add_resubmission_date_contribution_message/ContributionMessage'

View File

@ -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<Array<any>>) {
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<Array<any>>) {
await queryFn(`ALTER TABLE \`contribution_messages\` DROP COLUMN \`resubmission_at\`;`)
}

View File

@ -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',

View File

@ -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

View File

@ -4,9 +4,12 @@
<b-row class="mb-3 border border-197 p-1">
<b-col cols="10">
<small>{{ $d(new Date(message.createdAt), 'short') }}</small>
<div class="font-weight-bold" data-test="username">
<div class="font-weight-bold" data-test="username" v-if="isNotModerator">
{{ storeName.username }} {{ $t('contribution.isEdited') }}
</div>
<div class="font-weight-bold" data-test="moderator-name" v-else>
{{ $t('community.moderator') }} {{ $t('contribution.isEdited') }}
</div>
<div class="small">
{{ $t('contribution.oldContribution') }}
</div>