@openmrs/esm-patient-task-list-app 12.1.1-pre.10907

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.
Files changed (75) hide show
  1. package/.turbo/turbo-build.log +7 -0
  2. package/README.md +28 -0
  3. package/dist/105.js +1 -0
  4. package/dist/105.js.map +1 -0
  5. package/dist/117.js +1 -0
  6. package/dist/117.js.map +1 -0
  7. package/dist/149.js +1 -0
  8. package/dist/149.js.map +1 -0
  9. package/dist/229.js +43 -0
  10. package/dist/229.js.map +1 -0
  11. package/dist/304.js +1 -0
  12. package/dist/304.js.map +1 -0
  13. package/dist/339.js +1 -0
  14. package/dist/339.js.map +1 -0
  15. package/dist/378.js +1 -0
  16. package/dist/378.js.map +1 -0
  17. package/dist/396.js +1 -0
  18. package/dist/396.js.map +1 -0
  19. package/dist/409.js +6 -0
  20. package/dist/409.js.map +1 -0
  21. package/dist/466.js +1 -0
  22. package/dist/466.js.map +1 -0
  23. package/dist/61.js +1 -0
  24. package/dist/61.js.map +1 -0
  25. package/dist/66.js +1 -0
  26. package/dist/66.js.map +1 -0
  27. package/dist/697.js +1 -0
  28. package/dist/697.js.map +1 -0
  29. package/dist/712.js +1 -0
  30. package/dist/712.js.map +1 -0
  31. package/dist/720.js +1 -0
  32. package/dist/720.js.map +1 -0
  33. package/dist/752.js +1 -0
  34. package/dist/752.js.map +1 -0
  35. package/dist/771.js +1 -0
  36. package/dist/771.js.map +1 -0
  37. package/dist/789.js +1 -0
  38. package/dist/789.js.map +1 -0
  39. package/dist/989.js +1 -0
  40. package/dist/989.js.map +1 -0
  41. package/dist/main.js +6 -0
  42. package/dist/main.js.map +1 -0
  43. package/dist/openmrs-esm-patient-task-list-app.js +6 -0
  44. package/dist/openmrs-esm-patient-task-list-app.js.buildmanifest.json +651 -0
  45. package/dist/openmrs-esm-patient-task-list-app.js.map +1 -0
  46. package/dist/routes.json +1 -0
  47. package/jest.config.js +3 -0
  48. package/package.json +61 -0
  49. package/rspack.config.js +1 -0
  50. package/src/config-schema.ts +13 -0
  51. package/src/declarations.d.ts +3 -0
  52. package/src/index.ts +25 -0
  53. package/src/launch-button/task-list-launch-button.extension.tsx +20 -0
  54. package/src/loader/loader.component.tsx +10 -0
  55. package/src/loader/loader.scss +9 -0
  56. package/src/routes.json +28 -0
  57. package/src/types.d.ts +9 -0
  58. package/src/workspace/add-task-form.component.tsx +609 -0
  59. package/src/workspace/add-task-form.scss +49 -0
  60. package/src/workspace/add-task-form.test.tsx +615 -0
  61. package/src/workspace/delete-task.modal.test.tsx +99 -0
  62. package/src/workspace/delete-task.modal.tsx +71 -0
  63. package/src/workspace/delete-task.scss +7 -0
  64. package/src/workspace/task-details-view.component.tsx +212 -0
  65. package/src/workspace/task-details-view.scss +61 -0
  66. package/src/workspace/task-details-view.test.tsx +408 -0
  67. package/src/workspace/task-list-view.component.tsx +154 -0
  68. package/src/workspace/task-list-view.scss +111 -0
  69. package/src/workspace/task-list-view.test.tsx +246 -0
  70. package/src/workspace/task-list.resource.ts +543 -0
  71. package/src/workspace/task-list.scss +37 -0
  72. package/src/workspace/task-list.workspace.test.tsx +135 -0
  73. package/src/workspace/task-list.workspace.tsx +99 -0
  74. package/translations/en.json +66 -0
  75. package/tsconfig.json +4 -0
@@ -0,0 +1,609 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Controller, useForm } from 'react-hook-form';
4
+ import { zodResolver } from '@hookform/resolvers/zod';
5
+ import { z } from 'zod';
6
+ import { useSWRConfig } from 'swr';
7
+ import {
8
+ Button,
9
+ ButtonSet,
10
+ ComboBox,
11
+ ContentSwitcher,
12
+ Form,
13
+ FormGroup,
14
+ InlineNotification,
15
+ Layer,
16
+ Switch,
17
+ TextArea,
18
+ TextInput,
19
+ } from '@carbon/react';
20
+ import {
21
+ showSnackbar,
22
+ useLayoutType,
23
+ useConfig,
24
+ parseDate,
25
+ type Visit,
26
+ OpenmrsDatePicker,
27
+ getCoreTranslation,
28
+ } from '@openmrs/esm-framework';
29
+ import { type Config } from '../config-schema';
30
+ import Loader from '../loader/loader.component';
31
+ import styles from './add-task-form.scss';
32
+ import {
33
+ useProviderRoles,
34
+ saveTask,
35
+ updateTask,
36
+ taskListSWRKey,
37
+ getPriorityLabel,
38
+ type TaskInput,
39
+ type Priority,
40
+ type Task,
41
+ type SystemTask,
42
+ useFetchProviders,
43
+ useTask,
44
+ useSystemTasks,
45
+ } from './task-list.resource';
46
+
47
+ export interface AddTaskFormProps {
48
+ patientUuid: string;
49
+ activeVisit?: Visit;
50
+ onClose: () => void;
51
+ editTaskUuid?: string;
52
+ }
53
+
54
+ const optionSchema = z.object({
55
+ id: z.string(),
56
+ label: z.string().optional(),
57
+ });
58
+
59
+ const AddTaskForm: React.FC<AddTaskFormProps> = ({ patientUuid, activeVisit, onClose, editTaskUuid }) => {
60
+ const { t } = useTranslation();
61
+ const isEditMode = Boolean(editTaskUuid);
62
+
63
+ const { allowAssigningProviderRole } = useConfig<Config>();
64
+
65
+ const { task: existingTask, isLoading: isTaskLoading } = useTask(editTaskUuid ?? '');
66
+ const { providers, setProviderQuery, isLoading: isLoadingProviders } = useFetchProviders();
67
+ const { systemTasks, isLoading: isSystemTasksLoading } = useSystemTasks();
68
+
69
+ const [selectedSystemTask, setSelectedSystemTask] = useState<SystemTask | null>(null);
70
+ const [isCustomTaskName, setIsCustomTaskName] = useState(false);
71
+ const [customInputValue, setCustomInputValue] = useState('');
72
+
73
+ const { mutate } = useSWRConfig();
74
+
75
+ const schema = useMemo(
76
+ () =>
77
+ z
78
+ .object({
79
+ taskName: z.string().min(1),
80
+ dueDateType: z.enum(['THIS_VISIT', 'NEXT_VISIT', 'DATE']).optional(),
81
+ dueDate: z.string().optional(),
82
+ rationale: z.string().optional(),
83
+ assignee: optionSchema.optional(),
84
+ assigneeRole: optionSchema.optional(),
85
+ priority: z.enum(['high', 'medium', 'low']).optional(),
86
+ })
87
+ .refine((values) => !(values.assignee && values.assigneeRole), {
88
+ message: t('selectSingleAssignee', 'Select either a provider or a provider role, not both'),
89
+ path: ['assigneeRole'],
90
+ })
91
+ .refine((values) => values.dueDateType !== 'DATE' || values.dueDate, {
92
+ message: t('dueDateRequired', 'Due date is required when Date is selected'),
93
+ path: ['dueDate'],
94
+ }),
95
+ [t],
96
+ );
97
+
98
+ const {
99
+ control,
100
+ handleSubmit,
101
+ setValue,
102
+ watch,
103
+ reset,
104
+ formState: { errors },
105
+ } = useForm<z.infer<typeof schema>>({
106
+ resolver: zodResolver(schema),
107
+ defaultValues: {
108
+ taskName: '',
109
+ dueDateType: undefined,
110
+ dueDate: undefined,
111
+ rationale: '',
112
+ assignee: undefined,
113
+ assigneeRole: undefined,
114
+ priority: undefined,
115
+ },
116
+ });
117
+
118
+ // Populate form with existing task data when editing
119
+ useEffect(() => {
120
+ if (isEditMode && existingTask) {
121
+ const formattedDueDate =
122
+ existingTask.dueDate?.type === 'DATE' && existingTask.dueDate?.date
123
+ ? existingTask.dueDate.date.toISOString().split('T')[0]
124
+ : undefined;
125
+
126
+ reset({
127
+ taskName: existingTask.name,
128
+ dueDateType: existingTask.dueDate?.type,
129
+ dueDate: formattedDueDate,
130
+ rationale: existingTask.rationale ?? '',
131
+ assignee:
132
+ existingTask.assignee?.type === 'person'
133
+ ? { id: existingTask.assignee.uuid, label: existingTask.assignee.display }
134
+ : undefined,
135
+ assigneeRole:
136
+ existingTask.assignee?.type === 'role'
137
+ ? { id: existingTask.assignee.uuid, label: existingTask.assignee.display }
138
+ : undefined,
139
+ priority: existingTask.priority,
140
+ });
141
+
142
+ // Check if the existing task matches a system task
143
+ // If so, set the selected system task state
144
+ if (systemTasks.length > 0) {
145
+ const matchingSystemTask = existingTask.systemTaskUuid
146
+ ? systemTasks.find((st) => st.uuid === existingTask.systemTaskUuid)
147
+ : null;
148
+
149
+ if (matchingSystemTask) {
150
+ setSelectedSystemTask(matchingSystemTask);
151
+ setIsCustomTaskName(false);
152
+ setCustomInputValue('');
153
+ } else {
154
+ setSelectedSystemTask(null);
155
+ setIsCustomTaskName(true);
156
+ setCustomInputValue(existingTask.name);
157
+ }
158
+ }
159
+ }
160
+ }, [isEditMode, existingTask, reset, systemTasks]);
161
+
162
+ const selectedDueDateType = watch('dueDateType');
163
+
164
+ const providerOptions = useMemo(
165
+ () =>
166
+ providers.map((provider) => ({
167
+ id: provider.uuid,
168
+ label: provider.display,
169
+ })),
170
+ [providers],
171
+ );
172
+ const providerRoleOptions = useProviderRoles();
173
+
174
+ const handleSystemTaskSelected = useCallback(
175
+ (task: SystemTask) => {
176
+ setSelectedSystemTask(task);
177
+ setIsCustomTaskName(false);
178
+ setCustomInputValue('');
179
+ setValue('priority', task.priority ?? undefined);
180
+ setValue('rationale', task.rationale ?? '');
181
+ setValue(
182
+ 'assigneeRole',
183
+ task.defaultAssigneeRoleUuid
184
+ ? { id: task.defaultAssigneeRoleUuid, label: task.defaultAssigneeRoleDisplay }
185
+ : undefined,
186
+ );
187
+ setValue('assignee', undefined);
188
+ },
189
+ [setValue],
190
+ );
191
+
192
+ const handleCustomInput = useCallback(
193
+ (inputValue: string) => {
194
+ setSelectedSystemTask(null);
195
+ setIsCustomTaskName(inputValue.length > 0);
196
+ setCustomInputValue(inputValue);
197
+ setValue('priority', undefined);
198
+ setValue('rationale', '');
199
+ setValue('assigneeRole', undefined);
200
+ setValue('assignee', undefined);
201
+ },
202
+ [setValue],
203
+ );
204
+
205
+ const priorityItems = useMemo(
206
+ () => [
207
+ { id: 'high', label: t('priorityHigh', 'High') },
208
+ { id: 'medium', label: t('priorityMedium', 'Medium') },
209
+ { id: 'low', label: t('priorityLow', 'Low') },
210
+ ],
211
+ [t],
212
+ );
213
+
214
+ const handleFormSubmission = useCallback(
215
+ async (data: z.infer<typeof schema>) => {
216
+ try {
217
+ // For visit-based due dates, anchor to the current active visit
218
+ const visitUuid =
219
+ data.dueDateType === 'THIS_VISIT' || data.dueDateType === 'NEXT_VISIT' ? activeVisit?.uuid : undefined;
220
+
221
+ const assignee = data.assignee
222
+ ? { uuid: data.assignee.id, display: data.assignee.label, type: 'person' as const }
223
+ : data.assigneeRole
224
+ ? { uuid: data.assigneeRole.id, display: data.assigneeRole.label, type: 'role' as const }
225
+ : undefined;
226
+
227
+ // For visit-based due dates in edit mode, preserve the original reference visit
228
+ // to avoid re-anchoring the task to a different visit
229
+ const existingVisitUuid =
230
+ isEditMode &&
231
+ existingTask?.dueDate?.type === data.dueDateType &&
232
+ (data.dueDateType === 'THIS_VISIT' || data.dueDateType === 'NEXT_VISIT')
233
+ ? (existingTask.dueDate as { referenceVisitUuid?: string }).referenceVisitUuid
234
+ : undefined;
235
+
236
+ const dueDate: Task['dueDate'] = data.dueDateType
237
+ ? data.dueDateType === 'DATE'
238
+ ? { type: 'DATE', date: parseDate(data.dueDate!) }
239
+ : { type: data.dueDateType, referenceVisitUuid: existingVisitUuid ?? visitUuid }
240
+ : undefined;
241
+
242
+ if (isEditMode && existingTask) {
243
+ const updatedTask: Task = {
244
+ ...existingTask,
245
+ name: data.taskName.trim(),
246
+ dueDate,
247
+ rationale: data.rationale?.trim() || undefined,
248
+ assignee,
249
+ priority: data.priority,
250
+ systemTaskUuid: existingTask.systemTaskUuid,
251
+ };
252
+
253
+ await updateTask(patientUuid, updatedTask);
254
+ await mutate(taskListSWRKey(patientUuid));
255
+ showSnackbar({
256
+ title: t('taskUpdated', 'Task updated'),
257
+ kind: 'success',
258
+ });
259
+ } else {
260
+ const payload: TaskInput = {
261
+ name: data.taskName.trim(),
262
+ dueDate,
263
+ rationale: data.rationale?.trim() || undefined,
264
+ assignee,
265
+ priority: data.priority,
266
+ systemTaskUuid: selectedSystemTask?.uuid,
267
+ };
268
+
269
+ await saveTask(patientUuid, payload);
270
+ await mutate(taskListSWRKey(patientUuid));
271
+ showSnackbar({
272
+ title: t('taskAdded', 'Task added'),
273
+ kind: 'success',
274
+ });
275
+ }
276
+ onClose();
277
+ } catch (error) {
278
+ showSnackbar({
279
+ title: isEditMode ? t('taskUpdateFailed', 'Unable to update task') : t('taskAddFailed', 'Task add failed'),
280
+ kind: 'error',
281
+ });
282
+ }
283
+ },
284
+ [activeVisit, isEditMode, existingTask, patientUuid, mutate, t, selectedSystemTask, onClose],
285
+ );
286
+
287
+ if (isEditMode && isTaskLoading) {
288
+ return <Loader />;
289
+ }
290
+
291
+ return (
292
+ <>
293
+ <div className={styles.formContainer}>
294
+ <Form onSubmit={handleSubmit(handleFormSubmission)}>
295
+ <div className={styles.formSection}>
296
+ <h5 className={styles.formSectionHeader}>{t('task', 'Task')}</h5>
297
+ <InputWrapper>
298
+ <Controller
299
+ name="taskName"
300
+ control={control}
301
+ render={({ field }) => (
302
+ <TaskNameField
303
+ field={field}
304
+ invalid={Boolean(errors.taskName)}
305
+ invalidText={errors.taskName?.message ? t('taskNameRequired', 'Task name is required') : undefined}
306
+ systemTasks={systemTasks}
307
+ isSystemTasksLoading={isSystemTasksLoading}
308
+ isEditMode={isEditMode}
309
+ selectedSystemTask={selectedSystemTask}
310
+ isCustomTaskName={isCustomTaskName}
311
+ customInputValue={customInputValue}
312
+ onSystemTaskSelected={handleSystemTaskSelected}
313
+ onCustomInput={handleCustomInput}
314
+ />
315
+ )}
316
+ />
317
+ </InputWrapper>
318
+
319
+ <InputWrapper>
320
+ <FormGroup legendText={t('dueLabel', 'Due')}>
321
+ <Controller
322
+ name="dueDateType"
323
+ control={control}
324
+ render={({ field: { onChange, value } }) => {
325
+ const dueDateOptions = ['NONE', 'NEXT_VISIT', 'THIS_VISIT', 'DATE'] as const;
326
+ const idx = dueDateOptions.indexOf(value ?? 'NONE');
327
+ const selectedIndex = idx >= 0 ? idx : 0;
328
+
329
+ return (
330
+ <ContentSwitcher
331
+ selectedIndex={selectedIndex}
332
+ onChange={({ name }) => {
333
+ if (name === 'NONE') {
334
+ onChange(undefined);
335
+ setValue('dueDate', undefined);
336
+ } else {
337
+ onChange(name);
338
+ if (name === 'NEXT_VISIT' || name === 'THIS_VISIT') {
339
+ setValue('dueDate', undefined);
340
+ }
341
+ }
342
+ }}
343
+ size="md"
344
+ >
345
+ <Switch name="NONE" text={t('none', 'None')} />
346
+ <Switch name="NEXT_VISIT" text={t('nextVisit', 'Next visit')} disabled={!activeVisit} />
347
+ <Switch name="THIS_VISIT" text={t('thisVisit', 'This visit')} disabled={!activeVisit} />
348
+ <Switch name="DATE" text={t('date', 'Date')} />
349
+ </ContentSwitcher>
350
+ );
351
+ }}
352
+ />
353
+ </FormGroup>
354
+ {selectedDueDateType === 'DATE' && (
355
+ <div className={styles.datePickerWrapper}>
356
+ <Controller
357
+ name="dueDate"
358
+ control={control}
359
+ render={({ field }) => (
360
+ <OpenmrsDatePicker
361
+ id="dueDate"
362
+ labelText={t('dueDateLabel', 'Due date')}
363
+ minDate={new Date()}
364
+ invalid={Boolean(errors.dueDate)}
365
+ invalidText={errors.dueDate?.message}
366
+ value={field.value ? new Date(field.value) : undefined}
367
+ onChange={(date: Date) => field.onChange(date?.toISOString())}
368
+ />
369
+ )}
370
+ />
371
+ </div>
372
+ )}
373
+ </InputWrapper>
374
+
375
+ <InputWrapper>
376
+ <Controller
377
+ name="assignee"
378
+ control={control}
379
+ render={({ field }) => (
380
+ <ComboBox
381
+ id="assignee"
382
+ titleText={t('assignProviderLabel', 'Assign to provider')}
383
+ placeholder={t('assignProviderPlaceholder', 'Search providers')}
384
+ items={providerOptions}
385
+ itemToString={(item) => item?.label ?? ''}
386
+ selectedItem={field.value ?? null}
387
+ onChange={({ selectedItem }) => {
388
+ field.onChange(selectedItem ?? undefined);
389
+ if (selectedItem) {
390
+ setValue('assigneeRole', undefined, { shouldDirty: true, shouldValidate: true });
391
+ }
392
+ }}
393
+ onInputChange={(input) => setProviderQuery(input)}
394
+ helperText={
395
+ isLoadingProviders
396
+ ? `${getCoreTranslation('loading')}...`
397
+ : t('providerSearchHint', 'Start typing to search for providers')
398
+ }
399
+ invalid={Boolean(errors.assignee)}
400
+ invalidText={errors.assignee?.message}
401
+ />
402
+ )}
403
+ />
404
+ </InputWrapper>
405
+
406
+ {allowAssigningProviderRole && (
407
+ <InputWrapper>
408
+ <Controller
409
+ name="assigneeRole"
410
+ control={control}
411
+ render={({ field }) => (
412
+ <ComboBox
413
+ id="assigneeRole"
414
+ titleText={t('assignProviderRoleLabel', 'Assign to provider role')}
415
+ placeholder={t('assignProviderRolePlaceholder', 'Search provider roles')}
416
+ items={providerRoleOptions}
417
+ itemToString={(item) => item?.label ?? ''}
418
+ selectedItem={field.value ?? null}
419
+ onChange={({ selectedItem }) => {
420
+ field.onChange(selectedItem ?? undefined);
421
+ if (selectedItem) {
422
+ setValue('assignee', undefined, { shouldDirty: true, shouldValidate: true });
423
+ }
424
+ }}
425
+ invalid={Boolean(errors.assigneeRole)}
426
+ invalidText={errors.assigneeRole?.message}
427
+ />
428
+ )}
429
+ />
430
+ </InputWrapper>
431
+ )}
432
+
433
+ <InputWrapper>
434
+ <Controller
435
+ name="priority"
436
+ control={control}
437
+ render={({ field }) => (
438
+ <ComboBox
439
+ id="priority"
440
+ titleText={t('priorityLabel', 'Priority')}
441
+ placeholder={t('priorityPlaceholder', 'Select priority (optional)')}
442
+ items={priorityItems}
443
+ itemToString={(item) => item?.label ?? ''}
444
+ selectedItem={
445
+ field.value
446
+ ? {
447
+ id: field.value,
448
+ label: getPriorityLabel(field.value as Priority, t),
449
+ }
450
+ : null
451
+ }
452
+ onChange={({ selectedItem }) => {
453
+ field.onChange(selectedItem?.id ?? undefined);
454
+ }}
455
+ />
456
+ )}
457
+ />
458
+ </InputWrapper>
459
+ </div>
460
+
461
+ <div className={styles.formSection}>
462
+ <h5 className={styles.formSectionHeader}>{t('rationale', 'Rationale')}</h5>
463
+ <InputWrapper>
464
+ <Controller
465
+ name="rationale"
466
+ control={control}
467
+ render={({ field }) => (
468
+ <TextArea
469
+ id="rationale"
470
+ labelText={t('rationaleLabel', 'Explain briefly why this task is necessary (optional)')}
471
+ placeholder={t('rationalePlaceholder', 'Add a note here')}
472
+ rows={4}
473
+ enableCounter
474
+ maxLength={100}
475
+ {...field}
476
+ onChange={(event) => field.onChange(event.target.value)}
477
+ />
478
+ )}
479
+ />
480
+ </InputWrapper>
481
+ </div>
482
+ </Form>
483
+ </div>
484
+ <ButtonSet className={styles.bottomButtons}>
485
+ <Button kind="secondary" onClick={onClose} size="xl">
486
+ {isEditMode ? getCoreTranslation('cancel') : t('discard', 'Discard')}
487
+ </Button>
488
+ <Button kind="primary" size="xl" onClick={handleSubmit(handleFormSubmission)}>
489
+ {isEditMode ? t('saveTask', 'Save task') : t('addTaskButton', 'Add Task')}
490
+ </Button>
491
+ </ButtonSet>
492
+ </>
493
+ );
494
+ };
495
+
496
+ interface TaskNameFieldProps {
497
+ field: { onChange: (value: string) => void; value: string; name: string; onBlur: () => void };
498
+ invalid: boolean;
499
+ invalidText?: string;
500
+ systemTasks: SystemTask[];
501
+ isSystemTasksLoading: boolean;
502
+ isEditMode: boolean;
503
+ selectedSystemTask: SystemTask | null;
504
+ isCustomTaskName: boolean;
505
+ customInputValue: string;
506
+ onSystemTaskSelected: (task: SystemTask) => void;
507
+ onCustomInput: (inputValue: string) => void;
508
+ }
509
+
510
+ function TaskNameField({
511
+ field,
512
+ invalid,
513
+ invalidText,
514
+ systemTasks,
515
+ isSystemTasksLoading,
516
+ isEditMode,
517
+ selectedSystemTask,
518
+ isCustomTaskName,
519
+ customInputValue,
520
+ onSystemTaskSelected,
521
+ onCustomInput,
522
+ }: TaskNameFieldProps) {
523
+ const { t } = useTranslation();
524
+
525
+ if (!systemTasks.length || isSystemTasksLoading) {
526
+ return (
527
+ <TextInput
528
+ id="taskName"
529
+ labelText={t('taskNameLabel', 'Task name')}
530
+ placeholder={t('taskNamePlaceholder', 'Enter task name')}
531
+ invalid={invalid}
532
+ invalidText={invalidText}
533
+ {...field}
534
+ onChange={(event) => field.onChange(event.target.value)}
535
+ />
536
+ );
537
+ }
538
+
539
+ return (
540
+ <>
541
+ {isEditMode && selectedSystemTask && (
542
+ <InlineNotification
543
+ kind="info"
544
+ lowContrast
545
+ hideCloseButton
546
+ title={t('systemTaskEditTitle', "You're editing a system task")}
547
+ subtitle={t('systemTaskEditSubtitle', 'Task name cannot be changed.')}
548
+ className={styles.systemTaskNotification}
549
+ />
550
+ )}
551
+ <div className={selectedSystemTask ? styles.systemTaskSelected : undefined}>
552
+ <ComboBox
553
+ key={isEditMode && isCustomTaskName ? `custom-${customInputValue}` : 'system'}
554
+ id="taskName"
555
+ titleText={t('taskNameLabel', 'Task name')}
556
+ placeholder={t('taskNameComboBoxPlaceholder', 'Enter or select task name')}
557
+ items={systemTasks}
558
+ itemToString={(item: SystemTask | null) => item?.title ?? ''}
559
+ selectedItem={selectedSystemTask}
560
+ initialSelectedItem={
561
+ isEditMode && isCustomTaskName
562
+ ? ({ uuid: 'custom', name: 'custom', title: customInputValue } as SystemTask)
563
+ : undefined
564
+ }
565
+ disabled={isEditMode && !!selectedSystemTask}
566
+ allowCustomValue
567
+ onChange={({ selectedItem, inputValue }) => {
568
+ if (selectedItem) {
569
+ field.onChange(selectedItem.title);
570
+ onSystemTaskSelected(selectedItem);
571
+ } else if (inputValue !== undefined) {
572
+ field.onChange(inputValue);
573
+ onCustomInput(inputValue);
574
+ }
575
+ }}
576
+ onInputChange={(inputValue) => {
577
+ const matchingTask = systemTasks.find((st) => st.title.toLowerCase() === inputValue?.toLowerCase());
578
+ if (!matchingTask) {
579
+ field.onChange(inputValue ?? '');
580
+ onCustomInput(inputValue ?? '');
581
+ }
582
+ }}
583
+ invalid={invalid}
584
+ invalidText={invalidText}
585
+ />
586
+ {isCustomTaskName && !selectedSystemTask && (
587
+ <InlineNotification
588
+ kind="info"
589
+ lowContrast
590
+ hideCloseButton
591
+ title={t('customTaskName', 'Custom task')}
592
+ className={styles.customTaskNotification}
593
+ />
594
+ )}
595
+ </div>
596
+ </>
597
+ );
598
+ }
599
+
600
+ function InputWrapper({ children }: { children: React.ReactNode }) {
601
+ const isTablet = useLayoutType() === 'tablet';
602
+ return (
603
+ <Layer level={isTablet ? 1 : 0}>
604
+ <div className={styles.field}>{children}</div>
605
+ </Layer>
606
+ );
607
+ }
608
+
609
+ export default AddTaskForm;
@@ -0,0 +1,49 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/colors';
3
+ @use '@carbon/type';
4
+
5
+ .formContainer {
6
+ padding: 0 layout.$spacing-05;
7
+ height: 100%;
8
+ }
9
+
10
+ .formSection {
11
+ margin-bottom: layout.$spacing-07;
12
+ }
13
+
14
+ .formSectionHeader {
15
+ margin-bottom: layout.$spacing-05;
16
+ }
17
+
18
+ .field {
19
+ margin-bottom: layout.$spacing-05;
20
+ }
21
+
22
+ .bottomButtons {
23
+ :global(.cds--btn) {
24
+ width: 50%;
25
+ max-width: unset;
26
+ }
27
+ }
28
+
29
+ .datePickerWrapper {
30
+ margin-top: layout.$spacing-03;
31
+ }
32
+
33
+ // System task selection styling
34
+ .systemTaskSelected {
35
+ :global(.cds--combo-box) {
36
+ :global(.cds--text-input) {
37
+ background-color: colors.$blue-10;
38
+ border-color: colors.$blue-60;
39
+ }
40
+ }
41
+ }
42
+
43
+ .systemTaskNotification {
44
+ margin-bottom: layout.$spacing-04;
45
+ }
46
+
47
+ .customTaskNotification {
48
+ margin-top: layout.$spacing-03;
49
+ }