Task editing proof of concept.

This method uses url query params to detect when the page should be in
edit mode. There are two issues with the current approach:

1. The url query param is actually a pretty unintuitive pattern. It's
   disorienting to hit the back button and have it take you from edit
   mode to view mode rather than returning to the main task list.
2. The submit button does return the user to view mode, but caching
   results in the need for a refresh to see the updated task record.
   This can be solved.
This commit is contained in:
themodrnhakr 2025-09-29 21:13:26 -05:00
parent bee3fdf2ca
commit b13cee614e
3 changed files with 221 additions and 12 deletions

View File

@ -0,0 +1,134 @@
<script lang="ts">
import { enhance } from "$app/forms";
import type { Task } from "$lib/server/services/tasks";
import type { ActionData } from "../../../routes/tasks/[task_id]/$types";
let { task, form }: {
task: Task;
form?: ActionData;
} = $props();
const statusOptions = ["Open", "Pending", "Closed"];
const priorityOptions = ["High", "Medium", "Low"];
let currentStatus = $state(form?.data?.status ?? task.status);
let isClosed = $derived(currentStatus === "Closed" ? true : false);
$effect(() => {
if (isClosed) {
closeDateLocal = formatForDateTimeLocal(new Date(Date.now()).toISOString());
currentStatus = "Closed";
} else {
closeDateLocal = formatForDateTimeLocal("");
currentStatus = currentStatus === "Closed" ? "Open" : currentStatus;
}
});
const formatForDateTimeLocal = (isoString: string | null | undefined) => {
if (!isoString) return "";
return new Date(isoString).toISOString().slice(0, 16);
};
// Local, user-facing state for the input controls
let openDateLocal = $state(
formatForDateTimeLocal(form?.data?.openDate ?? task.openDate),
);
let closeDateLocal = $state(
formatForDateTimeLocal(form?.data?.closeDate ?? task.closeDate),
);
// Derived state that creates the full ISO string for form submission
let openDateForForm = $derived(
openDateLocal ? new Date(openDateLocal).toISOString() : "",
);
let closeDateForForm = $derived(
closeDateLocal ? new Date(closeDateLocal).toISOString() : "",
);
</script>
<form method="POST" use:enhance>
<input type="hidden" name="id" value={task.id} />
<!-- Hidden inputs hold the true UTC value to be submitted -->
<input type="hidden" name="openDate" value={openDateForForm} />
<input type="hidden" name="closeDate" value={closeDateForForm} />
<!-- General Details -->
<fieldset>
<legend>Details</legend>
<div>
<label for="description">Description</label>
<input
id="description"
name="description"
type="text"
value={form?.data?.description ?? task.description ?? ""}
/>
{#if form?.error && form.field === "description"}
<p>{form.error}</p>
{/if}
</div>
<div>
<label for="subtype">Sub-Type</label>
<input
id="subtype"
name="subtype"
type="text"
value={form?.data?.subtype ?? task.subtype ?? ""}
/>
</div>
<div>
<label for="status">Status</label>
<select
id="status"
name="status"
bind:value={currentStatus}
>
{#each statusOptions as option (option)}
<option value={option}>{option}</option>
{/each}
</select>
</div>
<div>
<label for="priority">Priority</label>
<select
id="priority"
name="priority"
value={form?.data?.priority ?? task.priority}
>
{#each priorityOptions as option (option)}
<option value={option}>{option}</option>
{/each}
</select>
</div>
<div>
<label for="openDate-local">Open Date</label>
<input
id="openDate-local"
type="datetime-local"
bind:value={openDateLocal}
/>
</div>
<div>
<label for="closeDate-local">Close Date</label>
<input
id="closeDate-local"
type="datetime-local"
bind:value={closeDateLocal}
/>
<input type="checkbox" bind:checked={isClosed}>
</div>
</fieldset>
<!-- Body -->
<fieldset>
<legend>Body</legend>
<textarea id="body" name="body" rows="10"
>{form?.data?.body ?? task.body ?? ''}</textarea>
</fieldset>
<button type="submit">Save Task</button>
{#if form?.error && !form.field}
<p>{form.error}</p>
{/if}
</form>

View File

@ -1,10 +1,12 @@
import logger from "$lib/server/logger";
import type { NewTask } from "$lib/server/services/tasks";
import TasksService from "$lib/server/services/tasks";
import { error } from "@sveltejs/kit";
import type { PageServerData } from "./$types";
import { error, fail, redirect } from "@sveltejs/kit";
import type { Actions, PageServerLoad } from "./$types";
export const load: PageServerData = async ({ params }) => {
const tasks = new TasksService("internal");
const tasks = new TasksService("internal");
export const load: PageServerLoad = async ({ params }) => {
const taskResult = await tasks.getByTaskId([params.task_id]);
if (taskResult.status === "failure" || !taskResult.data || taskResult.data.length === 0) {
@ -20,7 +22,7 @@ export const load: PageServerData = async ({ params }) => {
const [parent, children] = await Promise.all([
task.parent
? tasks.getByDbId([task.parent])
: Promise.resolve({ status: "ok" }),
: Promise.resolve({ status: "ok", data: [] }),
tasks.getByParent(task.id),
]);
@ -31,3 +33,45 @@ export const load: PageServerData = async ({ params }) => {
};
};
export const actions: Actions = {
default: async ({ request, url }) => {
const formData = await request.formData();
const data = Object.fromEntries(formData);
if (!data.description || typeof data.description !== "string" || !data.description.trim()) {
return fail(400, { data, error: "Description is required.", field: "description" });
}
const createDateFn = (dateStr: unknown) => {
// The client now sends a full ISO string or an empty string
if (dateStr && typeof dateStr === "string") {
return new Date(dateStr);
}
return null;
};
const taskData: NewTask = {
id: data.id ? Number(data.id) : undefined,
description: typeof data.description === "string" ? data.description : null,
body: typeof data.body === "string" ? data.body : null,
status: typeof data.status === "string" ? data.status : null,
priority: typeof data.priority === "string" ? data.priority : null,
subtype: typeof data.subtype === "string" ? data.subtype : null,
openDate: createDateFn(data.openDate),
closeDate: createDateFn(data.closeDate),
};
const upsertResult = await tasks.upsert(taskData);
if (upsertResult.status === "failure") {
return fail(500, { data, error: upsertResult.error, code: upsertResult.code });
}
if (!upsertResult.data) {
return fail(500, { data, error: "An unexpected error occurred: missing result data." });
}
// On a successful save, redirect back to the main task page
throw redirect(303, url.pathname);
},
};

View File

@ -1,11 +1,16 @@
<script lang="ts">
import { page } from "$app/stores";
import type { Task } from "$lib/server/services/tasks";
import TaskEdit from "$lib/ui/Tasks/TaskEdit.svelte";
import TaskView from "$lib/ui/Tasks/TaskView.svelte";
import type { PageProps } from "./$types";
let { data }: PageProps = $props();
let { data, form }: PageProps = $props();
console.log(data);
// The editing state is now derived directly from the URL query parameter.
let isEditing = $derived($page.url.searchParams.get("edit") === "true");
// --- Data Transformation for TaskView ---
const parentTask: Task | null =
(data.parent?.status === "ok" && data.parent.data)
? data.parent.data[0] ?? null
@ -29,11 +34,37 @@ const children = childrenTask.map(x => ({
}));
</script>
<TaskView {task} {parent} {children} />
<nav class="view-controls">
{#if isEditing}
<a href={$page.url.pathname} class="button">Cancel</a>
{:else}
<a href={`${$page.url.pathname}?edit=true`} class="button">Edit Task</a>
{/if}
</nav>
{#if isEditing}
<TaskEdit {task} {form} />
{:else}
<TaskView {task} {parent} {children} />
{/if}
<style>
.container {
.view-controls {
margin-bottom: 2rem;
display: flex;
gap: 1rem;
}
justify-content: flex-end;
}
.button {
/* Basic button styling for links */
display: inline-block;
padding: 0.5rem 1rem;
border: 1px solid #ccc;
border-radius: 4px;
text-decoration: none;
color: #333;
background-color: #f0f0f0;
}
.button:hover {
background-color: #e0e0e0;
}
</style>