1
0

add ability to delete submissions

This commit is contained in:
Michael Schramm 2023-12-11 17:52:18 +01:00
parent 2c9e1d3de0
commit b5d80cbff5
13 changed files with 209 additions and 12 deletions

View File

@ -24,6 +24,8 @@ Template for next version
- [API] fix change user role (https://github.com/ohmyform/api/pull/49)
- [API] update sqlite to 5.1.6
- [API] delete visitors on form delete (https://github.com/ohmyform/ohmyform/issues/181)
- [API] add mutation to delete submissions as form admin (https://github.com/ohmyform/ohmyform/issues/186)
- [UI] add interface to delete submissions as form admin (https://github.com/ohmyform/ohmyform/issues/186)
## [1.0.3] - 2022-03-27

View File

@ -20,7 +20,7 @@ export class SubmissionEntity {
@PrimaryGeneratedColumn()
public id: number
@OneToMany(() => SubmissionFieldEntity, field => field.submission, { eager: true })
@OneToMany(() => SubmissionFieldEntity, field => field.submission, { eager: true, cascade: true })
public fields: SubmissionFieldEntity[]
@ManyToOne(() => FormEntity, form => form.submissions, { eager: true })
@ -29,7 +29,7 @@ export class SubmissionEntity {
@RelationId('form')
readonly formId: number
@ManyToOne(() => VisitorEntity, visitor => visitor.submissions, { eager: true })
@ManyToOne(() => VisitorEntity, visitor => visitor.submissions, { eager: true, cascade: true })
public visitor: VisitorEntity
@Column()

View File

@ -1,3 +1,4 @@
import { SubmissionDeleteMutation } from './submission.delete.mutation'
import { SubmissionFieldResolver } from './submission.field.resolver'
import { SubmissionListQuery } from './submission.list.query'
import { SubmissionProgressResolver } from './submission.progress.resolver'
@ -10,6 +11,7 @@ import { SubmissionStatisticResolver } from './submission.statistic.resolver'
import { SubmissionFinishMutation } from './submission.finish.mutation'
export const submissionResolvers = [
SubmissionDeleteMutation,
SubmissionFieldResolver,
SubmissionListQuery,
SubmissionProgressResolver,

View File

@ -0,0 +1,34 @@
import { Injectable } from '@nestjs/common'
import { Args, ID, Mutation } from '@nestjs/graphql'
import { User } from '../../decorator/user.decorator'
import { DeletedModel } from '../../dto/deleted.model'
import { SubmissionEntity } from '../../entity/submission.entity'
import { UserEntity } from '../../entity/user.entity'
import { SubmissionByIdPipe } from '../../pipe/submission/submission.by.id.pipe'
import { FormService } from '../../service/form/form.service'
import { IdService } from '../../service/id.service'
import { SubmissionDeleteService } from '../../service/submission/submission.delete.service'
@Injectable()
export class SubmissionDeleteMutation {
constructor(
private readonly formService: FormService,
private readonly deleteService: SubmissionDeleteService,
private readonly idService: IdService,
) {
}
@Mutation(() => DeletedModel)
async submissionDelete(
@User() user: UserEntity,
@Args({ name: 'id', type: () => ID }, SubmissionByIdPipe) submission: SubmissionEntity,
): Promise<DeletedModel> {
if (!this.formService.isAdmin(submission.form, user)) {
throw new Error('invalid form')
}
await this.deleteService.delete(submission.id)
return new DeletedModel(this.idService.encode(submission.id))
}
}

View File

@ -4,6 +4,7 @@ import { Repository } from 'typeorm'
import { FormEntity } from '../../entity/form.entity'
import { SubmissionEntity } from '../../entity/submission.entity'
import { VisitorEntity } from '../../entity/visitor.entity'
import { SubmissionDeleteService } from '../submission/submission.delete.service'
@Injectable()
export class FormDeleteService {
@ -12,17 +13,22 @@ export class FormDeleteService {
private readonly formRepository: Repository<FormEntity>,
@InjectRepository(SubmissionEntity)
private readonly submissionRepository: Repository<SubmissionEntity>,
private readonly submissionDelete: SubmissionDeleteService,
@InjectRepository(VisitorEntity)
private readonly visitorRepository: Repository<VisitorEntity>,
) {
}
async delete(id: number): Promise<void> {
await this.submissionRepository.delete({
const submissions = await this.submissionRepository.find({
form: {
id,
},
})
await Promise.all(
submissions.map(submission => this.submissionDelete.delete(submission.id)),
)
await this.visitorRepository.delete({
form: {
id,

View File

@ -1,3 +1,4 @@
import { SubmissionDeleteService } from './submission.delete.service'
import { SubmissionHookService } from './submission.hook.service'
import { SubmissionNotificationService } from './submission.notification.service'
import { SubmissionService } from './submission.service'
@ -7,6 +8,7 @@ import { SubmissionStatisticService } from './submission.statistic.service'
import { SubmissionTokenService } from './submission.token.service'
export const submissionServices = [
SubmissionDeleteService,
SubmissionHookService,
SubmissionNotificationService,
SubmissionService,

View File

@ -0,0 +1,25 @@
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { SubmissionEntity } from '../../entity/submission.entity'
import { SubmissionFieldEntity } from '../../entity/submission.field.entity'
@Injectable()
export class SubmissionDeleteService {
constructor(
@InjectRepository(SubmissionEntity)
private readonly submissionRepository: Repository<SubmissionEntity>,
@InjectRepository(SubmissionFieldEntity)
private readonly fieldRepository: Repository<SubmissionFieldEntity>,
) {
}
async delete(id: number): Promise<void> {
await this.fieldRepository.delete({
submission: {
id,
},
})
await this.submissionRepository.delete(id)
}
}

View File

@ -20,5 +20,20 @@ const MUTATION = gql`
`
export const useFormDeleteMutation = (
data?: MutationHookOptions<Data, Variables>
): MutationTuple<Data, Variables> => useMutation<Data, Variables>(MUTATION, data)
options: MutationHookOptions<Data, Variables> = {}
): MutationTuple<Data, Variables> => {
const oldUpdate = options.update
options.update = (cache, result, options) => {
cache.evict({
fieldName: 'listForms',
})
cache.gc()
if (oldUpdate) {
oldUpdate(cache, result, options)
}
}
return useMutation<Data, Variables>(MUTATION, options)
}

View File

@ -0,0 +1,39 @@
import { MutationHookOptions, MutationTuple, useMutation } from '@apollo/client'
import { gql } from '@apollo/client/core'
interface Data {
submission: {
id
}
}
interface Variables {
id: string
}
const MUTATION = gql`
mutation submissionDelete($id: ID!) {
submission: submissionDelete(id: $id) {
id
}
}
`
export const useSubmissionDeleteMutation = (
options: MutationHookOptions<Data, Variables> = {}
): MutationTuple<Data, Variables> => {
const oldUpdate = options.update
options.update = (cache, result, options) => {
cache.evict({
fieldName: 'listSubmissions',
})
cache.gc()
if (oldUpdate) {
oldUpdate(cache, result, options)
}
}
return useMutation<Data, Variables>(MUTATION, options)
}

View File

@ -20,5 +20,20 @@ const MUTATION = gql`
`
export const useUserDeleteMutation = (
data?: MutationHookOptions<Data, Variables>
): MutationTuple<Data, Variables> => useMutation<Data, Variables>(MUTATION, data)
options: MutationHookOptions<Data, Variables> = {}
): MutationTuple<Data, Variables> => {
const oldUpdate = options.update
options.update = (cache, result, options) => {
cache.evict({
fieldName: 'listUsers',
})
cache.gc()
if (oldUpdate) {
oldUpdate(cache, result, options)
}
}
return useMutation<Data, Variables>(MUTATION, options)
}

View File

@ -1,8 +1,13 @@
{
"add": "Add Submission",
"city": "City",
"confirmDelete": "Really delete Submission?",
"country": "Country",
"created": "Created",
"delete.button": "Delete",
"deleteError": "Could not delete Submission",
"deleteNow": "Delete",
"deleted": "Submission deleted",
"device": {
"name": "Device Name",
"type": "Device Type"

View File

@ -1,4 +1,4 @@
import { Button, Progress, Table } from 'antd'
import { Button, message, Popconfirm, Progress, Table } from 'antd'
import { PaginationProps } from 'antd/es/pagination'
import { ProgressProps } from 'antd/lib/progress'
import { ColumnsType } from 'antd/lib/table/interface'
@ -10,12 +10,15 @@ import dayjs from 'dayjs'
import { NextPage } from 'next'
import Link from 'next/link'
import { useRouter } from 'next/router'
import React, { useState } from 'react'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ExportSubmissionAction } from '../../../../components/form/admin/export.submission.action'
import { SubmissionValues } from '../../../../components/form/admin/submission.values'
import { FormPagerFragment } from '../../../../graphql/fragment/form.pager.fragment'
import { SubmissionFragment } from '../../../../graphql/fragment/submission.fragment'
import {
useSubmissionDeleteMutation,
} from '../../../../graphql/mutation/submission.delete.mutation'
import { useSubmissionPagerQuery } from '../../../../graphql/query/submission.pager.query'
const Submissions: NextPage = () => {
@ -44,6 +47,21 @@ const Submissions: NextPage = () => {
setEntries(pager.entries)
},
})
const [deleteMutation] = useSubmissionDeleteMutation()
const doDelete = useCallback(async (id) => {
try {
await deleteMutation({
variables: {
id,
},
})
await message.success(t('submission:deleted'))
} catch (e) {
await message.error(t('submission:deleteError'))
}
}, [])
const columns: ColumnsType<SubmissionFragment> = [
{
@ -79,6 +97,22 @@ const Submissions: NextPage = () => {
},
responsive: ['lg'],
},
{
title: ' ',
render(_, submission) {
return (
<Popconfirm
title={t('submission:confirmDelete')}
onConfirm={() => doDelete(submission.id)}
>
<Button danger>
{t('submission:deleteNow')}
</Button>
</Popconfirm>
)
},
width: 100,
},
]
return (

View File

@ -1,4 +1,4 @@
import { Alert, Layout } from 'antd'
import { Alert, Layout, Space } from 'antd'
import { AuthFooter } from 'components/auth/footer'
import { GetStaticProps, NextPage } from 'next'
import getConfig from 'next/config'
@ -78,7 +78,25 @@ const Index: NextPage = () => {
{status.error && (
<Alert
message={`There is an error with your API connection: ${status.error.message}`}
type={'error'}
message={
<Space direction={'vertical'}>
<div>There is an error with your API connection:</div>
<code>{status.error.message}</code>
<div style={{
fontStyle: 'italic',
}}>
We need to be able to access the server graphql endpoint at /graphql,{' '}
if you only stared the{' '}
<a href={'https://hub.docker.com/r/ohmyform/ui'}>ohmyform/ui</a>{' '}
container you are missing the{' '}
<a href={'https://hub.docker.com/r/ohmyform/api'}>ohmyform/api</a>{' '}
container. As an alternative you can also start the{' '}
<a href={'https://hub.docker.com/r/ohmyform/ohmyform'}>ohmyform/ohmyform</a>{' '}
container which includes both the ui and the api.
</div>
</Space>
}
style={{marginBottom: 40, marginLeft: 16, marginRight: 16 }}
/>
)}