Skip to content

Commit 76d01a4

Browse files
authored
[CRM][Fullstack] - Save Changes in Task Editor (#241)
* setup routes * edit controller, service and hook * fix seeds and add state for saving * done minus monacode weirdness * lint * fix monacode bug * typo
1 parent e05e6eb commit 76d01a4

13 files changed

Lines changed: 319 additions & 83 deletions

File tree

prisma/seed-data/task-template.seed.ts

Lines changed: 63 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,15 @@ export const taskTemplatesData = [
8888
paragraph('two_sum_c3', ['- -10^9 <= target <= 10^9']),
8989
paragraph('two_sum_c4', ['- Only one valid answer exists.']),
9090
],
91+
// stdin: first line is space-separated nums, second line is target
92+
// stdout: space-separated indices
9193
publicTestCases: [
92-
{ input: { nums: [2, 7, 11, 15], target: 9 }, expected: [0, 1] },
93-
{ input: { nums: [3, 2, 4], target: 6 }, expected: [1, 2] },
94+
{ input: '2 7 11 15\n9', output: '0 1' },
95+
{ input: '3 2 4\n6', output: '1 2' },
9496
],
9597
privateTestCases: [
96-
{ input: { nums: [3, 3], target: 6 }, expected: [0, 1] },
97-
{ input: { nums: [1, 5, 3, 7, 9], target: 10 }, expected: [1, 3] },
98+
{ input: '3 3\n6', output: '0 1' },
99+
{ input: '1 5 3 7 9\n10', output: '1 3' },
98100
],
99101
orgId: 'org_nextlab_001',
100102
taskType: 'Single Function',
@@ -133,15 +135,12 @@ export const taskTemplatesData = [
133135
paragraph('rs_c1', ['- 1 <= s.length <= 10^5']),
134136
paragraph('rs_c2', ['- s[i] is a printable ascii character.']),
135137
],
136-
publicTestCases: [
137-
{ input: { s: ['h', 'e', 'l', 'l', 'o'] }, expected: ['o', 'l', 'l', 'e', 'h'] },
138-
],
138+
// stdin: space-separated characters
139+
// stdout: space-separated reversed characters
140+
publicTestCases: [{ input: 'h e l l o', output: 'o l l e h' }],
139141
privateTestCases: [
140-
{
141-
input: { s: ['H', 'a', 'n', 'n', 'a', 'h'] },
142-
expected: ['h', 'a', 'n', 'n', 'a', 'H'],
143-
},
144-
{ input: { s: ['A'] }, expected: ['A'] },
142+
{ input: 'H a n n a h', output: 'h a n n a H' },
143+
{ input: 'A', output: 'A' },
145144
],
146145
orgId: 'org_nextlab_001',
147146
taskType: 'Single Function',
@@ -180,13 +179,15 @@ export const taskTemplatesData = [
180179
paragraph('vp_c1', ['- 1 <= s.length <= 2 * 10^5']),
181180
paragraph('vp_c2', ['- s consists only of printable ASCII characters.']),
182181
],
182+
// stdin: the string
183+
// stdout: true or false
183184
publicTestCases: [
184-
{ input: { s: 'A man, a plan, a canal: Panama' }, expected: true },
185-
{ input: { s: 'race a car' }, expected: false },
185+
{ input: 'A man, a plan, a canal: Panama', output: 'true' },
186+
{ input: 'race a car', output: 'false' },
186187
],
187188
privateTestCases: [
188-
{ input: { s: ' ' }, expected: true },
189-
{ input: { s: 'Was it a car or a cat I saw?' }, expected: true },
189+
{ input: ' ', output: 'true' },
190+
{ input: 'Was it a car or a cat I saw?', output: 'true' },
190191
],
191192
orgId: 'org_nextlab_001',
192193
taskType: 'Single Function',
@@ -214,8 +215,19 @@ export const taskTemplatesData = [
214215
'Write a solution (or reduce an existing one) so it has as few characters as possible.',
215216
]),
216217
],
217-
publicTestCases: [{ input: {}, expected: null }],
218-
privateTestCases: [{ input: {}, expected: null }],
218+
// stdin: empty — no input needed
219+
// stdout: fizzbuzz output from 1 to 100
220+
publicTestCases: [
221+
{ input: '3', output: 'Fizz' },
222+
{ input: '5', output: 'Buzz' },
223+
{ input: '15', output: 'FizzBuzz' },
224+
],
225+
privateTestCases: [
226+
{ input: '1', output: '1' },
227+
{ input: '9', output: 'Fizz' },
228+
{ input: '10', output: 'Buzz' },
229+
{ input: '30', output: 'FizzBuzz' },
230+
],
219231
orgId: 'org_nextlab_001',
220232
taskType: 'Single Function',
221233
authorId: 'user_prof_fontenot_001',
@@ -224,7 +236,7 @@ export const taskTemplatesData = [
224236
id: 'task_template_quick_warmup_001',
225237
title: 'Quick Warmup',
226238
description: [] as object[],
227-
publicTestCases: [{ input: {}, expected: null }],
239+
publicTestCases: [{ input: '', output: '' }],
228240
privateTestCases: [],
229241
orgId: 'org_nextlab_001',
230242
taskType: null,
@@ -261,13 +273,15 @@ export const taskTemplatesData = [
261273
paragraph('ms_c1', ['- 1 <= nums.length <= 10^5']),
262274
paragraph('ms_c2', ['- -10^4 <= nums[i] <= 10^4']),
263275
],
276+
// stdin: space-separated nums
277+
// stdout: the maximum subarray sum
264278
publicTestCases: [
265-
{ input: { nums: [-2, 1, -3, 4, -1, 2, 1, -5, 4] }, expected: 6 },
266-
{ input: { nums: [1] }, expected: 1 },
279+
{ input: '-2 1 -3 4 -1 2 1 -5 4', output: '6' },
280+
{ input: '1', output: '1' },
267281
],
268282
privateTestCases: [
269-
{ input: { nums: [5, 4, -1, 7, 8] }, expected: 23 },
270-
{ input: { nums: [-1, -2, -3] }, expected: -1 },
283+
{ input: '5 4 -1 7 8', output: '23' },
284+
{ input: '-1 -2 -3', output: '-1' },
271285
],
272286
orgId: 'org_nextlab_001',
273287
taskType: 'Single Function',
@@ -304,13 +318,15 @@ export const taskTemplatesData = [
304318
paragraph('bs_c2', ['- -10^4 < nums[i], target < 10^4']),
305319
paragraph('bs_c3', ['- nums is sorted in ascending order.']),
306320
],
321+
// stdin: first line is space-separated nums, second line is target
322+
// stdout: the index, or -1
307323
publicTestCases: [
308-
{ input: { nums: [-1, 0, 3, 5, 9, 12], target: 9 }, expected: 4 },
309-
{ input: { nums: [-1, 0, 3, 5, 9, 12], target: 2 }, expected: -1 },
324+
{ input: '-1 0 3 5 9 12\n9', output: '4' },
325+
{ input: '-1 0 3 5 9 12\n2', output: '-1' },
310326
],
311327
privateTestCases: [
312-
{ input: { nums: [5], target: 5 }, expected: 0 },
313-
{ input: { nums: [2, 5], target: 5 }, expected: 1 },
328+
{ input: '5\n5', output: '0' },
329+
{ input: '2 5\n5', output: '1' },
314330
],
315331
orgId: 'org_nextlab_001',
316332
taskType: 'Single Function',
@@ -344,14 +360,16 @@ export const taskTemplatesData = [
344360
paragraph('vp2_c1', ['- 1 <= s.length <= 10^4']),
345361
paragraph('vp2_c2', ["- s consists of parentheses only '()[]{}'."]),
346362
],
363+
// stdin: the bracket string
364+
// stdout: true or false
347365
publicTestCases: [
348-
{ input: { s: '()' }, expected: true },
349-
{ input: { s: '()[]{}' }, expected: true },
350-
{ input: { s: '(]' }, expected: false },
366+
{ input: '()', output: 'true' },
367+
{ input: '()[]{}', output: 'true' },
368+
{ input: '(]', output: 'false' },
351369
],
352370
privateTestCases: [
353-
{ input: { s: '([)]' }, expected: false },
354-
{ input: { s: '{[]}' }, expected: true },
371+
{ input: '([)]', output: 'false' },
372+
{ input: '{[]}', output: 'true' },
355373
],
356374
orgId: 'org_nextlab_001',
357375
taskType: 'Single Function',
@@ -392,16 +410,16 @@ export const taskTemplatesData = [
392410
paragraph('mts_c2', ['- -100 <= Node.val <= 100']),
393411
paragraph('mts_c3', ['- Both list1 and list2 are sorted in non-decreasing order.']),
394412
],
413+
// stdin: first line is space-separated list1, second line is space-separated list2
414+
// empty line represents an empty list
415+
// stdout: space-separated merged list, or empty string for empty result
395416
publicTestCases: [
396-
{
397-
input: { list1: [1, 2, 4], list2: [1, 3, 4] },
398-
expected: [1, 1, 2, 3, 4, 4],
399-
},
400-
{ input: { list1: [], list2: [] }, expected: [] },
417+
{ input: '1 2 4\n1 3 4', output: '1 1 2 3 4 4' },
418+
{ input: '\n', output: '' },
401419
],
402420
privateTestCases: [
403-
{ input: { list1: [], list2: [0] }, expected: [0] },
404-
{ input: { list1: [1], list2: [2] }, expected: [1, 2] },
421+
{ input: '\n0', output: '0' },
422+
{ input: '1\n2', output: '1 2' },
405423
],
406424
orgId: 'org_nextlab_001',
407425
taskType: 'Single Function',
@@ -435,19 +453,15 @@ export const taskTemplatesData = [
435453
paragraph('lcp_c2', ['- 0 <= strs[i].length <= 200']),
436454
paragraph('lcp_c3', ['- strs[i] consists of only lowercase English letters.']),
437455
],
456+
// stdin: space-separated strings
457+
// stdout: the longest common prefix, or empty string
438458
publicTestCases: [
439-
{
440-
input: { strs: ['flower', 'flow', 'flight'] },
441-
expected: 'fl',
442-
},
443-
{
444-
input: { strs: ['dog', 'racecar', 'car'] },
445-
expected: '',
446-
},
459+
{ input: 'flower flow flight', output: 'fl' },
460+
{ input: 'dog racecar car', output: '' },
447461
],
448462
privateTestCases: [
449-
{ input: { strs: ['a'] }, expected: 'a' },
450-
{ input: { strs: ['ab', 'a'] }, expected: 'a' },
463+
{ input: 'a', output: 'a' },
464+
{ input: 'ab a', output: 'a' },
451465
],
452466
orgId: 'org_nextlab_001',
453467
taskType: 'Single Function',

src/app/(web)/crm/task-templates/[id]/edit/page.tsx

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ export default function TaskTemplateEditPage({ params }: { params: Promise<{ id:
4242
handleEditorContent,
4343
handleLanguageChange,
4444
handleTaskSolutionToggle,
45+
isSaving,
46+
saveTaskTemplate,
4547
} = useTaskTemplateEditPage(id);
4648

4749
if (isLoading) {
@@ -59,13 +61,22 @@ export default function TaskTemplateEditPage({ params }: { params: Promise<{ id:
5961
<div className="flex h-full w-full flex-col">
6062
<div className="flex items-center justify-between gap-2 border-b px-5 py-4">
6163
<div className="flex items-center gap-2">
62-
<Button variant="icon" onClick={() => router.push('/crm/templates')}>
64+
<Button
65+
variant="icon"
66+
onClick={() => router.push('/crm/templates')}
67+
disabled={isSaving}
68+
>
6369
<ChevronLeft className="size-5" />
6470
</Button>
6571
<h1 className="text-xl font-bold">{title}</h1>
6672
</div>
67-
<Button variant="secondary" className="px-4 py-2">
68-
Save Changes
73+
<Button
74+
variant="secondary"
75+
className="px-4 py-2"
76+
onClick={saveTaskTemplate}
77+
disabled={isSaving}
78+
>
79+
{isSaving ? 'Saving...' : 'Save Changes'}
6980
</Button>
7081
</div>
7182

@@ -82,22 +93,28 @@ export default function TaskTemplateEditPage({ params }: { params: Promise<{ id:
8293
setAvailableTags={setAvailableTags}
8394
languages={languages}
8495
setLanguages={setLanguages}
96+
isSaving={isSaving}
8597
/>
8698
</div>
8799

88100
<div className="flex w-2/3 flex-col bg-[#384150]">
89101
<div className="flex h-1/2 flex-col">
90102
<div className="border-b-sarge-gray-600 flex w-full justify-between border-b-1 text-white">
91-
<Tabs defaultValue="task" onValueChange={handleTaskSolutionToggle}>
103+
<Tabs
104+
defaultValue="task"
105+
onValueChange={isSaving ? undefined : handleTaskSolutionToggle}
106+
>
92107
<TabsList className="h-auto bg-transparent p-0">
93108
<TabsTrigger
94109
value="task"
110+
disabled={isSaving}
95111
className="data-[state=active]:!border-sarge-gray-600 relative h-full rounded-none !border-0 px-2.5 !text-white data-[state=active]:!border-x-1 data-[state=active]:!border-y-0 data-[state=active]:!bg-transparent data-[state=active]:!shadow-none data-[state=active]:after:absolute data-[state=active]:after:right-0 data-[state=active]:after:bottom-[-1px] data-[state=active]:after:left-0 data-[state=active]:after:h-[1px] data-[state=active]:after:bg-[#384150] data-[state=active]:after:content-['']"
96112
>
97113
Main
98114
</TabsTrigger>
99115
<TabsTrigger
100116
value="solution"
117+
disabled={isSaving}
101118
className="data-[state=active]:!border-sarge-gray-600 relative h-full rounded-none !border-0 px-2.5 !text-white data-[state=active]:!border-x-1 data-[state=active]:!border-y-0 data-[state=active]:!bg-transparent data-[state=active]:!shadow-none data-[state=active]:after:absolute data-[state=active]:after:right-0 data-[state=active]:after:bottom-[-1px] data-[state=active]:after:left-0 data-[state=active]:after:h-[1px] data-[state=active]:after:bg-[#384150] data-[state=active]:after:content-['']"
102119
>
103120
Solution
@@ -107,8 +124,10 @@ export default function TaskTemplateEditPage({ params }: { params: Promise<{ id:
107124
<div className="text-md flex items-center gap-1.5">
108125
<div>Language</div>
109126
<DropdownMenu>
110-
<DropdownMenuTrigger asChild>
111-
<div className="bg-sarge-primary-500 flex items-center gap-2.5 rounded-sm px-2.5 text-white">
127+
<DropdownMenuTrigger asChild disabled={isSaving}>
128+
<div
129+
className={`bg-sarge-primary-500 flex items-center gap-2.5 rounded-sm px-2.5 text-white ${isSaving ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}`}
130+
>
112131
{taskTemplate?.languages[selectedLanguage].language}
113132
<ChevronDown className="size-4" />
114133
</div>
@@ -142,6 +161,7 @@ export default function TaskTemplateEditPage({ params }: { params: Promise<{ id:
142161
}
143162
defaultValue={taskTemplate?.languages[selectedLanguage]?.stub}
144163
onMount={handleEditorContent}
164+
options={{ readOnly: isSaving }}
145165
/>
146166
</div>
147167
</div>
@@ -152,6 +172,7 @@ export default function TaskTemplateEditPage({ params }: { params: Promise<{ id:
152172
setPublicTestCases={setPublicTestCases}
153173
privateTestCases={privateTestCases}
154174
setPrivateTestCases={setPrivateTestCases}
175+
isSaving={isSaving}
155176
/>
156177
</div>
157178
</div>

src/app/api/task-templates/[id]/route.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { handleError } from '@/lib/utils/errors.utils';
22
import TaskTemplateService from '@/lib/services/task-template.service';
33
import { type NextRequest } from 'next/server';
4-
import { updateTaskTemplateSchema } from '@/lib/schemas/task-template.schema';
4+
import {
5+
taskTemplateEditorSaveSchema,
6+
updateTaskTemplateSchema,
7+
} from '@/lib/schemas/task-template.schema';
58
import { getSession } from '@/lib/utils/auth.utils';
69
import { assertRecruiterOrAbove } from '@/lib/utils/permissions.utils';
710

@@ -21,14 +24,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
2124
const session = await getSession();
2225
await assertRecruiterOrAbove(request.headers);
2326

27+
const orgId = session.activeOrganizationId;
2428
const id = (await params).id;
25-
const result = await TaskTemplateService.duplicateTaskTemplate(
26-
id,
27-
session.activeOrganizationId,
28-
session.userId
29-
);
29+
const body = await request.json();
30+
const parsed = taskTemplateEditorSaveSchema.parse(body);
31+
const result = await TaskTemplateService.editTaskTemplate(id, orgId, parsed);
3032

31-
return Response.json({ data: result }, { status: 201 });
33+
return Response.json({ data: result }, { status: 200 });
3234
} catch (err) {
3335
return handleError(err);
3436
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { type NextRequest } from 'next/server';
2+
import { handleError } from '@/lib/utils/errors.utils';
3+
import TaskTemplateService from '@/lib/services/task-template.service';
4+
import { getSession } from '@/lib/utils/auth.utils';
5+
import { assertRecruiterOrAbove } from '@/lib/utils/permissions.utils';
6+
7+
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
8+
try {
9+
const session = await getSession();
10+
await assertRecruiterOrAbove(request.headers);
11+
12+
const id = (await params).id;
13+
const result = await TaskTemplateService.duplicateTaskTemplate(
14+
id,
15+
session.activeOrganizationId,
16+
session.userId
17+
);
18+
19+
return Response.json({ data: result }, { status: 201 });
20+
} catch (err) {
21+
return handleError(err);
22+
}
23+
}

0 commit comments

Comments
 (0)