Skip to content

Commit 7f6aadf

Browse files
committed
♻️ Extract common component (#2083)
1 parent ee9e12e commit 7f6aadf

File tree

3 files changed

+156
-175
lines changed

3 files changed

+156
-175
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<script lang="ts">
2+
import { run } from 'svelte/legacy';
3+
4+
import { Breadcrumb, BreadcrumbItem } from 'svelte-5-ui-lib';
5+
6+
import HeadingOne from '$lib/components/HeadingOne.svelte';
7+
import WorkBookInputFields from '$lib/components/WorkBooks/WorkBookInputFields.svelte';
8+
import WorkBookTasksTable from '$lib/components/WorkBookTasks/WorkBookTasksTable.svelte';
9+
import TaskSearchBox from '$lib/components/TaskSearchBox.svelte';
10+
import InputFieldWrapper from '$lib/components/InputFieldWrapper.svelte';
11+
import SubmissionButton from '$lib/components/SubmissionButton.svelte';
12+
13+
import { preventEnterKey } from '$lib/actions/prevent_enter_key';
14+
15+
import type { Task, Tasks } from '$lib/types/task';
16+
import type {
17+
WorkBookTaskCreate,
18+
WorkBookTasksCreate,
19+
WorkBookTaskEdit,
20+
WorkBookTasksEdit,
21+
} from '$lib/types/workbook';
22+
23+
interface Props {
24+
pageTitle: string;
25+
breadcrumbTitle: string;
26+
isAdmin: boolean;
27+
// type is any, so we have no choice but to use it.
28+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
29+
superFormObject: any; // superForm object
30+
tasksMapByIds: Map<string, Task>;
31+
submitButtonLabel?: string;
32+
}
33+
34+
let {
35+
pageTitle,
36+
breadcrumbTitle,
37+
isAdmin,
38+
superFormObject,
39+
tasksMapByIds,
40+
submitButtonLabel = '',
41+
}: Props = $props();
42+
43+
const { form, message, errors, enhance } = superFormObject;
44+
45+
let workBookTasksForTable: WorkBookTasksCreate | WorkBookTasksEdit = $state([]);
46+
47+
// HACK: This is a workaround to ensure that the derived state is updated correctly.
48+
run(() => {
49+
workBookTasksForTable = $form.workBookTasks
50+
.map((workBookTask: WorkBookTaskCreate | WorkBookTaskEdit) => {
51+
const task = tasksMapByIds.get(workBookTask.taskId);
52+
53+
if (!task) {
54+
return null;
55+
}
56+
57+
return {
58+
contestId: task.contest_id,
59+
title: task.title,
60+
taskId: workBookTask.taskId,
61+
priority: workBookTask.priority,
62+
comment: workBookTask.comment,
63+
};
64+
})
65+
.filter(
66+
(item: WorkBookTaskCreate | WorkBookTaskEdit): item is NonNullable<typeof item> =>
67+
item !== null,
68+
);
69+
});
70+
71+
const truncateClass = 'min-w-[96px] max-w-[120px] sm:max-w-[300px] lg:max-w-[600px] truncate';
72+
const tasks: Tasks = $derived(Array.from(tasksMapByIds.values()));
73+
</script>
74+
75+
<div class="container mx-auto w-5/6">
76+
<form method="post" use:enhance use:preventEnterKey class="space-y-4">
77+
<HeadingOne title={pageTitle} />
78+
79+
<Breadcrumb aria-label="">
80+
<BreadcrumbItem href="/workbooks" home>問題集</BreadcrumbItem>
81+
82+
<BreadcrumbItem>
83+
<div class={truncateClass}>{breadcrumbTitle}</div>
84+
</BreadcrumbItem>
85+
</Breadcrumb>
86+
87+
<!-- Form for workbook -->
88+
<WorkBookInputFields
89+
bind:authorId={$form.authorId}
90+
bind:workBookTitle={$form.title}
91+
bind:description={$form.description}
92+
bind:editorialUrl={$form.editorialUrl}
93+
bind:isPublished={$form.isPublished}
94+
bind:isOfficial={$form.isOfficial}
95+
bind:isReplenished={$form.isReplenished}
96+
bind:workBookType={$form.workBookType}
97+
{isAdmin}
98+
message={$message}
99+
errors={$errors}
100+
/>
101+
102+
<!-- Search tasks -->
103+
<!-- HACK:
104+
Because the attributes are slightly different, we have no choice
105+
but to separate the data for storing in the database and for creating and editing workbooks.
106+
-->
107+
<div class="space-y-2">
108+
<TaskSearchBox {tasks} bind:workBookTasks={$form.workBookTasks} bind:workBookTasksForTable />
109+
<InputFieldWrapper
110+
inputFieldType="hidden"
111+
inputFieldName="workBookTasks"
112+
inputValue={$form.workBookTasks}
113+
message={$errors.workBookTasks?._errors}
114+
/>
115+
</div>
116+
117+
<!-- Show tasks stored in database and added by search -->
118+
<WorkBookTasksTable
119+
{tasksMapByIds}
120+
bind:workBookTasks={$form.workBookTasks}
121+
bind:workBookTasksForTable
122+
/>
123+
124+
<!-- Create or update button -->
125+
<div class="flex flex-wrap md:justify-center md:items-center">
126+
<SubmissionButton width="w-full md:max-w-md mt-4" labelName={submitButtonLabel} />
127+
</div>
128+
</form>
129+
</div>
Lines changed: 13 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,10 @@
11
<script lang="ts">
2-
import { Breadcrumb, BreadcrumbItem } from 'svelte-5-ui-lib';
32
import { superForm } from 'sveltekit-superforms/client';
43
5-
import {
6-
WorkBookType,
7-
type WorkBookTasksBase,
8-
type WorkBookTaskCreate,
9-
} from '$lib/types/workbook';
10-
import type { Task } from '$lib/types/task';
11-
12-
import { preventEnterKey } from '$lib/actions/prevent_enter_key';
4+
import WorkBookForm from '$lib/components/WorkBook/WorkBookForm.svelte';
135
14-
import HeadingOne from '$lib/components/HeadingOne.svelte';
15-
import WorkBookInputFields from '$lib/components/WorkBooks/WorkBookInputFields.svelte';
16-
import WorkBookTasksTable from '$lib/components/WorkBookTasks/WorkBookTasksTable.svelte';
17-
import TaskSearchBox from '$lib/components/TaskSearchBox.svelte';
18-
import InputFieldWrapper from '$lib/components/InputFieldWrapper.svelte';
19-
import SubmissionButton from '$lib/components/SubmissionButton.svelte';
6+
import { WorkBookType, type WorkBookTasksBase } from '$lib/types/workbook';
7+
import type { Task } from '$lib/types/task';
208
219
let { data } = $props();
2210
@@ -26,75 +14,23 @@
2614
...data.form,
2715
workBookTasks: [] as WorkBookTasksBase,
2816
};
29-
const { form, message, errors, enhance } = superForm(initialData, {
17+
const superFormObject = superForm(initialData, {
3018
dataType: 'json',
3119
});
20+
const { form } = superFormObject;
3221
3322
$form.authorId = data.author.id;
3423
$form.isOfficial = data.isAdmin;
3524
$form.workBookType = $form.isOfficial ? WorkBookType.CURRICULUM : WorkBookType.CREATED_BY_USER;
3625
37-
let workBookTasksForTable: WorkBookTaskCreate[] = $state([]);
38-
39-
$effect((): void => {
40-
workBookTasksForTable = [] as WorkBookTaskCreate[];
41-
});
42-
4326
const tasksMapByIds: Map<string, Task> = data.tasksMapByIds;
4427
</script>
4528

46-
<!-- TODO: 問題集の編集ページのコンポーネントとほぼ共通しているのでリファクタリング -->
47-
<div class="container mx-auto w-5/6">
48-
<form method="post" use:enhance use:preventEnterKey class="space-y-4">
49-
<HeadingOne title="問題集を作成" />
50-
51-
<Breadcrumb aria-label="">
52-
<BreadcrumbItem href="/workbooks" home>問題集</BreadcrumbItem>
53-
<BreadcrumbItem>
54-
<div class="min-w-[96px] max-w-[120px] truncate">問題集を作成</div>
55-
</BreadcrumbItem>
56-
</Breadcrumb>
57-
58-
<WorkBookInputFields
59-
bind:authorId={$form.authorId}
60-
bind:workBookTitle={$form.title}
61-
bind:description={$form.description}
62-
bind:editorialUrl={$form.editorialUrl}
63-
bind:isPublished={$form.isPublished}
64-
bind:isOfficial={$form.isOfficial}
65-
bind:isReplenished={$form.isReplenished}
66-
bind:workBookType={$form.workBookType}
67-
isAdmin={data.isAdmin}
68-
message={$message}
69-
errors={$errors}
70-
/>
71-
72-
<!-- 問題を検索 -->
73-
<!-- HACK: 属性が微妙に異なるため、やむなくデータベースへの保存用と問題集作成・編集用で分けている。 -->
74-
<div class="space-y-2">
75-
<TaskSearchBox
76-
tasks={Array.from(tasksMapByIds.values())}
77-
bind:workBookTasks={$form.workBookTasks}
78-
bind:workBookTasksForTable
79-
/>
80-
<InputFieldWrapper
81-
inputFieldType="hidden"
82-
inputFieldName="workBookTasks"
83-
inputValue={$form.workBookTasks}
84-
message={$errors.workBookTasks?._errors}
85-
/>
86-
</div>
87-
88-
<!-- 問題一覧 -->
89-
<WorkBookTasksTable
90-
{tasksMapByIds}
91-
bind:workBookTasks={$form.workBookTasks}
92-
bind:workBookTasksForTable
93-
/>
94-
95-
<!-- 作成ボタンを追加 -->
96-
<div class="flex flex-wrap md:justify-center md:items-center">
97-
<SubmissionButton width="w-full md:max-w-md mt-4" labelName="作成" />
98-
</div>
99-
</form>
100-
</div>
29+
<WorkBookForm
30+
pageTitle="問題集を作成"
31+
breadcrumbTitle={'問題集を作成'}
32+
isAdmin={data.isAdmin}
33+
{superFormObject}
34+
{tasksMapByIds}
35+
submitButtonLabel="作成"
36+
/>

src/routes/workbooks/edit/[slug]/+page.svelte

Lines changed: 14 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,14 @@
11
<script lang="ts">
2-
import { run } from 'svelte/legacy';
3-
42
import { superForm } from 'sveltekit-superforms/client';
5-
import { Breadcrumb, BreadcrumbItem } from 'svelte-5-ui-lib';
63
7-
import type { WorkBookTasksBase, WorkBookTasksEdit } from '$lib/types/workbook';
8-
import type { Task, Tasks } from '$lib/types/task.js';
4+
import WorkBookForm from '$lib/components/WorkBook/WorkBookForm.svelte';
5+
6+
import type { WorkBookTasksBase } from '$lib/types/workbook';
7+
import type { Task } from '$lib/types/task.js';
98
10-
import { preventEnterKey } from '$lib/actions/prevent_enter_key';
11-
import HeadingOne from '$lib/components/HeadingOne.svelte';
12-
import WorkBookInputFields from '$lib/components/WorkBooks/WorkBookInputFields.svelte';
13-
import WorkBookTasksTable from '$lib/components/WorkBookTasks/WorkBookTasksTable.svelte';
14-
import TaskSearchBox from '$lib/components/TaskSearchBox.svelte';
15-
import InputFieldWrapper from '$lib/components/InputFieldWrapper.svelte';
16-
import SubmissionButton from '$lib/components/SubmissionButton.svelte';
179
import { FORBIDDEN } from '$lib/constants/http-response-status-codes.js';
1810
1911
let { data } = $props();
20-
2112
let canView = $derived(data.status === FORBIDDEN ? false : true);
2213
2314
let workBook = data.workBook;
@@ -28,98 +19,23 @@
2819
...data.form,
2920
workBookTasks: workBook.workBookTasks as WorkBookTasksBase,
3021
};
31-
const { form, message, errors, enhance } = superForm(initialData, {
22+
const superFormObject = superForm(initialData, {
3223
dataType: 'json',
3324
});
3425
35-
const tasks: Tasks = data.tasks;
36-
37-
// データベースに基づいて、問題集の編集用データを作成
26+
// Create data of workbook for edit based on database.
3827
const tasksMapByIds: Map<string, Task> = data.tasksMapByIds;
39-
40-
let workBookTasksForTable: WorkBookTasksEdit = $state([]);
41-
42-
// HACK: $effect だと workBookTasksForTable が更新されない
43-
run(() => {
44-
workBookTasksForTable = $form.workBookTasks
45-
.map((workBookTask) => {
46-
const task = tasksMapByIds.get(workBookTask.taskId);
47-
48-
if (!task) {
49-
return null;
50-
}
51-
52-
return {
53-
contestId: task.contest_id,
54-
title: task.title,
55-
taskId: workBookTask.taskId,
56-
priority: workBookTask.priority,
57-
comment: workBookTask.comment,
58-
};
59-
})
60-
.filter((item): item is NonNullable<typeof item> => item !== null);
61-
});
6228
</script>
6329

6430
{#if canView}
65-
<!-- TODO: 問題集の作成ページのコンポーネントとほぼ共通しているのでリファクタリング -->
66-
<div class="container mx-auto w-5/6">
67-
<form method="post" use:enhance use:preventEnterKey class="space-y-4">
68-
<HeadingOne title="問題集を編集" />
69-
70-
<!-- TODO: コンポーネントとして切り出す -->
71-
<Breadcrumb aria-label="">
72-
<BreadcrumbItem href="/workbooks" home>問題集</BreadcrumbItem>
73-
<BreadcrumbItem>
74-
<div class="min-w-[96px] max-w-[120px] sm:max-w-[300px] lg:max-w-[600px] truncate">
75-
{workBook.title}
76-
</div>
77-
</BreadcrumbItem>
78-
</Breadcrumb>
79-
80-
<WorkBookInputFields
81-
bind:authorId={$form.authorId}
82-
bind:workBookTitle={$form.title}
83-
bind:description={$form.description}
84-
bind:editorialUrl={$form.editorialUrl}
85-
bind:isPublished={$form.isPublished}
86-
bind:isOfficial={$form.isOfficial}
87-
bind:isReplenished={$form.isReplenished}
88-
bind:workBookType={$form.workBookType}
89-
isAdmin={data.loggedInAsAdmin}
90-
message={$message}
91-
errors={$errors}
92-
/>
93-
94-
<!-- 問題を検索 -->
95-
<!-- HACK: 属性が微妙に異なるため、やむなくデータベースへの保存用と問題集作成・編集用で分けている。 -->
96-
<div class="space-y-2">
97-
<TaskSearchBox
98-
{tasks}
99-
bind:workBookTasks={$form.workBookTasks}
100-
bind:workBookTasksForTable
101-
/>
102-
<InputFieldWrapper
103-
inputFieldType="hidden"
104-
inputFieldName="workBookTasks"
105-
inputValue={$form.workBookTasks}
106-
message={$errors.workBookTasks?._errors}
107-
/>
108-
</div>
109-
110-
<!-- データベースに保存されている問題 + 検索で追加した問題を表示 -->
111-
<WorkBookTasksTable
112-
{tasksMapByIds}
113-
bind:workBookTasks={$form.workBookTasks}
114-
bind:workBookTasksForTable
115-
/>
116-
117-
<!-- 更新ボタン -->
118-
<div class="flex flex-wrap md:justify-center md:items-center">
119-
<SubmissionButton width="w-full md:max-w-md mt-4" labelName="更新" />
120-
</div>
121-
</form>
122-
</div>
31+
<WorkBookForm
32+
pageTitle="問題集を編集"
33+
breadcrumbTitle={workBook.title}
34+
isAdmin={data.loggedInAsAdmin}
35+
{superFormObject}
36+
{tasksMapByIds}
37+
submitButtonLabel="更新"
38+
/>
12339
{:else}
12440
<!-- TODO: コンポーネントとして抽出 -->
12541
<h1>{data.status}</h1>

0 commit comments

Comments
 (0)