@striae-org/striae 4.3.2 → 4.3.4

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 (34) hide show
  1. package/app/components/actions/case-import/orchestrator.ts +1 -1
  2. package/app/components/actions/case-manage.ts +50 -14
  3. package/app/components/audit/user-audit.module.css +49 -0
  4. package/app/components/audit/viewer/audit-entries-list.tsx +130 -48
  5. package/app/components/navbar/navbar.tsx +25 -12
  6. package/app/components/sidebar/case-import/case-import.tsx +56 -14
  7. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +7 -6
  8. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +9 -5
  9. package/app/components/sidebar/cases/cases-modal.module.css +19 -0
  10. package/app/components/sidebar/cases/cases-modal.tsx +23 -8
  11. package/app/routes/striae/hooks/use-striae-reset-helpers.ts +102 -0
  12. package/app/routes/striae/striae.tsx +72 -74
  13. package/app/routes/striae/utils/case-export.ts +37 -0
  14. package/app/routes/striae/utils/open-case-helper.ts +18 -0
  15. package/app/services/audit/audit-console-logger.ts +1 -1
  16. package/app/services/audit/audit-export-csv.ts +1 -1
  17. package/app/services/audit/audit-export-signing.ts +2 -2
  18. package/app/services/audit/audit-export.service.ts +1 -1
  19. package/app/services/audit/audit-worker-client.ts +1 -1
  20. package/app/services/audit/audit.service.ts +5 -75
  21. package/app/services/audit/builders/audit-event-builders-case-file.ts +3 -0
  22. package/app/services/audit/index.ts +2 -2
  23. package/app/types/audit.ts +8 -7
  24. package/app/utils/data/case-filters.ts +1 -1
  25. package/app/utils/ui/case-messages.ts +69 -0
  26. package/app/utils/ui/index.ts +1 -0
  27. package/package.json +5 -5
  28. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  29. package/workers/data-worker/wrangler.jsonc.example +1 -1
  30. package/workers/image-worker/wrangler.jsonc.example +1 -1
  31. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  32. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  33. package/workers/user-worker/wrangler.jsonc.example +1 -1
  34. package/wrangler.toml.example +1 -1
@@ -287,7 +287,7 @@ export async function importCaseForReview(
287
287
  throw new Error(`Case "${result.caseNumber}" already exists in your case list. You cannot import a case for review if you were the original analyst.`);
288
288
  }
289
289
  if (existingRegularCase && isArchivedExport) {
290
- throw new Error(`Cannot import this archive because case "${result.caseNumber}" already exists in your case list (active or archived). To import this archive, the existing case must first be deleted.`);
290
+ throw new Error(`Cannot import this archived case because "${result.caseNumber}" already exists in your regular case list. Delete the regular case before importing this archive.`);
291
291
  }
292
292
 
293
293
  // Step 2b: Check if read-only case already exists
@@ -14,7 +14,7 @@ import {
14
14
  signForensicManifest,
15
15
  removeCaseConfirmationSummary
16
16
  } from '~/utils/data';
17
- import { type CaseData, type ReadOnlyCaseData, type FileData, type AuditTrail, type CaseExportData } from '~/types';
17
+ import { type CaseData, type ReadOnlyCaseData, type FileData, type AuditTrail, type CaseExportData, type ValidationAuditEntry } from '~/types';
18
18
  import { auditService } from '~/services/audit';
19
19
  import { fetchImageApi } from '~/utils/api';
20
20
  import { exportCaseData, formatDateForFilename } from '~/components/actions/case-export';
@@ -27,7 +27,7 @@ import {
27
27
  getVerificationPublicKey,
28
28
  } from '~/utils/forensics';
29
29
  import { signAuditExport } from '~/services/audit/audit-export-signing';
30
- import { generateAuditSummary } from '~/services/audit/audit-query-helpers';
30
+ import { generateAuditSummary, sortAuditEntriesNewestFirst } from '~/services/audit/audit-query-helpers';
31
31
 
