feat: ampliar PATCH de tareas para descripción e edición en línea

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
webui
brobert 2 weeks ago
parent 4d0f9a6517
commit e8382cf85c

@ -2,6 +2,7 @@
import Badge from '$lib/ui/atoms/Badge.svelte';
import { dueStatus } from '$lib/utils/date';
import { success, error as toastError } from '$lib/stores/toasts';
import { tick } from 'svelte';
export let id: number;
export let description: string;
@ -18,6 +19,10 @@
let dateValue: string = due_date ?? '';
let busy = false;
// Edición de texto (inline)
let editingText = false;
let descEl: HTMLElement | null = null;
async function doClaim() {
if (busy) return;
busy = true;
@ -82,6 +87,7 @@
function toggleEdit() {
editing = !editing;
if (editing) editingText = false;
dateValue = due_date ?? '';
}
@ -91,12 +97,91 @@
dateValue = '';
saveDate();
}
function toggleEditText() {
editingText = !editingText;
if (editingText) {
editing = false;
// Asegurar que el elemento refleja el texto actual y enfocarlo
if (descEl) {
descEl.textContent = description;
}
tick().then(() => {
if (descEl) {
descEl.focus();
placeCaretAtEnd(descEl);
}
});
}
}
function placeCaretAtEnd(el: HTMLElement) {
const range = document.createRange();
range.selectNodeContents(el);
range.collapse(false);
const sel = window.getSelection();
sel?.removeAllRanges();
sel?.addRange(range);
}
async function saveText() {
if (busy) return;
const raw = (descEl?.textContent || '').replace(/\s+/g, ' ').trim();
if (raw.length < 1 || raw.length > 1000) {
toastError('La descripción debe tener entre 1 y 1000 caracteres.');
return;
}
if (raw === description) {
editingText = false;
return;
}
busy = true;
try {
const res = await fetch(`/api/tasks/${id}`, {
method: 'PATCH',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ description: raw })
});
if (res.ok) {
description = raw;
success('Descripción actualizada');
editingText = false;
} else {
const txt = await res.text();
toastError(txt || 'No se pudo actualizar la descripción');
}
} catch {
toastError('Error de red');
} finally {
busy = false;
}
}
function cancelText() {
if (descEl) {
descEl.textContent = description;
}
editingText = false;
}
</script>
<li class="task">
<div class="left">
<span class="code">#{code}</span>
<span class="desc">{description}</span>
<span
class="desc"
class:editing={editingText}
contenteditable={editingText}
role="textbox"
aria-label="Descripción de la tarea"
spellcheck="true"
bind:this={descEl}
on:keydown={(e) => {
if (e.key === 'Escape') { e.preventDefault(); cancelText(); }
else if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); saveText(); }
else if (e.key === 'Enter') { e.preventDefault(); }
}}
>{description}</span>
{#if due_date}
{#if status === 'overdue'}
<Badge tone="danger">vence: {due_date}</Badge>
@ -121,6 +206,13 @@
<button class="btn" on:click|preventDefault={doUnassign} disabled={busy}>Soltar</button>
{/if}
{#if !editingText}
<button class="btn secondary" on:click|preventDefault={toggleEditText} disabled={busy}>Editar texto</button>
{:else}
<button class="btn primary" on:click|preventDefault={saveText} disabled={busy}>Guardar</button>
<button class="btn ghost" on:click|preventDefault={cancelText} disabled={busy}>Cancelar</button>
{/if}
{#if !editing}
<button class="btn secondary" on:click|preventDefault={toggleEdit} disabled={busy}>Editar fecha</button>
{:else}
@ -154,6 +246,7 @@
color: var(--color-text-muted);
}
.desc { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.desc.editing { outline: 2px solid var(--color-primary); outline-offset: 2px; background: var(--color-surface); padding: 2px 4px; border-radius: 4px; white-space: normal; text-overflow: clip; }
.meta { justify-self: end; }
.muted { color: var(--color-text-muted); }

@ -31,22 +31,54 @@ export const PATCH: RequestHandler = async (event) => {
return new Response('Bad Request', { status: 400 });
}
// Validar que al menos se envíe algún campo editable
const hasDueField = Object.prototype.hasOwnProperty.call(payload ?? {}, 'due_date');
const hasDescField = Object.prototype.hasOwnProperty.call(payload ?? {}, 'description');
if (!hasDueField && !hasDescField) {
return new Response('Bad Request', { status: 400 });
}
// due_date (opcional)
const due_date_raw = payload?.due_date;
if (due_date_raw !== null && due_date_raw !== undefined && typeof due_date_raw !== 'string') {
if (hasDueField && due_date_raw !== null && due_date_raw !== undefined && typeof due_date_raw !== 'string') {
return new Response('Bad Request', { status: 400 });
}
const due_date =
due_date_raw == null || String(due_date_raw).trim() === ''
!hasDueField || due_date_raw == null || String(due_date_raw).trim() === ''
? null
: String(due_date_raw).trim();
if (due_date !== null && !isValidYmd(due_date)) {
if (hasDueField && due_date !== null && !isValidYmd(due_date)) {
return new Response(JSON.stringify({ error: 'invalid_due_date' }), {
status: 400,
headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' }
});
}
// description (opcional)
let description: string | undefined = undefined;
if (hasDescField) {
const descRaw = payload?.description;
if (descRaw !== null && descRaw !== undefined && typeof descRaw !== 'string') {
return new Response('Bad Request', { status: 400 });
}
if (descRaw == null) {
// No permitimos null en description (columna NOT NULL)
return new Response(JSON.stringify({ error: 'invalid_description' }), {
status: 400,
headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' }
});
}
const normalized = String(descRaw).replace(/\s+/g, ' ').trim();
if (normalized.length < 1 || normalized.length > 1000) {
return new Response(JSON.stringify({ error: 'invalid_description' }), {
status: 400,
headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' }
});
}
description = normalized;
}
const db = await getDb();
// Cargar tarea y validar abierta
@ -89,7 +121,13 @@ export const PATCH: RequestHandler = async (event) => {
}
// Aplicar actualización
db.prepare(`UPDATE tasks SET due_date = ? WHERE id = ?`).run(due_date, taskId);
if (hasDescField && hasDueField) {
db.prepare(`UPDATE tasks SET description = ?, due_date = ? WHERE id = ?`).run(description!, due_date, taskId);
} else if (hasDescField) {
db.prepare(`UPDATE tasks SET description = ? WHERE id = ?`).run(description!, taskId);
} else if (hasDueField) {
db.prepare(`UPDATE tasks SET due_date = ? WHERE id = ?`).run(due_date, taskId);
}
const updated = db
.prepare(`SELECT id, description, due_date, display_code FROM tasks WHERE id = ?`)

Loading…
Cancel
Save