@striae-org/striae 6.1.0 → 6.1.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/app/components/colors/colors.module.css +19 -0
- package/app/components/colors/colors.tsx +5 -1
- package/app/components/navbar/navbar.tsx +26 -11
- package/app/components/sidebar/cases/case-sidebar.tsx +21 -24
- package/app/components/sidebar/notes/notes-editor-form.tsx +29 -27
- package/app/components/sidebar/sidebar-container.tsx +0 -1
- package/app/components/sidebar/sidebar.tsx +0 -3
- package/app/routes/striae/striae.tsx +2 -3
- package/app/types/annotations.ts +4 -3
- package/app/utils/data/permissions.ts +123 -0
- package/package.json +139 -139
- package/workers/audit-worker/package.json +2 -2
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +2 -2
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/package.json +2 -2
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/package.json +2 -2
- package/workers/pdf-worker/src/report-types.ts +4 -2
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +2 -2
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
|
@@ -26,6 +26,11 @@
|
|
|
26
26
|
border-color: #adb5bd;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
.toggleButton:disabled {
|
|
30
|
+
cursor: not-allowed;
|
|
31
|
+
opacity: 0.6;
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
.colorWheel {
|
|
30
35
|
width: 180px;
|
|
31
36
|
height: 40px;
|
|
@@ -35,6 +40,11 @@
|
|
|
35
40
|
cursor: pointer;
|
|
36
41
|
}
|
|
37
42
|
|
|
43
|
+
.colorWheel:disabled {
|
|
44
|
+
cursor: not-allowed;
|
|
45
|
+
opacity: 0.6;
|
|
46
|
+
}
|
|
47
|
+
|
|
38
48
|
.colorGrid {
|
|
39
49
|
display: grid;
|
|
40
50
|
grid-template-columns: repeat(5, 1fr);
|
|
@@ -54,6 +64,15 @@
|
|
|
54
64
|
transform: scale(1.1);
|
|
55
65
|
}
|
|
56
66
|
|
|
67
|
+
.colorSwatch:disabled {
|
|
68
|
+
cursor: not-allowed;
|
|
69
|
+
opacity: 0.6;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.colorSwatch:disabled:hover {
|
|
73
|
+
transform: none;
|
|
74
|
+
}
|
|
75
|
+
|
|
57
76
|
.colorSwatch.selected {
|
|
58
77
|
border-color: #0d6efd;
|
|
59
78
|
box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25);
|
|
@@ -4,6 +4,7 @@ import styles from './colors.module.css';
|
|
|
4
4
|
interface ColorSelectorProps {
|
|
5
5
|
selectedColor: string;
|
|
6
6
|
onColorSelect: (color: string) => void;
|
|
7
|
+
disabled?: boolean;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
interface ColorOption {
|
|
@@ -24,7 +25,7 @@ const commonColors: ColorOption[] = [
|
|
|
24
25
|
{ value: '#ffffff', label: 'White' }
|
|
25
26
|
];
|
|
26
27
|
|
|
27
|
-
export const ColorSelector = ({ selectedColor, onColorSelect }: ColorSelectorProps) => {
|
|
28
|
+
export const ColorSelector = ({ selectedColor, onColorSelect, disabled = false }: ColorSelectorProps) => {
|
|
28
29
|
const [showColorWheel, setShowColorWheel] = useState(false);
|
|
29
30
|
|
|
30
31
|
return (
|
|
@@ -34,6 +35,7 @@ export const ColorSelector = ({ selectedColor, onColorSelect }: ColorSelectorPro
|
|
|
34
35
|
<button
|
|
35
36
|
onClick={() => setShowColorWheel(!showColorWheel)}
|
|
36
37
|
className={styles.toggleButton}
|
|
38
|
+
disabled={disabled}
|
|
37
39
|
>
|
|
38
40
|
{showColorWheel ? 'Presets' : 'Color Wheel'}
|
|
39
41
|
</button>
|
|
@@ -47,6 +49,7 @@ export const ColorSelector = ({ selectedColor, onColorSelect }: ColorSelectorPro
|
|
|
47
49
|
onChange={(e) => onColorSelect(e.target.value)}
|
|
48
50
|
className={styles.colorWheel}
|
|
49
51
|
title="Choose a color"
|
|
52
|
+
disabled={disabled}
|
|
50
53
|
/>
|
|
51
54
|
</>
|
|
52
55
|
) : (
|
|
@@ -59,6 +62,7 @@ export const ColorSelector = ({ selectedColor, onColorSelect }: ColorSelectorPro
|
|
|
59
62
|
onClick={() => onColorSelect(color.value)}
|
|
60
63
|
aria-label={`Select ${color.label}`}
|
|
61
64
|
title={color.label}
|
|
65
|
+
disabled={disabled}
|
|
62
66
|
/>
|
|
63
67
|
))}
|
|
64
68
|
</div>
|
|
@@ -4,7 +4,7 @@ import { SignOut } from '../actions/signout';
|
|
|
4
4
|
import { ManageProfile } from '../user/manage-profile';
|
|
5
5
|
import { CaseImport } from './case-import/case-import';
|
|
6
6
|
import { AuthContext } from '~/contexts/auth.context';
|
|
7
|
-
import { getUserData } from '~/utils/data';
|
|
7
|
+
import { getUserData, getNotesViewPermission, getNotesButtonTooltip } from '~/utils/data';
|
|
8
8
|
import { type ImportResult, type ConfirmationImportResult } from '~/types';
|
|
9
9
|
|
|
10
10
|
interface NavbarProps {
|
|
@@ -117,11 +117,28 @@ export const Navbar = ({
|
|
|
117
117
|
const disableLongRunningCaseActions = isUploading;
|
|
118
118
|
const isCaseManagementActive = true;
|
|
119
119
|
const isFileManagementActive = isFileMenuOpen || hasLoadedImage;
|
|
120
|
-
const canOpenImageNotes = hasLoadedImage;
|
|
121
|
-
const isImageNotesReadOnly = isReadOnly || isCurrentImageConfirmed || isUploading;
|
|
122
|
-
const isImageNotesActive = canOpenImageNotes;
|
|
123
120
|
const canDeleteCurrentFile = hasLoadedImage && !isReadOnly;
|
|
124
121
|
const isArchivedCase = Boolean(isReadOnly && archiveDetails?.archived);
|
|
122
|
+
|
|
123
|
+
// Use centralized permission helper for notes
|
|
124
|
+
const notesPermission = getNotesViewPermission({
|
|
125
|
+
imageLoaded: hasLoadedImage,
|
|
126
|
+
isUploading: isUploading || false,
|
|
127
|
+
isCheckingConfirmation: false, // Navbar doesn't track this granularly
|
|
128
|
+
isReadOnlyCase: isReadOnly || false,
|
|
129
|
+
isArchivedCase: isArchivedCase,
|
|
130
|
+
isConfirmedImage: isCurrentImageConfirmed || false
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const imageNotesTitle = getNotesButtonTooltip(notesPermission, {
|
|
134
|
+
isReadOnlyCase: isReadOnly,
|
|
135
|
+
isArchivedCase: isArchivedCase,
|
|
136
|
+
isConfirmedImage: isCurrentImageConfirmed
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const canOpenImageNotes = notesPermission.canOpen;
|
|
140
|
+
const isImageNotesActive = canOpenImageNotes;
|
|
141
|
+
|
|
125
142
|
const caseExportLabel = isArchivedCase
|
|
126
143
|
? 'Export Archive'
|
|
127
144
|
: isReadOnly
|
|
@@ -365,14 +382,12 @@ export const Navbar = ({
|
|
|
365
382
|
className={`${styles.navSectionButton} ${isImageNotesActive ? styles.navSectionButtonActive : ''}`}
|
|
366
383
|
disabled={!canOpenImageNotes}
|
|
367
384
|
aria-pressed={isImageNotesActive}
|
|
368
|
-
title={
|
|
369
|
-
!hasLoadedImage
|
|
370
|
-
? 'Load an image to enable image notes'
|
|
371
|
-
: isImageNotesReadOnly
|
|
372
|
-
? 'Image notes are view-only in this state'
|
|
373
|
-
: undefined
|
|
374
|
-
}
|
|
385
|
+
title={imageNotesTitle}
|
|
375
386
|
onClick={() => {
|
|
387
|
+
if (!notesPermission.canOpen) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
376
391
|
onOpenImageNotes?.();
|
|
377
392
|
}}
|
|
378
393
|
>
|
|
@@ -11,7 +11,9 @@ import {
|
|
|
11
11
|
import {
|
|
12
12
|
canUploadFile,
|
|
13
13
|
ensureCaseConfirmationSummary,
|
|
14
|
-
getCaseConfirmationSummary
|
|
14
|
+
getCaseConfirmationSummary,
|
|
15
|
+
getNotesViewPermission,
|
|
16
|
+
getNotesButtonTooltip
|
|
15
17
|
} from '~/utils/data';
|
|
16
18
|
import { type FileData } from '~/types';
|
|
17
19
|
|
|
@@ -28,7 +30,6 @@ interface CaseSidebarProps {
|
|
|
28
30
|
isReadOnly?: boolean;
|
|
29
31
|
isReviewOnlyCase?: boolean;
|
|
30
32
|
isArchivedCase?: boolean;
|
|
31
|
-
isConfirmed?: boolean;
|
|
32
33
|
confirmationSaveVersion?: number;
|
|
33
34
|
selectedFileId?: string;
|
|
34
35
|
isUploading?: boolean;
|
|
@@ -50,7 +51,6 @@ export const CaseSidebar = ({
|
|
|
50
51
|
isReadOnly = false,
|
|
51
52
|
isReviewOnlyCase = false,
|
|
52
53
|
isArchivedCase = false,
|
|
53
|
-
isConfirmed = false,
|
|
54
54
|
confirmationSaveVersion = 0,
|
|
55
55
|
selectedFileId,
|
|
56
56
|
isUploading = false,
|
|
@@ -238,27 +238,24 @@ const handleImageSelect = (file: FileData) => {
|
|
|
238
238
|
selectedFileId && !selectedFileConfirmationState
|
|
239
239
|
);
|
|
240
240
|
|
|
241
|
-
const isSelectedFileConfirmed =
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
:
|
|
256
|
-
|
|
257
|
-
:
|
|
258
|
-
|
|
259
|
-
: !imageLoaded
|
|
260
|
-
? 'Select an image first'
|
|
261
|
-
: undefined;
|
|
241
|
+
const isSelectedFileConfirmed = !!selectedFileConfirmationState?.isConfirmed;
|
|
242
|
+
|
|
243
|
+
// Use centralized permission helper
|
|
244
|
+
const notesPermission = getNotesViewPermission({
|
|
245
|
+
imageLoaded,
|
|
246
|
+
isUploading,
|
|
247
|
+
isCheckingConfirmation: isCheckingSelectedFileConfirmation,
|
|
248
|
+
isReadOnlyCase: isReadOnly,
|
|
249
|
+
isArchivedCase: isArchivedCase,
|
|
250
|
+
isConfirmedImage: isSelectedFileConfirmed
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const isImageNotesDisabled = !notesPermission.canOpen;
|
|
254
|
+
const imageNotesTitle = getNotesButtonTooltip(notesPermission, {
|
|
255
|
+
isReadOnlyCase: isReadOnly,
|
|
256
|
+
isArchivedCase: isArchivedCase,
|
|
257
|
+
isConfirmedImage: isSelectedFileConfirmed
|
|
258
|
+
});
|
|
262
259
|
|
|
263
260
|
const showCaseExportButton = Boolean(currentCase && isReadOnly);
|
|
264
261
|
const caseExportButtonLabel = isArchivedCase ? 'Export Archive' : 'Export Confirmations';
|
|
@@ -5,7 +5,7 @@ import { AddlNotesModal } from './addl-notes-modal';
|
|
|
5
5
|
import { ItemDetailsModal } from './item-details/item-details-modal';
|
|
6
6
|
import { buildItemDetailsSummary } from './item-details/item-details-shared';
|
|
7
7
|
import { getNotes, saveNotes } from '~/components/actions/notes-manage';
|
|
8
|
-
import { type AnnotationData, type BulletAnnotationData, type CartridgeCaseAnnotationData, type ShotshellAnnotationData, type ItemType } from '~/types/annotations';
|
|
8
|
+
import { type AnnotationData, type BulletAnnotationData, type CartridgeCaseAnnotationData, type ShotshellAnnotationData, type ItemType, type SupportLevel, type IndexType } from '~/types/annotations';
|
|
9
9
|
import { resolveEarliestAnnotationTimestamp } from '~/utils/ui';
|
|
10
10
|
import { auditService } from '~/services/audit';
|
|
11
11
|
import styles from './notes.module.css';
|
|
@@ -23,9 +23,6 @@ interface NotesEditorFormProps {
|
|
|
23
23
|
onRegisterSaveHandler?: (saveHandler: (() => Promise<boolean>) | null) => void;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
type SupportLevel = 'ID' | 'Exclusion' | 'Inconclusive';
|
|
27
|
-
type IndexType = 'number' | 'color';
|
|
28
|
-
|
|
29
26
|
interface NotesFormSnapshot {
|
|
30
27
|
leftCase: string;
|
|
31
28
|
rightCase: string;
|
|
@@ -154,7 +151,9 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
154
151
|
const [savedSnapshot, setSavedSnapshot] = useState<string>('');
|
|
155
152
|
const [hasLoadedSnapshot, setHasLoadedSnapshot] = useState(false);
|
|
156
153
|
const [isDirty, setIsDirty] = useState(false);
|
|
157
|
-
const
|
|
154
|
+
const areEditsDisabled = isUploading || isReadOnly || isConfirmedImage;
|
|
155
|
+
const isReadOnlyMode = isConfirmedImage || isReadOnly;
|
|
156
|
+
const canOpenModals = !isUploading;
|
|
158
157
|
|
|
159
158
|
const notificationHandler = useCallback((message: string, type: 'success' | 'error' | 'warning' = 'success') => {
|
|
160
159
|
if (externalShowNotification) {
|
|
@@ -687,7 +686,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
687
686
|
type="text"
|
|
688
687
|
value={leftCase}
|
|
689
688
|
onChange={(e) => setLeftCase(e.target.value)}
|
|
690
|
-
disabled={useCurrentCaseLeft ||
|
|
689
|
+
disabled={useCurrentCaseLeft || areEditsDisabled}
|
|
691
690
|
/>
|
|
692
691
|
</div>
|
|
693
692
|
<label className={`${styles.checkboxLabel} mb-4`}>
|
|
@@ -696,7 +695,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
696
695
|
checked={useCurrentCaseLeft}
|
|
697
696
|
onChange={(e) => setUseCurrentCaseLeft(e.target.checked)}
|
|
698
697
|
className={styles.checkbox}
|
|
699
|
-
disabled={
|
|
698
|
+
disabled={areEditsDisabled}
|
|
700
699
|
/>
|
|
701
700
|
<span>Use current case number</span>
|
|
702
701
|
</label>
|
|
@@ -707,7 +706,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
707
706
|
type="text"
|
|
708
707
|
value={leftItem}
|
|
709
708
|
onChange={(e) => setLeftItem(e.target.value)}
|
|
710
|
-
disabled={
|
|
709
|
+
disabled={areEditsDisabled}
|
|
711
710
|
/>
|
|
712
711
|
</div>
|
|
713
712
|
</div>
|
|
@@ -720,7 +719,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
720
719
|
type="text"
|
|
721
720
|
value={rightCase}
|
|
722
721
|
onChange={(e) => setRightCase(e.target.value)}
|
|
723
|
-
disabled={useCurrentCaseRight ||
|
|
722
|
+
disabled={useCurrentCaseRight || areEditsDisabled}
|
|
724
723
|
/>
|
|
725
724
|
</div>
|
|
726
725
|
<label className={`${styles.checkboxLabel} mb-4`}>
|
|
@@ -729,7 +728,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
729
728
|
checked={useCurrentCaseRight}
|
|
730
729
|
onChange={(e) => setUseCurrentCaseRight(e.target.checked)}
|
|
731
730
|
className={styles.checkbox}
|
|
732
|
-
disabled={
|
|
731
|
+
disabled={areEditsDisabled}
|
|
733
732
|
/>
|
|
734
733
|
<span>Use current case number</span>
|
|
735
734
|
</label>
|
|
@@ -740,7 +739,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
740
739
|
type="text"
|
|
741
740
|
value={rightItem}
|
|
742
741
|
onChange={(e) => setRightItem(e.target.value)}
|
|
743
|
-
disabled={
|
|
742
|
+
disabled={areEditsDisabled}
|
|
744
743
|
/>
|
|
745
744
|
</div>
|
|
746
745
|
</div>
|
|
@@ -751,6 +750,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
751
750
|
<ColorSelector
|
|
752
751
|
selectedColor={caseFontColor}
|
|
753
752
|
onColorSelect={setCaseFontColor}
|
|
753
|
+
disabled={areEditsDisabled}
|
|
754
754
|
/>
|
|
755
755
|
</div>
|
|
756
756
|
</>
|
|
@@ -795,7 +795,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
795
795
|
value={getSelectedItemData().itemType}
|
|
796
796
|
onChange={(e) => setSelectedItemData({ itemType: e.target.value as ItemType })}
|
|
797
797
|
className={styles.select}
|
|
798
|
-
disabled={
|
|
798
|
+
disabled={areEditsDisabled}
|
|
799
799
|
>
|
|
800
800
|
<option value="">Select item type...</option>
|
|
801
801
|
<option value="Bullet">Bullet</option>
|
|
@@ -810,7 +810,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
810
810
|
value={getSelectedItemData().customClass}
|
|
811
811
|
onChange={(e) => setSelectedItemData({ customClass: e.target.value })}
|
|
812
812
|
placeholder="Specify object type"
|
|
813
|
-
disabled={
|
|
813
|
+
disabled={areEditsDisabled}
|
|
814
814
|
/>
|
|
815
815
|
)}
|
|
816
816
|
|
|
@@ -819,7 +819,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
819
819
|
onChange={(e) => setSelectedItemData({ classNote: e.target.value })}
|
|
820
820
|
placeholder="Enter item details..."
|
|
821
821
|
className={styles.textarea}
|
|
822
|
-
disabled={
|
|
822
|
+
disabled={areEditsDisabled}
|
|
823
823
|
/>
|
|
824
824
|
</div>
|
|
825
825
|
</div>
|
|
@@ -838,7 +838,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
838
838
|
checked={getSelectedItemData().hasSubclass}
|
|
839
839
|
onChange={(e) => setSelectedItemData({ hasSubclass: e.target.checked })}
|
|
840
840
|
className={styles.checkbox}
|
|
841
|
-
disabled={
|
|
841
|
+
disabled={areEditsDisabled}
|
|
842
842
|
/>
|
|
843
843
|
<span>Potential subclass?</span>
|
|
844
844
|
</label>
|
|
@@ -866,7 +866,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
866
866
|
type="radio"
|
|
867
867
|
checked={indexType === 'color'}
|
|
868
868
|
onChange={() => setIndexType('color')}
|
|
869
|
-
disabled={
|
|
869
|
+
disabled={areEditsDisabled}
|
|
870
870
|
/>
|
|
871
871
|
<span>Color</span>
|
|
872
872
|
</label>
|
|
@@ -875,7 +875,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
875
875
|
type="radio"
|
|
876
876
|
checked={indexType === 'number'}
|
|
877
877
|
onChange={() => setIndexType('number')}
|
|
878
|
-
disabled={
|
|
878
|
+
disabled={areEditsDisabled}
|
|
879
879
|
/>
|
|
880
880
|
<span>Number/Letter</span>
|
|
881
881
|
</label>
|
|
@@ -887,12 +887,13 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
887
887
|
value={indexNumber}
|
|
888
888
|
onChange={(e) => setIndexNumber(e.target.value)}
|
|
889
889
|
placeholder="Enter index number"
|
|
890
|
-
disabled={
|
|
890
|
+
disabled={areEditsDisabled}
|
|
891
891
|
/>
|
|
892
892
|
) : indexType === 'color' ? (
|
|
893
893
|
<ColorSelector
|
|
894
894
|
selectedColor={indexColor}
|
|
895
895
|
onColorSelect={setIndexColor}
|
|
896
|
+
disabled={areEditsDisabled}
|
|
896
897
|
/>
|
|
897
898
|
) : null}
|
|
898
899
|
</div>
|
|
@@ -926,7 +927,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
926
927
|
}
|
|
927
928
|
}}
|
|
928
929
|
className={styles.select}
|
|
929
|
-
disabled={
|
|
930
|
+
disabled={areEditsDisabled}
|
|
930
931
|
>
|
|
931
932
|
<option value="">Select support level...</option>
|
|
932
933
|
<option value="ID">Identification</option>
|
|
@@ -939,7 +940,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
939
940
|
checked={includeConfirmation}
|
|
940
941
|
onChange={(e) => setIncludeConfirmation(e.target.checked)}
|
|
941
942
|
className={styles.checkbox}
|
|
942
|
-
disabled={
|
|
943
|
+
disabled={areEditsDisabled}
|
|
943
944
|
/>
|
|
944
945
|
<span>Include confirmation field</span>
|
|
945
946
|
</label>
|
|
@@ -953,7 +954,8 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
953
954
|
<button
|
|
954
955
|
onClick={() => setIsModalOpen(true)}
|
|
955
956
|
className={styles.notesButton}
|
|
956
|
-
|
|
957
|
+
disabled={!canOpenModals}
|
|
958
|
+
title={isUploading ? "Cannot open notes while uploading" : undefined}
|
|
957
959
|
>
|
|
958
960
|
Additional Notes
|
|
959
961
|
</button>
|
|
@@ -963,8 +965,8 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
963
965
|
<button
|
|
964
966
|
onClick={handleSave}
|
|
965
967
|
className={styles.saveButton}
|
|
966
|
-
disabled={
|
|
967
|
-
title={isConfirmedImage ? "Cannot save notes
|
|
968
|
+
disabled={areEditsDisabled}
|
|
969
|
+
title={isConfirmedImage ? "Cannot save notes - image is confirmed" : isUploading ? "Cannot save notes while uploading" : isReadOnly ? "Cannot save notes - case is read-only" : undefined}
|
|
968
970
|
>
|
|
969
971
|
Save Notes
|
|
970
972
|
</button>
|
|
@@ -974,13 +976,13 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
974
976
|
onClose={() => setIsModalOpen(false)}
|
|
975
977
|
notes={additionalNotes}
|
|
976
978
|
onSave={(notes) => {
|
|
977
|
-
if (
|
|
979
|
+
if (areEditsDisabled) {
|
|
978
980
|
return;
|
|
979
981
|
}
|
|
980
982
|
|
|
981
983
|
setAdditionalNotes(notes);
|
|
982
984
|
}}
|
|
983
|
-
isReadOnly={
|
|
985
|
+
isReadOnly={isReadOnlyMode}
|
|
984
986
|
showNotification={notificationHandler}
|
|
985
987
|
/>
|
|
986
988
|
<ItemDetailsModal
|
|
@@ -991,7 +993,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
991
993
|
cartridgeCaseData={getSelectedItemData().cartridgeCaseData}
|
|
992
994
|
shotshellData={getSelectedItemData().shotshellData}
|
|
993
995
|
onSave={(b, c, s) => {
|
|
994
|
-
if (
|
|
996
|
+
if (areEditsDisabled) {
|
|
995
997
|
return;
|
|
996
998
|
}
|
|
997
999
|
|
|
@@ -1010,7 +1012,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
1010
1012
|
}
|
|
1011
1013
|
}}
|
|
1012
1014
|
showNotification={notificationHandler}
|
|
1013
|
-
isReadOnly={
|
|
1015
|
+
isReadOnly={isReadOnlyMode}
|
|
1014
1016
|
/>
|
|
1015
1017
|
</>
|
|
1016
1018
|
)}
|
|
@@ -26,7 +26,6 @@ interface SidebarContainerProps {
|
|
|
26
26
|
isReadOnly?: boolean;
|
|
27
27
|
isReviewOnlyCase?: boolean;
|
|
28
28
|
isArchivedCase?: boolean;
|
|
29
|
-
isConfirmed?: boolean;
|
|
30
29
|
confirmationSaveVersion?: number;
|
|
31
30
|
isUploading?: boolean;
|
|
32
31
|
onUploadStatusChange?: (isUploading: boolean) => void;
|
|
@@ -22,7 +22,6 @@ interface SidebarProps {
|
|
|
22
22
|
isReadOnly?: boolean;
|
|
23
23
|
isReviewOnlyCase?: boolean;
|
|
24
24
|
isArchivedCase?: boolean;
|
|
25
|
-
isConfirmed?: boolean;
|
|
26
25
|
confirmationSaveVersion?: number;
|
|
27
26
|
isUploading?: boolean;
|
|
28
27
|
onUploadStatusChange?: (isUploading: boolean) => void;
|
|
@@ -43,7 +42,6 @@ export const Sidebar = ({
|
|
|
43
42
|
isReadOnly = false,
|
|
44
43
|
isReviewOnlyCase = false,
|
|
45
44
|
isArchivedCase = false,
|
|
46
|
-
isConfirmed = false,
|
|
47
45
|
confirmationSaveVersion = 0,
|
|
48
46
|
isUploading: initialIsUploading = false,
|
|
49
47
|
onUploadStatusChange,
|
|
@@ -93,7 +91,6 @@ export const Sidebar = ({
|
|
|
93
91
|
isReadOnly={isReadOnly}
|
|
94
92
|
isReviewOnlyCase={isReviewOnlyCase}
|
|
95
93
|
isArchivedCase={isArchivedCase}
|
|
96
|
-
isConfirmed={isConfirmed}
|
|
97
94
|
confirmationSaveVersion={confirmationSaveVersion}
|
|
98
95
|
selectedFileId={imageId}
|
|
99
96
|
isUploading={isUploading}
|
|
@@ -746,10 +746,10 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
746
746
|
const isCurrentImageConfirmed = hasLoadedImage && !!annotationData?.confirmationData;
|
|
747
747
|
|
|
748
748
|
useEffect(() => {
|
|
749
|
-
if (showNotes &&
|
|
749
|
+
if (showNotes && !hasLoadedImage) {
|
|
750
750
|
setShowNotes(false);
|
|
751
751
|
}
|
|
752
|
-
}, [showNotes, hasLoadedImage
|
|
752
|
+
}, [showNotes, hasLoadedImage]);
|
|
753
753
|
|
|
754
754
|
// Automatic save handler for annotation updates
|
|
755
755
|
const handleAnnotationUpdate = async (data: AnnotationData) => {
|
|
@@ -868,7 +868,6 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
868
868
|
isReadOnly={isReadOnlyCase}
|
|
869
869
|
isReviewOnlyCase={isReviewOnlyCase}
|
|
870
870
|
isArchivedCase={archiveDetails.archived}
|
|
871
|
-
isConfirmed={!!annotationData?.confirmationData}
|
|
872
871
|
confirmationSaveVersion={confirmationSaveVersion}
|
|
873
872
|
isUploading={isUploading}
|
|
874
873
|
onUploadStatusChange={setIsUploading}
|
package/app/types/annotations.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Annotation-related types and interfaces
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import type { ItemType, SupportLevel, IndexType } from '../../shared/types/annotation-literals';
|
|
4
|
+
export type { ItemType, SupportLevel, IndexType };
|
|
4
5
|
|
|
5
6
|
export interface BoxAnnotation {
|
|
6
7
|
id: string;
|
|
@@ -100,10 +101,10 @@ export interface AnnotationData {
|
|
|
100
101
|
cartridgeCaseData?: CartridgeCaseAnnotationData;
|
|
101
102
|
shotshellData?: ShotshellAnnotationData;
|
|
102
103
|
hasSubclass?: boolean;
|
|
103
|
-
indexType?:
|
|
104
|
+
indexType?: IndexType;
|
|
104
105
|
indexNumber?: string;
|
|
105
106
|
indexColor?: string;
|
|
106
|
-
supportLevel?:
|
|
107
|
+
supportLevel?: SupportLevel;
|
|
107
108
|
includeConfirmation: boolean;
|
|
108
109
|
confirmationData?: ConfirmationData;
|
|
109
110
|
leftAdditionalNotes?: string;
|
|
@@ -26,6 +26,16 @@ export interface CaseMetadata {
|
|
|
26
26
|
createdAt: string;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Result for notes viewing permissions
|
|
31
|
+
* Determines if notes can be viewed and in what mode (edit or view-only)
|
|
32
|
+
*/
|
|
33
|
+
export interface NotesViewPermission {
|
|
34
|
+
canOpen: boolean; // Can the notes panel be opened
|
|
35
|
+
isReadOnly: boolean; // Are notes in read-only mode (can view but not edit)
|
|
36
|
+
reason?: string; // Reason if notes cannot be opened
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
/**
|
|
30
40
|
* Get user data from KV store
|
|
31
41
|
*/
|
|
@@ -565,4 +575,117 @@ export const removeUserCase = async (user: User, caseNumber: string): Promise<vo
|
|
|
565
575
|
console.error('Error removing case from user:', error);
|
|
566
576
|
throw error;
|
|
567
577
|
}
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
// ============================================================================
|
|
581
|
+
// NOTES VIEW PERMISSIONS
|
|
582
|
+
// ============================================================================
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Determine if notes can be opened and viewed, and whether they should be in read-only mode
|
|
586
|
+
*
|
|
587
|
+
* Notes can be viewed in the following scenarios:
|
|
588
|
+
* - Normal active case with unconfirmed image: Can edit and save
|
|
589
|
+
* - Active case with confirmed image: Can view only (read-only)
|
|
590
|
+
* - Read-only case (review/confirmation case): Can view only (read-only)
|
|
591
|
+
* - Archived case: Can view only (read-only)
|
|
592
|
+
*
|
|
593
|
+
* Notes cannot be opened when:
|
|
594
|
+
* - Files are uploading
|
|
595
|
+
* - Confirmation status is still being checked
|
|
596
|
+
* - No image is loaded
|
|
597
|
+
*
|
|
598
|
+
* @param config Configuration object with state flags
|
|
599
|
+
* @returns NotesViewPermission object indicating if notes can be opened and if they're read-only
|
|
600
|
+
*/
|
|
601
|
+
export const getNotesViewPermission = (config: {
|
|
602
|
+
imageLoaded: boolean;
|
|
603
|
+
isUploading: boolean;
|
|
604
|
+
isCheckingConfirmation: boolean;
|
|
605
|
+
isReadOnlyCase?: boolean;
|
|
606
|
+
isArchivedCase?: boolean;
|
|
607
|
+
isConfirmedImage?: boolean;
|
|
608
|
+
}): NotesViewPermission => {
|
|
609
|
+
const {
|
|
610
|
+
imageLoaded,
|
|
611
|
+
isUploading,
|
|
612
|
+
isCheckingConfirmation,
|
|
613
|
+
isReadOnlyCase = false,
|
|
614
|
+
isArchivedCase = false,
|
|
615
|
+
isConfirmedImage = false
|
|
616
|
+
} = config;
|
|
617
|
+
|
|
618
|
+
// Cannot open if uploading files
|
|
619
|
+
if (isUploading) {
|
|
620
|
+
return {
|
|
621
|
+
canOpen: false,
|
|
622
|
+
isReadOnly: false,
|
|
623
|
+
reason: 'Cannot open notes while uploading'
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Cannot open if checking confirmation status
|
|
628
|
+
if (isCheckingConfirmation) {
|
|
629
|
+
return {
|
|
630
|
+
canOpen: false,
|
|
631
|
+
isReadOnly: false,
|
|
632
|
+
reason: 'Checking confirmation status...'
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Cannot open if no image is loaded
|
|
637
|
+
if (!imageLoaded) {
|
|
638
|
+
return {
|
|
639
|
+
canOpen: false,
|
|
640
|
+
isReadOnly: false,
|
|
641
|
+
reason: 'Select an image first'
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Can open, determine if read-only
|
|
646
|
+
const isReadOnly = isConfirmedImage || isReadOnlyCase || isArchivedCase;
|
|
647
|
+
|
|
648
|
+
return {
|
|
649
|
+
canOpen: true,
|
|
650
|
+
isReadOnly
|
|
651
|
+
};
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Get a user-friendly tooltip message for the Image Notes button
|
|
656
|
+
*
|
|
657
|
+
* This centralizes all the tooltip logic that appears in the navbar and sidebar
|
|
658
|
+
*
|
|
659
|
+
* @param permission NotesViewPermission object from getNotesViewPermission
|
|
660
|
+
* @param additionalContext Optional context for more specific messages
|
|
661
|
+
* @returns Tooltip string, or undefined if button has no specific tooltip
|
|
662
|
+
*/
|
|
663
|
+
export const getNotesButtonTooltip = (
|
|
664
|
+
permission: NotesViewPermission,
|
|
665
|
+
additionalContext?: {
|
|
666
|
+
isReadOnlyCase?: boolean;
|
|
667
|
+
isArchivedCase?: boolean;
|
|
668
|
+
isConfirmedImage?: boolean;
|
|
669
|
+
}
|
|
670
|
+
): string | undefined => {
|
|
671
|
+
// If cannot open, return the reason
|
|
672
|
+
if (!permission.canOpen) {
|
|
673
|
+
return permission.reason;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Can open - provide context-specific read-only messages
|
|
677
|
+
if (permission.isReadOnly && additionalContext) {
|
|
678
|
+
if (additionalContext.isConfirmedImage) {
|
|
679
|
+
return 'Image notes: viewing only (image is confirmed)';
|
|
680
|
+
}
|
|
681
|
+
if (additionalContext.isReadOnlyCase) {
|
|
682
|
+
return 'Image notes: viewing only (case is read-only)';
|
|
683
|
+
}
|
|
684
|
+
if (additionalContext.isArchivedCase) {
|
|
685
|
+
return 'Image notes: viewing only (case is archived)';
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// No tooltip needed for normal edit mode
|
|
690
|
+
return undefined;
|
|
568
691
|
};
|
package/package.json
CHANGED
|
@@ -1,140 +1,140 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@striae-org/striae",
|
|
3
|
-
"version": "6.1.
|
|
4
|
-
"private": false,
|
|
5
|
-
"description": "Striae is a specialized, cloud-native platform designed to streamline forensic firearms identification by providing an intuitive environment for digital comparison image annotation, authenticated confirmations, and automated report generation.",
|
|
6
|
-
"license": "Apache-2.0",
|
|
7
|
-
"homepage": "https://github.com/striae-org/striae/wiki",
|
|
8
|
-
"repository": {
|
|
9
|
-
"type": "git",
|
|
10
|
-
"url": "https://github.com/striae-org/striae.git"
|
|
11
|
-
},
|
|
12
|
-
"funding": {
|
|
13
|
-
"type": "github",
|
|
14
|
-
"url": "https://github.com/sponsors/striae-org"
|
|
15
|
-
},
|
|
16
|
-
"bugs": {
|
|
17
|
-
"url": "https://github.com/striae-org/striae/issues"
|
|
18
|
-
},
|
|
19
|
-
"keywords": [
|
|
20
|
-
"forensics",
|
|
21
|
-
"firearms",
|
|
22
|
-
"annotation",
|
|
23
|
-
"react",
|
|
24
|
-
"cloudflare-workers",
|
|
25
|
-
"authenticated",
|
|
26
|
-
"confirmations",
|
|
27
|
-
"chain-of-custody",
|
|
28
|
-
"audit-trail"
|
|
29
|
-
],
|
|
30
|
-
"publishConfig": {
|
|
31
|
-
"access": "public"
|
|
32
|
-
},
|
|
33
|
-
"files": [
|
|
34
|
-
"app/",
|
|
35
|
-
"!app/config",
|
|
36
|
-
"!app/routes/auth/login.tsx",
|
|
37
|
-
"!app/routes/auth/login.module.css",
|
|
38
|
-
"react-router.config.ts",
|
|
39
|
-
"load-context.ts",
|
|
40
|
-
"scripts/",
|
|
41
|
-
"functions/",
|
|
42
|
-
"public/",
|
|
43
|
-
"workers/",
|
|
44
|
-
"!workers/*/.wrangler",
|
|
45
|
-
"!workers/*/package-lock.json",
|
|
46
|
-
"!workers/*/worker-configuration.d.ts",
|
|
47
|
-
"!workers/*/wrangler.jsonc",
|
|
48
|
-
"!workers/*/src/**/*worker.ts",
|
|
49
|
-
"!workers/pdf-worker/src/assets/**/*",
|
|
50
|
-
"workers/pdf-worker/src/assets/generated-assets.example.ts",
|
|
51
|
-
"!workers/pdf-worker/src/formats/**/*",
|
|
52
|
-
"workers/pdf-worker/src/formats/format-striae.ts",
|
|
53
|
-
".env.example",
|
|
54
|
-
"firebase.json",
|
|
55
|
-
"tsconfig.json",
|
|
56
|
-
"vite.config.ts",
|
|
57
|
-
"/worker-configuration.d.ts",
|
|
58
|
-
"wrangler.toml.example",
|
|
59
|
-
"LICENSE"
|
|
60
|
-
],
|
|
61
|
-
"sideEffects": false,
|
|
62
|
-
"type": "module",
|
|
63
|
-
"scripts": {
|
|
64
|
-
"deploy:all": "bash ./scripts/deploy-all.sh",
|
|
65
|
-
"emulators": "firebase emulators:start --only auth",
|
|
66
|
-
"dev": "node ./scripts/dev.cjs && react-router dev",
|
|
67
|
-
"build": "node ./scripts/dev.cjs && react-router build",
|
|
68
|
-
"clean": "rm -rf build node_modules/.cache .cache",
|
|
69
|
-
"clean:build": "npm run clean && npm run build",
|
|
70
|
-
"deploy": "npm run build && wrangler pages deploy",
|
|
71
|
-
"publish:npm": "npm publish --access public --registry=https://registry.npmjs.org --@striae-org:registry=https://registry.npmjs.org",
|
|
72
|
-
"publish:npm:dry-run": "npm publish --dry-run --access public --registry=https://registry.npmjs.org --@striae-org:registry=https://registry.npmjs.org",
|
|
73
|
-
"publish:github": "npm publish --registry=https://npm.pkg.github.com --@striae-org:registry=https://npm.pkg.github.com",
|
|
74
|
-
"publish:github:dry-run": "npm publish --dry-run --registry=https://npm.pkg.github.com --@striae-org:registry=https://npm.pkg.github.com",
|
|
75
|
-
"publish:all": "npm run publish:npm && npm run publish:github",
|
|
76
|
-
"publish:all:dry-run": "npm run publish:npm:dry-run && npm run publish:github:dry-run",
|
|
77
|
-
"lint": "node ./scripts/run-eslint.cjs",
|
|
78
|
-
"start": "node ./scripts/dev.cjs && wrangler pages dev",
|
|
79
|
-
"typecheck": "react-router typegen && tsc",
|
|
80
|
-
"typegen": "wrangler types",
|
|
81
|
-
"preview": "npm run build && wrangler pages dev",
|
|
82
|
-
"cf-typegen": "wrangler types",
|
|
83
|
-
"enable-totp-mfa": "node ./scripts/enable-totp-mfa.mjs",
|
|
84
|
-
"unenroll-totp-mfa": "node ./scripts/unenroll-totp-mfa.mjs",
|
|
85
|
-
"update-versions": "node ./scripts/update-markdown-versions.cjs",
|
|
86
|
-
"update-compatibility-dates": "node ./scripts/update-compatibility-dates.cjs",
|
|
87
|
-
"deploy-config": "bash ./scripts/deploy-config.sh",
|
|
88
|
-
"update-env": "bash ./scripts/deploy-config.sh --update-env",
|
|
89
|
-
"install-workers": "bash ./scripts/install-workers.sh",
|
|
90
|
-
"deploy-workers": "npm run deploy-workers:audit && npm run deploy-workers:data && npm run deploy-workers:image && npm run deploy-workers:pdf && npm run deploy-workers:user",
|
|
91
|
-
"deploy-workers:secrets": "bash ./scripts/deploy-worker-secrets.sh",
|
|
92
|
-
"deploy-pages:secrets": "bash ./scripts/deploy-pages-secrets.sh",
|
|
93
|
-
"deploy-pages": "bash ./scripts/deploy-pages.sh",
|
|
94
|
-
"deploy-primershear": "bash ./scripts/deploy-primershear-emails.sh",
|
|
95
|
-
"deploy-members": "bash ./scripts/deploy-members-emails.sh",
|
|
96
|
-
"deploy-workers:audit": "cd workers/audit-worker && npm run deploy",
|
|
97
|
-
"deploy-workers:data": "cd workers/data-worker && npm run deploy",
|
|
98
|
-
"deploy-workers:image": "cd workers/image-worker && npm run deploy",
|
|
99
|
-
"deploy-workers:pdf": "cd workers/pdf-worker && npm run deploy",
|
|
100
|
-
"deploy-workers:user": "cd workers/user-worker && npm run deploy"
|
|
101
|
-
},
|
|
102
|
-
"dependencies": {
|
|
103
|
-
"@react-router/cloudflare": "^7.14.1",
|
|
104
|
-
"firebase": "^12.12.0",
|
|
105
|
-
"isbot": "^5.1.38",
|
|
106
|
-
"jszip": "^3.10.1",
|
|
107
|
-
"qrcode": "^1.5.4",
|
|
108
|
-
"react": "^19.2.5",
|
|
109
|
-
"react-dom": "^19.2.5",
|
|
110
|
-
"react-router": "^7.14.1"
|
|
111
|
-
},
|
|
112
|
-
"devDependencies": {
|
|
113
|
-
"@react-router/dev": "^7.14.1",
|
|
114
|
-
"@react-router/fs-routes": "^7.14.1",
|
|
115
|
-
"@types/qrcode": "^1.5.6",
|
|
116
|
-
"@types/react": "^19.2.14",
|
|
117
|
-
"@types/react-dom": "^19.2.3",
|
|
118
|
-
"@typescript-eslint/eslint-plugin": "^8.58.2",
|
|
119
|
-
"@typescript-eslint/parser": "^8.58.2",
|
|
120
|
-
"eslint": "^9.39.4",
|
|
121
|
-
"eslint-import-resolver-typescript": "^4.4.4",
|
|
122
|
-
"eslint-plugin-import": "^2.32.0",
|
|
123
|
-
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
124
|
-
"eslint-plugin-react": "^7.37.5",
|
|
125
|
-
"eslint-plugin-react-hooks": "^7.0.1",
|
|
126
|
-
"firebase-admin": "^13.8.0",
|
|
127
|
-
"modern-normalize": "^3.0.1",
|
|
128
|
-
"typescript": "^5.9.3",
|
|
129
|
-
"vite": "^7.3.2",
|
|
130
|
-
"vite-tsconfig-paths": "^6.1.1",
|
|
131
|
-
"wrangler": "^4.
|
|
132
|
-
},
|
|
133
|
-
"overrides": {
|
|
134
|
-
"@tootallnate/once": "3.0.1"
|
|
135
|
-
},
|
|
136
|
-
"engines": {
|
|
137
|
-
"node": ">=20.19.0"
|
|
138
|
-
},
|
|
139
|
-
"packageManager": "npm@11.12.0"
|
|
1
|
+
{
|
|
2
|
+
"name": "@striae-org/striae",
|
|
3
|
+
"version": "6.1.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Striae is a specialized, cloud-native platform designed to streamline forensic firearms identification by providing an intuitive environment for digital comparison image annotation, authenticated confirmations, and automated report generation.",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"homepage": "https://github.com/striae-org/striae/wiki",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/striae-org/striae.git"
|
|
11
|
+
},
|
|
12
|
+
"funding": {
|
|
13
|
+
"type": "github",
|
|
14
|
+
"url": "https://github.com/sponsors/striae-org"
|
|
15
|
+
},
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/striae-org/striae/issues"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"forensics",
|
|
21
|
+
"firearms",
|
|
22
|
+
"annotation",
|
|
23
|
+
"react",
|
|
24
|
+
"cloudflare-workers",
|
|
25
|
+
"authenticated",
|
|
26
|
+
"confirmations",
|
|
27
|
+
"chain-of-custody",
|
|
28
|
+
"audit-trail"
|
|
29
|
+
],
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"app/",
|
|
35
|
+
"!app/config",
|
|
36
|
+
"!app/routes/auth/login.tsx",
|
|
37
|
+
"!app/routes/auth/login.module.css",
|
|
38
|
+
"react-router.config.ts",
|
|
39
|
+
"load-context.ts",
|
|
40
|
+
"scripts/",
|
|
41
|
+
"functions/",
|
|
42
|
+
"public/",
|
|
43
|
+
"workers/",
|
|
44
|
+
"!workers/*/.wrangler",
|
|
45
|
+
"!workers/*/package-lock.json",
|
|
46
|
+
"!workers/*/worker-configuration.d.ts",
|
|
47
|
+
"!workers/*/wrangler.jsonc",
|
|
48
|
+
"!workers/*/src/**/*worker.ts",
|
|
49
|
+
"!workers/pdf-worker/src/assets/**/*",
|
|
50
|
+
"workers/pdf-worker/src/assets/generated-assets.example.ts",
|
|
51
|
+
"!workers/pdf-worker/src/formats/**/*",
|
|
52
|
+
"workers/pdf-worker/src/formats/format-striae.ts",
|
|
53
|
+
".env.example",
|
|
54
|
+
"firebase.json",
|
|
55
|
+
"tsconfig.json",
|
|
56
|
+
"vite.config.ts",
|
|
57
|
+
"/worker-configuration.d.ts",
|
|
58
|
+
"wrangler.toml.example",
|
|
59
|
+
"LICENSE"
|
|
60
|
+
],
|
|
61
|
+
"sideEffects": false,
|
|
62
|
+
"type": "module",
|
|
63
|
+
"scripts": {
|
|
64
|
+
"deploy:all": "bash ./scripts/deploy-all.sh",
|
|
65
|
+
"emulators": "firebase emulators:start --only auth",
|
|
66
|
+
"dev": "node ./scripts/dev.cjs && react-router dev",
|
|
67
|
+
"build": "node ./scripts/dev.cjs && react-router build",
|
|
68
|
+
"clean": "rm -rf build node_modules/.cache .cache",
|
|
69
|
+
"clean:build": "npm run clean && npm run build",
|
|
70
|
+
"deploy": "npm run build && wrangler pages deploy",
|
|
71
|
+
"publish:npm": "npm publish --access public --registry=https://registry.npmjs.org --@striae-org:registry=https://registry.npmjs.org",
|
|
72
|
+
"publish:npm:dry-run": "npm publish --dry-run --access public --registry=https://registry.npmjs.org --@striae-org:registry=https://registry.npmjs.org",
|
|
73
|
+
"publish:github": "npm publish --registry=https://npm.pkg.github.com --@striae-org:registry=https://npm.pkg.github.com",
|
|
74
|
+
"publish:github:dry-run": "npm publish --dry-run --registry=https://npm.pkg.github.com --@striae-org:registry=https://npm.pkg.github.com",
|
|
75
|
+
"publish:all": "npm run publish:npm && npm run publish:github",
|
|
76
|
+
"publish:all:dry-run": "npm run publish:npm:dry-run && npm run publish:github:dry-run",
|
|
77
|
+
"lint": "node ./scripts/run-eslint.cjs",
|
|
78
|
+
"start": "node ./scripts/dev.cjs && wrangler pages dev",
|
|
79
|
+
"typecheck": "react-router typegen && tsc",
|
|
80
|
+
"typegen": "wrangler types",
|
|
81
|
+
"preview": "npm run build && wrangler pages dev",
|
|
82
|
+
"cf-typegen": "wrangler types",
|
|
83
|
+
"enable-totp-mfa": "node ./scripts/enable-totp-mfa.mjs",
|
|
84
|
+
"unenroll-totp-mfa": "node ./scripts/unenroll-totp-mfa.mjs",
|
|
85
|
+
"update-versions": "node ./scripts/update-markdown-versions.cjs",
|
|
86
|
+
"update-compatibility-dates": "node ./scripts/update-compatibility-dates.cjs",
|
|
87
|
+
"deploy-config": "bash ./scripts/deploy-config.sh",
|
|
88
|
+
"update-env": "bash ./scripts/deploy-config.sh --update-env",
|
|
89
|
+
"install-workers": "bash ./scripts/install-workers.sh",
|
|
90
|
+
"deploy-workers": "npm run deploy-workers:audit && npm run deploy-workers:data && npm run deploy-workers:image && npm run deploy-workers:pdf && npm run deploy-workers:user",
|
|
91
|
+
"deploy-workers:secrets": "bash ./scripts/deploy-worker-secrets.sh",
|
|
92
|
+
"deploy-pages:secrets": "bash ./scripts/deploy-pages-secrets.sh",
|
|
93
|
+
"deploy-pages": "bash ./scripts/deploy-pages.sh",
|
|
94
|
+
"deploy-primershear": "bash ./scripts/deploy-primershear-emails.sh",
|
|
95
|
+
"deploy-members": "bash ./scripts/deploy-members-emails.sh",
|
|
96
|
+
"deploy-workers:audit": "cd workers/audit-worker && npm run deploy",
|
|
97
|
+
"deploy-workers:data": "cd workers/data-worker && npm run deploy",
|
|
98
|
+
"deploy-workers:image": "cd workers/image-worker && npm run deploy",
|
|
99
|
+
"deploy-workers:pdf": "cd workers/pdf-worker && npm run deploy",
|
|
100
|
+
"deploy-workers:user": "cd workers/user-worker && npm run deploy"
|
|
101
|
+
},
|
|
102
|
+
"dependencies": {
|
|
103
|
+
"@react-router/cloudflare": "^7.14.1",
|
|
104
|
+
"firebase": "^12.12.0",
|
|
105
|
+
"isbot": "^5.1.38",
|
|
106
|
+
"jszip": "^3.10.1",
|
|
107
|
+
"qrcode": "^1.5.4",
|
|
108
|
+
"react": "^19.2.5",
|
|
109
|
+
"react-dom": "^19.2.5",
|
|
110
|
+
"react-router": "^7.14.1"
|
|
111
|
+
},
|
|
112
|
+
"devDependencies": {
|
|
113
|
+
"@react-router/dev": "^7.14.1",
|
|
114
|
+
"@react-router/fs-routes": "^7.14.1",
|
|
115
|
+
"@types/qrcode": "^1.5.6",
|
|
116
|
+
"@types/react": "^19.2.14",
|
|
117
|
+
"@types/react-dom": "^19.2.3",
|
|
118
|
+
"@typescript-eslint/eslint-plugin": "^8.58.2",
|
|
119
|
+
"@typescript-eslint/parser": "^8.58.2",
|
|
120
|
+
"eslint": "^9.39.4",
|
|
121
|
+
"eslint-import-resolver-typescript": "^4.4.4",
|
|
122
|
+
"eslint-plugin-import": "^2.32.0",
|
|
123
|
+
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
124
|
+
"eslint-plugin-react": "^7.37.5",
|
|
125
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
126
|
+
"firebase-admin": "^13.8.0",
|
|
127
|
+
"modern-normalize": "^3.0.1",
|
|
128
|
+
"typescript": "^5.9.3",
|
|
129
|
+
"vite": "^7.3.2",
|
|
130
|
+
"vite-tsconfig-paths": "^6.1.1",
|
|
131
|
+
"wrangler": "^4.83.0"
|
|
132
|
+
},
|
|
133
|
+
"overrides": {
|
|
134
|
+
"@tootallnate/once": "3.0.1"
|
|
135
|
+
},
|
|
136
|
+
"engines": {
|
|
137
|
+
"node": ">=20.19.0"
|
|
138
|
+
},
|
|
139
|
+
"packageManager": "npm@11.12.0"
|
|
140
140
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "audit-worker",
|
|
3
|
-
"version": "6.1.
|
|
3
|
+
"version": "6.1.1",
|
|
4
4
|
"private": true,
|
|
5
5
|
"scripts": {
|
|
6
6
|
"deploy": "wrangler deploy",
|
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
"start": "wrangler dev"
|
|
9
9
|
},
|
|
10
10
|
"devDependencies": {
|
|
11
|
-
"wrangler": "^4.
|
|
11
|
+
"wrangler": "^4.83.0"
|
|
12
12
|
}
|
|
13
13
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "data-worker",
|
|
3
|
-
"version": "6.1.
|
|
3
|
+
"version": "6.1.1",
|
|
4
4
|
"private": true,
|
|
5
5
|
"scripts": {
|
|
6
6
|
"deploy": "wrangler deploy",
|
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
"start": "wrangler dev"
|
|
9
9
|
},
|
|
10
10
|
"devDependencies": {
|
|
11
|
-
"wrangler": "^4.
|
|
11
|
+
"wrangler": "^4.83.0"
|
|
12
12
|
}
|
|
13
13
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "image-worker",
|
|
3
|
-
"version": "6.1.
|
|
3
|
+
"version": "6.1.1",
|
|
4
4
|
"private": true,
|
|
5
5
|
"scripts": {
|
|
6
6
|
"deploy": "wrangler deploy",
|
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
"start": "wrangler dev"
|
|
9
9
|
},
|
|
10
10
|
"devDependencies": {
|
|
11
|
-
"wrangler": "^4.
|
|
11
|
+
"wrangler": "^4.83.0"
|
|
12
12
|
}
|
|
13
13
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pdf-worker",
|
|
3
|
-
"version": "6.1.
|
|
3
|
+
"version": "6.1.1",
|
|
4
4
|
"private": true,
|
|
5
5
|
"scripts": {
|
|
6
6
|
"generate:assets": "node scripts/generate-assets.js",
|
|
@@ -9,6 +9,6 @@
|
|
|
9
9
|
"start": "wrangler dev"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"wrangler": "^4.
|
|
12
|
+
"wrangler": "^4.83.0"
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { IndexType, SupportLevel } from '../../../shared/types/annotation-literals';
|
|
2
|
+
|
|
1
3
|
export interface BoxAnnotation {
|
|
2
4
|
x: number;
|
|
3
5
|
y: number;
|
|
@@ -16,7 +18,7 @@ export interface ConfirmationData {
|
|
|
16
18
|
|
|
17
19
|
export interface AnnotationData {
|
|
18
20
|
// Index annotations
|
|
19
|
-
indexType?:
|
|
21
|
+
indexType?: IndexType;
|
|
20
22
|
indexNumber?: string;
|
|
21
23
|
indexColor?: string;
|
|
22
24
|
|
|
@@ -31,7 +33,7 @@ export interface AnnotationData {
|
|
|
31
33
|
boxAnnotations?: BoxAnnotation[];
|
|
32
34
|
|
|
33
35
|
// ID/Support level annotations
|
|
34
|
-
supportLevel?:
|
|
36
|
+
supportLevel?: SupportLevel;
|
|
35
37
|
|
|
36
38
|
// Class annotations (left/right per-item)
|
|
37
39
|
leftItemType?: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "user-worker",
|
|
3
|
-
"version": "6.1.
|
|
3
|
+
"version": "6.1.1",
|
|
4
4
|
"private": true,
|
|
5
5
|
"scripts": {
|
|
6
6
|
"deploy": "wrangler deploy",
|
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
"start": "wrangler dev"
|
|
9
9
|
},
|
|
10
10
|
"devDependencies": {
|
|
11
|
-
"wrangler": "^4.
|
|
11
|
+
"wrangler": "^4.83.0"
|
|
12
12
|
}
|
|
13
13
|
}
|
package/wrangler.toml.example
CHANGED