@striae-org/striae 4.2.0 → 4.2.1
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/LICENSE +1 -1
- package/app/components/actions/case-manage.ts +50 -17
- package/app/components/audit/viewer/audit-entries-list.tsx +5 -2
- package/app/components/audit/viewer/use-audit-viewer-data.ts +6 -3
- package/app/components/audit/viewer/use-audit-viewer-export.ts +1 -1
- package/app/components/canvas/confirmation/confirmation.tsx +6 -2
- package/app/components/colors/colors.module.css +4 -3
- package/app/components/navbar/navbar.tsx +34 -9
- package/app/components/sidebar/cases/case-sidebar.tsx +44 -70
- package/app/components/sidebar/cases/cases-modal.tsx +76 -35
- package/app/components/sidebar/cases/cases.module.css +20 -0
- package/app/components/sidebar/files/files-modal.tsx +37 -39
- package/app/components/sidebar/notes/addl-notes-modal.tsx +82 -0
- package/app/components/sidebar/notes/{notes-sidebar.tsx → notes-editor-form.tsx} +37 -74
- package/app/components/sidebar/notes/notes-editor-modal.tsx +5 -7
- package/app/components/sidebar/notes/notes.module.css +27 -11
- package/app/components/sidebar/sidebar-container.tsx +1 -0
- package/app/components/sidebar/sidebar.tsx +3 -0
- package/app/{tailwind.css → global.css} +1 -3
- package/app/hooks/useOverlayDismiss.ts +6 -4
- package/app/root.tsx +1 -1
- package/app/routes/striae/striae.tsx +6 -0
- package/app/services/audit/audit.service.ts +2 -2
- package/app/services/audit/builders/audit-event-builders-case-file.ts +1 -1
- package/app/types/audit.ts +1 -0
- package/app/utils/data/confirmation-summary/summary-core.ts +279 -0
- package/app/utils/data/data-operations.ts +17 -861
- package/app/utils/data/index.ts +11 -1
- package/app/utils/data/operations/batch-operations.ts +113 -0
- package/app/utils/data/operations/case-operations.ts +168 -0
- package/app/utils/data/operations/confirmation-summary-operations.ts +301 -0
- package/app/utils/data/operations/file-annotation-operations.ts +196 -0
- package/app/utils/data/operations/index.ts +7 -0
- package/app/utils/data/operations/signing-operations.ts +225 -0
- package/app/utils/data/operations/types.ts +42 -0
- package/app/utils/data/operations/validation-operations.ts +48 -0
- package/app/utils/forensics/export-verification.ts +40 -111
- package/functions/api/_shared/firebase-auth.ts +2 -7
- package/functions/api/image/[[path]].ts +20 -23
- package/functions/api/pdf/[[path]].ts +27 -8
- package/package.json +5 -10
- package/scripts/deploy-primershear-emails.sh +1 -1
- package/worker-configuration.d.ts +2 -2
- package/workers/audit-worker/package.json +1 -1
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +1 -1
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/package.json +1 -1
- package/workers/image-worker/src/image-worker.example.ts +16 -5
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/package.json +1 -1
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/package.json +1 -1
- package/workers/pdf-worker/src/formats/format-striae.ts +1 -7
- package/workers/pdf-worker/src/pdf-worker.example.ts +37 -58
- package/workers/pdf-worker/src/report-types.ts +3 -3
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +1 -1
- package/workers/user-worker/src/user-worker.example.ts +17 -0
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/NOTICE +0 -13
- package/app/components/sidebar/notes/notes-modal.tsx +0 -52
- package/postcss.config.js +0 -6
- package/tailwind.config.ts +0 -22
|
@@ -3,7 +3,7 @@ import { useState, useContext, useEffect } from 'react';
|
|
|
3
3
|
import { AuthContext } from '~/contexts/auth.context';
|
|
4
4
|
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
5
5
|
import { deleteFile } from '~/components/actions/image-manage';
|
|
6
|
-
import {
|
|
6
|
+
import { ensureCaseConfirmationSummary } from '~/utils/data';
|
|
7
7
|
import { type FileData } from '~/types';
|
|
8
8
|
import styles from './files-modal.module.css';
|
|
9
9
|
|
|
@@ -16,6 +16,7 @@ interface FilesModalProps {
|
|
|
16
16
|
setFiles: React.Dispatch<React.SetStateAction<FileData[]>>;
|
|
17
17
|
isReadOnly?: boolean;
|
|
18
18
|
selectedFileId?: string;
|
|
19
|
+
confirmationSaveVersion?: number;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
const FILES_PER_PAGE = 10;
|
|
@@ -28,7 +29,17 @@ interface FileConfirmationStatus {
|
|
|
28
29
|
};
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
export const FilesModal = ({
|
|
32
|
+
export const FilesModal = ({
|
|
33
|
+
isOpen,
|
|
34
|
+
onClose,
|
|
35
|
+
onFileSelect,
|
|
36
|
+
currentCase,
|
|
37
|
+
files,
|
|
38
|
+
setFiles,
|
|
39
|
+
isReadOnly = false,
|
|
40
|
+
selectedFileId,
|
|
41
|
+
confirmationSaveVersion = 0
|
|
42
|
+
}: FilesModalProps) => {
|
|
32
43
|
const { user } = useContext(AuthContext);
|
|
33
44
|
const [error, setError] = useState<string | null>(null);
|
|
34
45
|
const [currentPage, setCurrentPage] = useState(0);
|
|
@@ -48,54 +59,36 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
|
|
|
48
59
|
const endIndex = startIndex + FILES_PER_PAGE;
|
|
49
60
|
const currentFiles = files.slice(startIndex, endIndex);
|
|
50
61
|
|
|
51
|
-
//
|
|
62
|
+
// Hydrate confirmation status from shared summary document.
|
|
52
63
|
useEffect(() => {
|
|
53
|
-
|
|
54
|
-
const visibleFiles = files.slice(
|
|
55
|
-
currentPage * FILES_PER_PAGE,
|
|
56
|
-
currentPage * FILES_PER_PAGE + FILES_PER_PAGE
|
|
57
|
-
);
|
|
64
|
+
let isCancelled = false;
|
|
58
65
|
|
|
59
|
-
|
|
66
|
+
const fetchConfirmationStatuses = async () => {
|
|
67
|
+
if (!isOpen || !currentCase || !user || files.length === 0) {
|
|
68
|
+
if (!isCancelled) {
|
|
69
|
+
setFileConfirmationStatus({});
|
|
70
|
+
}
|
|
60
71
|
return;
|
|
61
72
|
}
|
|
62
73
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const annotations = await getFileAnnotations(user, currentCase, file.id);
|
|
67
|
-
return {
|
|
68
|
-
fileId: file.id,
|
|
69
|
-
includeConfirmation: annotations?.includeConfirmation ?? false,
|
|
70
|
-
isConfirmed: !!(annotations?.includeConfirmation && annotations?.confirmationData),
|
|
71
|
-
};
|
|
72
|
-
} catch (err) {
|
|
73
|
-
console.error(`Error fetching annotations for file ${file.id}:`, err);
|
|
74
|
-
return {
|
|
75
|
-
fileId: file.id,
|
|
76
|
-
includeConfirmation: false,
|
|
77
|
-
isConfirmed: false,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
74
|
+
const caseSummary = await ensureCaseConfirmationSummary(user, currentCase, files).catch((err) => {
|
|
75
|
+
console.error(`Error fetching confirmation summary for case ${currentCase}:`, err);
|
|
76
|
+
return null;
|
|
80
77
|
});
|
|
81
78
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Build the statuses map from results
|
|
86
|
-
const statuses: FileConfirmationStatus = {};
|
|
87
|
-
results.forEach((result) => {
|
|
88
|
-
statuses[result.fileId] = {
|
|
89
|
-
includeConfirmation: result.includeConfirmation,
|
|
90
|
-
isConfirmed: result.isConfirmed,
|
|
91
|
-
};
|
|
92
|
-
});
|
|
79
|
+
if (!caseSummary || isCancelled) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
93
82
|
|
|
94
|
-
setFileConfirmationStatus(
|
|
83
|
+
setFileConfirmationStatus(caseSummary.filesById);
|
|
95
84
|
};
|
|
96
85
|
|
|
97
86
|
fetchConfirmationStatuses();
|
|
98
|
-
|
|
87
|
+
|
|
88
|
+
return () => {
|
|
89
|
+
isCancelled = true;
|
|
90
|
+
};
|
|
91
|
+
}, [isOpen, currentCase, files, user, confirmationSaveVersion]);
|
|
99
92
|
|
|
100
93
|
const handleFileSelect = (file: FileData) => {
|
|
101
94
|
onFileSelect?.(file);
|
|
@@ -121,6 +114,11 @@ export const FilesModal = ({ isOpen, onClose, onFileSelect, currentCase, files,
|
|
|
121
114
|
// Remove the deleted file from the list
|
|
122
115
|
const updatedFiles = files.filter(f => f.id !== fileId);
|
|
123
116
|
setFiles(updatedFiles);
|
|
117
|
+
setFileConfirmationStatus((previous) => {
|
|
118
|
+
const next = { ...previous };
|
|
119
|
+
delete next[fileId];
|
|
120
|
+
return next;
|
|
121
|
+
});
|
|
124
122
|
|
|
125
123
|
if (deleteResult.imageMissing) {
|
|
126
124
|
setError(`File record deleted. Image asset "${deleteResult.fileName}" was not found and was skipped.`);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
3
|
+
import styles from './notes.module.css';
|
|
4
|
+
|
|
5
|
+
interface AddlNotesModalProps {
|
|
6
|
+
isOpen: boolean;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
notes: string;
|
|
9
|
+
onSave: (notes: string) => void;
|
|
10
|
+
showNotification?: (message: string, type: 'success' | 'error' | 'warning') => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const AddlNotesModal = ({ isOpen, onClose, notes, onSave, showNotification }: AddlNotesModalProps) => {
|
|
14
|
+
const [tempNotes, setTempNotes] = useState(notes);
|
|
15
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (isOpen) {
|
|
19
|
+
setTempNotes(notes);
|
|
20
|
+
}
|
|
21
|
+
}, [isOpen, notes]);
|
|
22
|
+
const {
|
|
23
|
+
requestClose,
|
|
24
|
+
overlayProps,
|
|
25
|
+
getCloseButtonProps
|
|
26
|
+
} = useOverlayDismiss({
|
|
27
|
+
isOpen,
|
|
28
|
+
onClose
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!isOpen) return null;
|
|
32
|
+
|
|
33
|
+
const handleSave = async () => {
|
|
34
|
+
setIsSaving(true);
|
|
35
|
+
try {
|
|
36
|
+
await Promise.resolve(onSave(tempNotes));
|
|
37
|
+
showNotification?.('Notes saved successfully.', 'success');
|
|
38
|
+
requestClose();
|
|
39
|
+
} catch (error) {
|
|
40
|
+
const message = error instanceof Error ? error.message : 'Failed to save notes.';
|
|
41
|
+
showNotification?.(message, 'error');
|
|
42
|
+
} finally {
|
|
43
|
+
setIsSaving(false);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
className={styles.modalOverlay}
|
|
50
|
+
aria-label="Close notes dialog"
|
|
51
|
+
{...overlayProps}
|
|
52
|
+
>
|
|
53
|
+
<div className={styles.modal}>
|
|
54
|
+
<button {...getCloseButtonProps({ ariaLabel: 'Close notes dialog' })}>×</button>
|
|
55
|
+
<h5 className={styles.modalTitle}>Additional Notes</h5>
|
|
56
|
+
<textarea
|
|
57
|
+
value={tempNotes}
|
|
58
|
+
onChange={(e) => setTempNotes(e.target.value)}
|
|
59
|
+
className={styles.modalTextarea}
|
|
60
|
+
placeholder="Enter additional notes..."
|
|
61
|
+
/>
|
|
62
|
+
<div className={styles.modalButtons}>
|
|
63
|
+
<button
|
|
64
|
+
onClick={handleSave}
|
|
65
|
+
className={styles.saveButton}
|
|
66
|
+
disabled={isSaving}
|
|
67
|
+
aria-busy={isSaving}
|
|
68
|
+
>
|
|
69
|
+
{isSaving ? 'Saving...' : 'Save'}
|
|
70
|
+
</button>
|
|
71
|
+
<button
|
|
72
|
+
onClick={requestClose}
|
|
73
|
+
className={styles.cancelButton}
|
|
74
|
+
disabled={isSaving}
|
|
75
|
+
>
|
|
76
|
+
Cancel
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
@@ -1,36 +1,31 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react';
|
|
2
2
|
import type { User } from 'firebase/auth';
|
|
3
3
|
import { ColorSelector } from '~/components/colors/colors';
|
|
4
|
-
import {
|
|
4
|
+
import { AddlNotesModal } from './addl-notes-modal';
|
|
5
5
|
import { getNotes, saveNotes } from '~/components/actions/notes-manage';
|
|
6
6
|
import { type AnnotationData } from '~/types/annotations';
|
|
7
7
|
import { resolveEarliestAnnotationTimestamp } from '~/utils/ui';
|
|
8
8
|
import { auditService } from '~/services/audit';
|
|
9
9
|
import styles from './notes.module.css';
|
|
10
10
|
|
|
11
|
-
interface
|
|
11
|
+
interface NotesEditorFormProps {
|
|
12
12
|
currentCase: string;
|
|
13
|
-
onReturn: () => void;
|
|
14
13
|
user: User;
|
|
15
14
|
imageId: string;
|
|
16
15
|
onAnnotationRefresh?: () => void;
|
|
17
16
|
originalFileName?: string;
|
|
18
17
|
isUploading?: boolean;
|
|
19
|
-
|
|
20
|
-
stickyActionBar?: boolean;
|
|
21
|
-
compactLayout?: boolean;
|
|
18
|
+
showNotification?: (message: string, type: 'success' | 'error' | 'warning') => void;
|
|
22
19
|
}
|
|
23
20
|
|
|
24
21
|
type SupportLevel = 'ID' | 'Exclusion' | 'Inconclusive';
|
|
25
22
|
type ClassType = 'Bullet' | 'Cartridge Case' | 'Other';
|
|
26
23
|
type IndexType = 'number' | 'color';
|
|
27
24
|
|
|
28
|
-
export const
|
|
25
|
+
export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefresh, originalFileName, isUploading = false, showNotification: externalShowNotification }: NotesEditorFormProps) => {
|
|
29
26
|
// Loading/Saving Notes States
|
|
30
27
|
const [isLoading, setIsLoading] = useState(false);
|
|
31
28
|
const [loadError, setLoadError] = useState<string>();
|
|
32
|
-
const [saveError, setSaveError] = useState<string>();
|
|
33
|
-
const [saveSuccess, setSaveSuccess] = useState(false);
|
|
34
29
|
const [isConfirmedImage, setIsConfirmedImage] = useState(false);
|
|
35
30
|
// Case numbers state
|
|
36
31
|
const [leftCase, setLeftCase] = useState('');
|
|
@@ -65,14 +60,18 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
65
60
|
const [isSupportOpen, setIsSupportOpen] = useState(true);
|
|
66
61
|
const areInputsDisabled = isUploading || isConfirmedImage;
|
|
67
62
|
|
|
63
|
+
const notificationHandler = (message: string, type: 'success' | 'error' | 'warning' = 'success') => {
|
|
64
|
+
if (externalShowNotification) {
|
|
65
|
+
externalShowNotification(message, type);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
68
69
|
useEffect(() => {
|
|
69
70
|
const loadExistingNotes = async () => {
|
|
70
71
|
if (!imageId || !currentCase) return;
|
|
71
72
|
|
|
72
73
|
setIsLoading(true);
|
|
73
74
|
setLoadError(undefined);
|
|
74
|
-
setSaveError(undefined);
|
|
75
|
-
setSaveSuccess(false);
|
|
76
75
|
setIsConfirmedImage(false);
|
|
77
76
|
|
|
78
77
|
try {
|
|
@@ -128,9 +127,6 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
128
127
|
return;
|
|
129
128
|
}
|
|
130
129
|
|
|
131
|
-
setSaveError(undefined);
|
|
132
|
-
setSaveSuccess(false);
|
|
133
|
-
|
|
134
130
|
let existingData: AnnotationData | null = null;
|
|
135
131
|
|
|
136
132
|
try {
|
|
@@ -139,7 +135,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
139
135
|
|
|
140
136
|
if (existingData?.confirmationData) {
|
|
141
137
|
setIsConfirmedImage(true);
|
|
142
|
-
|
|
138
|
+
notificationHandler('This image is confirmed. Notes cannot be modified.', 'error');
|
|
143
139
|
return;
|
|
144
140
|
}
|
|
145
141
|
|
|
@@ -193,13 +189,12 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
193
189
|
existingData,
|
|
194
190
|
annotationData,
|
|
195
191
|
currentCase,
|
|
196
|
-
'notes-
|
|
192
|
+
'notes-editor-form',
|
|
197
193
|
imageId,
|
|
198
194
|
originalFileName
|
|
199
195
|
);
|
|
200
196
|
|
|
201
|
-
|
|
202
|
-
setTimeout(() => setSaveSuccess(false), 3000);
|
|
197
|
+
notificationHandler('Notes saved successfully.', 'success');
|
|
203
198
|
|
|
204
199
|
// Refresh annotation data after saving notes
|
|
205
200
|
if (onAnnotationRefresh) {
|
|
@@ -210,9 +205,9 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
210
205
|
const errorMessage = error instanceof Error ? error.message : '';
|
|
211
206
|
if (errorMessage.toLowerCase().includes('confirmed image')) {
|
|
212
207
|
setIsConfirmedImage(true);
|
|
213
|
-
|
|
208
|
+
notificationHandler('This image is confirmed. Notes cannot be modified.', 'error');
|
|
214
209
|
} else {
|
|
215
|
-
|
|
210
|
+
notificationHandler('Failed to save notes. Please try again.', 'error');
|
|
216
211
|
}
|
|
217
212
|
|
|
218
213
|
// Audit logging for failed annotation save
|
|
@@ -223,7 +218,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
223
218
|
existingData,
|
|
224
219
|
null, // Failed save, no new value
|
|
225
220
|
currentCase,
|
|
226
|
-
'notes-
|
|
221
|
+
'notes-editor-form',
|
|
227
222
|
imageId,
|
|
228
223
|
originalFileName
|
|
229
224
|
);
|
|
@@ -234,7 +229,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
234
229
|
};
|
|
235
230
|
|
|
236
231
|
return (
|
|
237
|
-
<div className={`${styles.
|
|
232
|
+
<div className={`${styles.notesEditorForm} ${styles.editorLayout}`}>
|
|
238
233
|
{isLoading ? (
|
|
239
234
|
<div className={styles.loading}>Loading notes...</div>
|
|
240
235
|
) : loadError ? (
|
|
@@ -247,10 +242,6 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
247
242
|
</div>
|
|
248
243
|
)}
|
|
249
244
|
|
|
250
|
-
{saveError && (
|
|
251
|
-
<div className={styles.errorMessage}>{saveError}</div>
|
|
252
|
-
)}
|
|
253
|
-
|
|
254
245
|
<div className={styles.section}>
|
|
255
246
|
<button
|
|
256
247
|
type="button"
|
|
@@ -297,17 +288,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
297
288
|
disabled={areInputsDisabled}
|
|
298
289
|
/>
|
|
299
290
|
</div>
|
|
300
|
-
{compactLayout && (
|
|
301
|
-
<div className={styles.caseInput}>
|
|
302
|
-
<label htmlFor="colorSelect">Font</label>
|
|
303
|
-
<ColorSelector
|
|
304
|
-
selectedColor={caseFontColor}
|
|
305
|
-
onColorSelect={setCaseFontColor}
|
|
306
|
-
/>
|
|
307
|
-
</div>
|
|
308
|
-
)}
|
|
309
291
|
</div>
|
|
310
|
-
{!compactLayout && <hr />}
|
|
311
292
|
{/* Right side inputs */}
|
|
312
293
|
<div className={styles.inputGroup}>
|
|
313
294
|
<div className={styles.caseInput}>
|
|
@@ -342,21 +323,20 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
342
323
|
</div>
|
|
343
324
|
</div>
|
|
344
325
|
</div>
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
)}
|
|
326
|
+
<hr />
|
|
327
|
+
<div className={styles.fontColorRow}>
|
|
328
|
+
<label htmlFor="colorSelect">Font</label>
|
|
329
|
+
<ColorSelector
|
|
330
|
+
selectedColor={caseFontColor}
|
|
331
|
+
onColorSelect={setCaseFontColor}
|
|
332
|
+
/>
|
|
333
|
+
</div>
|
|
354
334
|
</>
|
|
355
335
|
)}
|
|
356
336
|
</div>
|
|
357
337
|
|
|
358
|
-
<div className={
|
|
359
|
-
<div className={`${styles.section} ${
|
|
338
|
+
<div className={styles.compactSectionGrid}>
|
|
339
|
+
<div className={`${styles.section} ${styles.compactFullSection}`}>
|
|
360
340
|
<button
|
|
361
341
|
type="button"
|
|
362
342
|
className={styles.sectionToggle}
|
|
@@ -368,7 +348,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
368
348
|
</button>
|
|
369
349
|
{isClassOpen && (
|
|
370
350
|
<>
|
|
371
|
-
<div className={
|
|
351
|
+
<div className={styles.classCharacteristicsColumns}>
|
|
372
352
|
<div className={styles.classCharacteristicsMain}>
|
|
373
353
|
<div className={styles.classCharacteristics}>
|
|
374
354
|
<select
|
|
@@ -415,18 +395,16 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
415
395
|
</label>
|
|
416
396
|
</div>
|
|
417
397
|
|
|
418
|
-
{
|
|
419
|
-
<
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
</div>
|
|
423
|
-
)}
|
|
398
|
+
<div className={styles.characteristicsPlaceholder}>
|
|
399
|
+
<h6 className={styles.placeholderTitle}>Characteristics Details</h6>
|
|
400
|
+
<p className={styles.placeholderText}>This section is reserved for future development.</p>
|
|
401
|
+
</div>
|
|
424
402
|
</div>
|
|
425
403
|
</>
|
|
426
404
|
)}
|
|
427
405
|
</div>
|
|
428
406
|
|
|
429
|
-
<div className={`${styles.section} ${
|
|
407
|
+
<div className={`${styles.section} ${styles.compactHalfSection}`}>
|
|
430
408
|
<button
|
|
431
409
|
type="button"
|
|
432
410
|
className={styles.sectionToggle}
|
|
@@ -477,7 +455,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
477
455
|
)}
|
|
478
456
|
</div>
|
|
479
457
|
|
|
480
|
-
<div className={`${styles.section} ${
|
|
458
|
+
<div className={`${styles.section} ${styles.compactHalfSection}`}>
|
|
481
459
|
<button
|
|
482
460
|
type="button"
|
|
483
461
|
className={styles.sectionToggle}
|
|
@@ -538,7 +516,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
538
516
|
</button>
|
|
539
517
|
</div>
|
|
540
518
|
|
|
541
|
-
<div className={`${styles.notesActionBar} ${
|
|
519
|
+
<div className={`${styles.notesActionBar} ${styles.notesActionBarSticky}`}>
|
|
542
520
|
<button
|
|
543
521
|
onClick={handleSave}
|
|
544
522
|
className={styles.saveButton}
|
|
@@ -547,28 +525,13 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
547
525
|
>
|
|
548
526
|
Save Notes
|
|
549
527
|
</button>
|
|
550
|
-
{showReturnButton && (
|
|
551
|
-
<button
|
|
552
|
-
onClick={onReturn}
|
|
553
|
-
className={styles.returnButton}
|
|
554
|
-
disabled={isUploading}
|
|
555
|
-
title={isUploading ? "Cannot return while uploading" : undefined}
|
|
556
|
-
>
|
|
557
|
-
Return to Case Management
|
|
558
|
-
</button>
|
|
559
|
-
)}
|
|
560
528
|
</div>
|
|
561
|
-
|
|
562
|
-
{saveSuccess && (
|
|
563
|
-
<div className={styles.successMessage}>
|
|
564
|
-
Notes saved successfully!
|
|
565
|
-
</div>
|
|
566
|
-
)}
|
|
567
|
-
<NotesModal
|
|
529
|
+
<AddlNotesModal
|
|
568
530
|
isOpen={isModalOpen}
|
|
569
531
|
onClose={() => setIsModalOpen(false)}
|
|
570
532
|
notes={additionalNotes}
|
|
571
533
|
onSave={setAdditionalNotes}
|
|
534
|
+
showNotification={notificationHandler}
|
|
572
535
|
/>
|
|
573
536
|
</>
|
|
574
537
|
)}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { User } from 'firebase/auth';
|
|
2
2
|
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
3
|
-
import {
|
|
3
|
+
import { NotesEditorForm } from './notes-editor-form';
|
|
4
4
|
import styles from './notes-editor-modal.module.css';
|
|
5
5
|
|
|
6
6
|
interface NotesEditorModalProps {
|
|
@@ -12,6 +12,7 @@ interface NotesEditorModalProps {
|
|
|
12
12
|
originalFileName?: string;
|
|
13
13
|
onAnnotationRefresh?: () => void;
|
|
14
14
|
isUploading?: boolean;
|
|
15
|
+
showNotification?: (message: string, type: 'success' | 'error' | 'warning') => void;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export const NotesEditorModal = ({
|
|
@@ -23,9 +24,9 @@ export const NotesEditorModal = ({
|
|
|
23
24
|
originalFileName,
|
|
24
25
|
onAnnotationRefresh,
|
|
25
26
|
isUploading = false,
|
|
27
|
+
showNotification,
|
|
26
28
|
}: NotesEditorModalProps) => {
|
|
27
29
|
const {
|
|
28
|
-
requestClose,
|
|
29
30
|
overlayProps,
|
|
30
31
|
getCloseButtonProps,
|
|
31
32
|
} = useOverlayDismiss({
|
|
@@ -47,17 +48,14 @@ export const NotesEditorModal = ({
|
|
|
47
48
|
</button>
|
|
48
49
|
</div>
|
|
49
50
|
<div className={styles.content}>
|
|
50
|
-
<
|
|
51
|
+
<NotesEditorForm
|
|
51
52
|
currentCase={currentCase}
|
|
52
|
-
onReturn={requestClose}
|
|
53
53
|
user={user}
|
|
54
54
|
imageId={imageId}
|
|
55
55
|
onAnnotationRefresh={onAnnotationRefresh}
|
|
56
56
|
originalFileName={originalFileName}
|
|
57
57
|
isUploading={isUploading}
|
|
58
|
-
|
|
59
|
-
stickyActionBar={true}
|
|
60
|
-
compactLayout={true}
|
|
58
|
+
showNotification={showNotification}
|
|
61
59
|
/>
|
|
62
60
|
</div>
|
|
63
61
|
</div>
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
.
|
|
1
|
+
.notesEditorForm {
|
|
2
2
|
padding: 0.3rem;
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
-
.
|
|
5
|
+
.editorLayout .caseNumbers {
|
|
6
6
|
display: grid;
|
|
7
7
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
8
8
|
gap: 1.25rem;
|
|
9
9
|
align-items: start;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
.
|
|
12
|
+
.editorLayout .caseNumbers > .inputGroup + .inputGroup {
|
|
13
13
|
border-left: 1px solid #dee2e6;
|
|
14
14
|
padding-left: 1.25rem;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
.
|
|
17
|
+
.editorLayout .inputGroup {
|
|
18
18
|
margin-bottom: 0;
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
margin-bottom: 1.5rem;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
.
|
|
38
|
+
.editorLayout .notesActionBarSticky {
|
|
39
39
|
margin-top: 0.25rem;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
.
|
|
42
|
+
.editorLayout .compactSectionGrid > .compactHalfSection + .compactHalfSection {
|
|
43
43
|
border-left: 1px solid #dee2e6;
|
|
44
44
|
padding-left: 1.25rem;
|
|
45
45
|
}
|
|
@@ -49,19 +49,19 @@
|
|
|
49
49
|
padding-left: 1.25rem;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
.
|
|
52
|
+
.editorLayout .additionalNotesRow {
|
|
53
53
|
border-top: 1px solid #dee2e6;
|
|
54
54
|
padding-top: 1.25rem;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
@media (max-width: 980px) {
|
|
58
|
-
.
|
|
58
|
+
.editorLayout .caseNumbers,
|
|
59
59
|
.compactSectionGrid {
|
|
60
60
|
grid-template-columns: 1fr;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
.
|
|
64
|
-
.
|
|
63
|
+
.editorLayout .caseNumbers > .inputGroup + .inputGroup,
|
|
64
|
+
.editorLayout
|
|
65
65
|
.compactSectionGrid
|
|
66
66
|
> .compactHalfSection
|
|
67
67
|
+ .compactHalfSection,
|
|
@@ -152,9 +152,25 @@ textarea:focus {
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
.caseNumbers {
|
|
155
|
+
display: grid;
|
|
156
|
+
grid-template-columns: 1fr 1fr;
|
|
157
|
+
gap: 1.5rem;
|
|
155
158
|
margin-bottom: 2rem;
|
|
156
159
|
}
|
|
157
160
|
|
|
161
|
+
.fontColorRow {
|
|
162
|
+
display: flex;
|
|
163
|
+
flex-direction: column;
|
|
164
|
+
gap: 0.75rem;
|
|
165
|
+
margin-bottom: 2rem;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.fontColorRow label {
|
|
169
|
+
font-size: 0.95rem;
|
|
170
|
+
font-weight: 600;
|
|
171
|
+
color: #212529;
|
|
172
|
+
}
|
|
173
|
+
|
|
158
174
|
.caseInput {
|
|
159
175
|
display: flex;
|
|
160
176
|
flex-direction: column;
|
|
@@ -382,7 +398,7 @@ textarea:focus {
|
|
|
382
398
|
width: 100%;
|
|
383
399
|
}
|
|
384
400
|
|
|
385
|
-
.
|
|
401
|
+
.editorLayout .additionalNotesRow {
|
|
386
402
|
grid-column: 1 / -1;
|
|
387
403
|
}
|
|
388
404
|
|
|
@@ -8,6 +8,7 @@ import { type FileData } from '~/types';
|
|
|
8
8
|
interface SidebarProps {
|
|
9
9
|
user: User;
|
|
10
10
|
onImageSelect: (file: FileData) => void;
|
|
11
|
+
onOpenCase: () => void;
|
|
11
12
|
imageId?: string;
|
|
12
13
|
currentCase: string;
|
|
13
14
|
files: FileData[];
|
|
@@ -27,6 +28,7 @@ interface SidebarProps {
|
|
|
27
28
|
export const Sidebar = ({
|
|
28
29
|
user,
|
|
29
30
|
onImageSelect,
|
|
31
|
+
onOpenCase,
|
|
30
32
|
imageId,
|
|
31
33
|
currentCase,
|
|
32
34
|
imageLoaded,
|
|
@@ -74,6 +76,7 @@ export const Sidebar = ({
|
|
|
74
76
|
<CaseSidebar
|
|
75
77
|
user={user}
|
|
76
78
|
onImageSelect={onImageSelect}
|
|
79
|
+
onOpenCase={onOpenCase}
|
|
77
80
|
currentCase={currentCase}
|
|
78
81
|
imageLoaded={imageLoaded}
|
|
79
82
|
setImageLoaded={setImageLoaded}
|
|
@@ -85,11 +85,13 @@ export const useOverlayDismiss = ({
|
|
|
85
85
|
}
|
|
86
86
|
}, [closeOnBackdrop, requestClose]);
|
|
87
87
|
|
|
88
|
+
const isBackdropDismissInteractive = closeOnBackdrop && canDismiss;
|
|
89
|
+
|
|
88
90
|
const overlayProps = {
|
|
89
|
-
role: 'button'
|
|
90
|
-
tabIndex: 0,
|
|
91
|
-
onMouseDown: handleOverlayMouseDown,
|
|
92
|
-
onKeyDown: handleOverlayKeyDown,
|
|
91
|
+
role: isBackdropDismissInteractive ? 'button' : 'presentation',
|
|
92
|
+
tabIndex: isBackdropDismissInteractive ? 0 : undefined,
|
|
93
|
+
onMouseDown: isBackdropDismissInteractive ? handleOverlayMouseDown : undefined,
|
|
94
|
+
onKeyDown: isBackdropDismissInteractive ? handleOverlayKeyDown : undefined,
|
|
93
95
|
style: { cursor: 'default' as const },
|
|
94
96
|
};
|
|
95
97
|
|
package/app/root.tsx
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
import { AuthProvider } from '~/components/auth/auth-provider';
|
|
17
17
|
import { auth } from '~/services/firebase';
|
|
18
18
|
import styles from '~/styles/root.module.css';
|
|
19
|
-
import './
|
|
19
|
+
import './global.css';
|
|
20
20
|
|
|
21
21
|
export const links: LinksFunction = () => [
|
|
22
22
|
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|