@openmrs/esm-task-list-app 1.0.0
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.
- package/.editorconfig +12 -0
- package/.eslintignore +2 -0
- package/.eslintrc +57 -0
- package/.husky/pre-commit +7 -0
- package/.husky/pre-push +6 -0
- package/.prettierignore +13 -0
- package/.turbo.json +18 -0
- package/.yarn/plugins/@yarnpkg/plugin-outdated.cjs +35 -0
- package/LICENSE +401 -0
- package/README.md +28 -0
- package/__mocks__/react-i18next.js +73 -0
- package/example.env +6 -0
- package/jest.config.js +34 -0
- package/package.json +108 -0
- package/prettier.config.js +8 -0
- package/src/config-schema.ts +13 -0
- package/src/declarations.d.ts +5 -0
- package/src/index.ts +24 -0
- package/src/launch-button/task-list-launch-button.extension.tsx +20 -0
- package/src/loader/loader.component.tsx +12 -0
- package/src/loader/loader.scss +9 -0
- package/src/routes.json +28 -0
- package/src/types.d.ts +9 -0
- package/src/workspace/add-task-form.component.tsx +551 -0
- package/src/workspace/add-task-form.scss +58 -0
- package/src/workspace/add-task-form.test.tsx +458 -0
- package/src/workspace/delete-task.modal.tsx +71 -0
- package/src/workspace/delete-task.scss +7 -0
- package/src/workspace/task-details-view.component.tsx +212 -0
- package/src/workspace/task-details-view.scss +67 -0
- package/src/workspace/task-details-view.test.tsx +411 -0
- package/src/workspace/task-list-view.component.tsx +154 -0
- package/src/workspace/task-list-view.scss +150 -0
- package/src/workspace/task-list.resource.ts +570 -0
- package/src/workspace/task-list.scss +37 -0
- package/src/workspace/task-list.workspace.tsx +88 -0
- package/tools/i18next-parser.config.js +89 -0
- package/tools/setup-tests.ts +8 -0
- package/tools/update-openmrs-deps.mjs +43 -0
- package/translations/en.json +63 -0
- package/tsconfig.json +24 -0
- package/webpack.config.js +1 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { Checkbox, Tile, Tag, Layer } from '@carbon/react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { showSnackbar, useLayoutType, EmptyCardIllustration } from '@openmrs/esm-framework';
|
|
6
|
+
import { type Task, useTaskList, setTaskStatusCompleted, getPriorityLabel } from './task-list.resource';
|
|
7
|
+
import Loader from '../loader/loader.component';
|
|
8
|
+
import styles from './task-list-view.scss';
|
|
9
|
+
|
|
10
|
+
export interface TaskListViewProps {
|
|
11
|
+
patientUuid: string;
|
|
12
|
+
onTaskClick?: (task: Task) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const TaskListView: React.FC<TaskListViewProps> = ({ patientUuid, onTaskClick }) => {
|
|
16
|
+
const { t } = useTranslation();
|
|
17
|
+
const { tasks, isLoading, error, mutate } = useTaskList(patientUuid);
|
|
18
|
+
const isTablet = useLayoutType() === 'tablet';
|
|
19
|
+
const [pendingUpdates, setPendingUpdates] = useState<Set<string>>(new Set());
|
|
20
|
+
|
|
21
|
+
const addPendingUpdate = useCallback((uuid: string) => {
|
|
22
|
+
setPendingUpdates((current) => {
|
|
23
|
+
const next = new Set(current);
|
|
24
|
+
next.add(uuid);
|
|
25
|
+
return next;
|
|
26
|
+
});
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
const removePendingUpdate = useCallback((uuid: string) => {
|
|
30
|
+
setPendingUpdates((current) => {
|
|
31
|
+
const next = new Set(current);
|
|
32
|
+
next.delete(uuid);
|
|
33
|
+
return next;
|
|
34
|
+
});
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
const handleToggle = useCallback(
|
|
38
|
+
async (task: Task, checked: boolean) => {
|
|
39
|
+
addPendingUpdate(task.uuid);
|
|
40
|
+
try {
|
|
41
|
+
await setTaskStatusCompleted(patientUuid, task, checked);
|
|
42
|
+
await mutate();
|
|
43
|
+
} catch (_error) {
|
|
44
|
+
showSnackbar({
|
|
45
|
+
title: t('taskUpdateFailed', 'Unable to update task'),
|
|
46
|
+
kind: 'error',
|
|
47
|
+
});
|
|
48
|
+
} finally {
|
|
49
|
+
removePendingUpdate(task.uuid);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
[addPendingUpdate, mutate, patientUuid, removePendingUpdate, t],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const isOverdue = useCallback((task: Task) => {
|
|
56
|
+
if (task.completed || !task.dueDate) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
const dueDate = task.dueDate.date;
|
|
60
|
+
if (!dueDate) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const now = new Date();
|
|
64
|
+
// Create new Date objects for comparison without mutating originals
|
|
65
|
+
const nowDateOnly = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
66
|
+
const dueDateOnly = new Date(dueDate.getFullYear(), dueDate.getMonth(), dueDate.getDate());
|
|
67
|
+
return dueDateOnly < nowDateOnly;
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
if (isLoading) {
|
|
71
|
+
return <Loader />;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (error) {
|
|
75
|
+
return <p className={styles.errorText}>{t('taskLoadError', 'There was a problem loading the task list.')}</p>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!tasks || tasks.length === 0) {
|
|
79
|
+
return (
|
|
80
|
+
<Layer>
|
|
81
|
+
<Tile className={styles.emptyStateTile}>
|
|
82
|
+
<div className={styles.emptyStateTileContent}>
|
|
83
|
+
<EmptyCardIllustration />
|
|
84
|
+
<p className={styles.emptyStateContent}>{t('noTasksMessage', 'No tasks to display')}</p>
|
|
85
|
+
</div>
|
|
86
|
+
</Tile>
|
|
87
|
+
</Layer>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<ul className={styles.taskList}>
|
|
93
|
+
{tasks.map((task) => {
|
|
94
|
+
const isUpdating = pendingUpdates.has(task.uuid);
|
|
95
|
+
const overdue = isOverdue(task);
|
|
96
|
+
const assigneeDisplay = task.assignee
|
|
97
|
+
? (task.assignee.display ?? task.assignee.uuid)
|
|
98
|
+
: t('noAssignment', 'No assignment');
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<li key={task.uuid}>
|
|
102
|
+
<Tile
|
|
103
|
+
role="listitem"
|
|
104
|
+
className={classNames(styles.taskTile, {
|
|
105
|
+
[styles.tabletTaskTile]: isTablet,
|
|
106
|
+
[styles.completedTile]: task.completed,
|
|
107
|
+
})}
|
|
108
|
+
>
|
|
109
|
+
<div className={styles.taskTileCheckbox}>
|
|
110
|
+
<div
|
|
111
|
+
className={classNames(styles.checkboxWrapper, {
|
|
112
|
+
[styles.completedCheckbox]: task.completed,
|
|
113
|
+
})}
|
|
114
|
+
>
|
|
115
|
+
<Checkbox
|
|
116
|
+
id={`task-${task.uuid}`}
|
|
117
|
+
labelText=""
|
|
118
|
+
checked={task.completed}
|
|
119
|
+
disabled={isUpdating}
|
|
120
|
+
onChange={(_, { checked }) => handleToggle(task, checked)}
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
<button onClick={() => onTaskClick?.(task)} className={styles.taskTileContentButton}>
|
|
125
|
+
<div className={styles.taskNameWrapper}>
|
|
126
|
+
<span>{task.name}</span>
|
|
127
|
+
{task.rationale && <div className={styles.taskRationalePreview}>{task.rationale}</div>}
|
|
128
|
+
<div className={styles.taskAssignee}>{assigneeDisplay}</div>
|
|
129
|
+
</div>
|
|
130
|
+
<div className={styles.taskTags}>
|
|
131
|
+
{task.priority && (
|
|
132
|
+
<Tag
|
|
133
|
+
type={task.priority === 'high' ? 'red' : task.priority === 'medium' ? 'gray' : 'green'}
|
|
134
|
+
size="sm"
|
|
135
|
+
>
|
|
136
|
+
{getPriorityLabel(task.priority, t)}
|
|
137
|
+
</Tag>
|
|
138
|
+
)}
|
|
139
|
+
{overdue && (
|
|
140
|
+
<Tag type="red" size="sm">
|
|
141
|
+
{t('overdue', 'Overdue')}
|
|
142
|
+
</Tag>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
</button>
|
|
146
|
+
</Tile>
|
|
147
|
+
</li>
|
|
148
|
+
);
|
|
149
|
+
})}
|
|
150
|
+
</ul>
|
|
151
|
+
);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export default TaskListView;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/type';
|
|
4
|
+
@use '@openmrs/esm-styleguide/src/vars' as *;
|
|
5
|
+
|
|
6
|
+
.taskList {
|
|
7
|
+
padding: layout.$spacing-03;
|
|
8
|
+
display: grid;
|
|
9
|
+
gap: layout.$spacing-02;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.helperText {
|
|
13
|
+
margin: layout.$spacing-05 0;
|
|
14
|
+
color: $interactive-01;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.errorText {
|
|
18
|
+
margin: layout.$spacing-05 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.taskTile {
|
|
22
|
+
padding: 0;
|
|
23
|
+
border: 1px solid $grey-2;
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: row;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.tabletTaskTile {
|
|
29
|
+
&:not(:last-of-type) {
|
|
30
|
+
margin-bottom: layout.$spacing-04;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.taskTileCheckbox {
|
|
35
|
+
padding: layout.$spacing-03 0 layout.$spacing-03 layout.$spacing-05;
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: flex-start;
|
|
38
|
+
gap: layout.$spacing-03;
|
|
39
|
+
min-width: 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.taskTileContentButton {
|
|
43
|
+
padding: layout.$spacing-03 layout.$spacing-05;
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: flex-start;
|
|
46
|
+
gap: layout.$spacing-03;
|
|
47
|
+
min-width: 0;
|
|
48
|
+
border: none;
|
|
49
|
+
background-color: transparent;
|
|
50
|
+
text-align: left;
|
|
51
|
+
flex: 1;
|
|
52
|
+
transition: background-color 1s ease-in-out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.taskTileContentButton:hover {
|
|
56
|
+
background-color: rgba(colors.$gray-100, 0.03);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.completedTile {
|
|
60
|
+
opacity: 0.65;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.taskNameWrapper {
|
|
64
|
+
display: flex;
|
|
65
|
+
flex-direction: column;
|
|
66
|
+
gap: layout.$spacing-02;
|
|
67
|
+
flex: 1;
|
|
68
|
+
min-width: 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.taskRationalePreview {
|
|
72
|
+
font-size: 0.875rem;
|
|
73
|
+
color: $text-02;
|
|
74
|
+
line-height: 1.4;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.taskAssignee {
|
|
78
|
+
@include type.type-style('label-01');
|
|
79
|
+
color: $text-02;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.taskTags {
|
|
83
|
+
display: flex;
|
|
84
|
+
gap: layout.$spacing-02;
|
|
85
|
+
flex-shrink: 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.checkboxWrapper {
|
|
89
|
+
:global(.cds--checkbox-label) {
|
|
90
|
+
font-weight: 600;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.completedCheckbox {
|
|
95
|
+
:global(.cds--checkbox-label) {
|
|
96
|
+
text-decoration: line-through;
|
|
97
|
+
color: $text-02;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.dueDate {
|
|
102
|
+
font-size: 0.875rem;
|
|
103
|
+
color: $text-02;
|
|
104
|
+
white-space: nowrap;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.taskBody {
|
|
108
|
+
display: grid;
|
|
109
|
+
gap: layout.$spacing-03;
|
|
110
|
+
color: $text-02;
|
|
111
|
+
font-size: 0.875rem;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.taskMeta {
|
|
115
|
+
display: flex;
|
|
116
|
+
flex-wrap: wrap;
|
|
117
|
+
column-gap: layout.$spacing-02;
|
|
118
|
+
row-gap: layout.$spacing-01;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.metaLabel {
|
|
122
|
+
font-weight: 600;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.taskRationale {
|
|
126
|
+
display: grid;
|
|
127
|
+
gap: layout.$spacing-02;
|
|
128
|
+
|
|
129
|
+
p {
|
|
130
|
+
margin: 0;
|
|
131
|
+
line-height: 1.4;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.emptyStateTile {
|
|
136
|
+
padding: layout.$spacing-06 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.emptyStateTileContent {
|
|
140
|
+
display: flex;
|
|
141
|
+
flex-direction: column;
|
|
142
|
+
align-items: center;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.emptyStateContent {
|
|
146
|
+
@include type.type-style('heading-compact-01');
|
|
147
|
+
color: colors.$gray-70;
|
|
148
|
+
margin-top: layout.$spacing-05;
|
|
149
|
+
margin-bottom: layout.$spacing-03;
|
|
150
|
+
}
|