add ability to delete submissions
This commit is contained in:
parent
2c9e1d3de0
commit
b5d80cbff5
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
|
34
api/src/resolver/submission/submission.delete.mutation.ts
Normal file
34
api/src/resolver/submission/submission.delete.mutation.ts
Normal 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))
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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,
|
||||
|
25
api/src/service/submission/submission.delete.service.ts
Normal file
25
api/src/service/submission/submission.delete.service.ts
Normal 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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
39
ui/graphql/mutation/submission.delete.mutation.ts
Normal file
39
ui/graphql/mutation/submission.delete.mutation.ts
Normal 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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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 (
|
||||
|
@ -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 }}
|
||||
/>
|
||||
)}
|
||||
|
Loading…
Reference in New Issue
Block a user