32
32
  /**
33
33
  * Delete a file without individual audit logging (for bulk operations)
@@ -182,8 +182,9 @@ export const checkExistingCase = async (user: User, caseNumber: string): Promise
182
182
  return null;
183
183
  }
184
184
 
185
- // Check if this is a read-only case - if so, don't consider it as an existing regular case
186
- if ('isReadOnly' in caseData && caseData.isReadOnly) {
185
+ // Imported review cases are read-only and should not be treated as regular cases.
186
+ // Archived cases remain regular case records even if legacy data includes isReadOnly.
187
+ if ('isReadOnly' in caseData && caseData.isReadOnly && !caseData.archived) {
187
188
  return null;
188
189
  }
189
190
 
@@ -212,11 +213,6 @@ export const checkCaseIsReadOnly = async (user: User, caseNumber: string): Promi
212
213
  return false;
213
214
  }
214
215
 
215
- // Archived cases are always treated as read-only.
216
- if (caseData.archived) {
217
- return true;
218
- }
219
-
220
216
  // Use type guard to check for isReadOnly property safely
221
217
  return isReadOnlyCaseData(caseData) ? !!caseData.isReadOnly : false;
222
218
 
@@ -397,15 +393,23 @@ export const renameCase = async (
397
393
  // 5) Delete old case number in user's KV entry
398
394
  await removeUserCase(user, oldCaseNumber);
399
395
 
400
- // Log successful case rename
396
+ // Log successful case rename under the original case number context
401
397
  const endTime = Date.now();
402
398
  await auditService.logCaseRename(
403
399
  user,
404
- newCaseNumber, // Use new case number as the current context
400
+ oldCaseNumber,
405
401
  oldCaseNumber,
406
402
  newCaseNumber
407
403
  );
408
404
 
405
+ // Log creation of the new case number as a rename-derived case
406
+ await auditService.logCaseCreation(
407
+ user,
408
+ newCaseNumber,
409
+ newCaseNumber,
410
+ oldCaseNumber
411
+ );
412
+
409
413
  console.log(`✅ Case renamed: ${oldCaseNumber} → ${newCaseNumber} (${endTime - startTime}ms)`);
410
414
 
411
415
  } catch (error) {
@@ -748,7 +752,7 @@ export const archiveCase = async (
748
752
  archivedBy: user.uid,
749
753
  archivedByDisplay,
750
754
  archiveReason: archiveReason?.trim() || undefined,
751
- isReadOnly: true,
755
+ isReadOnly: false,
752
756
  } as CaseData;
753
757
 
754
758
  const exportData = await exportCaseData(user, caseNumber, { includeMetadata: true });
@@ -812,11 +816,43 @@ export const archiveCase = async (
812
816
  startDate: caseData.createdAt,
813
817
  endDate: archivedAt,
814
818
  });
819
+
820
+ // Ensure the bundled archive trail includes the archival event itself.
821
+ const archiveAuditEntry: ValidationAuditEntry = {
822
+ timestamp: archivedAt,
823
+ userId: user.uid,
824
+ userEmail: user.email || '',
825
+ action: 'case-archive',
826
+ result: 'success',
827
+ details: {
828
+ fileName: `${caseNumber}.case`,
829
+ fileType: 'case-package',
830
+ validationErrors: [],
831
+ caseNumber,
832
+ workflowPhase: 'casework',
833
+ caseDetails: {
834
+ newCaseName: caseNumber,
835
+ archiveReason: archiveReason?.trim() || 'No reason provided',
836
+ totalFiles: archiveData.files?.length || 0,
837
+ lastModified: archivedAt,
838
+ },
839
+ performanceMetrics: {
840
+ processingTimeMs: Date.now() - startTime,
841
+ fileSizeBytes: 0,
842
+ },
843
+ },
844
+ };
845
+
846
+ const auditEntriesWithArchive = sortAuditEntriesNewestFirst([
847
+ ...auditEntries,
848
+ archiveAuditEntry,
849
+ ]);
850
+
815
851
  const auditTrail: AuditTrail = {
816
852
  caseNumber,
817
853
  workflowId: `${caseNumber}-archive-${Date.now()}`,
818
- entries: auditEntries,
819
- summary: generateAuditSummary(auditEntries),
854
+ entries: auditEntriesWithArchive,
855
+ summary: generateAuditSummary(auditEntriesWithArchive),
820
856
  };
821
857
 
822
858
  const auditTrailPayload = {
@@ -522,11 +522,60 @@
522
522
  white-space: nowrap;
523
523
  }
524
524
 
525
+ .entryHeaderActions {
526
+ display: flex;
527
+ align-items: center;
528
+ gap: 8px;
529
+ margin-left: auto;
530
+ }
531
+
532
+ .entryDetailsToggle {
533
+ background: color-mix(in lab, var(--primary) 10%, transparent);
534
+ color: color-mix(in lab, var(--primary) 65%, var(--text));
535
+ border: 1px solid color-mix(in lab, var(--primary) 30%, transparent);
536
+ padding: 4px 8px;
537
+ border-radius: 999px;
538
+ font-size: 0.75rem;
539
+ font-weight: var(--fontWeightMedium);
540
+ cursor: pointer;
541
+ transition: background-color var(--durationS) var(--bezierFastoutSlowin);
542
+ }
543
+
544
+ .entryDetailsToggle:hover {
545
+ background: color-mix(in lab, var(--primary) 16%, transparent);
546
+ }
547
+
548
+ .entryDetailsToggle:focus-visible {
549
+ outline: 2px solid color-mix(in lab, var(--primary) 45%, transparent);
550
+ outline-offset: 2px;
551
+ }
552
+
525
553
  /* Entry Details */
