Add delete action for recurring templates
There was no way to remove a template once created (e.g. test/scratch recurring tasks) - only edit/pause via active. DELETE /api/templates/:id detaches any tasks already spawned from it (template_id cleared) rather than blocking on the FK or cascading, so existing task history survives as ordinary one-off tasks while the recurring schedule itself is removed. Delete button in the edit modal footer, only shown when editing an existing template - not on new/duplicate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
690de73172
commit
763e7be6ca
3 changed files with 36 additions and 2 deletions
|
|
@ -122,6 +122,17 @@ export async function templateRoutes(app) {
|
||||||
return rows[0]
|
return rows[0]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// DELETE /api/templates/:id — stops the recurring schedule; any tasks already
|
||||||
|
// spawned from it are detached (template_id cleared) rather than blocked or
|
||||||
|
// cascaded, so they remain as ordinary task history.
|
||||||
|
app.delete('/api/templates/:id', { preHandler: requireCap('manage_templates') }, async (req, reply) => {
|
||||||
|
const { rows: existing } = await pool.query('SELECT id FROM task_templates WHERE id = $1', [req.params.id])
|
||||||
|
if (!existing.length) return reply.status(404).send({ error: 'Template not found' })
|
||||||
|
await pool.query('UPDATE tasks SET template_id = NULL WHERE template_id = $1', [req.params.id])
|
||||||
|
await pool.query('DELETE FROM task_templates WHERE id = $1', [req.params.id])
|
||||||
|
return { ok: true }
|
||||||
|
})
|
||||||
|
|
||||||
// POST /api/templates/run-due — manually trigger the scheduler sweep
|
// POST /api/templates/run-due — manually trigger the scheduler sweep
|
||||||
app.post('/api/templates/run-due', { preHandler: requireCap('manage_templates') }, async (req) => {
|
app.post('/api/templates/run-due', { preHandler: requireCap('manage_templates') }, async (req) => {
|
||||||
const spawned = await spawnDueTemplates(req.log)
|
const spawned = await spawnDueTemplates(req.log)
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,9 @@ export function createTemplate(body: Record<string, unknown>): Promise<Template>
|
||||||
export function updateTemplate(id: number, body: Record<string, unknown>): Promise<Template> {
|
export function updateTemplate(id: number, body: Record<string, unknown>): Promise<Template> {
|
||||||
return request(`/templates/${id}`, { method: 'PATCH', body: JSON.stringify(body) })
|
return request(`/templates/${id}`, { method: 'PATCH', body: JSON.stringify(body) })
|
||||||
}
|
}
|
||||||
|
export function deleteTemplate(id: number): Promise<{ ok: boolean }> {
|
||||||
|
return request(`/templates/${id}`, { method: 'DELETE' })
|
||||||
|
}
|
||||||
export function runDueTemplates(): Promise<{ ok: boolean; spawned: number }> {
|
export function runDueTemplates(): Promise<{ ok: boolean; spawned: number }> {
|
||||||
return request('/templates/run-due', { method: 'POST', body: JSON.stringify({}) })
|
return request('/templates/run-due', { method: 'POST', body: JSON.stringify({}) })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'
|
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { Plus, X, Play, Copy, ChevronRight } from 'lucide-react'
|
import { Plus, X, Play, Copy, ChevronRight, Trash2 } from 'lucide-react'
|
||||||
import type { Template, TemplateGroup, Category, Location, Asset, Priority, ScheduleMode } from '../types'
|
import type { Template, TemplateGroup, Category, Location, Asset, Priority, ScheduleMode } from '../types'
|
||||||
import { PRIORITIES, PRIORITY_LABELS, can } from '../types'
|
import { PRIORITIES, PRIORITY_LABELS, can } from '../types'
|
||||||
import {
|
import {
|
||||||
fetchTemplates, createTemplate, updateTemplate, runDueTemplates, fetchLocations, fetchAssets,
|
fetchTemplates, createTemplate, updateTemplate, deleteTemplate, runDueTemplates, fetchLocations, fetchAssets,
|
||||||
fetchTemplateGroups, upsertTemplateGroup,
|
fetchTemplateGroups, upsertTemplateGroup,
|
||||||
} from '../api'
|
} from '../api'
|
||||||
import { useAuth } from '../components/AuthGate'
|
import { useAuth } from '../components/AuthGate'
|
||||||
|
|
@ -194,6 +194,18 @@ export default function Recurring() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function remove(id: number) {
|
||||||
|
if (!window.confirm('Delete this recurring template? Any tasks it already spawned stay in your history, but the schedule stops.')) return
|
||||||
|
try {
|
||||||
|
await deleteTemplate(id)
|
||||||
|
setForm(null)
|
||||||
|
setError(null)
|
||||||
|
load()
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Delete failed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderRow(t: Template) {
|
function renderRow(t: Template) {
|
||||||
return (
|
return (
|
||||||
<tr key={t.id} className={canManage ? 'clickable' : ''} onClick={() => openEdit(t)}>
|
<tr key={t.id} className={canManage ? 'clickable' : ''} onClick={() => openEdit(t)}>
|
||||||
|
|
@ -399,6 +411,14 @@ export default function Recurring() {
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{form.id && (
|
||||||
|
<div className="modal-footer-actions">
|
||||||
|
<button className="btn-ghost-sm btn-ghost-danger" onClick={() => remove(form.id!)} title="Delete this recurring template">
|
||||||
|
<Trash2 size={13} strokeWidth={1.75} /> Delete template
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="modal-actions">
|
<div className="modal-actions">
|
||||||
<button className="btn" onClick={() => setForm(null)}>Cancel</button>
|
<button className="btn" onClick={() => setForm(null)}>Cancel</button>
|
||||||
<button className="btn btn-primary" onClick={save}>Save</button>
|
<button className="btn btn-primary" onClick={save}>Save</button>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue