@abgov/nx-adsp 5.9.0-beta.1 → 5.9.0-beta.3

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 (24) hide show
  1. package/generators.json +5 -0
  2. package/package.json +1 -1
  3. package/src/generators/react-form/files/__fileName__/__fileName__.slice.ts__tmpl__ +2 -2
  4. package/src/generators/react-form/files/__fileName__/__fileName__.tsx__tmpl__ +2 -96
  5. package/src/generators/react-form/includes/input-template.ejs +107 -0
  6. package/src/generators/react-form/react-form.js +1 -1
  7. package/src/generators/react-form/react-form.js.map +1 -1
  8. package/src/generators/react-form/react-forms.spec.ts +1 -0
  9. package/src/generators/react-form/schema.d.ts +1 -0
  10. package/src/generators/react-form/schema.json +3 -3
  11. package/src/generators/react-task-list/files/__fileName__/__fileName__.module.css__tmpl__ +80 -0
  12. package/src/generators/react-task-list/files/__fileName__/__fileName__.slice.spec.ts__tmpl__ +26 -0
  13. package/src/generators/react-task-list/files/__fileName__/__fileName__.slice.ts__tmpl__ +723 -0
  14. package/src/generators/react-task-list/files/__fileName__/__fileName__.tsx__tmpl__ +595 -0
  15. package/src/generators/react-task-list/files/__fileName__/task.d.ts__tmpl__ +30 -0
  16. package/src/generators/react-task-list/react-task-list.d.ts +3 -0
  17. package/src/generators/react-task-list/react-task-list.js +124 -0
  18. package/src/generators/react-task-list/react-task-list.js.map +1 -0
  19. package/src/generators/react-task-list/react-task-list.spec.ts +71 -0
  20. package/src/generators/react-task-list/schema.d.ts +14 -0
  21. package/src/generators/react-task-list/schema.json +45 -0
  22. package/src/utils/form.d.ts +1 -0
  23. package/src/utils/form.js.map +1 -1
  24. package/src/utils/task.d.ts +6 -0
@@ -0,0 +1,595 @@
1
+ import {
2
+ GoABadge,
3
+ GoABlock,
4
+ GoAButton,
5
+ GoAButtonGroup,
6
+ GoACallout,
7
+ GoAContainer,
8
+ GoADetails,
9
+ GoADivider,
10
+ GoADropdown,
11
+ GoADropdownItem,
12
+ GoAFormItem,
13
+ GoAModal,
14
+ GoANotification,
15
+ GoARadioGroup,
16
+ GoARadioItem,
17
+ GoASpinner,
18
+ GoATable,
19
+ } from '@abgov/react-components';
20
+ import { FunctionComponent, ReactNode, useEffect, useState } from 'react';
21
+ import { useDispatch, useSelector } from 'react-redux';
22
+ import { AppDispatch } from '../../store';
23
+ import {
24
+ TaskFilter,
25
+ TaskMetric,
26
+ TaskUser,
27
+ assignTask,
28
+ cancelTask,
29
+ completeTask,
30
+ getBusy,
31
+ getFilter,
32
+ getLive,
33
+ getModal,
34
+ getOpenTask,
35
+ getQueueUser,
36
+ getQueueWorkers,
37
+ getTaskMetrics,
38
+ getTasks,
39
+ initializeQueue,
40
+ <%= propertyName %>TaskListActions,
41
+ setTaskPriority,
42
+ startTask,
43
+ } from './<%= fileName %>.slice';
44
+ import styles from './<%= fileName %>.module.css';
45
+ import { Person, Task } from './task';
46
+
47
+ interface TaskListItemProps {
48
+ task: Task;
49
+ isOpen: boolean;
50
+ user: TaskUser;
51
+ onSelect: (task: Task) => void;
52
+ onAssign: (task: Task) => void;
53
+ onSetPriority: (task: Task) => void;
54
+ onOpen: (task: Task) => void;
55
+ }
56
+
57
+ function formatTaskAge(createdOn: string): string {
58
+ if (!createdOn) {
59
+ return '';
60
+ }
61
+
62
+ const hours = Math.round((Date.now() - new Date(createdOn).getTime()) / 36e5);
63
+ if (hours < 24) {
64
+ return `${hours} hours`;
65
+ } else {
66
+ return `${Math.round((hours * 10) / 24) / 10} days`;
67
+ }
68
+ }
69
+
70
+ interface TaskHeaderProps {
71
+ className?: string;
72
+ open: Task;
73
+ isLive: boolean;
74
+ onClickTasks: () => void;
75
+ }
76
+
77
+ const TaskHeader: FunctionComponent<TaskHeaderProps> = ({
78
+ className,
79
+ open,
80
+ isLive,
81
+ onClickTasks,
82
+ }) => {
83
+ return (
84
+ <div className={className}>
85
+ <div>
86
+ {open ? (
87
+ <>
88
+ <GoAButton mt="s" type="tertiary" onClick={onClickTasks}>
89
+ Tasks
90
+ </GoAButton>
91
+ <span>/</span>
92
+ <span>
93
+ {open?.name} - {open?.description}
94
+ </span>
95
+ </>
96
+ ) : (
97
+ <span>Tasks</span>
98
+ )}
99
+ <span>
100
+ {isLive ? (
101
+ <GoABadge mt="m" mb="s" type="success" content="Live" />
102
+ ) : (
103
+ <GoABadge mt="m" mb="s" type="information" content="Not live" />
104
+ )}
105
+ </span>
106
+ </div>
107
+ <GoADivider mb="m" />
108
+ </div>
109
+ );
110
+ };
111
+
112
+ interface TaskMetricsProps {
113
+ metrics: TaskMetric[];
114
+ }
115
+
116
+ const TaskMetrics: FunctionComponent<TaskMetricsProps> = ({ metrics }) => {
117
+ return (
118
+ <GoABlock mt="m">
119
+ {metrics.map((metric) => (
120
+ <GoAContainer type="non-interactive" accent="thin">
121
+ <label>{metric.name}</label>
122
+ <div style={{ marginTop: 16 }}>
123
+ <span
124
+ style={{
125
+ fontWeight: 'bold',
126
+ fontSize: 'xx-large',
127
+ marginRight: 6,
128
+ }}
129
+ >
130
+ {metric.value}
131
+ </span>
132
+ <span style={{ fontWeight: 'lighter' }}>{metric.unit}</span>
133
+ </div>
134
+ </GoAContainer>
135
+ ))}
136
+ </GoABlock>
137
+ );
138
+ };
139
+
140
+ const TaskListItem: FunctionComponent<TaskListItemProps> = ({
141
+ task,
142
+ isOpen,
143
+ user,
144
+ onSelect,
145
+ onAssign,
146
+ onSetPriority,
147
+ onOpen,
148
+ }) => {
149
+ return (
150
+ <tr onClick={() => onSelect(task)}>
151
+ <td>{task.priority}</td>
152
+ <td>{formatTaskAge(task.createdOn)}</td>
153
+ <td>
154
+ <span>{task.name}</span> - <span>{task.description}</span>
155
+ </td>
156
+ <td>{task.status}</td>
157
+ <td>
158
+ {task.assignment?.assignedTo
159
+ ? task.assignment.assignedTo.name
160
+ : 'No one'}
161
+ </td>
162
+ <td>
163
+ <GoAButtonGroup alignment="end">
164
+ {user.isAssigner && (
165
+ <>
166
+ <GoAButton
167
+ size="compact"
168
+ type="secondary"
169
+ onClick={() => onAssign(task)}
170
+ >
171
+ Assign
172
+ </GoAButton>
173
+ <GoAButton
174
+ size="compact"
175
+ type="secondary"
176
+ onClick={() => onSetPriority(task)}
177
+ >
178
+ Set priority
179
+ </GoAButton>
180
+ </>
181
+ )}
182
+ {!user.isAssigner && user.isWorker && (
183
+ <GoAButton
184
+ size="compact"
185
+ type="secondary"
186
+ onClick={() => onAssign(task)}
187
+ disabled={task.assignment?.assignedTo?.id === user.id}
188
+ >
189
+ Assign to me
190
+ </GoAButton>
191
+ )}
192
+ <GoAButton
193
+ size="compact"
194
+ type="primary"
195
+ disabled={isOpen}
196
+ onClick={() => onOpen(task)}
197
+ >
198
+ Open
199
+ </GoAButton>
200
+ </GoAButtonGroup>
201
+ </td>
202
+ </tr>
203
+ );
204
+ };
205
+
206
+ interface TaskListProps {
207
+ className?: string;
208
+ metrics: TaskMetric[];
209
+ filter: TaskFilter;
210
+ tasks: Task[];
211
+ selected: Task;
212
+ open: Task;
213
+ user: TaskUser;
214
+ onSetFilter: (filter: TaskFilter) => void;
215
+ onSelect: (task: Task) => void;
216
+ onAssign: (task: Task) => void;
217
+ onSetPriority: (task: Task) => void;
218
+ onOpen: (task: Task) => void;
219
+ }
220
+
221
+ const TaskList: FunctionComponent<TaskListProps> = ({
222
+ className,
223
+ metrics,
224
+ filter,
225
+ tasks,
226
+ open,
227
+ user,
228
+ onSetFilter,
229
+ onSelect,
230
+ onAssign,
231
+ onSetPriority,
232
+ onOpen,
233
+ }) => {
234
+ return (
235
+ <div className={className} data-opened={!!open}>
236
+ <div>
237
+ <TaskMetrics metrics={metrics} />
238
+ <GoAFormItem label="Filter" mr="s">
239
+ <GoADropdown
240
+ onChange={(_, filter) => onSetFilter(filter as TaskFilter)}
241
+ value={filter}
242
+ >
243
+ <GoADropdownItem label="Active" value="active" />
244
+ <GoADropdownItem label="My tasks" value="assigned" />
245
+ <GoADropdownItem label="Pending" value="pending" />
246
+ </GoADropdown>
247
+ </GoAFormItem>
248
+ </div>
249
+ <GoATable mt="l" width="100%">
250
+ <colgroup>
251
+ <col style={{ width: 95 }} />
252
+ <col style={{ width: 90 }} />
253
+ <col />
254
+ <col style={{ width: 95 }} />
255
+ <col />
256
+ <col style={{ width: 330 }} />
257
+ </colgroup>
258
+ <thead>
259
+ <tr>
260
+ <th>Priority</th>
261
+ <th>Age</th>
262
+ <th>Task</th>
263
+ <th>Status</th>
264
+ <th>Assigned</th>
265
+ <th style={{ textAlign: 'right' }}>Actions</th>
266
+ </tr>
267
+ </thead>
268
+ <tbody>
269
+ {tasks.map((task) => (
270
+ <TaskListItem
271
+ key={task.id}
272
+ task={task}
273
+ isOpen={open?.id === task.id}
274
+ user={user}
275
+ onSelect={onSelect}
276
+ onAssign={onAssign}
277
+ onSetPriority={onSetPriority}
278
+ onOpen={onOpen}
279
+ />
280
+ ))}
281
+ </tbody>
282
+ </GoATable>
283
+ </div>
284
+ );
285
+ };
286
+
287
+ interface TaskDetailsProps {
288
+ className?: string;
289
+ open: Task;
290
+ children: ReactNode;
291
+ }
292
+
293
+ const TaskDetails: FunctionComponent<TaskDetailsProps> = ({
294
+ className,
295
+ open,
296
+ children,
297
+ }) => {
298
+ return (
299
+ <div data-opened={!!open} className={className}>
300
+ {open && children}
301
+ </div>
302
+ );
303
+ };
304
+
305
+ interface TaskAssignmentModalProps {
306
+ user: TaskUser;
307
+ task: Task;
308
+ open: boolean;
309
+ workers: Person[];
310
+ executing: boolean;
311
+ onAssign: (assignTo: Person) => void;
312
+ onClose: () => void;
313
+ }
314
+
315
+ const TaskAssignmentModal: FunctionComponent<TaskAssignmentModalProps> = ({
316
+ user,
317
+ task,
318
+ workers,
319
+ open,
320
+ executing,
321
+ onAssign,
322
+ onClose,
323
+ }) => {
324
+ const [selected, setSelected] = useState<string>();
325
+ useEffect(() => {
326
+ setSelected(task?.assignment?.assignedTo?.id || '');
327
+ }, [task]);
328
+
329
+ return (
330
+ <GoAModal heading="Assign task" open={open} onClose={onClose}>
331
+ <form>
332
+ <p>
333
+ Assign task {task?.name} to a person. Only the assigned person will be
334
+ able to progress the task.
335
+ </p>
336
+ <div>
337
+ {task?.assignment?.assignedTo ? (
338
+ <>
339
+ <span>Task is currently assigned to: </span>
340
+ <span>{task.assignment.assignedTo.name}</span>
341
+ </>
342
+ ) : (
343
+ <span>Task is currently not assigned.</span>
344
+ )}
345
+ </div>
346
+ {user.isAssigner ? (
347
+ <GoAFormItem label="Assign task to" mt="m" mb="4xl">
348
+ <GoADropdown
349
+ native={true}
350
+ value={task?.assignment?.assignedTo?.id}
351
+ onChange={(_, id) => setSelected(id as string)}
352
+ >
353
+ <GoADropdownItem key="no one" value="" label="No one" />
354
+ {workers.map((w) => (
355
+ <GoADropdownItem key={w.id} value={w.id} label={w.name} />
356
+ ))}
357
+ </GoADropdown>
358
+ </GoAFormItem>
359
+ ) : (
360
+ <div>Assign this task to yourself?</div>
361
+ )}
362
+ <GoAButtonGroup alignment="end" mt="4xl">
363
+ <GoAButton type="secondary" onClick={onClose}>
364
+ Cancel
365
+ </GoAButton>
366
+ <GoAButton
367
+ disabled={
368
+ executing ||
369
+ (user.isAssigner &&
370
+ selected === (task?.assignment?.assignedTo?.id || ''))
371
+ }
372
+ type="primary"
373
+ onClick={() => {
374
+ onAssign(
375
+ user.isAssigner
376
+ ? selected
377
+ ? workers.find((w) => w.id === selected)
378
+ : null
379
+ : user
380
+ );
381
+ }}
382
+ >
383
+ Assign
384
+ </GoAButton>
385
+ </GoAButtonGroup>
386
+ </form>
387
+ </GoAModal>
388
+ );
389
+ };
390
+
391
+ interface TaskPriorityModal {
392
+ task: Task;
393
+ open: boolean;
394
+ executing: boolean;
395
+ onSetPriority: (priority: string) => void;
396
+ onClose: () => void;
397
+ }
398
+
399
+ const TaskPriorityModal: FunctionComponent<TaskPriorityModal> = ({
400
+ task,
401
+ open,
402
+ executing,
403
+ onSetPriority,
404
+ onClose,
405
+ }) => {
406
+ const [priority, setPriority] = useState<string>();
407
+ useEffect(() => {
408
+ setPriority(task?.priority);
409
+ }, [task]);
410
+
411
+ return (
412
+ <GoAModal heading="Set task priority" open={open} onClose={onClose}>
413
+ <form>
414
+ <p>
415
+ Set the priority for {task?.name}. Higher priority tasks will appear
416
+ at the top of the list.
417
+ </p>
418
+ <div>
419
+ <span>Priority is currently set to: </span>
420
+ <span>{task?.priority}</span>
421
+ </div>
422
+ <GoAFormItem label="Set priority to" mt="m">
423
+ <GoARadioGroup
424
+ name="priority"
425
+ value={task?.priority}
426
+ onChange={(_, value) => setPriority(value)}
427
+ >
428
+ <GoARadioItem name="Urgent" value="Urgent" />
429
+ <GoARadioItem name="High" value="High" />
430
+ <GoARadioItem name="Normal" value="Normal" />
431
+ </GoARadioGroup>
432
+ </GoAFormItem>
433
+ <GoAButtonGroup alignment="end" mt="4xl">
434
+ <GoAButton type="secondary" onClick={onClose}>
435
+ Cancel
436
+ </GoAButton>
437
+ <GoAButton
438
+ type="primary"
439
+ disabled={executing || (priority && priority === task?.priority)}
440
+ onClick={() => onSetPriority(priority)}
441
+ >
442
+ Set priority
443
+ </GoAButton>
444
+ </GoAButtonGroup>
445
+ </form>
446
+ </GoAModal>
447
+ );
448
+ };
449
+
450
+ export const <%= className %>TaskList: FunctionComponent = () => {
451
+ const user = useSelector(getQueueUser);
452
+ const live = useSelector(getLive);
453
+ const metrics = useSelector(getTaskMetrics);
454
+ const filter = useSelector(getFilter);
455
+ const busy = useSelector(getBusy);
456
+ const modal = useSelector(getModal);
457
+ const tasks = useSelector(getTasks);
458
+ const open = useSelector(getOpenTask);
459
+ const workers = useSelector(getQueueWorkers);
460
+
461
+ const dispatch = useDispatch<AppDispatch>();
462
+ useEffect(() => {
463
+ dispatch(
464
+ initializeQueue({ namespace: '<%= queueNamespace %>', name: '<%= queueName %>' })
465
+ );
466
+ }, [dispatch]);
467
+
468
+ return (
469
+ <div className={styles.taskList}>
470
+ <GoANotification type="important">
471
+ This is a generated rapid prototype. Use it as a starting point to build
472
+ the right thing for users.
473
+ </GoANotification>
474
+ <TaskHeader
475
+ className={styles.header}
476
+ open={open}
477
+ isLive={live}
478
+ onClickTasks={() => dispatch(<%= propertyName %>TaskListActions.setOpenTask())}
479
+ />
480
+ <TaskDetails className={styles.details} open={open}>
481
+ {/* Replace this with task detail view so user can view and complete task. */}
482
+ <div className={styles.detailsPlaceholder}>
483
+ <div>
484
+ <GoACallout type="information" heading="Task detail view">
485
+ This is a placeholder for the task detail view. Replace with your
486
+ own custom view for the specific type of task that users will work
487
+ with.
488
+ </GoACallout>
489
+ <GoADetails ml="s" heading="Show task specific view">
490
+ Replace this with a custom view so user can view and perform
491
+ tasks. For example, if the task is to process a submission, show
492
+ the form fields and attached files for the assessor.
493
+ </GoADetails>
494
+ <GoADetails ml="s" heading="Update task based on user actions">
495
+ Tasks can be started, completed, and cancelled. Perform task
496
+ lifecycle actions as part of the task specific user action. For
497
+ example, if the task is to process a submission, the assessor's
498
+ action to record a decision should complete the task.
499
+ </GoADetails>
500
+ </div>
501
+ <GoAButtonGroup alignment="end" mt="l">
502
+ <GoAButton
503
+ type="secondary"
504
+ onClick={() => dispatch(<%= propertyName %>TaskListActions.setOpenTask())}
505
+ >
506
+ Close
507
+ </GoAButton>
508
+ {open?.status === 'Pending' && (
509
+ <GoAButton
510
+ disabled={busy.executing}
511
+ onClick={() => dispatch(startTask({ taskId: open?.id }))}
512
+ >
513
+ Start task
514
+ </GoAButton>
515
+ )}
516
+ {open?.status === 'In Progress' && (
517
+ <>
518
+ <GoAButton
519
+ type="secondary"
520
+ disabled={busy.executing}
521
+ onClick={() =>
522
+ dispatch(cancelTask({ taskId: open?.id, reason: null }))
523
+ }
524
+ >
525
+ Cancel task
526
+ </GoAButton>
527
+ <GoAButton
528
+ disabled={busy.executing}
529
+ onClick={() => dispatch(completeTask({ taskId: open?.id }))}
530
+ >
531
+ Complete task
532
+ </GoAButton>
533
+ </>
534
+ )}
535
+ </GoAButtonGroup>
536
+ </div>
537
+ </TaskDetails>
538
+ {busy.initializing ||
539
+ (busy.loading && (
540
+ <div className={styles.loading}>
541
+ <GoASpinner size="large" type="infinite" />
542
+ </div>
543
+ ))}
544
+ <TaskList
545
+ className={styles.list}
546
+ metrics={metrics}
547
+ filter={filter}
548
+ tasks={tasks}
549
+ open={open}
550
+ selected={null}
551
+ user={user}
552
+ onSetFilter={(filter) =>
553
+ dispatch(<%= propertyName %>TaskListActions.setFilter(filter))
554
+ }
555
+ onSelect={() => {
556
+ // not used
557
+ }}
558
+ onAssign={(task) =>
559
+ dispatch(<%= propertyName %>TaskListActions.setTaskToAssign(task))
560
+ }
561
+ onSetPriority={(task) =>
562
+ dispatch(<%= propertyName %>TaskListActions.setTaskToPrioritize(task))
563
+ }
564
+ onOpen={(task) => dispatch(<%= propertyName %>TaskListActions.setOpenTask(task.id))}
565
+ />
566
+ <TaskAssignmentModal
567
+ user={user}
568
+ task={modal.taskToAssign}
569
+ workers={workers}
570
+ open={!!modal.taskToAssign}
571
+ executing={busy.executing}
572
+ onAssign={(assignTo) =>
573
+ dispatch(assignTask({ taskId: modal.taskToAssign.id, assignTo }))
574
+ }
575
+ onClose={() => dispatch(<%= propertyName %>TaskListActions.setTaskToAssign(null))}
576
+ />
577
+ <TaskPriorityModal
578
+ task={modal.taskToPrioritize}
579
+ open={!!modal.taskToPrioritize}
580
+ executing={busy.executing}
581
+ onSetPriority={(priority) =>
582
+ dispatch(
583
+ setTaskPriority({
584
+ taskId: modal.taskToPrioritize.id,
585
+ priority,
586
+ })
587
+ )
588
+ }
589
+ onClose={() =>
590
+ dispatch(<%= propertyName %>TaskListActions.setTaskToPrioritize(null))
591
+ }
592
+ />
593
+ </div>
594
+ );
595
+ };
@@ -0,0 +1,30 @@
1
+ export interface QueueDefinition {
2
+ namespace: string;
3
+ name: string;
4
+ description: string;
5
+ assignerRoles: string[];
6
+ workerRoles: string[];
7
+ }
8
+
9
+ export interface Task {
10
+ id: string;
11
+ name: string;
12
+ description: string;
13
+ priority: string;
14
+ status: 'Pending' | 'In Progress' | 'Stopped' | 'Cancelled' | 'Completed';
15
+ createdOn: string;
16
+ startedOn: string;
17
+ endedOn: string;
18
+ assignment: {
19
+ assignedTo: {
20
+ id: string;
21
+ name: string;
22
+ };
23
+ };
24
+ }
25
+
26
+ export interface Person {
27
+ id: string;
28
+ name: string;
29
+ email: string;
30
+ }
@@ -0,0 +1,3 @@
1
+ import { Tree } from '@nrwl/devkit';
2
+ import { Schema } from './schema';
3
+ export default function (host: Tree, options: Schema): Promise<void>;