Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/api/resetFeedbackCategory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { z } from 'zod'
import { ResetMode } from '../constants/resetMode'
import api from './api'
import makeValidatedRequest from './makeValidatedRequest'

export const resetFeedbackCategory = makeValidatedRequest(
(gameId: number, categoryId: number, mode: ResetMode) =>
api.delete(`/games/${gameId}/game-feedback/categories/${categoryId}/feedback?mode=${mode}`),
z.object({ deletedCount: z.number() }),
)
69 changes: 46 additions & 23 deletions src/modals/FeedbackCategoryDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { IconRefresh, IconTrash } from '@tabler/icons-react'
import { MouseEvent, useContext, useState } from 'react'
import { useRecoilValue } from 'recoil'
import { KeyedMutator } from 'swr'
Expand All @@ -20,12 +21,14 @@ type FeedbackCategoryDetailsProps = {
modalState: [boolean, (open: boolean) => void]
mutate: KeyedMutator<{ feedbackCategories: GameFeedbackCategory[] }>
editingCategory: GameFeedbackCategory | null
onResetClick?: () => void
}

export default function FeedbackCategoryDetails({
modalState,
mutate,
editingCategory,
onResetClick,
}: FeedbackCategoryDetailsProps) {
const [, setOpen] = modalState
const [isLoading, setLoading] = useState(false)
Expand Down Expand Up @@ -183,6 +186,41 @@ export default function FeedbackCategoryDetails({
value={anonymised}
/>

{editingCategory &&
canPerformAction(user, PermissionBasedAction.DELETE_FEEDBACK_CATEGORY) && (
<div className='space-y-2 rounded border border-red-400 bg-red-100 p-4'>
<p className='font-semibold'>Danger zone</p>

<div className='space-y-2'>
<p>Once taken, these actions are irreversible.</p>
<div className='flex space-x-2'>
{onResetClick && (
<Button
type='button'
onClick={onResetClick}
variant='red'
className='w-auto!'
icon={<IconRefresh />}
>
<span>Reset</span>
</Button>
)}

<Button
type='button'
isLoading={isDeleting}
onClick={onDeleteClick}
variant='red'
className='w-auto!'
icon={<IconTrash />}
>
<span>Delete</span>
</Button>
</div>
</div>
</div>
)}

{error && <ErrorMessage error={error} />}
</div>

Expand All @@ -199,29 +237,14 @@ export default function FeedbackCategoryDetails({
</div>
)}
{editingCategory && (
<div className='flex space-x-2'>
{canPerformAction(user, PermissionBasedAction.DELETE_FEEDBACK_CATEGORY) && (
<div className='w-full md:w-32'>
<Button
type='button'
isLoading={isDeleting}
onClick={onDeleteClick}
variant='red'
>
Delete
</Button>
</div>
)}

<div className='w-full md:w-32'>
<Button
disabled={!internalName || !displayName || !description || isDeleting}
isLoading={isLoading}
onClick={onUpdateClick}
>
Update
</Button>
</div>
<div className='w-full md:w-32'>
<Button
disabled={!internalName || !displayName || !description || isDeleting}
isLoading={isLoading}
onClick={onUpdateClick}
>
Update
</Button>
</div>
)}
<div className='w-full md:w-32'>
Expand Down
114 changes: 114 additions & 0 deletions src/modals/ResetFeedbackCategory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import clsx from 'clsx'
import { useContext, useState } from 'react'
import { useRecoilValue } from 'recoil'
import { resetFeedbackCategory } from '../api/resetFeedbackCategory'
import Button from '../components/Button'
import ErrorMessage, { TaloError } from '../components/ErrorMessage'
import Modal from '../components/Modal'
import Select from '../components/Select'
import TextInput from '../components/TextInput'
import ToastContext from '../components/toast/ToastContext'
import { ResetMode, resetModeOptions } from '../constants/resetMode'
import { GameFeedbackCategory } from '../entities/gameFeedbackCategory'
import activeGameState, { SelectedActiveGame } from '../state/activeGameState'
import buildError from '../utils/buildError'

type ResetFeedbackCategoryProps = {
modalState: [boolean, (goBack: boolean) => void]
editingCategory: GameFeedbackCategory | null
}

export function ResetFeedbackCategory({ modalState, editingCategory }: ResetFeedbackCategoryProps) {
const [, goBack] = modalState
const [isLoading, setLoading] = useState(false)
const [error, setError] = useState<TaloError | null>(null)

const activeGame = useRecoilValue(activeGameState) as SelectedActiveGame

const [confirmText, setConfirmText] = useState('')
const [resetMode, setResetMode] = useState<ResetMode>('dev')

const [isMenuOpen, setMenuOpen] = useState(false)

const toast = useContext(ToastContext)

const onResetClick = async () => {
setLoading(true)
setError(null)

try {
const res = await resetFeedbackCategory(activeGame.id, editingCategory!.id, resetMode)
toast.trigger(
`${res.deletedCount} ${res.deletedCount === 1 ? 'entry was' : 'entries were'} deleted`,
)
goBack(true)
} catch (err) {
setError(buildError(err))
setLoading(false)
}
}

return (
<Modal
id='reset-feedback-category'
title={`Reset ${editingCategory?.name}`}
modalState={[true, () => goBack(true)]}
className={clsx('flex flex-col', {
'md:h-[55vh]!': isMenuOpen,
})}
>
<form className='flex grow flex-col'>
<div className='space-y-4 p-4'>
<p>
After clicking <b>Reset</b>, all feedback in this category matching the selected reset
mode will be permanently deleted. This action cannot be undone.
</p>

<div className='w-full'>
<label htmlFor='reset-mode' className='mb-1 block font-semibold'>
Reset mode
</label>
<Select
inputId='reset-mode'
onChange={(option) => setResetMode(option!.value)}
defaultValue={resetModeOptions.find((option) => option.value === 'dev')}
options={resetModeOptions}
onMenuOpen={() => setMenuOpen(true)}
onMenuClose={() => setMenuOpen(false)}
/>
</div>

<TextInput
id='confirm'
variant='modal'
label='Type "confirm" below'
placeholder='confirm'
onChange={setConfirmText}
value={confirmText}
/>

{error && <ErrorMessage error={error} />}
</div>

<div className='mt-auto flex flex-col space-y-4 border-t border-gray-200 p-4 md:flex-row-reverse md:justify-between md:space-y-0'>
<div className='w-full md:w-32'>
<Button
type='button'
isLoading={isLoading}
disabled={confirmText !== 'confirm'}
onClick={onResetClick}
variant='red'
>
Reset
</Button>
</div>
<div className='w-full md:w-32'>
<Button type='button' variant='grey' onClick={() => goBack(false)}>
Back
</Button>
</div>
</div>
</form>
</Modal>
)
}
27 changes: 24 additions & 3 deletions src/pages/FeedbackCategories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IconPlus } from '@tabler/icons-react'
import { format } from 'date-fns'
import { useEffect, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useRecoilValue } from 'recoil'
import useFeedbackCategories from '../api/useFeedbackCategories'
import Button from '../components/Button'
Expand All @@ -14,11 +14,13 @@ import TableCell from '../components/tables/TableCell'
import routes from '../constants/routes'
import { GameFeedbackCategory } from '../entities/gameFeedbackCategory'
import FeedbackCategoryDetails from '../modals/FeedbackCategoryDetails'
import { ResetFeedbackCategory } from '../modals/ResetFeedbackCategory'
import activeGameState, { SelectedActiveGame } from '../state/activeGameState'
import useSortedItems from '../utils/useSortedItems'

export default function FeedbackCategories() {
const [showModal, setShowModal] = useState(false)
const [showResetModal, setShowResetModal] = useState(false)
const [editingCategory, setEditingCategory] = useState<GameFeedbackCategory | null>(null)

const activeGame = useRecoilValue(activeGameState) as SelectedActiveGame
Expand All @@ -27,14 +29,26 @@ export default function FeedbackCategories() {
const sortedFeedbackCategories = useSortedItems(feedbackCategories, 'internalName', 'asc')

useEffect(() => {
if (!showModal) setEditingCategory(null)
}, [showModal, setEditingCategory])
if (!showModal && !showResetModal) setEditingCategory(null)
}, [showModal, showResetModal])

const onEditCategoryClick = (category: GameFeedbackCategory) => {
setEditingCategory(category)
setShowModal(true)
}

const onResetCategoryClick = useCallback(() => {
setShowModal(false)
setShowResetModal(true)
}, [])

const onResetCategoryCloseClick = useCallback((close: boolean) => {
setShowResetModal(false)
if (!close) {
setShowModal(true)
}
}, [])

return (
<Page
title='Feedback categories'
Expand Down Expand Up @@ -95,6 +109,13 @@ export default function FeedbackCategories() {
modalState={[showModal, setShowModal]}
mutate={mutate}
editingCategory={editingCategory}
onResetClick={onResetCategoryClick}
/>
)}
{showResetModal && (
<ResetFeedbackCategory
modalState={[showResetModal, onResetCategoryCloseClick]}
editingCategory={editingCategory}
/>
)}
</Page>
Expand Down