@striae-org/striae 6.0.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/app/components/actions/case-export/core-export.ts +11 -2
- package/app/components/actions/case-export/download-handlers.ts +3 -1
- package/app/components/canvas/canvas.module.css +1 -1
- package/app/components/canvas/canvas.tsx +32 -11
- package/app/components/icon/icons.svg +1 -1
- package/app/components/icon/manifest.json +1 -1
- package/app/components/navbar/navbar.tsx +10 -9
- package/app/components/sidebar/cases/case-sidebar.tsx +6 -1
- package/app/components/sidebar/files/files-modal.tsx +39 -15
- package/app/components/sidebar/notes/addl-notes-modal.tsx +9 -2
- package/app/components/sidebar/notes/{class-details/class-details-fields.tsx → item-details/item-details-fields.tsx} +10 -10
- package/app/components/sidebar/notes/{class-details/class-details-modal.tsx → item-details/item-details-modal.tsx} +20 -22
- package/app/components/sidebar/notes/{class-details/class-details-sections.tsx → item-details/item-details-sections.tsx} +16 -16
- package/app/components/sidebar/notes/{class-details/class-details-shared.ts → item-details/item-details-shared.ts} +4 -3
- package/app/components/sidebar/notes/{class-details/use-class-details-state.ts → item-details/use-item-details-state.ts} +4 -4
- package/app/components/sidebar/notes/notes-editor-form.tsx +333 -124
- package/app/components/sidebar/notes/notes-editor-modal.tsx +3 -0
- package/app/components/sidebar/notes/notes.module.css +40 -20
- package/app/components/sidebar/sidebar-container.tsx +8 -0
- package/app/components/sidebar/sidebar.tsx +3 -0
- package/app/components/toolbar/toolbar.tsx +5 -5
- package/{members.emails.example → app/config-example/members.emails} +1 -1
- package/{primershear.emails.example → app/config-example/primershear.emails} +1 -1
- package/app/hooks/useFileListPreferences.ts +22 -17
- package/app/routes/striae/striae.tsx +4 -10
- package/app/types/annotations.ts +28 -5
- package/app/utils/data/confirmation-summary/summary-core.ts +40 -8
- package/app/utils/data/file-filters.ts +39 -17
- package/package.json +139 -141
- package/scripts/deploy-config.sh +33 -0
- package/scripts/deploy-members-emails.sh +4 -4
- package/scripts/deploy-primershear-emails.sh +3 -3
- 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/formats/format-striae.ts +65 -8
- package/workers/pdf-worker/src/report-types.ts +13 -1
- 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
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ This npm package publishes the Striae application source for teams that run/deve
|
|
|
8
8
|
|
|
9
9
|
- Application: [https://striae.app](https://striae.app)
|
|
10
10
|
- Source repository: [https://github.com/striae-org/striae](https://github.com/striae-org/striae)
|
|
11
|
+
- Installation guide: [https://github.com/striae-org/striae/wiki/Installation-Guide](https://github.com/striae-org/striae/wiki/Installation-Guide)
|
|
11
12
|
- Releases: [https://github.com/striae-org/striae/releases](https://github.com/striae-org/striae/releases)
|
|
12
13
|
- Security policy: [https://github.com/striae-org/striae/security/policy](https://github.com/striae-org/striae/security/policy)
|
|
13
14
|
|
|
@@ -55,9 +56,10 @@ Excluded (by design):
|
|
|
55
56
|
|
|
56
57
|
## License
|
|
57
58
|
|
|
58
|
-
See `LICENSE
|
|
59
|
+
See `LICENSE`.
|
|
59
60
|
|
|
60
61
|
## Support
|
|
61
62
|
|
|
63
|
+
- Striae Community: [https://community.striae.org](https://community.striae.org)
|
|
62
64
|
- Support page: [https://www.striae.org/support](https://www.striae.org/support)
|
|
63
65
|
- Contact: [info@striae.org](mailto:info@striae.org)
|
|
@@ -63,17 +63,26 @@ export async function exportCaseData(
|
|
|
63
63
|
try {
|
|
64
64
|
annotations = await getNotes(user, caseNumber, file.id) || undefined;
|
|
65
65
|
|
|
66
|
-
// Check if file has any annotation data beyond just defaults
|
|
66
|
+
// Check if file has any annotation data beyond just defaults.
|
|
67
|
+
// Includes left/right split fields and legacy single-value fields as fallbacks.
|
|
67
68
|
hasAnnotations = !!(annotations && (
|
|
68
69
|
annotations.additionalNotes ||
|
|
70
|
+
annotations.leftAdditionalNotes ||
|
|
71
|
+
annotations.rightAdditionalNotes ||
|
|
69
72
|
annotations.classNote ||
|
|
73
|
+
annotations.leftClassNote ||
|
|
74
|
+
annotations.rightClassNote ||
|
|
70
75
|
annotations.customClass ||
|
|
76
|
+
annotations.leftCustomClass ||
|
|
77
|
+
annotations.rightCustomClass ||
|
|
71
78
|
annotations.leftCase ||
|
|
72
79
|
annotations.rightCase ||
|
|
73
80
|
annotations.leftItem ||
|
|
74
81
|
annotations.rightItem ||
|
|
82
|
+
annotations.leftItemType ||
|
|
83
|
+
annotations.rightItemType ||
|
|
75
84
|
annotations.supportLevel ||
|
|
76
|
-
annotations.
|
|
85
|
+
annotations.itemType ||
|
|
77
86
|
(annotations.boxAnnotations && annotations.boxAnnotations.length > 0)
|
|
78
87
|
));
|
|
79
88
|
|
|
@@ -95,10 +95,12 @@ export async function downloadCaseAsZip(
|
|
|
95
95
|
exportData.metadata.exportedByName ||
|
|
96
96
|
exportData.metadata.exportedBy ||
|
|
97
97
|
'Unknown';
|
|
98
|
+
// Don't add forensic warning comment to encrypted content; it will break JSON parsing on decryption.
|
|
99
|
+
// The archive package already includes forensic metadata in README.txt and FORENSIC_MANIFEST.json.
|
|
98
100
|
const caseJsonContent = await generateJSONContent(
|
|
99
101
|
exportData,
|
|
100
102
|
options.includeUserInfo,
|
|
101
|
-
|
|
103
|
+
false
|
|
102
104
|
);
|
|
103
105
|
|
|
104
106
|
const archivePackage = await buildArchivePackage({
|
|
@@ -167,7 +167,8 @@ export const Canvas = ({
|
|
|
167
167
|
}, [imageUrl, resetImageLoadState]);
|
|
168
168
|
|
|
169
169
|
useEffect(() => {
|
|
170
|
-
|
|
170
|
+
const hasAnySubclass = annotationData?.leftHasSubclass || annotationData?.rightHasSubclass || annotationData?.hasSubclass;
|
|
171
|
+
if (!activeAnnotations?.has('item') || !hasAnySubclass) {
|
|
171
172
|
const flashResetTimer = window.setTimeout(() => {
|
|
172
173
|
clearFlashingState();
|
|
173
174
|
}, 0);
|
|
@@ -187,7 +188,7 @@ export const Canvas = ({
|
|
|
187
188
|
}, 60000);
|
|
188
189
|
|
|
189
190
|
return () => clearInterval(flashInterval);
|
|
190
|
-
}, [activeAnnotations, annotationData?.hasSubclass, clearFlashingState]);
|
|
191
|
+
}, [activeAnnotations, annotationData?.leftHasSubclass, annotationData?.rightHasSubclass, annotationData?.hasSubclass, clearFlashingState]);
|
|
191
192
|
|
|
192
193
|
const getErrorMessage = () => {
|
|
193
194
|
if (error) return error;
|
|
@@ -302,15 +303,35 @@ export const Canvas = ({
|
|
|
302
303
|
<div className={styles.imageAndNotesContainer}>
|
|
303
304
|
<div className={styles.imageContainer}>
|
|
304
305
|
<div className={styles.imageWrapper}>
|
|
305
|
-
{/*
|
|
306
|
-
{activeAnnotations?.has('
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
306
|
+
{/* Item Type - Above Image */}
|
|
307
|
+
{activeAnnotations?.has('item') && annotationData && (() => {
|
|
308
|
+
// Resolve display values from left/right fields, falling back to legacy single-set fields.
|
|
309
|
+
// When both sides are populated and differ, combine them as "Left / Right".
|
|
310
|
+
// classType is a legacy field kept for backward compat with older annotations (also handled in PDF generation).
|
|
311
|
+
const leftValue = annotationData.leftCustomClass || annotationData.leftItemType;
|
|
312
|
+
const rightValue = annotationData.rightCustomClass || annotationData.rightItemType;
|
|
313
|
+
const legacyValue = annotationData.customClass || annotationData.itemType || annotationData.classType;
|
|
314
|
+
const displayValue =
|
|
315
|
+
leftValue && rightValue && leftValue !== rightValue
|
|
316
|
+
? `${leftValue} / ${rightValue}`
|
|
317
|
+
: leftValue || rightValue || legacyValue;
|
|
318
|
+
const leftClassNote = annotationData.leftClassNote?.trim();
|
|
319
|
+
const rightClassNote = annotationData.rightClassNote?.trim();
|
|
320
|
+
const legacyClassNote = annotationData.classNote?.trim();
|
|
321
|
+
const displayClassNote =
|
|
322
|
+
leftClassNote && rightClassNote && leftClassNote !== rightClassNote
|
|
323
|
+
? `${leftClassNote} / ${rightClassNote}`
|
|
324
|
+
: leftClassNote || rightClassNote || legacyClassNote;
|
|
325
|
+
if (!displayValue) return null;
|
|
326
|
+
return (
|
|
327
|
+
<div className={styles.classCharacteristics}>
|
|
328
|
+
<div className={styles.classText}>
|
|
329
|
+
{displayValue}
|
|
330
|
+
{displayClassNote && ` (${displayClassNote})`}
|
|
331
|
+
</div>
|
|
311
332
|
</div>
|
|
312
|
-
|
|
313
|
-
)}
|
|
333
|
+
);
|
|
334
|
+
})()}
|
|
314
335
|
|
|
315
336
|
<img
|
|
316
337
|
ref={imageRef}
|
|
@@ -440,7 +461,7 @@ export const Canvas = ({
|
|
|
440
461
|
)}
|
|
441
462
|
|
|
442
463
|
{/* Subclass Warning - Bottom Right of Canvas */}
|
|
443
|
-
{activeAnnotations?.has('
|
|
464
|
+
{activeAnnotations?.has('item') && annotationData && (annotationData.leftHasSubclass || annotationData.rightHasSubclass || annotationData.hasSubclass) && (
|
|
444
465
|
<div className={`${styles.subclassWarning} ${isFlashing ? styles.flashing : ''}`}>
|
|
445
466
|
<div className={styles.subclassText}>
|
|
446
467
|
POTENTIAL SUBCLASS
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<symbol id="number" viewBox="0 0 24 24">
|
|
4
4
|
<path d="M1,9h24M1,17h24M10,1l-3,24M19,1l-3,24" style="stroke:#1e1e1e; stroke-linecap:round; stroke-linejoin:round; stroke-width:2px;" />
|
|
5
5
|
</symbol>
|
|
6
|
-
<symbol id="
|
|
6
|
+
<symbol id="item" viewBox="0 0 24 24">
|
|
7
7
|
<path d="M12,24c-1.66,0-3.22-.31-4.68-.95-1.46-.63-2.73-1.48-3.81-2.56s-1.94-2.35-2.57-3.81c-.63-1.46-.94-3.02-.94-4.68s.31-3.22.94-4.68,1.49-2.73,2.57-3.81,2.35-1.94,3.81-2.57,3.02-.94,4.68-.94,3.22.31,4.68.94c1.46.63,2.73,1.49,3.81,2.57s1.94,2.35,2.56,3.81c.63,1.46.95,3.02.95,4.68s-.31,3.22-.95,4.68c-.63,1.46-1.48,2.73-2.56,3.81s-2.35,1.94-3.81,2.56c-1.46.63-3.02.95-4.68.95ZM12,21.6c2.68,0,4.95-.93,6.81-2.79s2.79-4.13,2.79-6.81-.93-4.95-2.79-6.81-4.13-2.79-6.81-2.79-4.95.93-6.81,2.79-2.79,4.13-2.79,6.81.93,4.95,2.79,6.81,4.13,2.79,6.81,2.79Z" />
|
|
8
8
|
<path d="M12.29,15.89c-.5,0-.97-.09-1.4-.28-.44-.19-.82-.45-1.14-.77-.32-.32-.58-.71-.77-1.14-.19-.44-.28-.91-.28-1.4s.09-.97.28-1.4c.19-.44.45-.82.77-1.14.32-.32.7-.58,1.14-.77.44-.19.91-.28,1.4-.28s.97.09,1.4.28c.44.19.82.45,1.14.77.32.32.58.7.77,1.14.19.44.28.91.28,1.4s-.09.97-.28,1.4c-.19.44-.45.82-.77,1.14-.32.32-.71.58-1.14.77-.44.19-.91.28-1.4.28ZM12.29,15.17c.8,0,1.49-.28,2.04-.84s.84-1.24.84-2.04-.28-1.48-.84-2.04-1.24-.84-2.04-.84-1.48.28-2.04.84-.84,1.24-.84,2.04.28,1.49.84,2.04,1.24.84,2.04.84Z" />
|
|
9
9
|
</symbol>
|
|
@@ -117,11 +117,12 @@ export const Navbar = ({
|
|
|
117
117
|
const disableLongRunningCaseActions = isUploading;
|
|
118
118
|
const isCaseManagementActive = true;
|
|
119
119
|
const isFileManagementActive = isFileMenuOpen || hasLoadedImage;
|
|
120
|
-
const canOpenImageNotes = hasLoadedImage
|
|
120
|
+
const canOpenImageNotes = hasLoadedImage;
|
|
121
|
+
const isImageNotesReadOnly = isReadOnly || isCurrentImageConfirmed || isUploading;
|
|
121
122
|
const isImageNotesActive = canOpenImageNotes;
|
|
122
123
|
const canDeleteCurrentFile = hasLoadedImage && !isReadOnly;
|
|
123
|
-
const
|
|
124
|
-
const caseExportLabel =
|
|
124
|
+
const isArchivedCase = Boolean(isReadOnly && archiveDetails?.archived);
|
|
125
|
+
const caseExportLabel = isArchivedCase
|
|
125
126
|
? 'Export Archive'
|
|
126
127
|
: isReadOnly
|
|
127
128
|
? 'Export Confirmations'
|
|
@@ -184,13 +185,15 @@ export const Navbar = ({
|
|
|
184
185
|
type="button"
|
|
185
186
|
role="menuitem"
|
|
186
187
|
className={`${styles.caseMenuItem} ${styles.caseMenuItemExport}`}
|
|
187
|
-
disabled={!hasLoadedCase || disableLongRunningCaseActions}
|
|
188
|
+
disabled={!hasLoadedCase || disableLongRunningCaseActions || (isArchivedCase && isReviewOnlyCase)}
|
|
188
189
|
title={
|
|
189
190
|
!hasLoadedCase
|
|
190
191
|
? 'Load a case to export case data'
|
|
191
192
|
: disableLongRunningCaseActions
|
|
192
193
|
? 'Export is unavailable while files are uploading'
|
|
193
|
-
:
|
|
194
|
+
: isArchivedCase && isReviewOnlyCase
|
|
195
|
+
? 'Cannot export imported archive packages'
|
|
196
|
+
: undefined
|
|
194
197
|
}
|
|
195
198
|
onClick={() => {
|
|
196
199
|
onOpenCaseExport?.();
|
|
@@ -365,10 +368,8 @@ export const Navbar = ({
|
|
|
365
368
|
title={
|
|
366
369
|
!hasLoadedImage
|
|
367
370
|
? 'Load an image to enable image notes'
|
|
368
|
-
:
|
|
369
|
-
? '
|
|
370
|
-
: isReadOnly
|
|
371
|
-
? 'Image notes are disabled for read-only cases'
|
|
371
|
+
: isImageNotesReadOnly
|
|
372
|
+
? 'Image notes are view-only in this state'
|
|
372
373
|
: undefined
|
|
373
374
|
}
|
|
374
375
|
onClick={() => {
|
|
@@ -26,6 +26,7 @@ interface CaseSidebarProps {
|
|
|
26
26
|
setFiles: React.Dispatch<React.SetStateAction<FileData[]>>;
|
|
27
27
|
currentCase: string | null;
|
|
28
28
|
isReadOnly?: boolean;
|
|
29
|
+
isReviewOnlyCase?: boolean;
|
|
29
30
|
isArchivedCase?: boolean;
|
|
30
31
|
isConfirmed?: boolean;
|
|
31
32
|
confirmationSaveVersion?: number;
|
|
@@ -47,6 +48,7 @@ export const CaseSidebar = ({
|
|
|
47
48
|
setFiles,
|
|
48
49
|
currentCase,
|
|
49
50
|
isReadOnly = false,
|
|
51
|
+
isReviewOnlyCase = false,
|
|
50
52
|
isArchivedCase = false,
|
|
51
53
|
isConfirmed = false,
|
|
52
54
|
confirmationSaveVersion = 0,
|
|
@@ -260,11 +262,14 @@ const handleImageSelect = (file: FileData) => {
|
|
|
260
262
|
|
|
261
263
|
const showCaseExportButton = Boolean(currentCase && isReadOnly);
|
|
262
264
|
const caseExportButtonLabel = isArchivedCase ? 'Export Archive' : 'Export Confirmations';
|
|
265
|
+
const isImportedArchive = isArchivedCase && isReviewOnlyCase;
|
|
263
266
|
|
|
264
267
|
const exportCaseTitle = isUploading
|
|
265
268
|
? 'Cannot export while uploading'
|
|
266
269
|
: !currentCase
|
|
267
270
|
? 'Load a case first'
|
|
271
|
+
: isImportedArchive
|
|
272
|
+
? 'Cannot export imported archive packages'
|
|
268
273
|
: undefined;
|
|
269
274
|
|
|
270
275
|
return (
|
|
@@ -389,7 +394,7 @@ return (
|
|
|
389
394
|
<button
|
|
390
395
|
className={styles.confirmationExportButton}
|
|
391
396
|
onClick={onOpenCaseExport}
|
|
392
|
-
disabled={isUploading || !currentCase}
|
|
397
|
+
disabled={isUploading || !currentCase || isImportedArchive}
|
|
393
398
|
title={exportCaseTitle}
|
|
394
399
|
>
|
|
395
400
|
{caseExportButtonLabel}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
type FilesModalSortBy,
|
|
11
11
|
type FilesModalConfirmationFilter,
|
|
12
|
-
type
|
|
12
|
+
type FilesModalItemTypeFilter,
|
|
13
13
|
getFilesForModal,
|
|
14
14
|
} from '~/utils/data/file-filters';
|
|
15
15
|
import { deleteFile } from '~/components/actions/image-manage';
|
|
@@ -61,12 +61,24 @@ function formatDate(dateString: string): string {
|
|
|
61
61
|
return new Date(parsed).toLocaleDateString();
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
function
|
|
65
|
-
|
|
64
|
+
function getItemTypeLabel(summary: FileConfirmationSummary): string {
|
|
65
|
+
const itemTypes = [
|
|
66
|
+
summary.leftItemType,
|
|
67
|
+
summary.rightItemType,
|
|
68
|
+
summary.itemType,
|
|
69
|
+
].filter((value): value is NonNullable<FileConfirmationSummary['itemType']> => Boolean(value));
|
|
70
|
+
|
|
71
|
+
const uniqueItemTypes = Array.from(new Set(itemTypes));
|
|
72
|
+
|
|
73
|
+
if (uniqueItemTypes.length === 0) {
|
|
66
74
|
return 'Unset';
|
|
67
75
|
}
|
|
68
76
|
|
|
69
|
-
|
|
77
|
+
if (uniqueItemTypes.length === 1) {
|
|
78
|
+
return uniqueItemTypes[0];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return `${uniqueItemTypes[0]} / ${uniqueItemTypes[1]}`;
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
function getConfirmationLabel(summary: FileConfirmationSummary): string {
|
|
@@ -110,7 +122,7 @@ export const FilesModal = ({
|
|
|
110
122
|
preferences,
|
|
111
123
|
setSortBy,
|
|
112
124
|
setConfirmationFilter,
|
|
113
|
-
|
|
125
|
+
setItemTypeFilter,
|
|
114
126
|
resetPreferences,
|
|
115
127
|
} = useFileListPreferences();
|
|
116
128
|
const {
|
|
@@ -125,7 +137,7 @@ export const FilesModal = ({
|
|
|
125
137
|
const hasCustomPreferences =
|
|
126
138
|
preferences.sortBy !== DEFAULT_FILES_MODAL_PREFERENCES.sortBy ||
|
|
127
139
|
preferences.confirmationFilter !== DEFAULT_FILES_MODAL_PREFERENCES.confirmationFilter ||
|
|
128
|
-
preferences.
|
|
140
|
+
preferences.itemTypeFilter !== DEFAULT_FILES_MODAL_PREFERENCES.itemTypeFilter;
|
|
129
141
|
|
|
130
142
|
const existingFileIdSet = useMemo(
|
|
131
143
|
() => new Set(files.map((file) => file.id)),
|
|
@@ -162,6 +174,9 @@ export const FilesModal = ({
|
|
|
162
174
|
(effectiveCurrentPage + 1) * FILES_PER_PAGE
|
|
163
175
|
);
|
|
164
176
|
|
|
177
|
+
const shouldForceItemTypeSummaryRefresh =
|
|
178
|
+
preferences.sortBy === 'itemType' || preferences.itemTypeFilter !== 'all';
|
|
179
|
+
|
|
165
180
|
useEffect(() => {
|
|
166
181
|
let isCancelled = false;
|
|
167
182
|
|
|
@@ -173,7 +188,9 @@ export const FilesModal = ({
|
|
|
173
188
|
return;
|
|
174
189
|
}
|
|
175
190
|
|
|
176
|
-
const caseSummary = await ensureCaseConfirmationSummary(user, currentCase, files
|
|
191
|
+
const caseSummary = await ensureCaseConfirmationSummary(user, currentCase, files, {
|
|
192
|
+
forceRefresh: shouldForceItemTypeSummaryRefresh,
|
|
193
|
+
}).catch((err) => {
|
|
177
194
|
console.error(`Error fetching confirmation summary for case ${currentCase}:`, err);
|
|
178
195
|
return null;
|
|
179
196
|
});
|
|
@@ -190,7 +207,14 @@ export const FilesModal = ({
|
|
|
190
207
|
return () => {
|
|
191
208
|
isCancelled = true;
|
|
192
209
|
};
|
|
193
|
-
}, [
|
|
210
|
+
}, [
|
|
211
|
+
isOpen,
|
|
212
|
+
currentCase,
|
|
213
|
+
files,
|
|
214
|
+
user,
|
|
215
|
+
confirmationSaveVersion,
|
|
216
|
+
shouldForceItemTypeSummaryRefresh,
|
|
217
|
+
]);
|
|
194
218
|
|
|
195
219
|
const toggleDeleteSelection = (fileId: string) => {
|
|
196
220
|
setDeleteSelectedFileIds((previous) => {
|
|
@@ -339,7 +363,7 @@ export const FilesModal = ({
|
|
|
339
363
|
<option value="recent">Date Uploaded</option>
|
|
340
364
|
<option value="filename">File Name</option>
|
|
341
365
|
<option value="confirmation">Confirmation Status</option>
|
|
342
|
-
<option value="
|
|
366
|
+
<option value="itemType">Item Type</option>
|
|
343
367
|
</select>
|
|
344
368
|
</div>
|
|
345
369
|
|
|
@@ -361,12 +385,12 @@ export const FilesModal = ({
|
|
|
361
385
|
</div>
|
|
362
386
|
|
|
363
387
|
<div className={styles.controlGroup}>
|
|
364
|
-
<label htmlFor="files-
|
|
388
|
+
<label htmlFor="files-item-filter">Item Type</label>
|
|
365
389
|
<select
|
|
366
|
-
id="files-
|
|
367
|
-
value={preferences.
|
|
390
|
+
id="files-item-filter"
|
|
391
|
+
value={preferences.itemTypeFilter}
|
|
368
392
|
onChange={(event) => {
|
|
369
|
-
|
|
393
|
+
setItemTypeFilter(event.target.value as FilesModalItemTypeFilter);
|
|
370
394
|
setCurrentPage(0);
|
|
371
395
|
}}
|
|
372
396
|
>
|
|
@@ -434,7 +458,7 @@ export const FilesModal = ({
|
|
|
434
458
|
const isOpenSelected = effectiveOpenSelectedFileId === file.id;
|
|
435
459
|
const isDeleteSelected = effectiveDeleteSelectedFileIds.has(file.id);
|
|
436
460
|
const confirmationLabel = getConfirmationLabel(summary);
|
|
437
|
-
const
|
|
461
|
+
const itemTypeLabel = getItemTypeLabel(summary);
|
|
438
462
|
|
|
439
463
|
let confirmationClass = '';
|
|
440
464
|
if (summary.includeConfirmation) {
|
|
@@ -472,7 +496,7 @@ export const FilesModal = ({
|
|
|
472
496
|
</div>
|
|
473
497
|
<div className={styles.fileMetaRow}>
|
|
474
498
|
<span className={styles.fileDate}>Uploaded: {formatDate(file.uploadedAt)}</span>
|
|
475
|
-
<span className={styles.classTypeBadge}>
|
|
499
|
+
<span className={styles.classTypeBadge}>Item: {itemTypeLabel}</span>
|
|
476
500
|
</div>
|
|
477
501
|
</div>
|
|
478
502
|
|
|
@@ -7,10 +7,11 @@ interface AddlNotesModalProps {
|
|
|
7
7
|
onClose: () => void;
|
|
8
8
|
notes: string;
|
|
9
9
|
onSave: (notes: string) => void;
|
|
10
|
+
isReadOnly?: boolean;
|
|
10
11
|
showNotification?: (message: string, type: 'success' | 'error' | 'warning') => void;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
export const AddlNotesModal = ({ isOpen, onClose, notes, onSave, showNotification }: AddlNotesModalProps) => {
|
|
14
|
+
export const AddlNotesModal = ({ isOpen, onClose, notes, onSave, isReadOnly = false, showNotification }: AddlNotesModalProps) => {
|
|
14
15
|
const [tempNotes, setTempNotes] = useState(notes);
|
|
15
16
|
const [isSaving, setIsSaving] = useState(false);
|
|
16
17
|
|
|
@@ -31,6 +32,11 @@ export const AddlNotesModal = ({ isOpen, onClose, notes, onSave, showNotificatio
|
|
|
31
32
|
if (!isOpen) return null;
|
|
32
33
|
|
|
33
34
|
const handleSave = async () => {
|
|
35
|
+
if (isReadOnly) {
|
|
36
|
+
showNotification?.('This case is read-only. Notes cannot be modified.', 'error');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
34
40
|
setIsSaving(true);
|
|
35
41
|
try {
|
|
36
42
|
await Promise.resolve(onSave(tempNotes));
|
|
@@ -58,12 +64,13 @@ export const AddlNotesModal = ({ isOpen, onClose, notes, onSave, showNotificatio
|
|
|
58
64
|
onChange={(e) => setTempNotes(e.target.value)}
|
|
59
65
|
className={styles.modalTextarea}
|
|
60
66
|
placeholder="Enter additional notes..."
|
|
67
|
+
disabled={isReadOnly}
|
|
61
68
|
/>
|
|
62
69
|
<div className={styles.modalButtons}>
|
|
63
70
|
<button
|
|
64
71
|
onClick={handleSave}
|
|
65
72
|
className={styles.saveButton}
|
|
66
|
-
disabled={isSaving}
|
|
73
|
+
disabled={isSaving || isReadOnly}
|
|
67
74
|
aria-busy={isSaving}
|
|
68
75
|
>
|
|
69
76
|
{isSaving ? 'Saving...' : 'Save'}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import styles from '../notes.module.css';
|
|
3
|
-
import { CUSTOM, handleSelectWithCustom } from './
|
|
3
|
+
import { CUSTOM, handleSelectWithCustom } from './item-details-shared';
|
|
4
4
|
|
|
5
5
|
interface BaseFieldProps {
|
|
6
6
|
label: string;
|
|
@@ -37,7 +37,7 @@ interface CheckboxFieldProps {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const fieldClassName = (fullWidth = false): string =>
|
|
40
|
-
fullWidth ? `${styles.
|
|
40
|
+
fullWidth ? `${styles.itemDetailsField} ${styles.itemDetailsFieldFull}` : styles.itemDetailsField;
|
|
41
41
|
|
|
42
42
|
export const TextField = ({
|
|
43
43
|
label,
|
|
@@ -50,14 +50,14 @@ export const TextField = ({
|
|
|
50
50
|
min,
|
|
51
51
|
}: TextFieldProps) => (
|
|
52
52
|
<div className={fieldClassName(fullWidth)}>
|
|
53
|
-
<span className={styles.
|
|
53
|
+
<span className={styles.itemDetailsLabel}>{label}</span>
|
|
54
54
|
<input
|
|
55
55
|
type={type}
|
|
56
56
|
min={min}
|
|
57
57
|
aria-label={label}
|
|
58
58
|
value={value}
|
|
59
59
|
onChange={(event) => onChange(event.target.value)}
|
|
60
|
-
className={styles.
|
|
60
|
+
className={styles.itemDetailsInput}
|
|
61
61
|
disabled={disabled}
|
|
62
62
|
placeholder={placeholder}
|
|
63
63
|
/>
|
|
@@ -74,12 +74,12 @@ export const SelectField = ({
|
|
|
74
74
|
fullWidth = false,
|
|
75
75
|
}: SelectFieldProps) => (
|
|
76
76
|
<div className={fieldClassName(fullWidth)}>
|
|
77
|
-
<span className={styles.
|
|
77
|
+
<span className={styles.itemDetailsLabel}>{label}</span>
|
|
78
78
|
<select
|
|
79
79
|
aria-label={label}
|
|
80
80
|
value={value}
|
|
81
81
|
onChange={(event) => onChange(event.target.value)}
|
|
82
|
-
className={styles.
|
|
82
|
+
className={styles.itemDetailsInput}
|
|
83
83
|
disabled={disabled}
|
|
84
84
|
>
|
|
85
85
|
<option value="">{placeholder}</option>
|
|
@@ -101,12 +101,12 @@ export const SelectWithCustomField = ({
|
|
|
101
101
|
fullWidth = false,
|
|
102
102
|
}: SelectWithCustomFieldProps) => (
|
|
103
103
|
<div className={fieldClassName(fullWidth)}>
|
|
104
|
-
<span className={styles.
|
|
104
|
+
<span className={styles.itemDetailsLabel}>{label}</span>
|
|
105
105
|
<select
|
|
106
106
|
aria-label={label}
|
|
107
107
|
value={isCustom ? CUSTOM : value}
|
|
108
108
|
onChange={(event) => handleSelectWithCustom(event.target.value, onChange, onCustomChange)}
|
|
109
|
-
className={styles.
|
|
109
|
+
className={styles.itemDetailsInput}
|
|
110
110
|
disabled={disabled}
|
|
111
111
|
>
|
|
112
112
|
<option value="">{placeholder}</option>
|
|
@@ -119,7 +119,7 @@ export const SelectWithCustomField = ({
|
|
|
119
119
|
aria-label={label}
|
|
120
120
|
value={value}
|
|
121
121
|
onChange={(event) => onChange(event.target.value)}
|
|
122
|
-
className={styles.
|
|
122
|
+
className={styles.itemDetailsInput}
|
|
123
123
|
disabled={disabled}
|
|
124
124
|
placeholder={customPlaceholder}
|
|
125
125
|
/>
|
|
@@ -133,7 +133,7 @@ export const CheckboxField = ({
|
|
|
133
133
|
onChange,
|
|
134
134
|
disabled,
|
|
135
135
|
}: CheckboxFieldProps) => (
|
|
136
|
-
<label className={styles.
|
|
136
|
+
<label className={styles.itemDetailsCheckboxLabel}>
|
|
137
137
|
<input
|
|
138
138
|
type="checkbox"
|
|
139
139
|
aria-label={label}
|
|
@@ -3,18 +3,16 @@ import type {
|
|
|
3
3
|
BulletAnnotationData,
|
|
4
4
|
CartridgeCaseAnnotationData,
|
|
5
5
|
ShotshellAnnotationData,
|
|
6
|
+
ItemType,
|
|
6
7
|
} from '~/types/annotations';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
} from './class-details-shared';
|
|
10
|
-
import { BulletSection, CartridgeCaseSection, ShotshellSection } from './class-details-sections';
|
|
11
|
-
import { useClassDetailsState } from './use-class-details-state';
|
|
8
|
+
import { BulletSection, CartridgeCaseSection, ShotshellSection } from './item-details-sections';
|
|
9
|
+
import { useItemDetailsState } from './use-item-details-state';
|
|
12
10
|
import styles from '../notes.module.css';
|
|
13
11
|
|
|
14
|
-
interface
|
|
12
|
+
interface ItemDetailsModalProps {
|
|
15
13
|
isOpen: boolean;
|
|
16
14
|
onClose: () => void;
|
|
17
|
-
|
|
15
|
+
itemType: ItemType | '';
|
|
18
16
|
bulletData?: BulletAnnotationData;
|
|
19
17
|
cartridgeCaseData?: CartridgeCaseAnnotationData;
|
|
20
18
|
shotshellData?: ShotshellAnnotationData;
|
|
@@ -27,17 +25,17 @@ interface ClassDetailsModalProps {
|
|
|
27
25
|
isReadOnly?: boolean;
|
|
28
26
|
}
|
|
29
27
|
|
|
30
|
-
const
|
|
28
|
+
const ItemDetailsModalContent = ({
|
|
31
29
|
isOpen,
|
|
32
30
|
onClose,
|
|
33
|
-
|
|
31
|
+
itemType,
|
|
34
32
|
bulletData,
|
|
35
33
|
cartridgeCaseData,
|
|
36
34
|
shotshellData,
|
|
37
35
|
onSave,
|
|
38
36
|
showNotification,
|
|
39
37
|
isReadOnly = false,
|
|
40
|
-
}:
|
|
38
|
+
}: ItemDetailsModalProps) => {
|
|
41
39
|
const {
|
|
42
40
|
bullet,
|
|
43
41
|
cartridgeCase,
|
|
@@ -45,7 +43,7 @@ const ClassDetailsModalContent = ({
|
|
|
45
43
|
isSaving,
|
|
46
44
|
setIsSaving,
|
|
47
45
|
buildSaveData,
|
|
48
|
-
} =
|
|
46
|
+
} = useItemDetailsState({
|
|
49
47
|
bulletData,
|
|
50
48
|
cartridgeCaseData,
|
|
51
49
|
shotshellData,
|
|
@@ -55,10 +53,10 @@ const ClassDetailsModalContent = ({
|
|
|
55
53
|
|
|
56
54
|
if (!isOpen) return null;
|
|
57
55
|
|
|
58
|
-
const showBullet =
|
|
59
|
-
const showCartridge =
|
|
60
|
-
const showShotshell =
|
|
61
|
-
const showHeaders =
|
|
56
|
+
const showBullet = itemType === 'Bullet' || itemType === 'Other' || itemType === '';
|
|
57
|
+
const showCartridge = itemType === 'Cartridge Case' || itemType === 'Other' || itemType === '';
|
|
58
|
+
const showShotshell = itemType === 'Shotshell' || itemType === 'Other' || itemType === '';
|
|
59
|
+
const showHeaders = itemType === 'Other' || itemType === '';
|
|
62
60
|
|
|
63
61
|
const handleSave = async () => {
|
|
64
62
|
setIsSaving(true);
|
|
@@ -90,10 +88,10 @@ const ClassDetailsModalContent = ({
|
|
|
90
88
|
aria-label="Close class details dialog"
|
|
91
89
|
{...overlayProps}
|
|
92
90
|
>
|
|
93
|
-
<div className={`${styles.modal} ${styles.
|
|
91
|
+
<div className={`${styles.modal} ${styles.itemDetailsModal}`}>
|
|
94
92
|
<button {...getCloseButtonProps({ ariaLabel: 'Close class details dialog' })}>×</button>
|
|
95
93
|
<h5 className={styles.modalTitle}>Class Characteristic Details</h5>
|
|
96
|
-
<div className={styles.
|
|
94
|
+
<div className={styles.itemDetailsContent}>
|
|
97
95
|
{showBullet && (
|
|
98
96
|
<BulletSection
|
|
99
97
|
showHeader={showHeaders}
|
|
@@ -118,10 +116,10 @@ const ClassDetailsModalContent = ({
|
|
|
118
116
|
/>
|
|
119
117
|
)}
|
|
120
118
|
</div>
|
|
121
|
-
<div className={`${styles.modalButtons} ${styles.
|
|
119
|
+
<div className={`${styles.modalButtons} ${styles.itemDetailsModalButtons}`}>
|
|
122
120
|
<button
|
|
123
121
|
onClick={handleSave}
|
|
124
|
-
className={`${styles.saveButton} ${styles.
|
|
122
|
+
className={`${styles.saveButton} ${styles.itemDetailsModalAction}`}
|
|
125
123
|
disabled={isSaving || isReadOnly}
|
|
126
124
|
aria-busy={isSaving}
|
|
127
125
|
>
|
|
@@ -129,7 +127,7 @@ const ClassDetailsModalContent = ({
|
|
|
129
127
|
</button>
|
|
130
128
|
<button
|
|
131
129
|
onClick={requestClose}
|
|
132
|
-
className={`${styles.cancelButton} ${styles.
|
|
130
|
+
className={`${styles.cancelButton} ${styles.itemDetailsModalAction}`}
|
|
133
131
|
disabled={isSaving}
|
|
134
132
|
>
|
|
135
133
|
Cancel
|
|
@@ -140,8 +138,8 @@ const ClassDetailsModalContent = ({
|
|
|
140
138
|
);
|
|
141
139
|
};
|
|
142
140
|
|
|
143
|
-
export const
|
|
141
|
+
export const ItemDetailsModal = (props: ItemDetailsModalProps) => {
|
|
144
142
|
if (!props.isOpen) return null;
|
|
145
143
|
|
|
146
|
-
return <
|
|
144
|
+
return <ItemDetailsModalContent {...props} />;
|
|
147
145
|
};
|