mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge pull request #3252 from gradido/wiedervorlage
feat(admin): resubmission
This commit is contained in:
commit
e1b40166e3
@ -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({
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -70,6 +70,7 @@ const propsData = {
|
||||
{ key: 'confirm', label: 'save' },
|
||||
],
|
||||
toggleDetails: false,
|
||||
hideResubmission: true,
|
||||
}
|
||||
|
||||
const mocks = {
|
||||
|
||||
@ -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>
|
||||
|
||||
63
admin/src/components/input/TimePicker.spec.js
Normal file
63
admin/src/components/input/TimePicker.spec.js
Normal 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'])
|
||||
})
|
||||
})
|
||||
48
admin/src/components/input/TimePicker.vue
Normal file
48
admin/src/components/input/TimePicker.vue
Normal 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>
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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'],
|
||||
|
||||
@ -49,6 +49,7 @@ export default {
|
||||
// may be at some point we need a pagination here
|
||||
return {
|
||||
statusFilter: this.statusFilter,
|
||||
hideResubmission: true,
|
||||
}
|
||||
},
|
||||
update({ adminListContributions }) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -22,4 +22,8 @@ export class SearchContributionsFilterArgs {
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
@IsBoolean()
|
||||
noHashtag?: boolean | null
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
@IsBoolean()
|
||||
hideResubmission?: boolean | null
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 + '%'
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -1 +1 @@
|
||||
export { ContributionMessage } from './0075-contribution_message_add_index/ContributionMessage'
|
||||
export { ContributionMessage } from './0077-add_resubmission_date_contribution_message/ContributionMessage'
|
||||
|
||||
@ -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\`;`)
|
||||
}
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user