@striae-org/striae 4.1.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/.env.example +8 -0
- package/LICENSE +1 -1
- package/app/components/actions/case-export/core-export.ts +14 -8
- package/app/components/actions/case-export/data-processing.ts +1 -0
- package/app/components/actions/case-export/download-handlers.ts +7 -0
- package/app/components/actions/case-export/metadata-helpers.ts +2 -1
- package/app/components/actions/case-import/confirmation-import.ts +12 -2
- package/app/components/actions/case-import/orchestrator.ts +78 -32
- package/app/components/actions/case-import/storage-operations.ts +97 -8
- package/app/components/actions/case-import/zip-processing.ts +159 -86
- package/app/components/actions/case-manage.ts +463 -8
- package/app/components/actions/confirm-export.ts +9 -2
- package/app/components/actions/image-manage.ts +77 -44
- package/app/components/audit/user-audit-viewer.tsx +19 -8
- package/app/components/audit/user-audit.module.css +21 -0
- package/app/components/audit/viewer/audit-entries-list.tsx +12 -2
- package/app/components/audit/viewer/audit-filters-panel.tsx +1 -0
- package/app/components/audit/viewer/audit-viewer-utils.ts +2 -0
- package/app/components/audit/viewer/use-audit-viewer-data.ts +24 -1
- package/app/components/audit/viewer/use-audit-viewer-export.ts +1 -1
- package/app/components/canvas/box-annotations/box-annotations.module.css +22 -18
- package/app/components/canvas/box-annotations/box-annotations.tsx +15 -0
- package/app/components/canvas/canvas.module.css +64 -54
- package/app/components/canvas/canvas.tsx +14 -16
- package/app/components/canvas/confirmation/confirmation.module.css +1 -0
- package/app/components/canvas/confirmation/confirmation.tsx +12 -14
- package/app/components/colors/colors.module.css +4 -3
- package/app/components/navbar/case-modals/archive-case-modal.module.css +110 -0
- package/app/components/navbar/case-modals/archive-case-modal.tsx +129 -0
- package/app/components/navbar/case-modals/open-case-modal.module.css +81 -0
- package/app/components/navbar/case-modals/open-case-modal.tsx +120 -0
- package/app/components/navbar/case-modals/rename-case-modal.module.css +81 -0
- package/app/components/navbar/case-modals/rename-case-modal.tsx +107 -0
- package/app/components/navbar/navbar.module.css +447 -0
- package/app/components/navbar/navbar.tsx +402 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +1 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +15 -16
- package/app/components/sidebar/case-export/case-export.module.css +1 -0
- package/app/components/sidebar/case-export/case-export.tsx +8 -46
- package/app/components/sidebar/case-import/case-import.module.css +23 -0
- package/app/components/sidebar/case-import/case-import.tsx +64 -16
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +20 -1
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +15 -0
- package/app/components/sidebar/cases/case-sidebar.tsx +68 -588
- package/app/components/sidebar/cases/cases-modal.module.css +1 -0
- package/app/components/sidebar/cases/cases-modal.tsx +82 -43
- package/app/components/sidebar/cases/cases.module.css +82 -21
- package/app/components/sidebar/files/files-modal.module.css +1 -0
- package/app/components/sidebar/files/files-modal.tsx +49 -52
- package/app/components/sidebar/notes/addl-notes-modal.tsx +82 -0
- package/app/components/sidebar/notes/{notes-sidebar.tsx → notes-editor-form.tsx} +187 -138
- package/app/components/sidebar/notes/notes-editor-modal.module.css +49 -0
- package/app/components/sidebar/notes/notes-editor-modal.tsx +64 -0
- package/app/components/sidebar/notes/notes.module.css +170 -1
- package/app/components/sidebar/sidebar-container.tsx +16 -28
- package/app/components/sidebar/sidebar.module.css +5 -69
- package/app/components/sidebar/sidebar.tsx +27 -125
- package/app/components/sidebar/upload/image-upload-zone.module.css +13 -13
- package/app/components/user/inactivity-warning.module.css +1 -0
- package/app/components/user/inactivity-warning.tsx +15 -2
- package/app/components/user/manage-profile.tsx +23 -10
- package/app/{tailwind.css → global.css} +1 -3
- package/app/hooks/useOverlayDismiss.ts +54 -4
- package/app/root.tsx +1 -1
- package/app/routes/auth/login.tsx +785 -774
- package/app/routes/striae/striae.module.css +10 -3
- package/app/routes/striae/striae.tsx +475 -30
- package/app/services/audit/audit.service.ts +173 -27
- package/app/services/audit/builders/audit-event-builders-case-file.ts +43 -0
- package/app/services/audit/builders/audit-event-builders-workflow.ts +2 -0
- package/app/services/audit/builders/index.ts +1 -0
- package/app/types/audit.ts +4 -1
- package/app/types/case.ts +29 -0
- package/app/types/import.ts +3 -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/data/permissions.ts +16 -1
- package/app/utils/forensics/audit-export-signature.ts +5 -1
- package/app/utils/forensics/confirmation-signature.ts +3 -0
- package/app/utils/forensics/export-verification.ts +426 -22
- 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 +7 -12
- package/scripts/deploy-primershear-emails.sh +2 -1
- package/worker-configuration.d.ts +3 -3
- package/workers/audit-worker/package.json +1 -1
- package/workers/audit-worker/worker-configuration.d.ts +7448 -11323
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +1 -1
- package/workers/data-worker/worker-configuration.d.ts +7448 -11323
- 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/worker-configuration.d.ts +7447 -11322
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/package.json +1 -1
- package/workers/keys-worker/worker-configuration.d.ts +7447 -11322
- 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 +9 -14
- 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/worker-configuration.d.ts +7448 -11323
- 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/worker-configuration.d.ts +7448 -11323
- 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 -53
- package/postcss.config.js +0 -6
- package/public/.well-known/keybase.txt +0 -56
- package/tailwind.config.ts +0 -22
|
@@ -1,33 +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;
|
|
18
|
+
showNotification?: (message: string, type: 'success' | 'error' | 'warning') => void;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
type SupportLevel = 'ID' | 'Exclusion' | 'Inconclusive';
|
|
22
22
|
type ClassType = 'Bullet' | 'Cartridge Case' | 'Other';
|
|
23
23
|
type IndexType = 'number' | 'color';
|
|
24
24
|
|
|
25
|
-
export const
|
|
25
|
+
export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefresh, originalFileName, isUploading = false, showNotification: externalShowNotification }: NotesEditorFormProps) => {
|
|
26
26
|
// Loading/Saving Notes States
|
|
27
27
|
const [isLoading, setIsLoading] = useState(false);
|
|
28
28
|
const [loadError, setLoadError] = useState<string>();
|
|
29
|
-
const [saveError, setSaveError] = useState<string>();
|
|
30
|
-
const [saveSuccess, setSaveSuccess] = useState(false);
|
|
31
29
|
const [isConfirmedImage, setIsConfirmedImage] = useState(false);
|
|
32
30
|
// Case numbers state
|
|
33
31
|
const [leftCase, setLeftCase] = useState('');
|
|
@@ -56,16 +54,24 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
56
54
|
// Additional Notes Modal
|
|
57
55
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
58
56
|
const [additionalNotes, setAdditionalNotes] = useState('');
|
|
57
|
+
const [isCaseInfoOpen, setIsCaseInfoOpen] = useState(true);
|
|
58
|
+
const [isClassOpen, setIsClassOpen] = useState(true);
|
|
59
|
+
const [isIndexOpen, setIsIndexOpen] = useState(true);
|
|
60
|
+
const [isSupportOpen, setIsSupportOpen] = useState(true);
|
|
59
61
|
const areInputsDisabled = isUploading || isConfirmedImage;
|
|
60
62
|
|
|
63
|
+
const notificationHandler = (message: string, type: 'success' | 'error' | 'warning' = 'success') => {
|
|
64
|
+
if (externalShowNotification) {
|
|
65
|
+
externalShowNotification(message, type);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
61
69
|
useEffect(() => {
|
|
62
70
|
const loadExistingNotes = async () => {
|
|
63
71
|
if (!imageId || !currentCase) return;
|
|
64
72
|
|
|
65
73
|
setIsLoading(true);
|
|
66
74
|
setLoadError(undefined);
|
|
67
|
-
setSaveError(undefined);
|
|
68
|
-
setSaveSuccess(false);
|
|
69
75
|
setIsConfirmedImage(false);
|
|
70
76
|
|
|
71
77
|
try {
|
|
@@ -121,9 +127,6 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
121
127
|
return;
|
|
122
128
|
}
|
|
123
129
|
|
|
124
|
-
setSaveError(undefined);
|
|
125
|
-
setSaveSuccess(false);
|
|
126
|
-
|
|
127
130
|
let existingData: AnnotationData | null = null;
|
|
128
131
|
|
|
129
132
|
try {
|
|
@@ -132,7 +135,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
132
135
|
|
|
133
136
|
if (existingData?.confirmationData) {
|
|
134
137
|
setIsConfirmedImage(true);
|
|
135
|
-
|
|
138
|
+
notificationHandler('This image is confirmed. Notes cannot be modified.', 'error');
|
|
136
139
|
return;
|
|
137
140
|
}
|
|
138
141
|
|
|
@@ -186,13 +189,12 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
186
189
|
existingData,
|
|
187
190
|
annotationData,
|
|
188
191
|
currentCase,
|
|
189
|
-
'notes-
|
|
192
|
+
'notes-editor-form',
|
|
190
193
|
imageId,
|
|
191
194
|
originalFileName
|
|
192
195
|
);
|
|
193
196
|
|
|
194
|
-
|
|
195
|
-
setTimeout(() => setSaveSuccess(false), 3000);
|
|
197
|
+
notificationHandler('Notes saved successfully.', 'success');
|
|
196
198
|
|
|
197
199
|
// Refresh annotation data after saving notes
|
|
198
200
|
if (onAnnotationRefresh) {
|
|
@@ -203,9 +205,9 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
203
205
|
const errorMessage = error instanceof Error ? error.message : '';
|
|
204
206
|
if (errorMessage.toLowerCase().includes('confirmed image')) {
|
|
205
207
|
setIsConfirmedImage(true);
|
|
206
|
-
|
|
208
|
+
notificationHandler('This image is confirmed. Notes cannot be modified.', 'error');
|
|
207
209
|
} else {
|
|
208
|
-
|
|
210
|
+
notificationHandler('Failed to save notes. Please try again.', 'error');
|
|
209
211
|
}
|
|
210
212
|
|
|
211
213
|
// Audit logging for failed annotation save
|
|
@@ -216,7 +218,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
216
218
|
existingData,
|
|
217
219
|
null, // Failed save, no new value
|
|
218
220
|
currentCase,
|
|
219
|
-
'notes-
|
|
221
|
+
'notes-editor-form',
|
|
220
222
|
imageId,
|
|
221
223
|
originalFileName
|
|
222
224
|
);
|
|
@@ -227,7 +229,7 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
227
229
|
};
|
|
228
230
|
|
|
229
231
|
return (
|
|
230
|
-
<div className={styles.
|
|
232
|
+
<div className={`${styles.notesEditorForm} ${styles.editorLayout}`}>
|
|
231
233
|
{isLoading ? (
|
|
232
234
|
<div className={styles.loading}>Loading notes...</div>
|
|
233
235
|
) : loadError ? (
|
|
@@ -240,12 +242,18 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
240
242
|
</div>
|
|
241
243
|
)}
|
|
242
244
|
|
|
243
|
-
{saveError && (
|
|
244
|
-
<div className={styles.errorMessage}>{saveError}</div>
|
|
245
|
-
)}
|
|
246
|
-
|
|
247
245
|
<div className={styles.section}>
|
|
248
|
-
<
|
|
246
|
+
<button
|
|
247
|
+
type="button"
|
|
248
|
+
className={styles.sectionToggle}
|
|
249
|
+
onClick={() => setIsCaseInfoOpen((prev) => !prev)}
|
|
250
|
+
aria-expanded={isCaseInfoOpen}
|
|
251
|
+
>
|
|
252
|
+
<span className={styles.sectionTitle}>Case Information</span>
|
|
253
|
+
<span className={styles.sectionToggleIcon}>{isCaseInfoOpen ? '−' : '+'}</span>
|
|
254
|
+
</button>
|
|
255
|
+
{isCaseInfoOpen && (
|
|
256
|
+
<>
|
|
249
257
|
<hr />
|
|
250
258
|
<div className={styles.caseNumbers}>
|
|
251
259
|
{/* Left side inputs */}
|
|
@@ -279,9 +287,8 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
279
287
|
onChange={(e) => setLeftItem(e.target.value)}
|
|
280
288
|
disabled={areInputsDisabled}
|
|
281
289
|
/>
|
|
282
|
-
</div>
|
|
290
|
+
</div>
|
|
283
291
|
</div>
|
|
284
|
-
<hr />
|
|
285
292
|
{/* Right side inputs */}
|
|
286
293
|
<div className={styles.inputGroup}>
|
|
287
294
|
<div className={styles.caseInput}>
|
|
@@ -316,63 +323,99 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
316
323
|
</div>
|
|
317
324
|
</div>
|
|
318
325
|
</div>
|
|
319
|
-
|
|
320
|
-
|
|
326
|
+
<hr />
|
|
327
|
+
<div className={styles.fontColorRow}>
|
|
328
|
+
<label htmlFor="colorSelect">Font</label>
|
|
329
|
+
<ColorSelector
|
|
321
330
|
selectedColor={caseFontColor}
|
|
322
331
|
onColorSelect={setCaseFontColor}
|
|
323
|
-
/>
|
|
332
|
+
/>
|
|
333
|
+
</div>
|
|
334
|
+
</>
|
|
335
|
+
)}
|
|
324
336
|
</div>
|
|
325
337
|
|
|
326
|
-
<div className={styles.
|
|
327
|
-
|
|
328
|
-
<
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
<
|
|
340
|
-
|
|
341
|
-
|
|
338
|
+
<div className={styles.compactSectionGrid}>
|
|
339
|
+
<div className={`${styles.section} ${styles.compactFullSection}`}>
|
|
340
|
+
<button
|
|
341
|
+
type="button"
|
|
342
|
+
className={styles.sectionToggle}
|
|
343
|
+
onClick={() => setIsClassOpen((prev) => !prev)}
|
|
344
|
+
aria-expanded={isClassOpen}
|
|
345
|
+
>
|
|
346
|
+
<span className={styles.sectionTitle}>Class Characteristics</span>
|
|
347
|
+
<span className={styles.sectionToggleIcon}>{isClassOpen ? '−' : '+'}</span>
|
|
348
|
+
</button>
|
|
349
|
+
{isClassOpen && (
|
|
350
|
+
<>
|
|
351
|
+
<div className={styles.classCharacteristicsColumns}>
|
|
352
|
+
<div className={styles.classCharacteristicsMain}>
|
|
353
|
+
<div className={styles.classCharacteristics}>
|
|
354
|
+
<select
|
|
355
|
+
id="classType"
|
|
356
|
+
aria-label="Class Type"
|
|
357
|
+
value={classType}
|
|
358
|
+
onChange={(e) => setClassType(e.target.value as ClassType)}
|
|
359
|
+
className={styles.select}
|
|
360
|
+
disabled={areInputsDisabled}
|
|
361
|
+
>
|
|
362
|
+
<option value="">Select class type...</option>
|
|
363
|
+
<option value="Bullet">Bullet</option>
|
|
364
|
+
<option value="Cartridge Case">Cartridge Case</option>
|
|
365
|
+
<option value="Other">Other</option>
|
|
366
|
+
</select>
|
|
342
367
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
368
|
+
{classType === 'Other' && (
|
|
369
|
+
<input
|
|
370
|
+
type="text"
|
|
371
|
+
value={customClass}
|
|
372
|
+
onChange={(e) => setCustomClass(e.target.value)}
|
|
373
|
+
placeholder="Specify object type"
|
|
374
|
+
disabled={areInputsDisabled}
|
|
375
|
+
/>
|
|
376
|
+
)}
|
|
352
377
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
378
|
+
<textarea
|
|
379
|
+
value={classNote}
|
|
380
|
+
onChange={(e) => setClassNote(e.target.value)}
|
|
381
|
+
placeholder="Enter class characteristic details..."
|
|
382
|
+
className={styles.textarea}
|
|
383
|
+
disabled={areInputsDisabled}
|
|
384
|
+
/>
|
|
385
|
+
</div>
|
|
386
|
+
<label className={`${styles.checkboxLabel} mb-4`}>
|
|
387
|
+
<input
|
|
388
|
+
type="checkbox"
|
|
389
|
+
checked={hasSubclass}
|
|
390
|
+
onChange={(e) => setHasSubclass(e.target.checked)}
|
|
391
|
+
className={styles.checkbox}
|
|
392
|
+
disabled={areInputsDisabled}
|
|
393
|
+
/>
|
|
394
|
+
<span>Potential subclass?</span>
|
|
395
|
+
</label>
|
|
396
|
+
</div>
|
|
397
|
+
|
|
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>
|
|
402
|
+
</div>
|
|
403
|
+
</>
|
|
404
|
+
)}
|
|
371
405
|
</div>
|
|
372
406
|
|
|
373
|
-
<div className={styles.section}>
|
|
374
|
-
<
|
|
375
|
-
|
|
407
|
+
<div className={`${styles.section} ${styles.compactHalfSection}`}>
|
|
408
|
+
<button
|
|
409
|
+
type="button"
|
|
410
|
+
className={styles.sectionToggle}
|
|
411
|
+
onClick={() => setIsIndexOpen((prev) => !prev)}
|
|
412
|
+
aria-expanded={isIndexOpen}
|
|
413
|
+
>
|
|
414
|
+
<span className={styles.sectionTitle}>Index Type</span>
|
|
415
|
+
<span className={styles.sectionToggleIcon}>{isIndexOpen ? '−' : '+'}</span>
|
|
416
|
+
</button>
|
|
417
|
+
{isIndexOpen && (
|
|
418
|
+
<div className={styles.indexing}>
|
|
376
419
|
<div className={styles.radioGroup}>
|
|
377
420
|
<label className={styles.radioLabel}>
|
|
378
421
|
<input
|
|
@@ -409,80 +452,86 @@ export const NotesSidebar = ({ currentCase, onReturn, user, imageId, onAnnotatio
|
|
|
409
452
|
/>
|
|
410
453
|
) : null}
|
|
411
454
|
</div>
|
|
455
|
+
)}
|
|
412
456
|
</div>
|
|
413
457
|
|
|
414
|
-
<div className={styles.section}>
|
|
415
|
-
<
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
458
|
+
<div className={`${styles.section} ${styles.compactHalfSection}`}>
|
|
459
|
+
<button
|
|
460
|
+
type="button"
|
|
461
|
+
className={styles.sectionToggle}
|
|
462
|
+
onClick={() => setIsSupportOpen((prev) => !prev)}
|
|
463
|
+
aria-expanded={isSupportOpen}
|
|
464
|
+
>
|
|
465
|
+
<span className={styles.sectionTitle}>Support Level</span>
|
|
466
|
+
<span className={styles.sectionToggleIcon}>{isSupportOpen ? '−' : '+'}</span>
|
|
467
|
+
</button>
|
|
468
|
+
{isSupportOpen && (
|
|
469
|
+
<>
|
|
470
|
+
<div className={styles.support}>
|
|
471
|
+
<select
|
|
472
|
+
id="supportLevel"
|
|
473
|
+
aria-label="Support Level"
|
|
474
|
+
value={supportLevel}
|
|
475
|
+
onChange={(e) => {
|
|
476
|
+
const newSupportLevel = e.target.value as SupportLevel;
|
|
477
|
+
setSupportLevel(newSupportLevel);
|
|
478
|
+
|
|
479
|
+
// Automatically check confirmation field when ID is selected
|
|
480
|
+
if (newSupportLevel === 'ID') {
|
|
481
|
+
setIncludeConfirmation(true);
|
|
482
|
+
}
|
|
483
|
+
}}
|
|
484
|
+
className={styles.select}
|
|
485
|
+
disabled={areInputsDisabled}
|
|
486
|
+
>
|
|
487
|
+
<option value="">Select support level...</option>
|
|
488
|
+
<option value="ID">Identification</option>
|
|
489
|
+
<option value="Exclusion">Exclusion</option>
|
|
490
|
+
<option value="Inconclusive">Inconclusive</option>
|
|
491
|
+
</select>
|
|
492
|
+
<label className={`${styles.checkboxLabel} mb-4`}>
|
|
493
|
+
<input
|
|
494
|
+
type="checkbox"
|
|
495
|
+
checked={includeConfirmation}
|
|
496
|
+
onChange={(e) => setIncludeConfirmation(e.target.checked)}
|
|
497
|
+
className={styles.checkbox}
|
|
498
|
+
disabled={areInputsDisabled}
|
|
499
|
+
/>
|
|
500
|
+
<span>Include confirmation field</span>
|
|
501
|
+
</label>
|
|
502
|
+
</div>
|
|
503
|
+
</>
|
|
504
|
+
)}
|
|
457
505
|
</div>
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
506
|
+
</div>
|
|
507
|
+
|
|
508
|
+
<div className={styles.additionalNotesRow}>
|
|
509
|
+
<button
|
|
510
|
+
onClick={() => setIsModalOpen(true)}
|
|
511
|
+
className={styles.notesButton}
|
|
461
512
|
disabled={areInputsDisabled}
|
|
462
|
-
title={isConfirmedImage ? "Cannot
|
|
513
|
+
title={isConfirmedImage ? "Cannot edit notes for confirmed images" : isUploading ? "Cannot add notes while uploading" : undefined}
|
|
463
514
|
>
|
|
464
|
-
|
|
515
|
+
Additional Notes
|
|
465
516
|
</button>
|
|
466
|
-
|
|
467
|
-
{saveSuccess && (
|
|
468
|
-
<div className={styles.successMessage}>
|
|
469
|
-
Notes saved successfully!
|
|
470
|
-
</div>
|
|
471
|
-
)}
|
|
517
|
+
</div>
|
|
472
518
|
|
|
473
|
-
<
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
519
|
+
<div className={`${styles.notesActionBar} ${styles.notesActionBarSticky}`}>
|
|
520
|
+
<button
|
|
521
|
+
onClick={handleSave}
|
|
522
|
+
className={styles.saveButton}
|
|
523
|
+
disabled={areInputsDisabled}
|
|
524
|
+
title={isConfirmedImage ? "Cannot save notes for confirmed images" : isUploading ? "Cannot save notes while uploading" : undefined}
|
|
525
|
+
>
|
|
526
|
+
Save Notes
|
|
527
|
+
</button>
|
|
528
|
+
</div>
|
|
529
|
+
<AddlNotesModal
|
|
482
530
|
isOpen={isModalOpen}
|
|
483
531
|
onClose={() => setIsModalOpen(false)}
|
|
484
532
|
notes={additionalNotes}
|
|
485
533
|
onSave={setAdditionalNotes}
|
|
534
|
+
showNotification={notificationHandler}
|
|
486
535
|
/>
|
|
487
536
|
</>
|
|
488
537
|
)}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
.overlay {
|
|
2
|
+
position: fixed;
|
|
3
|
+
inset: 0;
|
|
4
|
+
background-color: color-mix(in lab, var(--background) 56%, transparent);
|
|
5
|
+
display: flex;
|
|
6
|
+
justify-content: center;
|
|
7
|
+
align-items: center;
|
|
8
|
+
z-index: var(--zIndex5);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.modal {
|
|
12
|
+
position: relative;
|
|
13
|
+
width: min(900px, calc(100vw - 2rem));
|
|
14
|
+
max-height: calc(100vh - 4rem);
|
|
15
|
+
background: var(--backgroundLight);
|
|
16
|
+
border-radius: var(--spaceXS);
|
|
17
|
+
box-shadow: 0 var(--spaceXS) var(--spaceL)
|
|
18
|
+
color-mix(in lab, var(--black) 16%, transparent);
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.header {
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
justify-content: space-between;
|
|
28
|
+
padding: var(--spaceM) var(--spaceL);
|
|
29
|
+
border-bottom: 1px solid color-mix(in lab, var(--text) 12%, transparent);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.title {
|
|
33
|
+
margin: 0;
|
|
34
|
+
color: var(--textTitle);
|
|
35
|
+
font-size: var(--fontSizeBodyM);
|
|
36
|
+
font-weight: var(--fontWeightMedium);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.closeButton {
|
|
40
|
+
background: none;
|
|
41
|
+
border: none;
|
|
42
|
+
color: var(--textLight);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.content {
|
|
46
|
+
padding: var(--spaceM) var(--spaceL);
|
|
47
|
+
overflow-y: auto;
|
|
48
|
+
max-height: calc(100vh - 11rem);
|
|
49
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { User } from 'firebase/auth';
|
|
2
|
+
import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
|
|
3
|
+
import { NotesEditorForm } from './notes-editor-form';
|
|
4
|
+
import styles from './notes-editor-modal.module.css';
|
|
5
|
+
|
|
6
|
+
interface NotesEditorModalProps {
|
|
7
|
+
isOpen: boolean;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
currentCase: string;
|
|
10
|
+
user: User;
|
|
11
|
+
imageId: string;
|
|
12
|
+
originalFileName?: string;
|
|
13
|
+
onAnnotationRefresh?: () => void;
|
|
14
|
+
isUploading?: boolean;
|
|
15
|
+
showNotification?: (message: string, type: 'success' | 'error' | 'warning') => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const NotesEditorModal = ({
|
|
19
|
+
isOpen,
|
|
20
|
+
onClose,
|
|
21
|
+
currentCase,
|
|
22
|
+
user,
|
|
23
|
+
imageId,
|
|
24
|
+
originalFileName,
|
|
25
|
+
onAnnotationRefresh,
|
|
26
|
+
isUploading = false,
|
|
27
|
+
showNotification,
|
|
28
|
+
}: NotesEditorModalProps) => {
|
|
29
|
+
const {
|
|
30
|
+
overlayProps,
|
|
31
|
+
getCloseButtonProps,
|
|
32
|
+
} = useOverlayDismiss({
|
|
33
|
+
isOpen,
|
|
34
|
+
onClose,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (!isOpen) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className={styles.overlay} aria-label="Close image notes dialog" {...overlayProps}>
|
|
43
|
+
<div className={styles.modal} role="dialog" aria-modal="true" aria-label="Image Notes">
|
|
44
|
+
<div className={styles.header}>
|
|
45
|
+
<h2 className={styles.title}>Image Notes</h2>
|
|
46
|
+
<button className={styles.closeButton} {...getCloseButtonProps({ ariaLabel: 'Close image notes dialog' })}>
|
|
47
|
+
×
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
50
|
+
<div className={styles.content}>
|
|
51
|
+
<NotesEditorForm
|
|
52
|
+
currentCase={currentCase}
|
|
53
|
+
user={user}
|
|
54
|
+
imageId={imageId}
|
|
55
|
+
onAnnotationRefresh={onAnnotationRefresh}
|
|
56
|
+
originalFileName={originalFileName}
|
|
57
|
+
isUploading={isUploading}
|
|
58
|
+
showNotification={showNotification}
|
|
59
|
+
/>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
};
|