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] fix change user role (https://github.com/ohmyform/api/pull/49)
|
||||||
- [API] update sqlite to 5.1.6
|
- [API] update sqlite to 5.1.6
|
||||||
- [API] delete visitors on form delete (https://github.com/ohmyform/ohmyform/issues/181)
|
- [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
|
## [1.0.3] - 2022-03-27
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ export class SubmissionEntity {
|
|||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
public id: number
|
public id: number
|
||||||
|
|
||||||
@OneToMany(() => SubmissionFieldEntity, field => field.submission, { eager: true })
|
@OneToMany(() => SubmissionFieldEntity, field => field.submission, { eager: true, cascade: true })
|
||||||
public fields: SubmissionFieldEntity[]
|
public fields: SubmissionFieldEntity[]
|
||||||
|
|
||||||
@ManyToOne(() => FormEntity, form => form.submissions, { eager: true })
|
@ManyToOne(() => FormEntity, form => form.submissions, { eager: true })
|
||||||
@ -29,7 +29,7 @@ export class SubmissionEntity {
|
|||||||
@RelationId('form')
|
@RelationId('form')
|
||||||
readonly formId: number
|
readonly formId: number
|
||||||
|
|
||||||
@ManyToOne(() => VisitorEntity, visitor => visitor.submissions, { eager: true })
|
@ManyToOne(() => VisitorEntity, visitor => visitor.submissions, { eager: true, cascade: true })
|
||||||
public visitor: VisitorEntity
|
public visitor: VisitorEntity
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { SubmissionDeleteMutation } from './submission.delete.mutation'
|
||||||
import { SubmissionFieldResolver } from './submission.field.resolver'
|
import { SubmissionFieldResolver } from './submission.field.resolver'
|
||||||
import { SubmissionListQuery } from './submission.list.query'
|
import { SubmissionListQuery } from './submission.list.query'
|
||||||
import { SubmissionProgressResolver } from './submission.progress.resolver'
|
import { SubmissionProgressResolver } from './submission.progress.resolver'
|
||||||
@ -10,6 +11,7 @@ import { SubmissionStatisticResolver } from './submission.statistic.resolver'
|
|||||||
import { SubmissionFinishMutation } from './submission.finish.mutation'
|
import { SubmissionFinishMutation } from './submission.finish.mutation'
|
||||||
|
|
||||||
export const submissionResolvers = [
|
export const submissionResolvers = [
|
||||||
|
SubmissionDeleteMutation,
|
||||||
SubmissionFieldResolver,
|
SubmissionFieldResolver,
|
||||||
SubmissionListQuery,
|
SubmissionListQuery,
|
||||||
SubmissionProgressResolver,
|
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 { FormEntity } from '../../entity/form.entity'
|
||||||
import { SubmissionEntity } from '../../entity/submission.entity'
|
import { SubmissionEntity } from '../../entity/submission.entity'
|
||||||
import { VisitorEntity } from '../../entity/visitor.entity'
|
import { VisitorEntity } from '../../entity/visitor.entity'
|
||||||
|
import { SubmissionDeleteService } from '../submission/submission.delete.service'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FormDeleteService {
|
export class FormDeleteService {
|
||||||
@ -12,17 +13,22 @@ export class FormDeleteService {
|
|||||||
private readonly formRepository: Repository<FormEntity>,
|
private readonly formRepository: Repository<FormEntity>,
|
||||||
@InjectRepository(SubmissionEntity)
|
@InjectRepository(SubmissionEntity)
|
||||||
private readonly submissionRepository: Repository<SubmissionEntity>,
|
private readonly submissionRepository: Repository<SubmissionEntity>,
|
||||||
|
private readonly submissionDelete: SubmissionDeleteService,
|
||||||
@InjectRepository(VisitorEntity)
|
@InjectRepository(VisitorEntity)
|
||||||
private readonly visitorRepository: Repository<VisitorEntity>,
|
private readonly visitorRepository: Repository<VisitorEntity>,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: number): Promise<void> {
|
async delete(id: number): Promise<void> {
|
||||||
await this.submissionRepository.delete({
|
const submissions = await this.submissionRepository.find({
|
||||||
form: {
|
form: {
|
||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
submissions.map(submission => this.submissionDelete.delete(submission.id)),
|
||||||
|
)
|
||||||
await this.visitorRepository.delete({
|
await this.visitorRepository.delete({
|
||||||
form: {
|
form: {
|
||||||
id,
|
id,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { SubmissionDeleteService } from './submission.delete.service'
|
||||||
import { SubmissionHookService } from './submission.hook.service'
|
import { SubmissionHookService } from './submission.hook.service'
|
||||||
import { SubmissionNotificationService } from './submission.notification.service'
|
import { SubmissionNotificationService } from './submission.notification.service'
|
||||||
import { SubmissionService } from './submission.service'
|
import { SubmissionService } from './submission.service'
|
||||||
@ -7,6 +8,7 @@ import { SubmissionStatisticService } from './submission.statistic.service'
|
|||||||
import { SubmissionTokenService } from './submission.token.service'
|
import { SubmissionTokenService } from './submission.token.service'
|
||||||
|
|
||||||
export const submissionServices = [
|
export const submissionServices = [
|
||||||
|
SubmissionDeleteService,
|
||||||
SubmissionHookService,
|
SubmissionHookService,
|
||||||
SubmissionNotificationService,
|
SubmissionNotificationService,
|
||||||
SubmissionService,
|
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 = (
|
export const useFormDeleteMutation = (
|
||||||
data?: MutationHookOptions<Data, Variables>
|
options: MutationHookOptions<Data, Variables> = {}
|
||||||
): MutationTuple<Data, Variables> => useMutation<Data, Variables>(MUTATION, data)
|
): 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 = (
|
export const useUserDeleteMutation = (
|
||||||
data?: MutationHookOptions<Data, Variables>
|
options: MutationHookOptions<Data, Variables> = {}
|
||||||
): MutationTuple<Data, Variables> => useMutation<Data, Variables>(MUTATION, data)
|
): 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",
|
"add": "Add Submission",
|
||||||
"city": "City",
|
"city": "City",
|
||||||
|
"confirmDelete": "Really delete Submission?",
|
||||||
"country": "Country",
|
"country": "Country",
|
||||||
"created": "Created",
|
"created": "Created",
|
||||||
|
"delete.button": "Delete",
|
||||||
|
"deleteError": "Could not delete Submission",
|
||||||
|
"deleteNow": "Delete",
|
||||||
|
"deleted": "Submission deleted",
|
||||||
"device": {
|
"device": {
|
||||||
"name": "Device Name",
|
"name": "Device Name",
|
||||||
"type": "Device Type"
|
"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 { PaginationProps } from 'antd/es/pagination'
|
||||||
import { ProgressProps } from 'antd/lib/progress'
|
import { ProgressProps } from 'antd/lib/progress'
|
||||||
import { ColumnsType } from 'antd/lib/table/interface'
|
import { ColumnsType } from 'antd/lib/table/interface'
|
||||||
@ -10,12 +10,15 @@ import dayjs from 'dayjs'
|
|||||||
import { NextPage } from 'next'
|
import { NextPage } from 'next'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import React, { useState } from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { ExportSubmissionAction } from '../../../../components/form/admin/export.submission.action'
|
import { ExportSubmissionAction } from '../../../../components/form/admin/export.submission.action'
|
||||||
import { SubmissionValues } from '../../../../components/form/admin/submission.values'
|
import { SubmissionValues } from '../../../../components/form/admin/submission.values'
|
||||||
import { FormPagerFragment } from '../../../../graphql/fragment/form.pager.fragment'
|
import { FormPagerFragment } from '../../../../graphql/fragment/form.pager.fragment'
|
||||||
import { SubmissionFragment } from '../../../../graphql/fragment/submission.fragment'
|
import { SubmissionFragment } from '../../../../graphql/fragment/submission.fragment'
|
||||||
|
import {
|
||||||
|
useSubmissionDeleteMutation,
|
||||||
|
} from '../../../../graphql/mutation/submission.delete.mutation'
|
||||||
import { useSubmissionPagerQuery } from '../../../../graphql/query/submission.pager.query'
|
import { useSubmissionPagerQuery } from '../../../../graphql/query/submission.pager.query'
|
||||||
|
|
||||||
const Submissions: NextPage = () => {
|
const Submissions: NextPage = () => {
|
||||||
@ -44,6 +47,21 @@ const Submissions: NextPage = () => {
|
|||||||
setEntries(pager.entries)
|
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> = [
|
const columns: ColumnsType<SubmissionFragment> = [
|
||||||
{
|
{
|
||||||
@ -79,6 +97,22 @@ const Submissions: NextPage = () => {
|
|||||||
},
|
},
|
||||||
responsive: ['lg'],
|
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 (
|
return (
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Alert, Layout } from 'antd'
|
import { Alert, Layout, Space } from 'antd'
|
||||||
import { AuthFooter } from 'components/auth/footer'
|
import { AuthFooter } from 'components/auth/footer'
|
||||||
import { GetStaticProps, NextPage } from 'next'
|
import { GetStaticProps, NextPage } from 'next'
|
||||||
import getConfig from 'next/config'
|
import getConfig from 'next/config'
|
||||||
@ -78,8 +78,26 @@ const Index: NextPage = () => {
|
|||||||
|
|
||||||
{status.error && (
|
{status.error && (
|
||||||
<Alert
|
<Alert
|
||||||
message={`There is an error with your API connection: ${status.error.message}`}
|
type={'error'}
|
||||||
style={{ marginBottom: 40, marginLeft: 16, marginRight: 16 }}
|
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 }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<AuthFooter />
|
<AuthFooter />
|
||||||
|
Loading…
Reference in New Issue
Block a user