Skip to content

Commit 39c8566

Browse files
committed
♻️ Extract dropdown as component (#1706)
1 parent 1e39b93 commit 39c8566

File tree

2 files changed

+212
-145
lines changed

2 files changed

+212
-145
lines changed
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
<!-- Usage
2+
// Script
3+
<script lang="ts">
4+
import type { TaskResult } from '$lib/types/task';
5+
6+
import UpdatingDropdown from '$lib/components/SubmissionStatus/UpdatingDropdown.svelte';
7+
8+
interface Props {
9+
taskResult: TaskResult;
10+
isLoggedIn: boolean;
11+
onupdate?: (updatedTask: TaskResult) => void;
12+
}
13+
14+
let { taskResult, isLoggedIn, onupdate = () => {} }: Props = $props();
15+
16+
let updatingDropdown: UpdatingDropdown;
17+
</script>
18+
19+
20+
// Component
21+
<button
22+
type="button"
23+
onclick={() => updatingDropdown.toggle()} // Open / close the dropdown.
24+
>
25+
26+
<UpdatingDropdown bind:this={updatingDropdown} {taskResult} {isLoggedIn} {onupdate} />
27+
-->
28+
<script lang="ts">
29+
import { getStores } from '$app/stores';
30+
import { enhance } from '$app/forms';
31+
32+
import { Dropdown, DropdownUl, DropdownLi, uiHelpers } from 'svelte-5-ui-lib';
33+
34+
import type { TaskResult } from '$lib/types/task';
35+
36+
import InputFieldWrapper from '$lib/components/InputFieldWrapper.svelte';
37+
38+
import { submission_statuses } from '$lib/services/submission_status';
39+
import { errorMessageStore } from '$lib/stores/error_message';
40+
41+
interface Props {
42+
taskResult: TaskResult;
43+
isLoggedIn: boolean;
44+
onupdate: (updatedTask: TaskResult) => void; // Ensure to update task result in parent component.
45+
}
46+
47+
let { taskResult, isLoggedIn, onupdate }: Props = $props();
48+
49+
const { page } = getStores();
50+
let activeUrl = $state($page.url.pathname);
51+
52+
let dropdown = uiHelpers();
53+
let dropdownStatus = $state(false);
54+
let closeDropdown = dropdown.close;
55+
56+
$effect(() => {
57+
activeUrl = $page.url.pathname;
58+
dropdownStatus = dropdown.isOpen;
59+
});
60+
61+
export function toggle(): void {
62+
dropdown.toggle();
63+
}
64+
65+
let selectedSubmissionStatus = $state<SubmissionStatus>();
66+
let showForm = $state(false);
67+
68+
function handleClick(submissionStatus: {
69+
innerId: string;
70+
innerName: string;
71+
labelName: string;
72+
}): void {
73+
selectedSubmissionStatus = submissionStatus;
74+
showForm = true;
75+
76+
// Submit after the form is rendered.
77+
setTimeout(() => {
78+
const submitButton = document.querySelector(
79+
'#submissionStatusForm button[type="submit"]',
80+
) as HTMLButtonElement;
81+
82+
if (submitButton) {
83+
// Submit the form via the enhance directive by clicking the button.
84+
submitButton.click();
85+
}
86+
}, 10);
87+
}
88+
89+
type SubmissionStatus = {
90+
innerId: string;
91+
innerName: string;
92+
labelName: string;
93+
};
94+
95+
type EnhanceForSubmit = {
96+
formData: FormData;
97+
action: URL;
98+
cancel: () => void;
99+
};
100+
101+
const FAILED_TO_UPDATE_SUBMISSION_STATUS =
102+
'回答状況の更新に失敗しました。もう一度試してください。';
103+
104+
const handleSubmit = (submissionStatus: SubmissionStatus) => {
105+
return ({ formData, action, cancel }: EnhanceForSubmit) => {
106+
// Cancel the default form submission.
107+
cancel();
108+
109+
// Submit data manually using fetch API.
110+
fetch(action, {
111+
method: 'POST',
112+
body: formData,
113+
headers: {
114+
Accept: 'application/json',
115+
},
116+
})
117+
.then((response) => response.json())
118+
.then(() => {
119+
const updatedTaskResult = updateTaskResult(taskResult, submissionStatus);
120+
onupdate(updatedTaskResult);
121+
})
122+
.catch((error) => {
123+
console.error('Failed to update submission status: ', error);
124+
errorMessageStore.setAndClearAfterTimeout(FAILED_TO_UPDATE_SUBMISSION_STATUS, 10000);
125+
})
126+
.finally(() => {
127+
closeDropdown();
128+
showForm = false;
129+
});
130+
131+
// Do not change anything in SvelteKit.
132+
return () => {};
133+
};
134+
};
135+
136+
function updateTaskResult(
137+
taskResult: TaskResult,
138+
submissionStatus: SubmissionStatus,
139+
): TaskResult {
140+
return {
141+
...taskResult,
142+
status_name: submissionStatus.innerName,
143+
status_id: submissionStatus.innerId,
144+
submission_status_label_name: submissionStatus.labelName,
145+
is_ac: submissionStatus.innerName === 'ac',
146+
updated_at: new Date(),
147+
};
148+
}
149+
150+
// TODO: When customizing submission status, implement DB fetching for status options.
151+
const submissionStatusOptions = submission_statuses.map((status) => {
152+
const option = {
153+
innerId: status.id,
154+
innerName: status.status_name,
155+
labelName: status.label_name,
156+
};
157+
return option;
158+
});
159+
</script>
160+
161+
<div class="relative">
162+
<Dropdown
163+
{activeUrl}
164+
{dropdownStatus}
165+
{closeDropdown}
166+
class="absolute w-24 z-20 left-auto right-0 mt-8"
167+
>
168+
<DropdownUl>
169+
{#if isLoggedIn}
170+
{#each submissionStatusOptions as submissionStatus}
171+
<DropdownLi href="javascript:void(0)" onclick={() => handleClick(submissionStatus)}>
172+
{submissionStatus.labelName}
173+
</DropdownLi>
174+
{/each}
175+
{/if}
176+
</DropdownUl>
177+
</Dropdown>
178+
179+
{#if showForm && selectedSubmissionStatus}
180+
{@render submissionStatusForm(taskResult, selectedSubmissionStatus)}
181+
{/if}
182+
</div>
183+
184+
{#snippet submissionStatusForm(selectedTaskResult: TaskResult, submissionStatus: SubmissionStatus)}
185+
<form
186+
id="submissionStatusForm"
187+
method="POST"
188+
action="?/update"
189+
style="display:none;"
190+
use:enhance={handleSubmit(submissionStatus)}
191+
>
192+
<!-- Task id -->
193+
<InputFieldWrapper
194+
inputFieldType="hidden"
195+
inputFieldName="taskId"
196+
inputValue={selectedTaskResult.task_id}
197+
/>
198+
199+
<!-- Submission status -->
200+
<InputFieldWrapper
201+
inputFieldType="hidden"
202+
inputFieldName="submissionStatus"
203+
inputValue={submissionStatus.innerName}
204+
/>
205+
206+
<button type="submit">Submit</button>
207+
</form>
208+
{/snippet}
Lines changed: 4 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
11
<script lang="ts">
2-
import { getStores } from '$app/stores';
3-
import { enhance } from '$app/forms';
4-
5-
import { Dropdown, DropdownUl, DropdownLi, uiHelpers } from 'svelte-5-ui-lib';
62
import EllipsisVertical from 'lucide-svelte/icons/ellipsis-vertical';
73
84
import type { TaskResult } from '$lib/types/task';
95
106
import GradeLabel from '$lib/components/GradeLabel.svelte';
117
import ExternalLinkWrapper from '$lib/components/ExternalLinkWrapper.svelte';
12-
import InputFieldWrapper from '$lib/components/InputFieldWrapper.svelte';
13-
14-
import { submission_statuses } from '$lib/services/submission_status';
8+
import UpdatingDropdown from '$lib/components/SubmissionStatus/UpdatingDropdown.svelte';
159
1610
import { getTaskUrl, removeTaskIndexFromTitle } from '$lib/utils/task';
1711
@@ -23,55 +17,7 @@
2317
2418
let { taskResult, isLoggedIn, onupdate = () => {} }: Props = $props();
2519
26-
const { page } = getStores();
27-
let activeUrl = $state($page.url.pathname);
28-
29-
let dropdown = uiHelpers();
30-
let dropdownStatus = $state(false);
31-
let closeDropdown = dropdown.close;
32-
33-
$effect(() => {
34-
activeUrl = $page.url.pathname;
35-
dropdownStatus = dropdown.isOpen;
36-
});
37-
38-
let selectedSubmissionStatus = $state<{
39-
innerId: string;
40-
innerName: string;
41-
labelName: string;
42-
}>();
43-
let showForm = $state(false);
44-
45-
function handleSubmit(submissionStatus: {
46-
innerId: string;
47-
innerName: string;
48-
labelName: string;
49-
}): void {
50-
selectedSubmissionStatus = submissionStatus;
51-
showForm = true;
52-
53-
// Submit after the form is rendered.
54-
setTimeout(() => {
55-
const submitButton = document.querySelector(
56-
'#submissionStatusForm button[type="submit"]',
57-
) as HTMLButtonElement;
58-
59-
if (submitButton) {
60-
// Submit the form via the enhance directive by clicking the button.
61-
submitButton.click();
62-
}
63-
}, 10);
64-
}
65-
66-
// FIXME: When customizing submission status, implement DB fetching for status options.
67-
const submissionStatusOptions = submission_statuses.map((status) => {
68-
const option = {
69-
innerId: status.id,
70-
innerName: status.status_name,
71-
labelName: status.label_name,
72-
};
73-
return option;
74-
});
20+
let updatingDropdown: UpdatingDropdown;
7521
</script>
7622

7723
<div class="flex items-center w-full space-x-1 text-left text-sm sm:text-md">
@@ -116,99 +62,12 @@
11662
<button
11763
type="button"
11864
class="flex-shrink-0 w-6 ml-auto"
119-
onclick={dropdown.toggle}
65+
onclick={() => updatingDropdown.toggle()}
12066
aria-label="Update submission for {selectedTaskResult.title}"
12167
>
12268
<EllipsisVertical class="w-4 h-4 mx-auto" />
12369
</button>
12470

125-
{@render dropdownList(selectedTaskResult)}
71+
<UpdatingDropdown bind:this={updatingDropdown} {taskResult} {isLoggedIn} {onupdate} />
12672
</div>
12773
{/snippet}
128-
129-
{#snippet dropdownList(selectedTaskResult: TaskResult)}
130-
<div class="relative">
131-
<Dropdown
132-
{activeUrl}
133-
{dropdownStatus}
134-
{closeDropdown}
135-
class="absolute w-24 z-20 left-auto right-0 mt-8"
136-
>
137-
<DropdownUl>
138-
{#each submissionStatusOptions as submissionStatus}
139-
<DropdownLi href="javascript:void(0)" onclick={() => handleSubmit(submissionStatus)}>
140-
{submissionStatus.labelName}
141-
</DropdownLi>
142-
{/each}
143-
</DropdownUl>
144-
</Dropdown>
145-
146-
{#if showForm && selectedSubmissionStatus}
147-
{@render submissionStatusForm(selectedTaskResult, selectedSubmissionStatus)}
148-
{/if}
149-
</div>
150-
{/snippet}
151-
152-
{#snippet submissionStatusForm(
153-
selectedTaskResult: TaskResult,
154-
submissionStatus: { innerId: string; innerName: string; labelName: string },
155-
)}
156-
<form
157-
id="submissionStatusForm"
158-
method="POST"
159-
action="?/update"
160-
style="display:none;"
161-
use:enhance={({ formData, action, cancel }) => {
162-
// Cancel the default form submission.
163-
cancel();
164-
165-
// Submit data manually using fetch API.
166-
fetch(action, {
167-
method: 'POST',
168-
body: formData,
169-
headers: {
170-
Accept: 'application/json',
171-
},
172-
})
173-
.then((response) => response.json())
174-
.then(() => {
175-
const updatedTaskResult = {
176-
...taskResult,
177-
status_name: submissionStatus.innerName,
178-
status_id: submissionStatus.innerId,
179-
submission_status_label_name: submissionStatus.labelName,
180-
is_ac: submissionStatus.innerName === 'ac',
181-
updated_at: new Date(),
182-
};
183-
184-
onupdate(updatedTaskResult);
185-
})
186-
.catch((error) => {
187-
console.error('Failed to submit task status:', error);
188-
})
189-
.finally(() => {
190-
closeDropdown();
191-
showForm = false;
192-
});
193-
194-
// Do not change anything in SvelteKit.
195-
return () => {};
196-
}}
197-
>
198-
<!-- Task id -->
199-
<InputFieldWrapper
200-
inputFieldType="hidden"
201-
inputFieldName="taskId"
202-
inputValue={selectedTaskResult.task_id}
203-
/>
204-
205-
<!-- Submission status -->
206-
<InputFieldWrapper
207-
inputFieldType="hidden"
208-
inputFieldName="submissionStatus"
209-
inputValue={submissionStatus.innerName}
210-
/>
211-
212-
<button type="submit">Submit</button>
213-
</form>
214-
{/snippet}

0 commit comments

Comments
 (0)