feat(frontend): add task inactivate and delete actions
Extend the local task store with active state, inactivation, and hard delete support. Update the edit task page and tests so inactive tasks are hidden from normal lists and task lifecycle actions are available from the details view.
This commit is contained in:
@@ -1,6 +1,10 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import {
|
import {
|
||||||
|
createTask,
|
||||||
|
deleteTask,
|
||||||
|
getAllTasks,
|
||||||
getTask,
|
getTask,
|
||||||
|
inactivateTask,
|
||||||
getTasksByEntity,
|
getTasksByEntity,
|
||||||
updateTask,
|
updateTask,
|
||||||
type EntityTaskResponse,
|
type EntityTaskResponse,
|
||||||
@@ -13,6 +17,7 @@ const taskOne: EntityTaskResponse = {
|
|||||||
prompt: 'Summarize jokes',
|
prompt: 'Summarize jokes',
|
||||||
scheduleCron: '0 9 * * 1',
|
scheduleCron: '0 9 * * 1',
|
||||||
emailLookback: 'last_week',
|
emailLookback: 'last_week',
|
||||||
|
active: true,
|
||||||
createdAt: '2026-03-26T10:00:00Z',
|
createdAt: '2026-03-26T10:00:00Z',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,6 +28,7 @@ const taskTwo: EntityTaskResponse = {
|
|||||||
prompt: 'Escalate sandwich policy',
|
prompt: 'Escalate sandwich policy',
|
||||||
scheduleCron: '0 11 1 * *',
|
scheduleCron: '0 11 1 * *',
|
||||||
emailLookback: 'last_month',
|
emailLookback: 'last_month',
|
||||||
|
active: false,
|
||||||
createdAt: '2026-03-26T11:00:00Z',
|
createdAt: '2026-03-26T11:00:00Z',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,12 +44,66 @@ describe('tasksApi', () => {
|
|||||||
await expect(getTask('task-1')).resolves.toEqual(taskOne)
|
await expect(getTask('task-1')).resolves.toEqual(taskOne)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should_returnInactiveTask_when_getTaskCalledWithInactiveId', async () => {
|
||||||
|
localStorage.setItem('condado:entity-tasks', JSON.stringify([taskOne, taskTwo]))
|
||||||
|
|
||||||
|
await expect(getTask('task-2')).resolves.toEqual(taskTwo)
|
||||||
|
})
|
||||||
|
|
||||||
it('should_returnTasksForEntity_when_getTasksByEntityCalled', async () => {
|
it('should_returnTasksForEntity_when_getTasksByEntityCalled', async () => {
|
||||||
localStorage.setItem('condado:entity-tasks', JSON.stringify([taskOne, taskTwo]))
|
localStorage.setItem('condado:entity-tasks', JSON.stringify([taskOne, taskTwo]))
|
||||||
|
|
||||||
await expect(getTasksByEntity('entity-1')).resolves.toEqual([taskOne])
|
await expect(getTasksByEntity('entity-1')).resolves.toEqual([taskOne])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should_hideInactiveTasks_when_getAllTasksCalled', async () => {
|
||||||
|
localStorage.setItem('condado:entity-tasks', JSON.stringify([taskOne, taskTwo]))
|
||||||
|
|
||||||
|
await expect(getAllTasks()).resolves.toEqual([taskOne])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should_hideInactiveTasksForEntity_when_getTasksByEntityCalled', async () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'condado:entity-tasks',
|
||||||
|
JSON.stringify([
|
||||||
|
taskOne,
|
||||||
|
taskTwo,
|
||||||
|
{
|
||||||
|
...taskOne,
|
||||||
|
id: 'task-3',
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
await expect(getTasksByEntity('entity-1')).resolves.toEqual([taskOne])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should_storeActiveTaskByDefault_when_createTaskCalled', async () => {
|
||||||
|
vi.spyOn(crypto, 'randomUUID').mockReturnValue('00000000-0000-0000-0000-000000000003')
|
||||||
|
|
||||||
|
const createdTask = await createTask({
|
||||||
|
entityId: 'entity-1',
|
||||||
|
name: 'Daily Check-in',
|
||||||
|
prompt: 'Ask about ceremonial coffee',
|
||||||
|
scheduleCron: '0 8 * * 1-5',
|
||||||
|
emailLookback: 'last_day',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(createdTask).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: '00000000-0000-0000-0000-000000000003',
|
||||||
|
active: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
expect(JSON.parse(localStorage.getItem('condado:entity-tasks') ?? '[]')).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: '00000000-0000-0000-0000-000000000003',
|
||||||
|
active: true,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
it('should_updateStoredTask_when_updateTaskCalled', async () => {
|
it('should_updateStoredTask_when_updateTaskCalled', async () => {
|
||||||
localStorage.setItem('condado:entity-tasks', JSON.stringify([taskOne, taskTwo]))
|
localStorage.setItem('condado:entity-tasks', JSON.stringify([taskOne, taskTwo]))
|
||||||
|
|
||||||
@@ -73,4 +133,30 @@ describe('tasksApi', () => {
|
|||||||
taskTwo,
|
taskTwo,
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should_markTaskInactive_when_inactivateTaskCalled', async () => {
|
||||||
|
localStorage.setItem('condado:entity-tasks', JSON.stringify([taskOne, taskTwo]))
|
||||||
|
|
||||||
|
const updatedTask = await inactivateTask('task-1')
|
||||||
|
|
||||||
|
expect(updatedTask).toEqual({
|
||||||
|
...taskOne,
|
||||||
|
active: false,
|
||||||
|
})
|
||||||
|
expect(JSON.parse(localStorage.getItem('condado:entity-tasks') ?? '[]')).toEqual([
|
||||||
|
{
|
||||||
|
...taskOne,
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
taskTwo,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should_removeTask_when_deleteTaskCalled', async () => {
|
||||||
|
localStorage.setItem('condado:entity-tasks', JSON.stringify([taskOne, taskTwo]))
|
||||||
|
|
||||||
|
await deleteTask('task-1')
|
||||||
|
|
||||||
|
expect(JSON.parse(localStorage.getItem('condado:entity-tasks') ?? '[]')).toEqual([taskTwo])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -68,6 +68,7 @@ describe('CreateTaskPage', () => {
|
|||||||
prompt: 'Talk about coffee',
|
prompt: 'Talk about coffee',
|
||||||
scheduleCron: '0 8 * * 1-5',
|
scheduleCron: '0 8 * * 1-5',
|
||||||
emailLookback: 'last_week',
|
emailLookback: 'last_week',
|
||||||
|
active: true,
|
||||||
createdAt: '2026-03-26T10:00:00Z',
|
createdAt: '2026-03-26T10:00:00Z',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -15,15 +15,22 @@ vi.mock('react-router-dom', async () => {
|
|||||||
vi.mock('@/api/entitiesApi')
|
vi.mock('@/api/entitiesApi')
|
||||||
vi.mock('@/api/tasksApi')
|
vi.mock('@/api/tasksApi')
|
||||||
|
|
||||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
function renderPage() {
|
||||||
<QueryClientProvider client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}>
|
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||||
<MemoryRouter initialEntries={['/entities/entity-1/tasks/task-1']}>
|
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
<Routes>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Route path="/entities/:entityId/tasks/:taskId" element={children} />
|
<MemoryRouter initialEntries={['/entities/entity-1/tasks/task-1']}>
|
||||||
</Routes>
|
<Routes>
|
||||||
</MemoryRouter>
|
<Route path="/entities/:entityId/tasks/:taskId" element={children} />
|
||||||
</QueryClientProvider>
|
</Routes>
|
||||||
)
|
</MemoryRouter>
|
||||||
|
</QueryClientProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
render(<EditTaskPage />, { wrapper })
|
||||||
|
|
||||||
|
return { queryClient }
|
||||||
|
}
|
||||||
|
|
||||||
const mockEntity = {
|
const mockEntity = {
|
||||||
id: 'entity-1',
|
id: 'entity-1',
|
||||||
@@ -44,6 +51,7 @@ const mockTask = {
|
|||||||
prompt: 'Summarize jokes',
|
prompt: 'Summarize jokes',
|
||||||
scheduleCron: '0 9 * * 1',
|
scheduleCron: '0 9 * * 1',
|
||||||
emailLookback: 'last_week' as const,
|
emailLookback: 'last_week' as const,
|
||||||
|
active: true,
|
||||||
createdAt: '2026-03-26T10:00:00Z',
|
createdAt: '2026-03-26T10:00:00Z',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,11 +59,13 @@ describe('EditTaskPage', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.mocked(entitiesApi.getEntity).mockResolvedValue(mockEntity)
|
vi.mocked(entitiesApi.getEntity).mockResolvedValue(mockEntity)
|
||||||
vi.mocked(tasksApi.getTask).mockResolvedValue(mockTask)
|
vi.mocked(tasksApi.getTask).mockResolvedValue(mockTask)
|
||||||
|
vi.mocked(tasksApi.inactivateTask).mockResolvedValue({ ...mockTask, active: false })
|
||||||
|
vi.mocked(tasksApi.deleteTask).mockResolvedValue(undefined)
|
||||||
mockNavigate.mockClear()
|
mockNavigate.mockClear()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should_renderPrefilledTaskForm_when_pageLoads', async () => {
|
it('should_renderPrefilledTaskForm_when_pageLoads', async () => {
|
||||||
render(<EditTaskPage />, { wrapper })
|
renderPage()
|
||||||
|
|
||||||
await screen.findByRole('link', { name: /back to entity a/i })
|
await screen.findByRole('link', { name: /back to entity a/i })
|
||||||
|
|
||||||
@@ -80,7 +90,8 @@ describe('EditTaskPage', () => {
|
|||||||
emailLookback: 'last_day',
|
emailLookback: 'last_day',
|
||||||
})
|
})
|
||||||
|
|
||||||
render(<EditTaskPage />, { wrapper })
|
const { queryClient } = renderPage()
|
||||||
|
const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries')
|
||||||
await screen.findByRole('link', { name: /back to entity a/i })
|
await screen.findByRole('link', { name: /back to entity a/i })
|
||||||
|
|
||||||
fireEvent.change(screen.getByLabelText(/task name/i), { target: { value: 'Daily Check-in' } })
|
fireEvent.change(screen.getByLabelText(/task name/i), { target: { value: 'Daily Check-in' } })
|
||||||
@@ -119,6 +130,41 @@ describe('EditTaskPage', () => {
|
|||||||
scheduleCron: '0 8 * * 1-5',
|
scheduleCron: '0 8 * * 1-5',
|
||||||
emailLookback: 'last_day',
|
emailLookback: 'last_day',
|
||||||
})
|
})
|
||||||
|
expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['entity-tasks', 'entity-1'] })
|
||||||
|
expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['entity-task', 'task-1'] })
|
||||||
|
expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['entity-tasks'] })
|
||||||
|
expect(mockNavigate).toHaveBeenCalledWith('/entities/entity-1')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should_inactivateTaskAndNavigateBack_when_inactivateClicked', async () => {
|
||||||
|
const { queryClient } = renderPage()
|
||||||
|
const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries')
|
||||||
|
|
||||||
|
await screen.findByRole('link', { name: /back to entity a/i })
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /inactivate/i }))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(tasksApi.inactivateTask).toHaveBeenCalledWith('task-1')
|
||||||
|
expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['entity-tasks', 'entity-1'] })
|
||||||
|
expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['entity-task', 'task-1'] })
|
||||||
|
expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['entity-tasks'] })
|
||||||
|
expect(mockNavigate).toHaveBeenCalledWith('/entities/entity-1')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should_deleteTaskAndNavigateBack_when_deleteClicked', async () => {
|
||||||
|
const { queryClient } = renderPage()
|
||||||
|
const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries')
|
||||||
|
|
||||||
|
await screen.findByRole('link', { name: /back to entity a/i })
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: /delete/i }))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(tasksApi.deleteTask).toHaveBeenCalledWith('task-1')
|
||||||
|
expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['entity-tasks', 'entity-1'] })
|
||||||
|
expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['entity-task', 'task-1'] })
|
||||||
|
expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: ['entity-tasks'] })
|
||||||
expect(mockNavigate).toHaveBeenCalledWith('/entities/entity-1')
|
expect(mockNavigate).toHaveBeenCalledWith('/entities/entity-1')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ describe('EntityDetailPage', () => {
|
|||||||
prompt: 'Summarize jokes',
|
prompt: 'Summarize jokes',
|
||||||
scheduleCron: '0 9 * * 1',
|
scheduleCron: '0 9 * * 1',
|
||||||
emailLookback: 'last_week',
|
emailLookback: 'last_week',
|
||||||
|
active: true,
|
||||||
createdAt: '2026-03-26T10:00:00Z',
|
createdAt: '2026-03-26T10:00:00Z',
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface EntityTaskResponse {
|
|||||||
prompt: string
|
prompt: string
|
||||||
scheduleCron: string
|
scheduleCron: string
|
||||||
emailLookback: EmailLookback
|
emailLookback: EmailLookback
|
||||||
|
active: boolean
|
||||||
createdAt: string
|
createdAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +28,12 @@ function readTasks(): EntityTaskResponse[] {
|
|||||||
if (!raw) return []
|
if (!raw) return []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return JSON.parse(raw) as EntityTaskResponse[]
|
return (JSON.parse(raw) as Array<Omit<EntityTaskResponse, 'active'> & { active?: boolean }>).map(
|
||||||
|
(task) => ({
|
||||||
|
...task,
|
||||||
|
active: task.active ?? true,
|
||||||
|
})
|
||||||
|
)
|
||||||
} catch {
|
} catch {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -61,12 +67,12 @@ export async function generateTaskPreview(data: EntityTaskCreateDto): Promise<st
|
|||||||
|
|
||||||
/** Returns all scheduled tasks currently configured in local storage. */
|
/** Returns all scheduled tasks currently configured in local storage. */
|
||||||
export async function getAllTasks(): Promise<EntityTaskResponse[]> {
|
export async function getAllTasks(): Promise<EntityTaskResponse[]> {
|
||||||
return readTasks()
|
return readTasks().filter((task) => task.active)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns scheduled tasks for a specific entity. */
|
/** Returns scheduled tasks for a specific entity. */
|
||||||
export async function getTasksByEntity(entityId: string): Promise<EntityTaskResponse[]> {
|
export async function getTasksByEntity(entityId: string): Promise<EntityTaskResponse[]> {
|
||||||
return readTasks().filter((task) => task.entityId === entityId)
|
return readTasks().filter((task) => task.entityId === entityId && task.active)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns one scheduled task by identifier. */
|
/** Returns one scheduled task by identifier. */
|
||||||
@@ -80,6 +86,7 @@ export async function createTask(data: EntityTaskCreateDto): Promise<EntityTaskR
|
|||||||
const task: EntityTaskResponse = {
|
const task: EntityTaskResponse = {
|
||||||
...data,
|
...data,
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
|
active: true,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,3 +115,26 @@ export async function updateTask(
|
|||||||
writeTasks(current.map((task) => (task.id === taskId ? updatedTask : task)))
|
writeTasks(current.map((task) => (task.id === taskId ? updatedTask : task)))
|
||||||
return updatedTask
|
return updatedTask
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Marks one scheduled task as inactive in local storage. */
|
||||||
|
export async function inactivateTask(taskId: string): Promise<EntityTaskResponse | null> {
|
||||||
|
const current = readTasks()
|
||||||
|
const existingTask = current.find((task) => task.id === taskId)
|
||||||
|
|
||||||
|
if (!existingTask) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedTask: EntityTaskResponse = {
|
||||||
|
...existingTask,
|
||||||
|
active: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
writeTasks(current.map((task) => (task.id === taskId ? updatedTask : task)))
|
||||||
|
return updatedTask
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Deletes one scheduled task from local storage. */
|
||||||
|
export async function deleteTask(taskId: string): Promise<void> {
|
||||||
|
writeTasks(readTasks().filter((task) => task.id !== taskId))
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|||||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||||
import { getEntity } from '../api/entitiesApi'
|
import { getEntity } from '../api/entitiesApi'
|
||||||
import {
|
import {
|
||||||
|
deleteTask,
|
||||||
generateTaskPreview,
|
generateTaskPreview,
|
||||||
getTask,
|
getTask,
|
||||||
|
inactivateTask,
|
||||||
updateTask,
|
updateTask,
|
||||||
type EmailLookback,
|
type EmailLookback,
|
||||||
} from '../api/tasksApi'
|
} from '../api/tasksApi'
|
||||||
@@ -93,6 +95,18 @@ const DEFAULT_TASK_FORM: TaskFormState = {
|
|||||||
emailLookback: 'last_week',
|
emailLookback: 'last_week',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function invalidateTaskQueries(
|
||||||
|
queryClient: ReturnType<typeof useQueryClient>,
|
||||||
|
entityId: string,
|
||||||
|
taskId: string
|
||||||
|
) {
|
||||||
|
await Promise.all([
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['entity-tasks', entityId] }),
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['entity-task', taskId] }),
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['entity-tasks'] }),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
export default function EditTaskPage() {
|
export default function EditTaskPage() {
|
||||||
const { entityId = '', taskId = '' } = useParams()
|
const { entityId = '', taskId = '' } = useParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -138,8 +152,23 @@ export default function EditTaskPage() {
|
|||||||
emailLookback: data.emailLookback,
|
emailLookback: data.emailLookback,
|
||||||
}),
|
}),
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await queryClient.invalidateQueries({ queryKey: ['entity-tasks', entityId] })
|
await invalidateTaskQueries(queryClient, entityId, taskId)
|
||||||
await queryClient.invalidateQueries({ queryKey: ['entity-task', taskId] })
|
navigate(`/entities/${entityId}`)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const inactivateTaskMutation = useMutation({
|
||||||
|
mutationFn: () => inactivateTask(taskId),
|
||||||
|
onSuccess: async () => {
|
||||||
|
await invalidateTaskQueries(queryClient, entityId, taskId)
|
||||||
|
navigate(`/entities/${entityId}`)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteTaskMutation = useMutation({
|
||||||
|
mutationFn: () => deleteTask(taskId),
|
||||||
|
onSuccess: async () => {
|
||||||
|
await invalidateTaskQueries(queryClient, entityId, taskId)
|
||||||
navigate(`/entities/${entityId}`)
|
navigate(`/entities/${entityId}`)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -386,6 +415,22 @@ export default function EditTaskPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-3 pb-8">
|
<div className="flex justify-end gap-3 pb-8">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => inactivateTaskMutation.mutate()}
|
||||||
|
disabled={!task.active || inactivateTaskMutation.isPending || deleteTaskMutation.isPending}
|
||||||
|
className="mr-auto rounded-md border border-amber-700 px-4 py-2 text-sm text-amber-200 hover:border-amber-500 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{inactivateTaskMutation.isPending ? 'Inactivating…' : 'Inactivate'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => deleteTaskMutation.mutate()}
|
||||||
|
disabled={deleteTaskMutation.isPending || inactivateTaskMutation.isPending}
|
||||||
|
className="rounded-md border border-red-800 px-4 py-2 text-sm text-red-200 hover:border-red-600 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{deleteTaskMutation.isPending ? 'Deleting…' : 'Delete'}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => navigate(`/entities/${entityId}`)}
|
onClick={() => navigate(`/entities/${entityId}`)}
|
||||||
|
|||||||
Reference in New Issue
Block a user