@topogram/template-todo 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -0
- package/implementation/README.md +9 -0
- package/implementation/backend/reference.js +206 -0
- package/implementation/backend/repository-reference.js +74 -0
- package/implementation/backend/repository-renderers.js +442 -0
- package/implementation/index.js +53 -0
- package/implementation/runtime/check-renderers.js +215 -0
- package/implementation/runtime/checks.js +120 -0
- package/implementation/runtime/reference.js +92 -0
- package/implementation/web/reference.js +51 -0
- package/implementation/web/renderers.js +1223 -0
- package/implementation/web/screens-reference.js +15 -0
- package/package.json +31 -0
- package/topogram/actors/actor-user.tg +6 -0
- package/topogram/capabilities/cap-complete-task.tg +12 -0
- package/topogram/capabilities/cap-create-project.tg +11 -0
- package/topogram/capabilities/cap-create-task.tg +14 -0
- package/topogram/capabilities/cap-create-user.tg +11 -0
- package/topogram/capabilities/cap-delete-task.tg +12 -0
- package/topogram/capabilities/cap-download-task-export.tg +10 -0
- package/topogram/capabilities/cap-export-tasks.tg +11 -0
- package/topogram/capabilities/cap-get-project.tg +11 -0
- package/topogram/capabilities/cap-get-task-export-job.tg +11 -0
- package/topogram/capabilities/cap-get-task.tg +11 -0
- package/topogram/capabilities/cap-get-user.tg +11 -0
- package/topogram/capabilities/cap-list-projects.tg +11 -0
- package/topogram/capabilities/cap-list-tasks.tg +11 -0
- package/topogram/capabilities/cap-list-users.tg +11 -0
- package/topogram/capabilities/cap-update-project.tg +12 -0
- package/topogram/capabilities/cap-update-task.tg +12 -0
- package/topogram/capabilities/cap-update-user.tg +12 -0
- package/topogram/components/component-ui-task-board.tg +33 -0
- package/topogram/components/component-ui-task-calendar.tg +30 -0
- package/topogram/components/component-ui-task-summary.tg +23 -0
- package/topogram/components/component-ui-task-table.tg +34 -0
- package/topogram/decisions/decision-task-ownership.tg +9 -0
- package/topogram/docs/glossary/user.md +22 -0
- package/topogram/docs/journeys/task-creation-and-ownership.md +57 -0
- package/topogram/entities/entity-project.tg +28 -0
- package/topogram/entities/entity-task.tg +38 -0
- package/topogram/entities/entity-user.tg +24 -0
- package/topogram/enums/enum-export-job-status.tg +3 -0
- package/topogram/enums/enum-project-status.tg +3 -0
- package/topogram/enums/enum-task-priority.tg +3 -0
- package/topogram/enums/enum-task-status.tg +3 -0
- package/topogram/operations/operation-task-creation-monitoring.tg +10 -0
- package/topogram/projections/proj-api.tg +177 -0
- package/topogram/projections/proj-db-postgres.tg +55 -0
- package/topogram/projections/proj-db-sqlite.tg +47 -0
- package/topogram/projections/proj-ui-shared.tg +133 -0
- package/topogram/projections/proj-ui-web-react.tg +92 -0
- package/topogram/projections/proj-ui-web.tg +92 -0
- package/topogram/rules/rule-no-task-creation-in-archived-project.tg +10 -0
- package/topogram/rules/rule-only-active-users-may-own-tasks.tg +10 -0
- package/topogram/shapes/shape-input-complete-task.tg +11 -0
- package/topogram/shapes/shape-input-create-project.tg +6 -0
- package/topogram/shapes/shape-input-create-task.tg +6 -0
- package/topogram/shapes/shape-input-create-user.tg +6 -0
- package/topogram/shapes/shape-input-delete-task.tg +10 -0
- package/topogram/shapes/shape-input-export-tasks.tg +13 -0
- package/topogram/shapes/shape-input-get-project.tg +10 -0
- package/topogram/shapes/shape-input-get-task-export-job.tg +10 -0
- package/topogram/shapes/shape-input-get-task.tg +10 -0
- package/topogram/shapes/shape-input-get-user.tg +10 -0
- package/topogram/shapes/shape-input-list-projects.tg +11 -0
- package/topogram/shapes/shape-input-list-tasks.tg +14 -0
- package/topogram/shapes/shape-input-list-users.tg +11 -0
- package/topogram/shapes/shape-input-update-project.tg +14 -0
- package/topogram/shapes/shape-input-update-task.tg +16 -0
- package/topogram/shapes/shape-input-update-user.tg +13 -0
- package/topogram/shapes/shape-output-project-card.tg +6 -0
- package/topogram/shapes/shape-output-project-detail.tg +6 -0
- package/topogram/shapes/shape-output-task-card.tg +19 -0
- package/topogram/shapes/shape-output-task-detail.tg +6 -0
- package/topogram/shapes/shape-output-task-export-callback.tg +14 -0
- package/topogram/shapes/shape-output-task-export-job.tg +13 -0
- package/topogram/shapes/shape-output-task-export-status.tg +17 -0
- package/topogram/shapes/shape-output-user-card.tg +6 -0
- package/topogram/shapes/shape-output-user-detail.tg +6 -0
- package/topogram/terms/term-user.tg +5 -0
- package/topogram/verifications/verification-create-task-policy.tg +15 -0
- package/topogram/verifications/verification-runtime-smoke.tg +16 -0
- package/topogram/verifications/verification-task-runtime-flow.tg +31 -0
- package/topogram-template.json +11 -0
- package/topogram.project.json +53 -0
|
@@ -0,0 +1,1223 @@
|
|
|
1
|
+
import { renderSvelteKitRedirectingAction } from "@topogram/cli/src/generator/surfaces/web/sveltekit-actions.js";
|
|
2
|
+
import {
|
|
3
|
+
renderSvelteKitComponentRegion
|
|
4
|
+
} from "@topogram/cli/template-helpers/sveltekit.js";
|
|
5
|
+
import { TODO_WEB_SCREEN_REFERENCE } from "./screens-reference.js";
|
|
6
|
+
|
|
7
|
+
export function renderTodoHomePage({
|
|
8
|
+
useTypescript,
|
|
9
|
+
demoPrimaryEnvVar,
|
|
10
|
+
screens,
|
|
11
|
+
projectionName,
|
|
12
|
+
homeDescription,
|
|
13
|
+
webReference
|
|
14
|
+
}) {
|
|
15
|
+
return `<script${useTypescript ? ' lang="ts"' : ""}>
|
|
16
|
+
import { ${demoPrimaryEnvVar} as DEMO_TASK_ID } from "$env/static/public";
|
|
17
|
+
|
|
18
|
+
const screens = ${JSON.stringify(screens, null, 2)};
|
|
19
|
+
const demoTaskRoute = DEMO_TASK_ID ? \`/tasks/\${DEMO_TASK_ID}\` : null;
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<main>
|
|
23
|
+
<div class="stack">
|
|
24
|
+
<section class="card hero">
|
|
25
|
+
<div>
|
|
26
|
+
<h1>${projectionName}</h1>
|
|
27
|
+
<p>${homeDescription}</p>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="button-row">
|
|
30
|
+
<a class="button-link" href="${webReference.nav.browseRoute}">${webReference.nav.browseLabel}</a>
|
|
31
|
+
<a class="button-link secondary" href="${webReference.nav.createRoute}">${webReference.nav.createLabel}</a>
|
|
32
|
+
{#if demoTaskRoute}
|
|
33
|
+
<a class="button-link secondary" href={demoTaskRoute}>${webReference.home.demoTaskLabel}</a>
|
|
34
|
+
{/if}
|
|
35
|
+
</div>
|
|
36
|
+
</section>
|
|
37
|
+
|
|
38
|
+
<section class="grid two">
|
|
39
|
+
{#each screens as screen}
|
|
40
|
+
<article class="card">
|
|
41
|
+
<h2>{screen.title}</h2>
|
|
42
|
+
{#if screen.navigable}
|
|
43
|
+
<p><a href={screen.route}>Open screen</a></p>
|
|
44
|
+
{:else if screen.route}
|
|
45
|
+
<p class="muted">${webReference.home.dynamicRouteText}</p>
|
|
46
|
+
<small class="route-hint">{screen.route}</small>
|
|
47
|
+
{:else}
|
|
48
|
+
<p class="muted">${webReference.home.noRouteText}</p>
|
|
49
|
+
{/if}
|
|
50
|
+
</article>
|
|
51
|
+
{/each}
|
|
52
|
+
</section>
|
|
53
|
+
</div>
|
|
54
|
+
</main>
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function renderTodoTaskRoutes({
|
|
59
|
+
useTypescript,
|
|
60
|
+
contract,
|
|
61
|
+
taskList,
|
|
62
|
+
taskDetail,
|
|
63
|
+
taskCreate,
|
|
64
|
+
taskEdit,
|
|
65
|
+
taskExports,
|
|
66
|
+
taskListLookups,
|
|
67
|
+
taskCreateLookups,
|
|
68
|
+
taskEditLookups,
|
|
69
|
+
projectEnvVar,
|
|
70
|
+
ownerEnvVar,
|
|
71
|
+
webReference,
|
|
72
|
+
prettyScreenKind
|
|
73
|
+
}) {
|
|
74
|
+
const files = {};
|
|
75
|
+
const editTaskVisibility = taskDetail.visibility?.find((entry) => entry.capability?.id === "cap_update_task") || null;
|
|
76
|
+
const completeTaskVisibility = taskDetail.visibility?.find((entry) => entry.capability?.id === "cap_complete_task") || null;
|
|
77
|
+
const deleteTaskVisibility = taskDetail.visibility?.find((entry) => entry.capability?.id === "cap_delete_task") || null;
|
|
78
|
+
const taskListHeroComponents = renderSvelteKitComponentRegion(taskList, "hero", {
|
|
79
|
+
componentContracts: contract.components,
|
|
80
|
+
itemsExpression: "data.result.items",
|
|
81
|
+
useTypescript
|
|
82
|
+
});
|
|
83
|
+
const taskListResultsComponents = renderSvelteKitComponentRegion(taskList, "results", {
|
|
84
|
+
componentContracts: contract.components,
|
|
85
|
+
itemsExpression: "data.result.items",
|
|
86
|
+
useTypescript
|
|
87
|
+
});
|
|
88
|
+
const taskListDefaultResults = `<ul class="task-list">
|
|
89
|
+
{#each data.result.items as task}
|
|
90
|
+
<li>
|
|
91
|
+
<div class="task-meta">
|
|
92
|
+
<a href={'/tasks/' + task.id}><strong>{task.title}</strong></a>
|
|
93
|
+
{#if task.description}<span class="muted">{task.description}</span>{/if}
|
|
94
|
+
<span class="muted">Priority: {task.priority ?? "medium"}</span>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="button-row">
|
|
97
|
+
<span class="badge">{task.priority ?? "medium"}</span>
|
|
98
|
+
<span class="badge">{task.status}</span>
|
|
99
|
+
</div>
|
|
100
|
+
</li>
|
|
101
|
+
{/each}
|
|
102
|
+
</ul>`;
|
|
103
|
+
|
|
104
|
+
files["tasks/+page.server.ts"] = `import { redirect, fail } from "@sveltejs/kit";
|
|
105
|
+
import type { Actions } from "./$types";
|
|
106
|
+
import { exportTasks } from "$lib/api/client";
|
|
107
|
+
|
|
108
|
+
export const actions: Actions = {
|
|
109
|
+
${renderSvelteKitRedirectingAction({
|
|
110
|
+
actionName: "export",
|
|
111
|
+
signature: "{ request, fetch }",
|
|
112
|
+
prelude: `const form = await request.formData();
|
|
113
|
+
const payload = {
|
|
114
|
+
project_id: String(form.get("project_id") || "") || undefined,
|
|
115
|
+
owner_id: String(form.get("owner_id") || "") || undefined,
|
|
116
|
+
status: String(form.get("status") || "") || undefined
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
let job;`,
|
|
120
|
+
tryStatement: "job = await exportTasks(fetch, payload);",
|
|
121
|
+
catchReturn:
|
|
122
|
+
'return fail(400, { exportError: error instanceof Error ? error.message : "Unable to start export", exportValues: payload });',
|
|
123
|
+
successStatement: "throw redirect(303, `/task-exports/${job.job_id}`);"
|
|
124
|
+
})}
|
|
125
|
+
};
|
|
126
|
+
`;
|
|
127
|
+
|
|
128
|
+
files["tasks/+page.ts"] = `import type { PageLoad } from "./$types";
|
|
129
|
+
import { listTasks } from "$lib/api/client";
|
|
130
|
+
import { listLookupOptions } from "$lib/api/lookups";
|
|
131
|
+
|
|
132
|
+
export const load: PageLoad = async ({ fetch, url }) => {
|
|
133
|
+
const limit = url.searchParams.get("limit");
|
|
134
|
+
const [result, projectOptions, ownerOptions] = await Promise.all([
|
|
135
|
+
listTasks(fetch, {
|
|
136
|
+
project_id: url.searchParams.get("project_id") ?? undefined,
|
|
137
|
+
owner_id: url.searchParams.get("owner_id") ?? undefined,
|
|
138
|
+
status: url.searchParams.get("status") ?? undefined,
|
|
139
|
+
after: url.searchParams.get("after") ?? undefined,
|
|
140
|
+
limit: limit ? Number(limit) : undefined
|
|
141
|
+
}),
|
|
142
|
+
${taskListLookups.project_id?.route ? `listLookupOptions(fetch, "${taskListLookups.project_id.route}")` : "Promise.resolve([])"},
|
|
143
|
+
${taskListLookups.owner_id?.route ? `listLookupOptions(fetch, "${taskListLookups.owner_id.route}")` : "Promise.resolve([])"}
|
|
144
|
+
]);
|
|
145
|
+
return {
|
|
146
|
+
screen: ${JSON.stringify({ id: taskList.id, title: taskList.title, collection: taskList.collection, web: taskList.web }, null, 2)},
|
|
147
|
+
filters: {
|
|
148
|
+
project_id: url.searchParams.get("project_id") ?? "",
|
|
149
|
+
owner_id: url.searchParams.get("owner_id") ?? "",
|
|
150
|
+
status: url.searchParams.get("status") ?? "",
|
|
151
|
+
limit: limit ?? ""
|
|
152
|
+
},
|
|
153
|
+
lookups: {
|
|
154
|
+
project_id: projectOptions,
|
|
155
|
+
owner_id: ownerOptions
|
|
156
|
+
},
|
|
157
|
+
result
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
`;
|
|
161
|
+
|
|
162
|
+
files["tasks/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
|
|
163
|
+
export let data;
|
|
164
|
+
export let form;
|
|
165
|
+
|
|
166
|
+
const buildNextHref = () => {
|
|
167
|
+
if (!data.result.next_cursor) return null;
|
|
168
|
+
const params = new URLSearchParams();
|
|
169
|
+
if (data.filters.project_id) params.set("project_id", data.filters.project_id);
|
|
170
|
+
if (data.filters.owner_id) params.set("owner_id", data.filters.owner_id);
|
|
171
|
+
if (data.filters.status) params.set("status", data.filters.status);
|
|
172
|
+
if (data.filters.limit) params.set("limit", String(data.filters.limit));
|
|
173
|
+
params.set("after", data.result.next_cursor);
|
|
174
|
+
return \`/tasks?\${params.toString()}\`;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const nextHref = buildNextHref();
|
|
178
|
+
</script>
|
|
179
|
+
|
|
180
|
+
<main>
|
|
181
|
+
<div class="stack">
|
|
182
|
+
<section class="card">
|
|
183
|
+
<div class="button-row" style="justify-content: space-between;">
|
|
184
|
+
<div>
|
|
185
|
+
<h1>${taskList.title || taskList.id}</h1>
|
|
186
|
+
<p>This ${prettyScreenKind(taskList.kind)} screen was generated from \`${taskList.id}\`.</p>
|
|
187
|
+
</div>
|
|
188
|
+
<a class="button-link" href="/tasks/new">Create Task</a>
|
|
189
|
+
</div>
|
|
190
|
+
${taskListHeroComponents ? `\n ${taskListHeroComponents}\n` : ""}
|
|
191
|
+
|
|
192
|
+
<form class="filters" method="GET">
|
|
193
|
+
<label>
|
|
194
|
+
Project
|
|
195
|
+
<select name="project_id">
|
|
196
|
+
<option value="">${taskListLookups.project_id?.emptyLabel || "All projects"}</option>
|
|
197
|
+
{#each data.lookups.project_id as option}
|
|
198
|
+
<option value={option.value} selected={option.value === (data.filters.project_id ?? "")}>{option.label}</option>
|
|
199
|
+
{/each}
|
|
200
|
+
</select>
|
|
201
|
+
</label>
|
|
202
|
+
<label>
|
|
203
|
+
Owner
|
|
204
|
+
<select name="owner_id">
|
|
205
|
+
<option value="">${taskListLookups.owner_id?.emptyLabel || "All owners"}</option>
|
|
206
|
+
{#each data.lookups.owner_id as option}
|
|
207
|
+
<option value={option.value} selected={option.value === (data.filters.owner_id ?? "")}>{option.label}</option>
|
|
208
|
+
{/each}
|
|
209
|
+
</select>
|
|
210
|
+
</label>
|
|
211
|
+
<label>
|
|
212
|
+
Status
|
|
213
|
+
<input name="status" value={data.filters.status ?? ""} />
|
|
214
|
+
</label>
|
|
215
|
+
<label>
|
|
216
|
+
Limit
|
|
217
|
+
<input name="limit" type="number" min="1" value={data.filters.limit ?? ""} />
|
|
218
|
+
</label>
|
|
219
|
+
<div class="button-row">
|
|
220
|
+
<button type="submit">Apply Filters</button>
|
|
221
|
+
<a class="button-link secondary" href="/tasks">Reset</a>
|
|
222
|
+
</div>
|
|
223
|
+
</form>
|
|
224
|
+
|
|
225
|
+
<form method="POST" action="?/export">
|
|
226
|
+
<input type="hidden" name="project_id" value={data.filters.project_id ?? ""} />
|
|
227
|
+
<input type="hidden" name="owner_id" value={data.filters.owner_id ?? ""} />
|
|
228
|
+
<input type="hidden" name="status" value={data.filters.status ?? ""} />
|
|
229
|
+
<div class="button-row">
|
|
230
|
+
<button type="submit">Start Export</button>
|
|
231
|
+
{#if form?.exportError}<span class="muted">{form.exportError}</span>{/if}
|
|
232
|
+
</div>
|
|
233
|
+
</form>
|
|
234
|
+
|
|
235
|
+
{#if data.result.items.length === 0}
|
|
236
|
+
<div class="empty-state">
|
|
237
|
+
<p><strong>${taskList.emptyState?.title || "No items"}</strong></p>
|
|
238
|
+
<p class="muted">${taskList.emptyState?.body || ""}</p>
|
|
239
|
+
</div>
|
|
240
|
+
{:else}
|
|
241
|
+
<p class="muted">Showing {data.result.items.length} task{data.result.items.length === 1 ? "" : "s"}.</p>
|
|
242
|
+
${taskListResultsComponents || taskListDefaultResults}
|
|
243
|
+
{#if nextHref}
|
|
244
|
+
<p><a class="button-link secondary" href={nextHref}>Next Page</a></p>
|
|
245
|
+
{/if}
|
|
246
|
+
{/if}
|
|
247
|
+
</section>
|
|
248
|
+
</div>
|
|
249
|
+
</main>
|
|
250
|
+
`;
|
|
251
|
+
|
|
252
|
+
files["tasks/[id]/+page.server.ts"] = `import { randomUUID } from "node:crypto";
|
|
253
|
+
import { redirect, fail } from "@sveltejs/kit";
|
|
254
|
+
import type { Actions } from "./$types";
|
|
255
|
+
import { completeTask, deleteTask } from "$lib/api/client";
|
|
256
|
+
|
|
257
|
+
export const actions: Actions = {
|
|
258
|
+
${renderSvelteKitRedirectingAction({
|
|
259
|
+
actionName: "complete",
|
|
260
|
+
signature: "{ request, fetch, params }",
|
|
261
|
+
prelude: `const form = await request.formData();
|
|
262
|
+
const updated_at = String(form.get("updated_at") || "");
|
|
263
|
+
const completed_at = String(form.get("completed_at") || "") || new Date().toISOString();
|
|
264
|
+
if (!updated_at) {
|
|
265
|
+
return fail(400, { actionError: "updated_at is required to complete this task." });
|
|
266
|
+
}`,
|
|
267
|
+
tryStatement: `await completeTask(fetch, params.id, { completed_at }, {
|
|
268
|
+
headers: {
|
|
269
|
+
"If-Match": updated_at,
|
|
270
|
+
"Idempotency-Key": randomUUID()
|
|
271
|
+
}
|
|
272
|
+
});`,
|
|
273
|
+
catchReturn: 'return fail(400, { actionError: error instanceof Error ? error.message : "Unable to complete task" });',
|
|
274
|
+
successStatement: "throw redirect(303, `/tasks/${params.id}`);"
|
|
275
|
+
})},
|
|
276
|
+
${renderSvelteKitRedirectingAction({
|
|
277
|
+
actionName: "delete",
|
|
278
|
+
signature: "{ request, fetch, params }",
|
|
279
|
+
prelude: `const form = await request.formData();
|
|
280
|
+
const updated_at = String(form.get("updated_at") || "");
|
|
281
|
+
if (!updated_at) {
|
|
282
|
+
return fail(400, { actionError: "updated_at is required to delete this task." });
|
|
283
|
+
}`,
|
|
284
|
+
tryStatement: `await deleteTask(fetch, params.id, {
|
|
285
|
+
headers: {
|
|
286
|
+
"If-Match": updated_at
|
|
287
|
+
}
|
|
288
|
+
});`,
|
|
289
|
+
catchReturn: 'return fail(400, { actionError: error instanceof Error ? error.message : "Unable to delete task" });',
|
|
290
|
+
successStatement: 'throw redirect(303, "/tasks");'
|
|
291
|
+
})}
|
|
292
|
+
};
|
|
293
|
+
`;
|
|
294
|
+
|
|
295
|
+
files["tasks/[id]/+page.ts"] = `import type { PageLoad } from "./$types";
|
|
296
|
+
import { getTask } from "$lib/api/client";
|
|
297
|
+
|
|
298
|
+
export const load: PageLoad = async ({ fetch, params, url }) => {
|
|
299
|
+
return {
|
|
300
|
+
screen: ${JSON.stringify({ id: taskDetail.id, title: taskDetail.title, web: taskDetail.web }, null, 2)},
|
|
301
|
+
task: await getTask(fetch, params.id),
|
|
302
|
+
visibilityDebug: {
|
|
303
|
+
userId: url.searchParams.get("topogram_auth_user_id") ?? "",
|
|
304
|
+
permissions: url.searchParams.get("topogram_auth_permissions") ?? "",
|
|
305
|
+
isAdmin: url.searchParams.get("topogram_auth_admin") ?? ""
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
`;
|
|
310
|
+
|
|
311
|
+
files["tasks/[id]/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
|
|
312
|
+
import { canShowAction } from "$lib/auth/visibility";
|
|
313
|
+
|
|
314
|
+
export let data;
|
|
315
|
+
export let form;
|
|
316
|
+
|
|
317
|
+
const editTaskVisibility = ${JSON.stringify(editTaskVisibility, null, 2)};
|
|
318
|
+
const completeTaskVisibility = ${JSON.stringify(completeTaskVisibility, null, 2)};
|
|
319
|
+
const deleteTaskVisibility = ${JSON.stringify(deleteTaskVisibility, null, 2)};
|
|
320
|
+
|
|
321
|
+
$: canEditTask = canShowAction(editTaskVisibility, data?.task, data?.visibilityDebug);
|
|
322
|
+
$: canCompleteTask = canShowAction(completeTaskVisibility, data?.task, data?.visibilityDebug);
|
|
323
|
+
$: canDeleteTask = canShowAction(deleteTaskVisibility, data?.task, data?.visibilityDebug);
|
|
324
|
+
</script>
|
|
325
|
+
|
|
326
|
+
<main>
|
|
327
|
+
<div class="stack">
|
|
328
|
+
<section class="card">
|
|
329
|
+
<div class="button-row" style="justify-content: space-between;">
|
|
330
|
+
<div>
|
|
331
|
+
<h1>{data.task.title}</h1>
|
|
332
|
+
<p>This ${prettyScreenKind(taskDetail.kind)} screen was generated from \`${taskDetail.id}\`.</p>
|
|
333
|
+
</div>
|
|
334
|
+
<span class="badge">{data.task.status}</span>
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
{#if data.task.description}
|
|
338
|
+
<p>{data.task.description}</p>
|
|
339
|
+
{:else}
|
|
340
|
+
<p class="muted">No description was provided for this task.</p>
|
|
341
|
+
{/if}
|
|
342
|
+
|
|
343
|
+
{#if form?.actionError}
|
|
344
|
+
<p><strong>{form.actionError}</strong></p>
|
|
345
|
+
{/if}
|
|
346
|
+
|
|
347
|
+
<dl class="definition-list">
|
|
348
|
+
<dt>Task ID</dt><dd>{data.task.id}</dd>
|
|
349
|
+
<dt>Project</dt><dd>{data.task.project_id}</dd>
|
|
350
|
+
<dt>Owner</dt><dd>{data.task.owner_id ?? "Unassigned"}</dd>
|
|
351
|
+
<dt>Priority</dt><dd>{data.task.priority ?? "medium"}</dd>
|
|
352
|
+
<dt>Created</dt><dd>{data.task.created_at}</dd>
|
|
353
|
+
<dt>Updated</dt><dd>{data.task.updated_at}</dd>
|
|
354
|
+
</dl>
|
|
355
|
+
|
|
356
|
+
<div class="button-row">
|
|
357
|
+
<a class="button-link secondary" href="/tasks">Back to Tasks</a>
|
|
358
|
+
{#if canEditTask}
|
|
359
|
+
<a class="button-link" href={"/tasks/" + data.task.id + "/edit"}>Edit Task</a>
|
|
360
|
+
{/if}
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
<div class="button-row">
|
|
364
|
+
{#if canCompleteTask}
|
|
365
|
+
<form method="POST" action="?/complete">
|
|
366
|
+
<input type="hidden" name="updated_at" value={data.task.updated_at} />
|
|
367
|
+
<button type="submit">Complete Task</button>
|
|
368
|
+
</form>
|
|
369
|
+
{/if}
|
|
370
|
+
{#if canDeleteTask}
|
|
371
|
+
<form method="POST" action="?/delete">
|
|
372
|
+
<input type="hidden" name="updated_at" value={data.task.updated_at} />
|
|
373
|
+
<button type="submit">Archive Task</button>
|
|
374
|
+
</form>
|
|
375
|
+
{/if}
|
|
376
|
+
</div>
|
|
377
|
+
</section>
|
|
378
|
+
</div>
|
|
379
|
+
</main>
|
|
380
|
+
`;
|
|
381
|
+
|
|
382
|
+
files["tasks/new/+page.ts"] = `import type { PageLoad } from "./$types";
|
|
383
|
+
import { listLookupOptions } from "$lib/api/lookups";
|
|
384
|
+
|
|
385
|
+
export const load: PageLoad = async ({ fetch }) => {
|
|
386
|
+
const [projectOptions, ownerOptions] = await Promise.all([
|
|
387
|
+
${taskCreateLookups.project_id?.route ? `listLookupOptions(fetch, "${taskCreateLookups.project_id.route}")` : "Promise.resolve([])"},
|
|
388
|
+
${taskCreateLookups.owner_id?.route ? `listLookupOptions(fetch, "${taskCreateLookups.owner_id.route}")` : "Promise.resolve([])"}
|
|
389
|
+
]);
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
lookups: {
|
|
393
|
+
project_id: projectOptions,
|
|
394
|
+
owner_id: ownerOptions
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
};
|
|
398
|
+
`;
|
|
399
|
+
|
|
400
|
+
files["tasks/new/+page.server.ts"] = `import { randomUUID } from "node:crypto";
|
|
401
|
+
import { redirect, fail } from "@sveltejs/kit";
|
|
402
|
+
import type { Actions } from "./$types";
|
|
403
|
+
import { createTask } from "$lib/api/client";
|
|
404
|
+
|
|
405
|
+
export const actions: Actions = {
|
|
406
|
+
${renderSvelteKitRedirectingAction({
|
|
407
|
+
actionName: "default",
|
|
408
|
+
signature: "{ request, fetch }",
|
|
409
|
+
prelude: `const form = await request.formData();
|
|
410
|
+
const payload = {
|
|
411
|
+
title: String(form.get("title") || ""),
|
|
412
|
+
description: String(form.get("description") || "") || undefined,
|
|
413
|
+
priority: String(form.get("priority") || "") || undefined,
|
|
414
|
+
owner_id: String(form.get("owner_id") || "") || undefined,
|
|
415
|
+
project_id: String(form.get("project_id") || ""),
|
|
416
|
+
due_at: String(form.get("due_at") || "") || undefined
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
if (!payload.title || !payload.project_id) {
|
|
420
|
+
return fail(400, { error: "Title and project are required.", values: payload });
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
let created;`,
|
|
424
|
+
tryStatement: `created = await createTask(fetch, payload, {
|
|
425
|
+
headers: {
|
|
426
|
+
"Idempotency-Key": randomUUID()
|
|
427
|
+
}
|
|
428
|
+
});`,
|
|
429
|
+
catchReturn:
|
|
430
|
+
'return fail(400, { error: error instanceof Error ? error.message : "Unable to create task", values: payload });',
|
|
431
|
+
successStatement: "throw redirect(303, `/tasks/${created.id}`);"
|
|
432
|
+
})}
|
|
433
|
+
};
|
|
434
|
+
`;
|
|
435
|
+
|
|
436
|
+
files["tasks/new/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
|
|
437
|
+
import { ${projectEnvVar} as DEMO_PROJECT_ID, ${ownerEnvVar} as DEMO_USER_ID } from "$env/static/public";
|
|
438
|
+
|
|
439
|
+
export let data;
|
|
440
|
+
export let form;
|
|
441
|
+
|
|
442
|
+
const values = {
|
|
443
|
+
title: form?.values?.title ?? "",
|
|
444
|
+
description: form?.values?.description ?? "",
|
|
445
|
+
priority: form?.values?.priority ?? "medium",
|
|
446
|
+
owner_id: form?.values?.owner_id ?? DEMO_USER_ID ?? "",
|
|
447
|
+
project_id: form?.values?.project_id ?? DEMO_PROJECT_ID ?? "",
|
|
448
|
+
due_at: form?.values?.due_at ?? ""
|
|
449
|
+
};
|
|
450
|
+
</script>
|
|
451
|
+
|
|
452
|
+
<main>
|
|
453
|
+
<div class="stack">
|
|
454
|
+
<section class="card">
|
|
455
|
+
<h1>${taskCreate.title || taskCreate.id}</h1>
|
|
456
|
+
<p>This ${prettyScreenKind(taskCreate.kind)} screen was generated from \`${taskCreate.id}\`.</p>
|
|
457
|
+
<p class="muted">${webReference.createPrimary.helperText}</p>
|
|
458
|
+
{#if form?.error}<p><strong>{form.error}</strong></p>{/if}
|
|
459
|
+
<form class="stack" method="POST">
|
|
460
|
+
<label>Title <input name="title" required value={values.title} /></label>
|
|
461
|
+
<label>Description <textarea name="description">{values.description}</textarea></label>
|
|
462
|
+
<label>
|
|
463
|
+
Priority
|
|
464
|
+
<select name="priority">
|
|
465
|
+
<option value="low" selected={values.priority === "low"}>low</option>
|
|
466
|
+
<option value="medium" selected={values.priority === "medium"}>medium</option>
|
|
467
|
+
<option value="high" selected={values.priority === "high"}>high</option>
|
|
468
|
+
</select>
|
|
469
|
+
</label>
|
|
470
|
+
<label>
|
|
471
|
+
Owner
|
|
472
|
+
<select name="owner_id">
|
|
473
|
+
<option value="">${taskCreateLookups.owner_id?.emptyLabel || "Unassigned"}</option>
|
|
474
|
+
{#each data.lookups.owner_id as option}
|
|
475
|
+
<option value={option.value} selected={option.value === values.owner_id}>{option.label}</option>
|
|
476
|
+
{/each}
|
|
477
|
+
</select>
|
|
478
|
+
</label>
|
|
479
|
+
<label>
|
|
480
|
+
Project
|
|
481
|
+
<select name="project_id" required>
|
|
482
|
+
<option value="">${webReference.createPrimary.projectPlaceholder}</option>
|
|
483
|
+
{#each data.lookups.project_id as option}
|
|
484
|
+
<option value={option.value} selected={option.value === values.project_id}>{option.label}</option>
|
|
485
|
+
{/each}
|
|
486
|
+
</select>
|
|
487
|
+
</label>
|
|
488
|
+
<label>Due At <input name="due_at" type="datetime-local" value={values.due_at} /></label>
|
|
489
|
+
<div class="button-row">
|
|
490
|
+
<button type="submit">${webReference.createPrimary.submitLabel}</button>
|
|
491
|
+
<a class="button-link secondary" href="/tasks">${webReference.createPrimary.cancelLabel}</a>
|
|
492
|
+
</div>
|
|
493
|
+
</form>
|
|
494
|
+
</section>
|
|
495
|
+
</div>
|
|
496
|
+
</main>
|
|
497
|
+
`;
|
|
498
|
+
|
|
499
|
+
files["tasks/[id]/edit/+page.ts"] = `import type { PageLoad } from "./$types";
|
|
500
|
+
import { getTask } from "$lib/api/client";
|
|
501
|
+
import { listLookupOptions } from "$lib/api/lookups";
|
|
502
|
+
|
|
503
|
+
export const load: PageLoad = async ({ fetch, params }) => {
|
|
504
|
+
const [task, ownerOptions] = await Promise.all([
|
|
505
|
+
getTask(fetch, params.id),
|
|
506
|
+
${taskEditLookups.owner_id?.route ? `listLookupOptions(fetch, "${taskEditLookups.owner_id.route}")` : "Promise.resolve([])"}
|
|
507
|
+
]);
|
|
508
|
+
return {
|
|
509
|
+
screen: ${JSON.stringify({ id: taskEdit.id, title: taskEdit.title, web: taskEdit.web }, null, 2)},
|
|
510
|
+
task,
|
|
511
|
+
lookups: {
|
|
512
|
+
owner_id: ownerOptions
|
|
513
|
+
},
|
|
514
|
+
values: {
|
|
515
|
+
title: task.title ?? "",
|
|
516
|
+
description: task.description ?? "",
|
|
517
|
+
priority: task.priority ?? "medium",
|
|
518
|
+
owner_id: task.owner_id ?? "",
|
|
519
|
+
due_at: task.due_at ? String(task.due_at).slice(0, 16) : "",
|
|
520
|
+
status: task.status ?? ""
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
};
|
|
524
|
+
`;
|
|
525
|
+
|
|
526
|
+
files["tasks/[id]/edit/+page.server.ts"] = `import { redirect, fail } from "@sveltejs/kit";
|
|
527
|
+
import type { Actions } from "./$types";
|
|
528
|
+
import { updateTask } from "$lib/api/client";
|
|
529
|
+
|
|
530
|
+
export const actions: Actions = {
|
|
531
|
+
${renderSvelteKitRedirectingAction({
|
|
532
|
+
actionName: "default",
|
|
533
|
+
signature: "{ request, fetch, params }",
|
|
534
|
+
prelude: `const form = await request.formData();
|
|
535
|
+
const updated_at = String(form.get("updated_at") || "");
|
|
536
|
+
const payload = {
|
|
537
|
+
title: String(form.get("title") || "") || undefined,
|
|
538
|
+
description: String(form.get("description") || "") || undefined,
|
|
539
|
+
priority: String(form.get("priority") || "") || undefined,
|
|
540
|
+
owner_id: String(form.get("owner_id") || "") || undefined,
|
|
541
|
+
due_at: String(form.get("due_at") || "") || undefined,
|
|
542
|
+
status: String(form.get("status") || "") || undefined
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
if (!updated_at) {
|
|
546
|
+
return fail(400, { error: "updated_at is required to update this task.", values: payload });
|
|
547
|
+
}`,
|
|
548
|
+
tryStatement: `await updateTask(fetch, params.id, payload, {
|
|
549
|
+
headers: {
|
|
550
|
+
"If-Match": updated_at
|
|
551
|
+
}
|
|
552
|
+
});`,
|
|
553
|
+
catchReturn:
|
|
554
|
+
'return fail(400, { error: error instanceof Error ? error.message : "Unable to update task", values: payload });',
|
|
555
|
+
successStatement: "throw redirect(303, `/tasks/${params.id}`);"
|
|
556
|
+
})}
|
|
557
|
+
};
|
|
558
|
+
`;
|
|
559
|
+
|
|
560
|
+
files["tasks/[id]/edit/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
|
|
561
|
+
export let data;
|
|
562
|
+
export let form;
|
|
563
|
+
|
|
564
|
+
const values = form?.values ?? data.values;
|
|
565
|
+
</script>
|
|
566
|
+
|
|
567
|
+
<main>
|
|
568
|
+
<div class="stack">
|
|
569
|
+
<section class="card">
|
|
570
|
+
<h1>${taskEdit.title || "Edit Task"}</h1>
|
|
571
|
+
<p>Update the mutable fields for <strong>{data.task.title}</strong>.</p>
|
|
572
|
+
{#if form?.error}<p><strong>{form.error}</strong></p>{/if}
|
|
573
|
+
<form class="stack" method="POST">
|
|
574
|
+
<input type="hidden" name="updated_at" value={data.task.updated_at} />
|
|
575
|
+
<label>Title <input name="title" value={values.title ?? ""} /></label>
|
|
576
|
+
<label>Description <textarea name="description">{values.description ?? ""}</textarea></label>
|
|
577
|
+
<label>
|
|
578
|
+
Priority
|
|
579
|
+
<select name="priority">
|
|
580
|
+
<option value="low" selected={(values.priority ?? "") === "low"}>low</option>
|
|
581
|
+
<option value="medium" selected={(values.priority ?? "") === "medium"}>medium</option>
|
|
582
|
+
<option value="high" selected={(values.priority ?? "") === "high"}>high</option>
|
|
583
|
+
</select>
|
|
584
|
+
</label>
|
|
585
|
+
<label>
|
|
586
|
+
Owner
|
|
587
|
+
<select name="owner_id">
|
|
588
|
+
<option value="">${taskEditLookups.owner_id?.emptyLabel || "Unassigned"}</option>
|
|
589
|
+
{#each data.lookups.owner_id as option}
|
|
590
|
+
<option value={option.value} selected={option.value === (values.owner_id ?? "")}>{option.label}</option>
|
|
591
|
+
{/each}
|
|
592
|
+
</select>
|
|
593
|
+
</label>
|
|
594
|
+
<label>Due At <input name="due_at" type="datetime-local" value={values.due_at ?? ""} /></label>
|
|
595
|
+
<label>
|
|
596
|
+
Status
|
|
597
|
+
<select name="status">
|
|
598
|
+
<option value="">Keep current ({data.task.status})</option>
|
|
599
|
+
<option value="draft">draft</option>
|
|
600
|
+
<option value="active">active</option>
|
|
601
|
+
<option value="completed">completed</option>
|
|
602
|
+
<option value="archived">archived</option>
|
|
603
|
+
</select>
|
|
604
|
+
</label>
|
|
605
|
+
<div class="button-row">
|
|
606
|
+
<button type="submit">Save Changes</button>
|
|
607
|
+
<a class="button-link secondary" href={"/tasks/" + data.task.id}>Cancel</a>
|
|
608
|
+
</div>
|
|
609
|
+
</form>
|
|
610
|
+
</section>
|
|
611
|
+
</div>
|
|
612
|
+
</main>
|
|
613
|
+
`;
|
|
614
|
+
|
|
615
|
+
files["task-exports/[job_id]/+page.ts"] = `import type { PageLoad } from "./$types";
|
|
616
|
+
import { getTaskExportJob } from "$lib/api/client";
|
|
617
|
+
|
|
618
|
+
export const load: PageLoad = async ({ fetch, params }) => {
|
|
619
|
+
try {
|
|
620
|
+
return {
|
|
621
|
+
screen: ${JSON.stringify({ id: taskExports.id, title: taskExports.title, web: taskExports.web }, null, 2)},
|
|
622
|
+
job: await getTaskExportJob(fetch, params.job_id),
|
|
623
|
+
notFound: false
|
|
624
|
+
};
|
|
625
|
+
} catch (error) {
|
|
626
|
+
if ((error as { status?: number }).status === 404) {
|
|
627
|
+
return {
|
|
628
|
+
screen: ${JSON.stringify({ id: taskExports.id, title: taskExports.title, web: taskExports.web }, null, 2)},
|
|
629
|
+
job: null,
|
|
630
|
+
notFound: true
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
throw error;
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
`;
|
|
637
|
+
|
|
638
|
+
files["task-exports/[job_id]/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
|
|
639
|
+
export let data;
|
|
640
|
+
</script>
|
|
641
|
+
|
|
642
|
+
<main>
|
|
643
|
+
<div class="stack">
|
|
644
|
+
<section class="card">
|
|
645
|
+
<h1>${taskExports.title || taskExports.id}</h1>
|
|
646
|
+
<p>This ${prettyScreenKind(taskExports.kind)} screen was generated from \`${taskExports.id}\`.</p>
|
|
647
|
+
{#if data.notFound}
|
|
648
|
+
<p>No export job was found for this id yet.</p>
|
|
649
|
+
<p class="muted">Start an export from a future generated action or revisit this page with a valid job id.</p>
|
|
650
|
+
{:else}
|
|
651
|
+
<dl class="definition-list">
|
|
652
|
+
<dt>Status</dt><dd><span class="badge">{data.job.status}</span></dd>
|
|
653
|
+
<dt>Submitted</dt><dd>{data.job.submitted_at}</dd>
|
|
654
|
+
{#if data.job.completed_at}<dt>Completed</dt><dd>{data.job.completed_at}</dd>{/if}
|
|
655
|
+
{#if data.job.error_message}<dt>Error</dt><dd>{data.job.error_message}</dd>{/if}
|
|
656
|
+
</dl>
|
|
657
|
+
{#if data.job.download_url}
|
|
658
|
+
<p><a class="button-link" href={data.job.download_url}>Download Export</a></p>
|
|
659
|
+
{/if}
|
|
660
|
+
{/if}
|
|
661
|
+
</section>
|
|
662
|
+
</div>
|
|
663
|
+
</main>
|
|
664
|
+
`;
|
|
665
|
+
|
|
666
|
+
const projectList = contract?.screens?.find((screen) => screen.id === TODO_WEB_SCREEN_REFERENCE.projectListScreenId);
|
|
667
|
+
const projectDetail = contract?.screens?.find((screen) => screen.id === TODO_WEB_SCREEN_REFERENCE.projectDetailScreenId);
|
|
668
|
+
const projectCreate = contract?.screens?.find((screen) => screen.id === TODO_WEB_SCREEN_REFERENCE.projectCreateScreenId);
|
|
669
|
+
const projectEdit = contract?.screens?.find((screen) => screen.id === TODO_WEB_SCREEN_REFERENCE.projectEditScreenId);
|
|
670
|
+
const userList = contract?.screens?.find((screen) => screen.id === TODO_WEB_SCREEN_REFERENCE.userListScreenId);
|
|
671
|
+
const userDetail = contract?.screens?.find((screen) => screen.id === TODO_WEB_SCREEN_REFERENCE.userDetailScreenId);
|
|
672
|
+
const userCreate = contract?.screens?.find((screen) => screen.id === TODO_WEB_SCREEN_REFERENCE.userCreateScreenId);
|
|
673
|
+
const userEdit = contract?.screens?.find((screen) => screen.id === TODO_WEB_SCREEN_REFERENCE.userEditScreenId);
|
|
674
|
+
|
|
675
|
+
if (projectList && projectDetail && projectCreate && projectEdit) {
|
|
676
|
+
files["projects/+page.ts"] = `import type { PageLoad } from "./$types";
|
|
677
|
+
import { requestCapability } from "$lib/api/client";
|
|
678
|
+
|
|
679
|
+
export const load: PageLoad = async ({ fetch, url }) => {
|
|
680
|
+
const limit = url.searchParams.get("limit");
|
|
681
|
+
return {
|
|
682
|
+
screen: ${JSON.stringify({ id: projectList.id, title: projectList.title, collection: projectList.collection, web: projectList.web }, null, 2)},
|
|
683
|
+
filters: {
|
|
684
|
+
limit: limit ?? ""
|
|
685
|
+
},
|
|
686
|
+
result: await requestCapability(fetch, "cap_list_projects", {
|
|
687
|
+
after: url.searchParams.get("after") ?? undefined,
|
|
688
|
+
limit: limit ? Number(limit) : undefined
|
|
689
|
+
})
|
|
690
|
+
};
|
|
691
|
+
};
|
|
692
|
+
`;
|
|
693
|
+
|
|
694
|
+
files["projects/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
|
|
695
|
+
export let data;
|
|
696
|
+
|
|
697
|
+
const buildNextHref = () => {
|
|
698
|
+
if (!data.result.next_cursor) return null;
|
|
699
|
+
const params = new URLSearchParams();
|
|
700
|
+
if (data.filters.limit) params.set("limit", String(data.filters.limit));
|
|
701
|
+
params.set("after", data.result.next_cursor);
|
|
702
|
+
return \`/projects?\${params.toString()}\`;
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
const nextHref = buildNextHref();
|
|
706
|
+
</script>
|
|
707
|
+
|
|
708
|
+
<main>
|
|
709
|
+
<div class="stack">
|
|
710
|
+
<section class="card">
|
|
711
|
+
<div class="button-row" style="justify-content: space-between;">
|
|
712
|
+
<div>
|
|
713
|
+
<h1>${projectList.title || projectList.id}</h1>
|
|
714
|
+
<p>This ${prettyScreenKind(projectList.kind)} screen was generated from \`${projectList.id}\`.</p>
|
|
715
|
+
</div>
|
|
716
|
+
<a class="button-link" href="/projects/new">Create Project</a>
|
|
717
|
+
</div>
|
|
718
|
+
|
|
719
|
+
{#if data.result.items.length === 0}
|
|
720
|
+
<div class="empty-state">
|
|
721
|
+
<p><strong>${projectList.emptyState?.title || "No projects yet"}</strong></p>
|
|
722
|
+
<p class="muted">${projectList.emptyState?.body || ""}</p>
|
|
723
|
+
</div>
|
|
724
|
+
{:else}
|
|
725
|
+
<ul class="task-list resource-list">
|
|
726
|
+
{#each data.result.items as project}
|
|
727
|
+
<li>
|
|
728
|
+
<div class="task-meta resource-meta">
|
|
729
|
+
<a href={'/projects/' + project.id}><strong>{project.name}</strong></a>
|
|
730
|
+
{#if project.description}<span class="muted">{project.description}</span>{/if}
|
|
731
|
+
<span class="muted">Owner: {project.owner_id || "Unassigned"}</span>
|
|
732
|
+
</div>
|
|
733
|
+
<span class="badge">{project.status}</span>
|
|
734
|
+
</li>
|
|
735
|
+
{/each}
|
|
736
|
+
</ul>
|
|
737
|
+
{#if nextHref}
|
|
738
|
+
<p><a class="button-link secondary" href={nextHref}>Next Page</a></p>
|
|
739
|
+
{/if}
|
|
740
|
+
{/if}
|
|
741
|
+
</section>
|
|
742
|
+
</div>
|
|
743
|
+
</main>
|
|
744
|
+
`;
|
|
745
|
+
|
|
746
|
+
files["projects/[id]/+page.ts"] = `import type { PageLoad } from "./$types";
|
|
747
|
+
import { requestCapability } from "$lib/api/client";
|
|
748
|
+
|
|
749
|
+
export const load: PageLoad = async ({ fetch, params }) => {
|
|
750
|
+
return {
|
|
751
|
+
screen: ${JSON.stringify({ id: projectDetail.id, title: projectDetail.title, web: projectDetail.web }, null, 2)},
|
|
752
|
+
project: await requestCapability(fetch, "cap_get_project", { project_id: params.id })
|
|
753
|
+
};
|
|
754
|
+
};
|
|
755
|
+
`;
|
|
756
|
+
|
|
757
|
+
files["projects/[id]/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
|
|
758
|
+
export let data;
|
|
759
|
+
</script>
|
|
760
|
+
|
|
761
|
+
<main>
|
|
762
|
+
<div class="stack">
|
|
763
|
+
<section class="card">
|
|
764
|
+
<div class="button-row" style="justify-content: space-between;">
|
|
765
|
+
<div>
|
|
766
|
+
<h1>{data.project.name}</h1>
|
|
767
|
+
<p>This ${prettyScreenKind(projectDetail.kind)} screen was generated from \`${projectDetail.id}\`.</p>
|
|
768
|
+
</div>
|
|
769
|
+
<span class="badge">{data.project.status}</span>
|
|
770
|
+
</div>
|
|
771
|
+
|
|
772
|
+
{#if data.project.description}
|
|
773
|
+
<p>{data.project.description}</p>
|
|
774
|
+
{:else}
|
|
775
|
+
<p class="muted">No description was provided for this project.</p>
|
|
776
|
+
{/if}
|
|
777
|
+
|
|
778
|
+
<dl class="definition-list">
|
|
779
|
+
<dt>Project ID</dt><dd>{data.project.id}</dd>
|
|
780
|
+
<dt>Status</dt><dd>{data.project.status}</dd>
|
|
781
|
+
<dt>Owner</dt><dd>{data.project.owner_id || "Unassigned"}</dd>
|
|
782
|
+
<dt>Created</dt><dd>{data.project.created_at}</dd>
|
|
783
|
+
</dl>
|
|
784
|
+
|
|
785
|
+
<div class="button-row">
|
|
786
|
+
<a class="button-link secondary" href="/projects">Back to Projects</a>
|
|
787
|
+
<a class="button-link" href={"/projects/" + data.project.id + "/edit"}>Edit Project</a>
|
|
788
|
+
</div>
|
|
789
|
+
</section>
|
|
790
|
+
</div>
|
|
791
|
+
</main>
|
|
792
|
+
`;
|
|
793
|
+
|
|
794
|
+
files["projects/new/+page.ts"] = `import type { PageLoad } from "./$types";
|
|
795
|
+
import { listLookupOptions } from "$lib/api/lookups";
|
|
796
|
+
|
|
797
|
+
export const load: PageLoad = async ({ fetch }) => {
|
|
798
|
+
return {
|
|
799
|
+
lookups: {
|
|
800
|
+
owner_id: await listLookupOptions(fetch, "/lookups/users")
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
};
|
|
804
|
+
`;
|
|
805
|
+
|
|
806
|
+
files["projects/new/+page.server.ts"] = `import { redirect, fail } from "@sveltejs/kit";
|
|
807
|
+
import type { Actions } from "./$types";
|
|
808
|
+
import { requestCapability } from "$lib/api/client";
|
|
809
|
+
|
|
810
|
+
export const actions: Actions = {
|
|
811
|
+
${renderSvelteKitRedirectingAction({
|
|
812
|
+
actionName: "default",
|
|
813
|
+
signature: "{ request, fetch }",
|
|
814
|
+
prelude: `const form = await request.formData();
|
|
815
|
+
const payload = {
|
|
816
|
+
name: String(form.get("name") || ""),
|
|
817
|
+
description: String(form.get("description") || "") || undefined,
|
|
818
|
+
status: String(form.get("status") || "") || "active",
|
|
819
|
+
owner_id: String(form.get("owner_id") || "") || undefined
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
if (!payload.name) {
|
|
823
|
+
return fail(400, { error: "Name is required.", values: payload });
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
let created;`,
|
|
827
|
+
tryStatement: `created = await requestCapability(fetch, "cap_create_project", payload);`,
|
|
828
|
+
catchReturn:
|
|
829
|
+
'return fail(400, { error: error instanceof Error ? error.message : "Unable to create project", values: payload });',
|
|
830
|
+
successStatement: "throw redirect(303, `/projects/${created.id}`);"
|
|
831
|
+
})}
|
|
832
|
+
};
|
|
833
|
+
`;
|
|
834
|
+
|
|
835
|
+
files["projects/new/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
|
|
836
|
+
export let data;
|
|
837
|
+
export let form;
|
|
838
|
+
|
|
839
|
+
const values = {
|
|
840
|
+
name: form?.values?.name ?? "",
|
|
841
|
+
description: form?.values?.description ?? "",
|
|
842
|
+
status: form?.values?.status ?? "active",
|
|
843
|
+
owner_id: form?.values?.owner_id ?? ""
|
|
844
|
+
};
|
|
845
|
+
</script>
|
|
846
|
+
|
|
847
|
+
<main>
|
|
848
|
+
<div class="stack">
|
|
849
|
+
<section class="card">
|
|
850
|
+
<h1>${projectCreate.title || projectCreate.id}</h1>
|
|
851
|
+
<p>This ${prettyScreenKind(projectCreate.kind)} screen was generated from \`${projectCreate.id}\`.</p>
|
|
852
|
+
{#if form?.error}<p><strong>{form.error}</strong></p>{/if}
|
|
853
|
+
<form class="stack" method="POST">
|
|
854
|
+
<label>Name <input name="name" required value={values.name} /></label>
|
|
855
|
+
<label>Description <textarea name="description">{values.description}</textarea></label>
|
|
856
|
+
<label>
|
|
857
|
+
Status
|
|
858
|
+
<select name="status">
|
|
859
|
+
<option value="active" selected={values.status === "active"}>active</option>
|
|
860
|
+
<option value="archived" selected={values.status === "archived"}>archived</option>
|
|
861
|
+
</select>
|
|
862
|
+
</label>
|
|
863
|
+
<label>
|
|
864
|
+
Owner
|
|
865
|
+
<select name="owner_id">
|
|
866
|
+
<option value="">Unassigned</option>
|
|
867
|
+
{#each data.lookups.owner_id as option}
|
|
868
|
+
<option value={option.value} selected={option.value === (values.owner_id ?? "")}>{option.label}</option>
|
|
869
|
+
{/each}
|
|
870
|
+
</select>
|
|
871
|
+
</label>
|
|
872
|
+
<div class="button-row">
|
|
873
|
+
<button type="submit">Create Project</button>
|
|
874
|
+
<a class="button-link secondary" href="/projects">Cancel</a>
|
|
875
|
+
</div>
|
|
876
|
+
</form>
|
|
877
|
+
</section>
|
|
878
|
+
</div>
|
|
879
|
+
</main>
|
|
880
|
+
`;
|
|
881
|
+
|
|
882
|
+
files["projects/[id]/edit/+page.ts"] = `import type { PageLoad } from "./$types";
|
|
883
|
+
import { requestCapability } from "$lib/api/client";
|
|
884
|
+
import { listLookupOptions } from "$lib/api/lookups";
|
|
885
|
+
|
|
886
|
+
export const load: PageLoad = async ({ fetch, params }) => {
|
|
887
|
+
const [project, ownerOptions] = await Promise.all([
|
|
888
|
+
requestCapability(fetch, "cap_get_project", { project_id: params.id }),
|
|
889
|
+
listLookupOptions(fetch, "/lookups/users")
|
|
890
|
+
]);
|
|
891
|
+
return {
|
|
892
|
+
screen: ${JSON.stringify({ id: projectEdit.id, title: projectEdit.title, web: projectEdit.web }, null, 2)},
|
|
893
|
+
project,
|
|
894
|
+
lookups: {
|
|
895
|
+
owner_id: ownerOptions
|
|
896
|
+
},
|
|
897
|
+
values: {
|
|
898
|
+
name: project.name ?? "",
|
|
899
|
+
description: project.description ?? "",
|
|
900
|
+
status: project.status ?? "active",
|
|
901
|
+
owner_id: project.owner_id ?? ""
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
};
|
|
905
|
+
`;
|
|
906
|
+
|
|
907
|
+
files["projects/[id]/edit/+page.server.ts"] = `import { redirect, fail } from "@sveltejs/kit";
|
|
908
|
+
import type { Actions } from "./$types";
|
|
909
|
+
import { requestCapability } from "$lib/api/client";
|
|
910
|
+
|
|
911
|
+
export const actions: Actions = {
|
|
912
|
+
${renderSvelteKitRedirectingAction({
|
|
913
|
+
actionName: "default",
|
|
914
|
+
signature: "{ request, fetch, params }",
|
|
915
|
+
prelude: `const form = await request.formData();
|
|
916
|
+
const payload = {
|
|
917
|
+
name: String(form.get("name") || "") || undefined,
|
|
918
|
+
description: String(form.get("description") || "") || undefined,
|
|
919
|
+
status: String(form.get("status") || "") || undefined,
|
|
920
|
+
owner_id: String(form.get("owner_id") || "") || undefined
|
|
921
|
+
};`,
|
|
922
|
+
tryStatement: `await requestCapability(fetch, "cap_update_project", { project_id: params.id, ...payload });`,
|
|
923
|
+
catchReturn:
|
|
924
|
+
'return fail(400, { error: error instanceof Error ? error.message : "Unable to update project", values: payload });',
|
|
925
|
+
successStatement: "throw redirect(303, `/projects/${params.id}`);"
|
|
926
|
+
})}
|
|
927
|
+
};
|
|
928
|
+
`;
|
|
929
|
+
|
|
930
|
+
files["projects/[id]/edit/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
|
|
931
|
+
export let data;
|
|
932
|
+
export let form;
|
|
933
|
+
|
|
934
|
+
const values = form?.values ?? data.values;
|
|
935
|
+
</script>
|
|
936
|
+
|
|
937
|
+
<main>
|
|
938
|
+
<div class="stack">
|
|
939
|
+
<section class="card">
|
|
940
|
+
<h1>${projectEdit.title || "Edit Project"}</h1>
|
|
941
|
+
<p>Update the mutable fields for <strong>{data.project.name}</strong>.</p>
|
|
942
|
+
{#if form?.error}<p><strong>{form.error}</strong></p>{/if}
|
|
943
|
+
<form class="stack" method="POST">
|
|
944
|
+
<label>Name <input name="name" value={values.name ?? ""} /></label>
|
|
945
|
+
<label>Description <textarea name="description">{values.description ?? ""}</textarea></label>
|
|
946
|
+
<label>
|
|
947
|
+
Status
|
|
948
|
+
<select name="status">
|
|
949
|
+
<option value="active" selected={(values.status ?? data.project.status) === "active"}>active</option>
|
|
950
|
+
<option value="archived" selected={(values.status ?? data.project.status) === "archived"}>archived</option>
|
|
951
|
+
</select>
|
|
952
|
+
</label>
|
|
953
|
+
<label>
|
|
954
|
+
Owner
|
|
955
|
+
<select name="owner_id">
|
|
956
|
+
<option value="">Unassigned</option>
|
|
957
|
+
{#each data.lookups.owner_id as option}
|
|
958
|
+
<option value={option.value} selected={option.value === (values.owner_id ?? "")}>{option.label}</option>
|
|
959
|
+
{/each}
|
|
960
|
+
</select>
|
|
961
|
+
</label>
|
|
962
|
+
<div class="button-row">
|
|
963
|
+
<button type="submit">Save Changes</button>
|
|
964
|
+
<a class="button-link secondary" href={"/projects/" + data.project.id}>Cancel</a>
|
|
965
|
+
</div>
|
|
966
|
+
</form>
|
|
967
|
+
</section>
|
|
968
|
+
</div>
|
|
969
|
+
</main>
|
|
970
|
+
`;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (userList && userDetail && userCreate && userEdit) {
|
|
974
|
+
files["users/+page.ts"] = `import type { PageLoad } from "./$types";
|
|
975
|
+
import { requestCapability } from "$lib/api/client";
|
|
976
|
+
|
|
977
|
+
export const load: PageLoad = async ({ fetch, url }) => {
|
|
978
|
+
const limit = url.searchParams.get("limit");
|
|
979
|
+
return {
|
|
980
|
+
screen: ${JSON.stringify({ id: userList.id, title: userList.title, collection: userList.collection, web: userList.web }, null, 2)},
|
|
981
|
+
filters: {
|
|
982
|
+
limit: limit ?? ""
|
|
983
|
+
},
|
|
984
|
+
result: await requestCapability(fetch, "cap_list_users", {
|
|
985
|
+
after: url.searchParams.get("after") ?? undefined,
|
|
986
|
+
limit: limit ? Number(limit) : undefined
|
|
987
|
+
})
|
|
988
|
+
};
|
|
989
|
+
};
|
|
990
|
+
`;
|
|
991
|
+
|
|
992
|
+
files["users/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
|
|
993
|
+
export let data;
|
|
994
|
+
|
|
995
|
+
const buildNextHref = () => {
|
|
996
|
+
if (!data.result.next_cursor) return null;
|
|
997
|
+
const params = new URLSearchParams();
|
|
998
|
+
if (data.filters.limit) params.set("limit", String(data.filters.limit));
|
|
999
|
+
params.set("after", data.result.next_cursor);
|
|
1000
|
+
return \`/users?\${params.toString()}\`;
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
const nextHref = buildNextHref();
|
|
1004
|
+
</script>
|
|
1005
|
+
|
|
1006
|
+
<main>
|
|
1007
|
+
<div class="stack">
|
|
1008
|
+
<section class="card">
|
|
1009
|
+
<div class="button-row" style="justify-content: space-between;">
|
|
1010
|
+
<div>
|
|
1011
|
+
<h1>${userList.title || userList.id}</h1>
|
|
1012
|
+
<p>This ${prettyScreenKind(userList.kind)} screen was generated from \`${userList.id}\`.</p>
|
|
1013
|
+
</div>
|
|
1014
|
+
<a class="button-link" href="/users/new">Create User</a>
|
|
1015
|
+
</div>
|
|
1016
|
+
|
|
1017
|
+
{#if data.result.items.length === 0}
|
|
1018
|
+
<div class="empty-state">
|
|
1019
|
+
<p><strong>${userList.emptyState?.title || "No users yet"}</strong></p>
|
|
1020
|
+
<p class="muted">${userList.emptyState?.body || ""}</p>
|
|
1021
|
+
</div>
|
|
1022
|
+
{:else}
|
|
1023
|
+
<ul class="task-list resource-list">
|
|
1024
|
+
{#each data.result.items as user}
|
|
1025
|
+
<li>
|
|
1026
|
+
<div class="task-meta resource-meta">
|
|
1027
|
+
<a href={'/users/' + user.id}><strong>{user.display_name}</strong></a>
|
|
1028
|
+
<span class="muted">{user.email}</span>
|
|
1029
|
+
</div>
|
|
1030
|
+
<span class="badge">{user.is_active ? "active" : "inactive"}</span>
|
|
1031
|
+
</li>
|
|
1032
|
+
{/each}
|
|
1033
|
+
</ul>
|
|
1034
|
+
{#if nextHref}
|
|
1035
|
+
<p><a class="button-link secondary" href={nextHref}>Next Page</a></p>
|
|
1036
|
+
{/if}
|
|
1037
|
+
{/if}
|
|
1038
|
+
</section>
|
|
1039
|
+
</div>
|
|
1040
|
+
</main>
|
|
1041
|
+
`;
|
|
1042
|
+
|
|
1043
|
+
files["users/[id]/+page.ts"] = `import type { PageLoad } from "./$types";
|
|
1044
|
+
import { requestCapability } from "$lib/api/client";
|
|
1045
|
+
|
|
1046
|
+
export const load: PageLoad = async ({ fetch, params }) => {
|
|
1047
|
+
return {
|
|
1048
|
+
screen: ${JSON.stringify({ id: userDetail.id, title: userDetail.title, web: userDetail.web }, null, 2)},
|
|
1049
|
+
user: await requestCapability(fetch, "cap_get_user", { user_id: params.id })
|
|
1050
|
+
};
|
|
1051
|
+
};
|
|
1052
|
+
`;
|
|
1053
|
+
|
|
1054
|
+
files["users/[id]/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
|
|
1055
|
+
export let data;
|
|
1056
|
+
</script>
|
|
1057
|
+
|
|
1058
|
+
<main>
|
|
1059
|
+
<div class="stack">
|
|
1060
|
+
<section class="card">
|
|
1061
|
+
<div class="button-row" style="justify-content: space-between;">
|
|
1062
|
+
<div>
|
|
1063
|
+
<h1>{data.user.display_name}</h1>
|
|
1064
|
+
<p>This ${prettyScreenKind(userDetail.kind)} screen was generated from \`${userDetail.id}\`.</p>
|
|
1065
|
+
</div>
|
|
1066
|
+
<span class="badge">{data.user.is_active ? "active" : "inactive"}</span>
|
|
1067
|
+
</div>
|
|
1068
|
+
|
|
1069
|
+
<dl class="definition-list">
|
|
1070
|
+
<dt>User ID</dt><dd>{data.user.id}</dd>
|
|
1071
|
+
<dt>Email</dt><dd>{data.user.email}</dd>
|
|
1072
|
+
<dt>Display Name</dt><dd>{data.user.display_name}</dd>
|
|
1073
|
+
<dt>Created</dt><dd>{data.user.created_at}</dd>
|
|
1074
|
+
</dl>
|
|
1075
|
+
|
|
1076
|
+
<div class="button-row">
|
|
1077
|
+
<a class="button-link secondary" href="/users">Back to Users</a>
|
|
1078
|
+
<a class="button-link" href={"/users/" + data.user.id + "/edit"}>Edit User</a>
|
|
1079
|
+
</div>
|
|
1080
|
+
</section>
|
|
1081
|
+
</div>
|
|
1082
|
+
</main>
|
|
1083
|
+
`;
|
|
1084
|
+
|
|
1085
|
+
files["users/new/+page.server.ts"] = `import { redirect, fail } from "@sveltejs/kit";
|
|
1086
|
+
import type { Actions } from "./$types";
|
|
1087
|
+
import { requestCapability } from "$lib/api/client";
|
|
1088
|
+
|
|
1089
|
+
export const actions: Actions = {
|
|
1090
|
+
${renderSvelteKitRedirectingAction({
|
|
1091
|
+
actionName: "default",
|
|
1092
|
+
signature: "{ request, fetch }",
|
|
1093
|
+
prelude: `const form = await request.formData();
|
|
1094
|
+
const payload = {
|
|
1095
|
+
email: String(form.get("email") || ""),
|
|
1096
|
+
display_name: String(form.get("display_name") || ""),
|
|
1097
|
+
is_active: form.get("is_active") === "true"
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
if (!payload.email || !payload.display_name) {
|
|
1101
|
+
return fail(400, { error: "Email and display name are required.", values: payload });
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
let created;`,
|
|
1105
|
+
tryStatement: `created = await requestCapability(fetch, "cap_create_user", payload);`,
|
|
1106
|
+
catchReturn:
|
|
1107
|
+
'return fail(400, { error: error instanceof Error ? error.message : "Unable to create user", values: payload });',
|
|
1108
|
+
successStatement: "throw redirect(303, `/users/${created.id}`);"
|
|
1109
|
+
})}
|
|
1110
|
+
};
|
|
1111
|
+
`;
|
|
1112
|
+
|
|
1113
|
+
files["users/new/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
|
|
1114
|
+
export let form;
|
|
1115
|
+
|
|
1116
|
+
const values = {
|
|
1117
|
+
email: form?.values?.email ?? "",
|
|
1118
|
+
display_name: form?.values?.display_name ?? "",
|
|
1119
|
+
is_active: form?.values?.is_active ?? true
|
|
1120
|
+
};
|
|
1121
|
+
</script>
|
|
1122
|
+
|
|
1123
|
+
<main>
|
|
1124
|
+
<div class="stack">
|
|
1125
|
+
<section class="card">
|
|
1126
|
+
<h1>${userCreate.title || userCreate.id}</h1>
|
|
1127
|
+
<p>This ${prettyScreenKind(userCreate.kind)} screen was generated from \`${userCreate.id}\`.</p>
|
|
1128
|
+
{#if form?.error}<p><strong>{form.error}</strong></p>{/if}
|
|
1129
|
+
<form class="stack" method="POST">
|
|
1130
|
+
<label>Email <input name="email" type="email" required value={values.email} /></label>
|
|
1131
|
+
<label>Display Name <input name="display_name" required value={values.display_name} /></label>
|
|
1132
|
+
<label>
|
|
1133
|
+
Active
|
|
1134
|
+
<select name="is_active">
|
|
1135
|
+
<option value="true" selected={values.is_active === true}>active</option>
|
|
1136
|
+
<option value="false" selected={values.is_active === false}>inactive</option>
|
|
1137
|
+
</select>
|
|
1138
|
+
</label>
|
|
1139
|
+
<div class="button-row">
|
|
1140
|
+
<button type="submit">Create User</button>
|
|
1141
|
+
<a class="button-link secondary" href="/users">Cancel</a>
|
|
1142
|
+
</div>
|
|
1143
|
+
</form>
|
|
1144
|
+
</section>
|
|
1145
|
+
</div>
|
|
1146
|
+
</main>
|
|
1147
|
+
`;
|
|
1148
|
+
|
|
1149
|
+
files["users/[id]/edit/+page.ts"] = `import type { PageLoad } from "./$types";
|
|
1150
|
+
import { requestCapability } from "$lib/api/client";
|
|
1151
|
+
|
|
1152
|
+
export const load: PageLoad = async ({ fetch, params }) => {
|
|
1153
|
+
const user = await requestCapability(fetch, "cap_get_user", { user_id: params.id });
|
|
1154
|
+
return {
|
|
1155
|
+
screen: ${JSON.stringify({ id: userEdit.id, title: userEdit.title, web: userEdit.web }, null, 2)},
|
|
1156
|
+
user,
|
|
1157
|
+
values: {
|
|
1158
|
+
email: user.email ?? "",
|
|
1159
|
+
display_name: user.display_name ?? "",
|
|
1160
|
+
is_active: user.is_active ?? true
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
};
|
|
1164
|
+
`;
|
|
1165
|
+
|
|
1166
|
+
files["users/[id]/edit/+page.server.ts"] = `import { redirect, fail } from "@sveltejs/kit";
|
|
1167
|
+
import type { Actions } from "./$types";
|
|
1168
|
+
import { requestCapability } from "$lib/api/client";
|
|
1169
|
+
|
|
1170
|
+
export const actions: Actions = {
|
|
1171
|
+
${renderSvelteKitRedirectingAction({
|
|
1172
|
+
actionName: "default",
|
|
1173
|
+
signature: "{ request, fetch, params }",
|
|
1174
|
+
prelude: `const form = await request.formData();
|
|
1175
|
+
const payload = {
|
|
1176
|
+
email: String(form.get("email") || "") || undefined,
|
|
1177
|
+
display_name: String(form.get("display_name") || "") || undefined,
|
|
1178
|
+
is_active: form.get("is_active") === "true"
|
|
1179
|
+
};`,
|
|
1180
|
+
tryStatement: `await requestCapability(fetch, "cap_update_user", { user_id: params.id, ...payload });`,
|
|
1181
|
+
catchReturn:
|
|
1182
|
+
'return fail(400, { error: error instanceof Error ? error.message : "Unable to update user", values: payload });',
|
|
1183
|
+
successStatement: "throw redirect(303, `/users/${params.id}`);"
|
|
1184
|
+
})}
|
|
1185
|
+
};
|
|
1186
|
+
`;
|
|
1187
|
+
|
|
1188
|
+
files["users/[id]/edit/+page.svelte"] = `<script${useTypescript ? ' lang="ts"' : ""}>
|
|
1189
|
+
export let data;
|
|
1190
|
+
export let form;
|
|
1191
|
+
|
|
1192
|
+
const values = form?.values ?? data.values;
|
|
1193
|
+
</script>
|
|
1194
|
+
|
|
1195
|
+
<main>
|
|
1196
|
+
<div class="stack">
|
|
1197
|
+
<section class="card">
|
|
1198
|
+
<h1>${userEdit.title || "Edit User"}</h1>
|
|
1199
|
+
<p>Update the mutable fields for <strong>{data.user.display_name}</strong>.</p>
|
|
1200
|
+
{#if form?.error}<p><strong>{form.error}</strong></p>{/if}
|
|
1201
|
+
<form class="stack" method="POST">
|
|
1202
|
+
<label>Email <input name="email" type="email" value={values.email ?? ""} /></label>
|
|
1203
|
+
<label>Display Name <input name="display_name" value={values.display_name ?? ""} /></label>
|
|
1204
|
+
<label>
|
|
1205
|
+
Active
|
|
1206
|
+
<select name="is_active">
|
|
1207
|
+
<option value="true" selected={(values.is_active ?? data.user.is_active) === true}>active</option>
|
|
1208
|
+
<option value="false" selected={(values.is_active ?? data.user.is_active) === false}>inactive</option>
|
|
1209
|
+
</select>
|
|
1210
|
+
</label>
|
|
1211
|
+
<div class="button-row">
|
|
1212
|
+
<button type="submit">Save Changes</button>
|
|
1213
|
+
<a class="button-link secondary" href={"/users/" + data.user.id}>Cancel</a>
|
|
1214
|
+
</div>
|
|
1215
|
+
</form>
|
|
1216
|
+
</section>
|
|
1217
|
+
</div>
|
|
1218
|
+
</main>
|
|
1219
|
+
`;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
return files;
|
|
1223
|
+
}
|