526
554
  .entryDetails {
527
555
  padding: 12px 14px;
528
556
  }
529
557
 
558
+ .expandedDetails {
559
+ margin-top: 10px;
560
+ padding-top: 10px;
561
+ border-top: 1px dashed color-mix(in lab, var(--textLight) 25%, transparent);
562
+ }
563
+
564
+ .expandedDetailsCode {
565
+ margin: 4px 0 0;
566
+ padding: 10px;
567
+ border-radius: 6px;
568
+ border: 1px solid color-mix(in lab, var(--textLight) 20%, transparent);
569
+ background: color-mix(in lab, var(--backgroundLight) 75%, transparent);
570
+ color: var(--text);
571
+ font-size: 0.78rem;
572
+ line-height: 1.4;
573
+ white-space: pre-wrap;
574
+ word-break: break-word;
575
+ max-height: 280px;
576
+ overflow: auto;
577
+ }
578
+
530
579
  .detailRow {
531
580
  display: flex;
532
581
  align-items: center;
@@ -1,3 +1,4 @@
1
+ import { useMemo, useState, type MouseEvent } from 'react';
1
2
  import { type ValidationAuditEntry } from '~/types';
2
3
  import { formatAuditTimestamp, getAuditActionIcon, getAuditStatusIcon } from './audit-viewer-utils';
3
4
  import styles from '../user-audit.module.css';
@@ -13,7 +14,57 @@ const isConfirmationImportEntry = (entry: ValidationAuditEntry): boolean => {
13
14
  );
14
15
  };
15
16
 
