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:
parent
bee3fdf2ca
commit
b13cee614e
134
src/lib/ui/Tasks/TaskEdit.svelte
Normal file
134
src/lib/ui/Tasks/TaskEdit.svelte
Normal 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>
|
||||||
@ -1,10 +1,12 @@
|
|||||||
import logger from "$lib/server/logger";
|
import logger from "$lib/server/logger";
|
||||||
|
import type { NewTask } from "$lib/server/services/tasks";
|
||||||
import TasksService from "$lib/server/services/tasks";
|
import TasksService from "$lib/server/services/tasks";
|
||||||
import { error } from "@sveltejs/kit";
|
import { error, fail, redirect } from "@sveltejs/kit";
|
||||||
import type { PageServerData } from "./$types";
|
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]);
|
const taskResult = await tasks.getByTaskId([params.task_id]);
|
||||||
|
|
||||||
if (taskResult.status === "failure" || !taskResult.data || taskResult.data.length === 0) {
|
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([
|
const [parent, children] = await Promise.all([
|
||||||
task.parent
|
task.parent
|
||||||
? tasks.getByDbId([task.parent])
|
? tasks.getByDbId([task.parent])
|
||||||
: Promise.resolve({ status: "ok" }),
|
: Promise.resolve({ status: "ok", data: [] }),
|
||||||
tasks.getByParent(task.id),
|
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);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@ -1,11 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { page } from "$app/stores";
|
||||||
import type { Task } from "$lib/server/services/tasks";
|
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 TaskView from "$lib/ui/Tasks/TaskView.svelte";
|
||||||
import type { PageProps } from "./$types";
|
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 =
|
const parentTask: Task | null =
|
||||||
(data.parent?.status === "ok" && data.parent.data)
|
(data.parent?.status === "ok" && data.parent.data)
|
||||||
? data.parent.data[0] ?? null
|
? data.parent.data[0] ?? null
|
||||||
@ -29,11 +34,37 @@ const children = childrenTask.map(x => ({
|
|||||||
}));
|
}));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<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} />
|
<TaskView {task} {parent} {children} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.container {
|
.view-controls {
|
||||||
|
margin-bottom: 2rem;
|
||||||
display: flex;
|
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>
|
</style>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user