Skip to content

Commit 4621a88

Browse files
authored
Merge pull request #1844 from AtCoder-NoviSteps/#1706
🎨 Enable to update submission status from drop down (#1706)
2 parents 78d271e + 8869409 commit 4621a88

File tree

3 files changed

+293
-35
lines changed

3 files changed

+293
-35
lines changed
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
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+
import { SIGNUP_PAGE, LOGIN_PAGE } from '$lib/constants/navbar-links';
42+
43+
interface Props {
44+
taskResult: TaskResult;
45+
isLoggedIn: boolean;
46+
onupdate: (updatedTask: TaskResult) => void; // Ensure to update task result in parent component.
47+
}
48+
49+
let { taskResult, isLoggedIn, onupdate }: Props = $props();
50+
51+
const { page } = getStores();
52+
let activeUrl = $state($page.url.pathname);
53+
54+
let dropdown = uiHelpers();
55+
let dropdownStatus = $state(false);
56+
let closeDropdown = dropdown.close;
57+
58+
$effect(() => {
59+
activeUrl = $page.url.pathname;
60+
dropdownStatus = dropdown.isOpen;
61+
});
62+
63+
export function toggle(): void {
64+
dropdown.toggle();
65+
}
66+
67+
let selectedSubmissionStatus = $state<SubmissionStatus>();
68+
let showForm = $state(false);
69+
70+
function handleClick(submissionStatus: {
71+
innerId: string;
72+
innerName: string;
73+
labelName: string;
74+
}): void {
75+
selectedSubmissionStatus = submissionStatus;
76+
showForm = true;
77+
78+
// Submit after the form is rendered.
79+
setTimeout(() => {
80+
const submitButton = document.querySelector(
81+
'#submissionStatusForm button[type="submit"]',
82+
) as HTMLButtonElement;
83+
84+
if (submitButton) {
85+
// Submit the form via the enhance directive by clicking the button.
86+
submitButton.click();
87+
}
88+
}, 10);
89+
}
90+
91+
type SubmissionStatus = {
92+
innerId: string;
93+
innerName: string;
94+
labelName: string;
95+
};
96+
97+
type EnhanceForSubmit = {
98+
formData: FormData;
99+
action: URL;
100+
cancel: () => void;
101+
};
102+
103+
const FAILED_TO_UPDATE_SUBMISSION_STATUS =
104+
'回答状況の更新に失敗しました。もう一度試してください。';
105+
106+
const handleSubmit = (submissionStatus: SubmissionStatus) => {
107+
return ({ formData, action, cancel }: EnhanceForSubmit) => {
108+
// Cancel the default form submission.
109+
cancel();
110+
111+
if (isSame(submissionStatus, taskResult)) {
112+
console.log('Skipping: Submission status already set to', submissionStatus.labelName);
113+
114+
resetDropdown();
115+
return () => {};
116+
}
117+
118+
// Submit data manually using fetch API.
119+
fetch(action, {
120+
method: 'POST',
121+
body: formData,
122+
headers: {
123+
Accept: 'application/json',
124+
},
125+
})
126+
.then((response) => response.json())
127+
.then(() => {
128+
const updatedTaskResult = updateTaskResult(taskResult, submissionStatus);
129+
onupdate(updatedTaskResult);
130+
})
131+
.catch((error) => {
132+
console.error('Failed to update submission status: ', error);
133+
errorMessageStore.setAndClearAfterTimeout(FAILED_TO_UPDATE_SUBMISSION_STATUS, 10000);
134+
})
135+
.finally(() => {
136+
resetDropdown();
137+
});
138+
139+
// Do not change anything in SvelteKit.
140+
return () => {};
141+
};
142+
};
143+
144+
function isSame(submissionStatus: SubmissionStatus, taskResult: TaskResult): boolean {
145+
return submissionStatus.innerName === taskResult.status_name;
146+
}
147+
148+
function resetDropdown(): void {
149+
closeDropdown();
150+
showForm = false;
151+
}
152+
153+
function updateTaskResult(
154+
taskResult: TaskResult,
155+
submissionStatus: SubmissionStatus,
156+
): TaskResult {
157+
return {
158+
...taskResult,
159+
status_name: submissionStatus.innerName,
160+
status_id: submissionStatus.innerId,
161+
submission_status_label_name: submissionStatus.labelName,
162+
is_ac: submissionStatus.innerName === 'ac',
163+
updated_at: new Date(),
164+
};
165+
}
166+
167+
// TODO: When customizing submission status, implement DB fetching for status options.
168+
const submissionStatusOptions = submission_statuses.map((status) => {
169+
const option = {
170+
innerId: status.id,
171+
innerName: status.status_name,
172+
labelName: status.label_name,
173+
};
174+
return option;
175+
});
176+
</script>
177+
178+
<div class="relative">
179+
<Dropdown
180+
{activeUrl}
181+
{dropdownStatus}
182+
{closeDropdown}
183+
class="absolute w-32 z-20 left-auto right-0 mt-8"
184+
>
185+
<DropdownUl>
186+
{#if isLoggedIn}
187+
{#each submissionStatusOptions as submissionStatus}
188+
<DropdownLi href="javascript:void(0)" onclick={() => handleClick(submissionStatus)}>
189+
{submissionStatus.labelName}
190+
</DropdownLi>
191+
{/each}
192+
{:else}
193+
<DropdownLi href={SIGNUP_PAGE}>アカウント作成</DropdownLi>
194+
<DropdownLi href={LOGIN_PAGE}>ログイン</DropdownLi>
195+
{/if}
196+
</DropdownUl>
197+
</Dropdown>
198+
199+
{#if showForm && selectedSubmissionStatus}
200+
{@render submissionStatusForm(taskResult, selectedSubmissionStatus)}
201+
{/if}
202+
</div>
203+
204+
{#snippet submissionStatusForm(selectedTaskResult: TaskResult, submissionStatus: SubmissionStatus)}
205+
<form
206+
id="submissionStatusForm"
207+
method="POST"
208+
action="?/update"
209+
style="display:none;"
210+
use:enhance={handleSubmit(submissionStatus)}
211+
>
212+
<!-- Task id -->
213+
<InputFieldWrapper
214+
inputFieldType="hidden"
215+
inputFieldName="taskId"
216+
inputValue={selectedTaskResult.task_id}
217+
/>
218+
219+
<!-- Submission status -->
220+
<InputFieldWrapper
221+
inputFieldType="hidden"
222+
inputFieldName="submissionStatus"
223+
inputValue={submissionStatus.innerName}
224+
/>
225+
226+
<button type="submit">Submit</button>
227+
</form>
228+
{/snippet}

src/lib/components/TaskTables/TaskTable.svelte

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import type { TaskResults, TaskResult } from '$lib/types/task';
1515
import type { ContestTableProvider } from '$lib/types/contest_table_provider';
1616
17-
import UpdatingModal from '$lib/components/SubmissionStatus/UpdatingModal.svelte';
1817
import TaskTableBodyCell from '$lib/components/TaskTables/TaskTableBodyCell.svelte';
1918
2019
import {
@@ -31,6 +30,7 @@
3130
3231
let { taskResults, isLoggedIn }: Props = $props();
3332
33+
// Prepare contest table provider based on the active contest type.
3434
let activeContestType = $state<ContestTableProviders>('abcLatest20Rounds');
3535
3636
let provider: ContestTableProvider = $derived(
@@ -52,20 +52,8 @@
5252
return provider.getContestRoundLabel(contestId);
5353
}
5454
55-
let updatingModal: UpdatingModal | null = null;
56-
57-
// WHY: () => updatingModal.openModal(taskResult) だけだと、updatingModalがnullの可能性があるため。
58-
function openModal(taskResult: TaskResult): void {
59-
if (updatingModal) {
60-
updatingModal.openModal(taskResult);
61-
} else {
62-
console.error('Failed to initialize UpdatingModal component.');
63-
}
64-
}
65-
6655
function getBodyCellClasses(contestId: string, taskIndex: string): string {
67-
const baseClasses =
68-
'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 px-1 py-1 border hover:brightness-125 transition-all';
56+
const baseClasses = 'w-1/2 xs:w-1/3 sm:w-1/4 md:w-1/5 lg:w-1/6 px-1 py-1 border';
6957
const backgroundColor = getBackgroundColor(taskTable[contestId][taskIndex]);
7058
7159
return `${baseClasses} ${backgroundColor}`;
@@ -80,6 +68,43 @@
8068
8169
return '';
8270
}
71+
72+
// Update task results dynamically.
73+
// Computational complexity of preparation table: O(N), where N is the number of task results.
74+
let taskResultsMap = $derived(() => {
75+
return taskResults.reduce((map: Map<string, TaskResult>, taskResult: TaskResult) => {
76+
if (!map.has(taskResult.task_id)) {
77+
map.set(taskResult.task_id, taskResult);
78+
}
79+
return map;
80+
}, new Map<string, TaskResult>());
81+
});
82+
83+
let taskIndicesMap = $derived(() => {
84+
const indices = new Map<string, number>();
85+
86+
taskResults.forEach((task, index) => {
87+
indices.set(task.task_id, index);
88+
});
89+
90+
return indices;
91+
});
92+
93+
function handleUpdateTaskResult(updatedTask: TaskResult): void {
94+
const map = taskResultsMap();
95+
96+
if (map.has(updatedTask.task_id)) {
97+
map.set(updatedTask.task_id, updatedTask);
98+
}
99+
100+
const index = taskIndicesMap().get(updatedTask.task_id);
101+
102+
if (index !== undefined) {
103+
const newTaskResults = [...taskResults];
104+
newTaskResults[index] = updatedTask;
105+
taskResults = newTaskResults;
106+
}
107+
}
83108
</script>
84109

85110
<!-- See: -->
@@ -117,8 +142,9 @@
117142

118143
{#if taskTableHeaderIds.length}
119144
{#each taskTableHeaderIds as taskTableHeaderId}
120-
<TableHeadCell class="text-center border" scope="col">{taskTableHeaderId}</TableHeadCell
121-
>
145+
<TableHeadCell class="text-center border" scope="col">
146+
{taskTableHeaderId}
147+
</TableHeadCell>
122148
{/each}
123149
{/if}
124150
</TableHead>
@@ -140,7 +166,7 @@
140166
<TaskTableBodyCell
141167
taskResult={taskTable[contestId][taskTableHeaderId]}
142168
{isLoggedIn}
143-
onClick={() => openModal(taskTable[contestId][taskTableHeaderId])}
169+
onupdate={(updatedTask: TaskResult) => handleUpdateTaskResult(updatedTask)}
144170
/>
145171
{/if}
146172
</TableBodyCell>
@@ -152,5 +178,3 @@
152178
</Table>
153179
</div>
154180
</div>
155-
156-
<UpdatingModal bind:this={updatingModal} {isLoggedIn} />

0 commit comments

Comments
 (0)