17
+ const isConfirmationEvent = (entry: ValidationAuditEntry): boolean => {
18
+ return (
19
+ entry.action === 'confirmation-create' ||
20
+ entry.action === 'confirmation-export' ||
21
+ entry.action === 'confirmation-import' ||
22
+ entry.action === 'confirm' ||
23
+ (entry.action === 'import' && entry.details.workflowPhase === 'confirmation') ||
24
+ (entry.action === 'export' && entry.details.workflowPhase === 'confirmation')
25
+ );
26
+ };
27
+
28
+ const supportsFullDetailsToggle = (entry: ValidationAuditEntry): boolean => {
29
+ return (
30
+ entry.action === 'annotation-create' ||
31
+ entry.action === 'annotation-edit' ||
32
+ entry.action === 'annotation-delete' ||
33
+ isConfirmationEvent(entry)
34
+ );
35
+ };
36
+
37
+ const getEntryKey = (entry: ValidationAuditEntry): string => {
38
+ return `${entry.timestamp}-${entry.userId}-${entry.action}-${entry.details.fileName || ''}-${entry.details.confirmationId || ''}`;
39
+ };
40
+
16
41
  export const AuditEntriesList = ({ entries }: AuditEntriesListProps) => {
42
+ const [expandedEntryKeys, setExpandedEntryKeys] = useState<Set<string>>(new Set());
43
+
44
+ const expandableEntries = useMemo(() => {
45
+ return new Set(entries.filter(supportsFullDetailsToggle).map(getEntryKey));
46
+ }, [entries]);
47
+
48
+ const toggleExpanded = (entryKey: string) => {
49
+ setExpandedEntryKeys((current) => {
50
+ const next = new Set(current);
51
+
52
+ if (next.has(entryKey)) {
53
+ next.delete(entryKey);
54
+ } else {
55
+ next.add(entryKey);
56
+ }
57
+
58
+ return next;
59
+ });
60
+ };
61
+
62
+ const handleToggleClick = (event: MouseEvent<HTMLButtonElement>, entryKey: string) => {
63
+ event.preventDefault();
64
+ event.stopPropagation();
65
+ toggleExpanded(entryKey);
66
+ };
67
+
17
68
  return (
18
69
  <div className={styles.entriesList}>
19
70
  <h3>Activity Log ({entries.length} entries)</h3>
@@ -22,30 +73,49 @@ export const AuditEntriesList = ({ entries }: AuditEntriesListProps) => {
22
73
  <p>No activities match the current filters.</p>
23
74
  </div>
24
75
  ) : (
25
- entries.map((entry) => (
26
- <div
27
- key={`${entry.timestamp}-${entry.userId}-${entry.action}-${entry.details.fileName || ''}`}
28
- className={`${styles.entry} ${styles[entry.result]}`}
29
- >
30
- <div className={styles.entryHeader}>
31
- <div className={styles.entryIcons}>
32
- <span className={styles.actionIcon}>{getAuditActionIcon(entry.action)}</span>
33
- <span className={styles.statusIcon}>{getAuditStatusIcon(entry.result)}</span>
34
- </div>
35
- <div className={styles.entryTitle}>
36
- <span className={styles.action}>{entry.action.toUpperCase().replace(/-/g, ' ')}</span>
37
- <span className={styles.fileName}>{entry.details.fileName}</span>
38
- </div>
39
- <div className={styles.entryTimestamp}>{formatAuditTimestamp(entry.timestamp)}</div>
40
- </div>
76
+ entries.map((entry) => {
77
+ const entryKey = getEntryKey(entry);
78
+ const isExpandable = expandableEntries.has(entryKey);
79
+ const isExpanded = expandedEntryKeys.has(entryKey);
41
80
 
42
- <div className={styles.entryDetails}>
43
- {entry.details.caseNumber && (
44
- <div className={styles.detailRow}>
45
- <span className={styles.detailLabel}>Case:</span>
46
- <span className={styles.detailValue}>{entry.details.caseNumber}</span>
81
+ return (
82
+ <div
83
+ key={entryKey}
84
+ className={`${styles.entry} ${styles[entry.result]}`}
85
+ >
86
+ <div className={styles.entryHeader}>
87
+ <div className={styles.entryIcons}>
88
+ <span className={styles.actionIcon}>{getAuditActionIcon(entry.action)}</span>
89
+ <span className={styles.statusIcon}>{getAuditStatusIcon(entry.result)}</span>
47
90
  </div>
48
- )}
91
+ <div className={styles.entryTitle}>
92
+ <span className={styles.action}>{entry.action.toUpperCase().replace(/-/g, ' ')}</span>
93
+ <span className={styles.fileName}>{entry.details.fileName}</span>
94
+ </div>
95
+
96
+ <div className={styles.entryHeaderActions}>
97
+ <div className={styles.entryTimestamp}>{formatAuditTimestamp(entry.timestamp)}</div>
98
+ {isExpandable && (
99
+ <button
100
+ type="button"
101
+ className={styles.entryDetailsToggle}
102
+ aria-expanded={isExpanded}
103
+ aria-label={isExpanded ? 'Hide full entry details' : 'Show full entry details'}
104
+ onClick={(event) => handleToggleClick(event, entryKey)}
105
+ >
106
+ {isExpanded ? 'Hide details' : 'Show details'}
107
+ </button>
108
+ )}
109
+ </div>
110
+ </div>
111
+
112
+ <div className={styles.entryDetails}>
113
+ {entry.details.caseNumber && (
114
+ <div className={styles.detailRow}>
115
+ <span className={styles.detailLabel}>Case:</span>
116
+ <span className={styles.detailValue}>{entry.details.caseNumber}</span>
117
+ </div>
118
+ )}
49
119
 
50
120
  {entry.details.userProfileDetails?.badgeId && (
51
121
  <div className={styles.detailRow}>
@@ -191,37 +261,49 @@ export const AuditEntriesList = ({ entries }: AuditEntriesListProps) => {
191
261
  </>
192
262
  )}
193
263
 
194
- {(entry.action === 'pdf-generate' || entry.action === 'confirm') && entry.details.fileDetails && (
195
- <>
196
- {entry.details.fileDetails.fileId && (
197
- <div className={styles.detailRow}>
198
- <span className={styles.detailLabel}>
199
- {entry.action === 'pdf-generate' ? 'Source File ID:' : 'Original Image ID:'}
200
- </span>
201
- <span className={styles.detailValue}>{entry.details.fileDetails.fileId}</span>
202
- </div>
203
- )}
264
+ {(entry.action === 'pdf-generate' || entry.action === 'confirm') && entry.details.fileDetails && (
265
+ <>
266
+ {entry.details.fileDetails.fileId && (
267
+ <div className={styles.detailRow}>
268
+ <span className={styles.detailLabel}>
269
+ {entry.action === 'pdf-generate' ? 'Source File ID:' : 'Original Image ID:'}
270
+ </span>
271
+ <span className={styles.detailValue}>{entry.details.fileDetails.fileId}</span>
272
+ </div>
273
+ )}
204
274
 
205
- {entry.details.fileDetails.originalFileName && (
206
- <div className={styles.detailRow}>
207
- <span className={styles.detailLabel}>
208
- {entry.action === 'pdf-generate' ? 'Source Filename:' : 'Original Filename:'}
209
- </span>
210
- <span className={styles.detailValue}>{entry.details.fileDetails.originalFileName}</span>
211
- </div>
212
- )}
275
+ {entry.details.fileDetails.originalFileName && (
276
+ <div className={styles.detailRow}>
277
+ <span className={styles.detailLabel}>
278
+ {entry.action === 'pdf-generate' ? 'Source Filename:' : 'Original Filename:'}
279
+ </span>
280
+ <span className={styles.detailValue}>{entry.details.fileDetails.originalFileName}</span>
281
+ </div>
282
+ )}
283
+
284
+ {entry.action === 'confirm' && entry.details.confirmationId && (
285
+ <div className={styles.detailRow}>
286
+ <span className={styles.detailLabel}>Confirmation ID:</span>
287
+ <span className={styles.detailValue}>{entry.details.confirmationId}</span>
288
+ </div>
289
+ )}
290
+ </>
291
+ )}
213
292
 
214
- {entry.action === 'confirm' && entry.details.confirmationId && (
293
+ {isExpandable && isExpanded && (
294
+ <div className={styles.expandedDetails}>
215
295
  <div className={styles.detailRow}>
216
- <span className={styles.detailLabel}>Confirmation ID:</span>
217
- <span className={styles.detailValue}>{entry.details.confirmationId}</span>
296
+ <span className={styles.detailLabel}>Full Entry Details:</span>
218
297
  </div>
219
- )}
220
- </>
221
- )}
298
+ <pre className={styles.expandedDetailsCode}>
299
+ {JSON.stringify(entry, null, 2)}
300
+ </pre>
301
+ </div>
302
+ )}
303
+ </div>
222
304
  </div>
223
- </div>
224
- ))
305
+ );
306
+ })
225
307
  )}
226
308
  </div>
227
309
  );
@@ -13,6 +13,7 @@ interface NavbarProps {
13
13
  isUploading?: boolean;
14
14
  company?: string;
15
15
  isReadOnly?: boolean;
16
+ isReviewOnlyCase?: boolean;
16
17
  currentCase?: string;
17
18
  currentFileName?: string;
18
19
  isCurrentImageConfirmed?: boolean;
@@ -43,6 +44,7 @@ export const Navbar = ({
43
44
  isUploading = false,
44
45
  company,
45
46
  isReadOnly = false,
47
+ isReviewOnlyCase = false,
46
48
  currentCase,
47
49
  currentFileName,
48
50
  isCurrentImageConfirmed = false,
@@ -119,16 +121,17 @@ export const Navbar = ({
119
121
  const disableLongRunningCaseActions = isUploading;
120
122
  const isCaseManagementActive = true;
121
123
  const isFileManagementActive = isFileMenuOpen || hasLoadedImage;
122
- const canOpenImageNotes = hasLoadedImage && !isCurrentImageConfirmed;
124
+ const canOpenImageNotes = hasLoadedImage && !isCurrentImageConfirmed && !isReadOnly;
123
125
  const isImageNotesActive = canOpenImageNotes;
124
126
  const canDeleteCurrentFile = hasLoadedImage && !isReadOnly;
127
+ const isArchivedRegularReadOnly = Boolean(isReadOnly && archiveDetails?.archived && !isReviewOnlyCase);
125
128
 
126
129
  return (
127
130
  <>
128
131
  <header className={styles.navbar} aria-label="Canvas top navigation">
129
132
  <div className={styles.companyLabelContainer}>
130
133
  <div className={styles.companyLabel}>
131
- {isReadOnly ? 'CASE REVIEW ONLY' : `${company}${user?.displayName ? ` | ${user.displayName}` : ''}${userBadgeId ? `, ${userBadgeId}` : ''}`}
134
+ {isReviewOnlyCase ? 'CASE REVIEW ONLY' : `${company}${user?.displayName ? ` | ${user.displayName}` : ''}${userBadgeId ? `, ${userBadgeId}` : ''}`}
132
135
  </div>
133
136
  </div>
134
137
  <div className={styles.navCenterTrack}>
@@ -153,8 +156,8 @@ export const Navbar = ({
153
156
  type="button"
154
157
  role="menuitem"
155
158
  className={`${styles.caseMenuItem} ${styles.caseMenuItemOpen}`}
156
- disabled={isReadOnly}
157
- title={isReadOnly ? 'Clear the read-only case first to open or switch cases' : undefined}
159
+ disabled={isReviewOnlyCase}
160
+ title={isReviewOnlyCase ? 'Clear the read-only case first to open or switch cases' : undefined}
158
161
  onClick={() => {
159
162
  onOpenCase?.();
160
163
  setIsCaseMenuOpen(false);
@@ -166,8 +169,8 @@ export const Navbar = ({
166
169
  type="button"
167
170
  role="menuitem"
168
171
  className={`${styles.caseMenuItem} ${styles.caseMenuItemList}`}
169
- disabled={isReadOnly}
170
- title={isReadOnly ? 'Clear the read-only case first to list all cases' : undefined}
172
+ disabled={isReviewOnlyCase}
173
+ title={isReviewOnlyCase ? 'Clear the read-only case first to list all cases' : undefined}
171
174
  onClick={() => {
172
175
  onOpenListAllCases?.();
173
176
  setIsCaseMenuOpen(false);
@@ -180,9 +183,11 @@ export const Navbar = ({
180
183
  type="button"
181
184
  role="menuitem"
182
185
  className={`${styles.caseMenuItem} ${styles.caseMenuItemExport}`}
183
- disabled={!hasLoadedCase || disableLongRunningCaseActions}
186
+ disabled={!hasLoadedCase || disableLongRunningCaseActions || isArchivedRegularReadOnly}
184
187
  title={
185
- !hasLoadedCase
188
+ isArchivedRegularReadOnly
189
+ ? 'Export is unavailable for archived cases loaded from your regular case list'
190
+ : !hasLoadedCase
186
191
  ? 'Load a case to export case data'
187
192
  : disableLongRunningCaseActions
188
193
  ? 'Export is unavailable while files are uploading'
@@ -209,7 +214,7 @@ export const Navbar = ({
209
214
  Case Audit Trail
210
215
  </button>
211
216
  <div className={styles.caseMenuSectionLabel}>Maintenance</div>
212
- {isReadOnly && (
217
+ {isReviewOnlyCase && (
213
218
  <button
214
219
  type="button"
215
220
  role="menuitem"
@@ -249,9 +254,9 @@ export const Navbar = ({
249
254
  type="button"
250
255
  role="menuitem"
251
256
  className={`${styles.caseMenuItem} ${styles.caseMenuItemDelete}`}
252
- disabled={!hasLoadedCase || disableLongRunningCaseActions || isReadOnly}
257
+ disabled={!hasLoadedCase || disableLongRunningCaseActions || isReviewOnlyCase}
253
258
  title={
254
- isReadOnly
259
+ isReviewOnlyCase
255
260
  ? 'Clear the read-only case first before deleting'
256
261
  : !hasLoadedCase
257
262
  ? 'Load a case to delete it'
@@ -370,7 +375,15 @@ export const Navbar = ({
370
375
  className={`${styles.navSectionButton} ${isImageNotesActive ? styles.navSectionButtonActive : ''}`}
371
376
  disabled={!canOpenImageNotes}
372
377
  aria-pressed={isImageNotesActive}
373
- title={!hasLoadedImage ? 'Load an image to enable image notes' : isCurrentImageConfirmed ? 'Confirmed images are read-only and viewable via toolbar only' : undefined}
378
+ title={
379
+ !hasLoadedImage
380
+ ? 'Load an image to enable image notes'
381
+ : isCurrentImageConfirmed
382
+ ? 'Confirmed images are read-only and viewable via toolbar only'
383
+ : isReadOnly
384
+ ? 'Image notes are disabled for read-only cases'
385
+ : undefined
386
+ }
374
387
  onClick={() => {
375
388
  onOpenImageNotes?.();
376
389
  }}