@striae-org/striae 4.2.1 → 4.3.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/app/components/navbar/case-modals/archive-case-modal.module.css +0 -76
- package/app/components/navbar/case-modals/archive-case-modal.tsx +9 -8
- package/app/components/navbar/case-modals/case-modal-shared.module.css +94 -0
- package/app/components/navbar/case-modals/delete-case-modal.module.css +9 -0
- package/app/components/navbar/case-modals/delete-case-modal.tsx +79 -0
- package/app/components/navbar/case-modals/open-case-modal.module.css +2 -1
- package/app/components/navbar/case-modals/rename-case-modal.module.css +0 -72
- package/app/components/navbar/case-modals/rename-case-modal.tsx +9 -8
- package/app/components/sidebar/cases/case-sidebar.tsx +49 -3
- package/app/components/sidebar/cases/cases-modal.module.css +312 -10
- package/app/components/sidebar/cases/cases-modal.tsx +690 -110
- package/app/components/sidebar/cases/cases.module.css +23 -0
- package/app/components/sidebar/files/delete-files-modal.module.css +26 -0
- package/app/components/sidebar/files/delete-files-modal.tsx +94 -0
- package/app/components/sidebar/files/files-modal.module.css +285 -44
- package/app/components/sidebar/files/files-modal.tsx +452 -145
- package/app/components/sidebar/notes/class-details-fields.tsx +146 -0
- package/app/components/sidebar/notes/class-details-modal.tsx +147 -0
- package/app/components/sidebar/notes/class-details-sections.tsx +561 -0
- package/app/components/sidebar/notes/class-details-shared.ts +239 -0
- package/app/components/sidebar/notes/notes-editor-form.tsx +43 -5
- package/app/components/sidebar/notes/notes.module.css +236 -4
- package/app/components/sidebar/notes/use-class-details-state.ts +371 -0
- package/app/components/sidebar/sidebar-container.tsx +1 -0
- package/app/components/sidebar/sidebar.tsx +12 -1
- package/app/hooks/useCaseListPreferences.ts +99 -0
- package/app/hooks/useFileListPreferences.ts +106 -0
- package/app/routes/striae/striae.tsx +1 -0
- package/app/types/annotations.ts +48 -1
- package/app/utils/data/case-filters.ts +127 -0
- package/app/utils/data/confirmation-summary/summary-core.ts +18 -2
- package/app/utils/data/file-filters.ts +201 -0
- package/functions/api/image/[[path]].ts +4 -0
- package/package.json +3 -4
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/src/formats/format-striae.ts +84 -118
- package/workers/pdf-worker/src/pdf-worker.example.ts +28 -10
- package/workers/pdf-worker/src/report-layout.ts +227 -0
- package/workers/pdf-worker/src/report-types.ts +20 -0
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/workers/pdf-worker/src/assets/icon-256.png +0 -0
- /package/workers/pdf-worker/src/assets/{generated-assets.ts → generated-assets.example.ts} +0 -0
package/app/types/annotations.ts
CHANGED
|
@@ -22,19 +22,66 @@ export interface ConfirmationData {
|
|
|
22
22
|
confirmedAt: string; // ISO timestamp of confirmation
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export interface BulletAnnotationData {
|
|
26
|
+
caliber?: string;
|
|
27
|
+
mass?: string;
|
|
28
|
+
diameter?: string;
|
|
29
|
+
calcDiameter?: string;
|
|
30
|
+
lgNumber?: number;
|
|
31
|
+
lgDirection?: string;
|
|
32
|
+
barrelType?: string;
|
|
33
|
+
// Width arrays should align with lgNumber:
|
|
34
|
+
// L1..Ln stored in order at lWidths[0..n-1], G1..Gn at gWidths[0..n-1].
|
|
35
|
+
lWidths?: string[];
|
|
36
|
+
gWidths?: string[];
|
|
37
|
+
jacketMetal?: string;
|
|
38
|
+
coreMetal?: string;
|
|
39
|
+
bulletType?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CartridgeCaseAnnotationData {
|
|
43
|
+
caliber?: string;
|
|
44
|
+
brand?: string;
|
|
45
|
+
metal?: string;
|
|
46
|
+
primerType?: string;
|
|
47
|
+
fpiShape?: string;
|
|
48
|
+
apertureShape?: string;
|
|
49
|
+
hasFpDrag?: boolean;
|
|
50
|
+
hasExtractorMarks?: boolean;
|
|
51
|
+
hasEjectorMarks?: boolean;
|
|
52
|
+
hasChamberMarks?: boolean;
|
|
53
|
+
hasMagazineLipMarks?: boolean;
|
|
54
|
+
hasPrimerShear?: boolean;
|
|
55
|
+
hasEjectionPortMarks?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ShotshellAnnotationData {
|
|
59
|
+
gauge?: string;
|
|
60
|
+
shotSize?: string;
|
|
61
|
+
metal?: string;
|
|
62
|
+
brand?: string;
|
|
63
|
+
fpiShape?: string;
|
|
64
|
+
hasExtractorMarks?: boolean;
|
|
65
|
+
hasEjectorMarks?: boolean;
|
|
66
|
+
hasChamberMarks?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
25
69
|
export interface AnnotationData {
|
|
26
70
|
leftCase: string;
|
|
27
71
|
rightCase: string;
|
|
28
72
|
leftItem: string;
|
|
29
73
|
rightItem: string;
|
|
30
74
|
caseFontColor?: string;
|
|
31
|
-
classType?: 'Bullet' | 'Cartridge Case' | 'Other';
|
|
75
|
+
classType?: 'Bullet' | 'Cartridge Case' | 'Shotshell' | 'Other';
|
|
32
76
|
customClass?: string;
|
|
33
77
|
classNote?: string;
|
|
34
78
|
indexType?: 'number' | 'color';
|
|
35
79
|
indexNumber?: string;
|
|
36
80
|
indexColor?: string;
|
|
37
81
|
supportLevel?: 'ID' | 'Exclusion' | 'Inconclusive';
|
|
82
|
+
bulletData?: BulletAnnotationData;
|
|
83
|
+
cartridgeCaseData?: CartridgeCaseAnnotationData;
|
|
84
|
+
shotshellData?: ShotshellAnnotationData;
|
|
38
85
|
hasSubclass?: boolean;
|
|
39
86
|
includeConfirmation: boolean;
|
|
40
87
|
confirmationData?: ConfirmationData;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
export type CasesModalSortBy = 'recent' | 'alphabetical';
|
|
2
|
+
|
|
3
|
+
export type CasesModalConfirmationFilter =
|
|
4
|
+
| 'all'
|
|
5
|
+
| 'pending'
|
|
6
|
+
| 'confirmed'
|
|
7
|
+
| 'none-requested';
|
|
8
|
+
|
|
9
|
+
export interface CasesModalPreferences {
|
|
10
|
+
sortBy: CasesModalSortBy;
|
|
11
|
+
confirmationFilter: CasesModalConfirmationFilter;
|
|
12
|
+
showArchivedOnly: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CasesModalCaseItem {
|
|
16
|
+
caseNumber: string;
|
|
17
|
+
createdAt: string;
|
|
18
|
+
archived: boolean;
|
|
19
|
+
isReadOnly: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CaseConfirmationStatusValue {
|
|
23
|
+
includeConfirmation: boolean;
|
|
24
|
+
isConfirmed: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const DEFAULT_CASE_CONFIRMATION_STATUS: CaseConfirmationStatusValue = {
|
|
28
|
+
includeConfirmation: false,
|
|
29
|
+
isConfirmed: false,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function compareCaseNumbersAlphabetically(a: string, b: string): number {
|
|
33
|
+
const getComponents = (value: string) => {
|
|
34
|
+
const numbers = value.match(/\d+/g)?.map(Number) || [];
|
|
35
|
+
const letters = value.match(/[A-Za-z]+/g)?.join('') || '';
|
|
36
|
+
return { numbers, letters };
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const left = getComponents(a);
|
|
40
|
+
const right = getComponents(b);
|
|
41
|
+
|
|
42
|
+
const maxLength = Math.max(left.numbers.length, right.numbers.length);
|
|
43
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
44
|
+
const leftNumber = left.numbers[index] || 0;
|
|
45
|
+
const rightNumber = right.numbers[index] || 0;
|
|
46
|
+
|
|
47
|
+
if (leftNumber !== rightNumber) {
|
|
48
|
+
return leftNumber - rightNumber;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return left.letters.localeCompare(right.letters);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseTimestamp(value: string): number {
|
|
56
|
+
const parsed = Date.parse(value);
|
|
57
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function matchesConfirmationFilter(
|
|
61
|
+
caseNumber: string,
|
|
62
|
+
confirmationFilter: CasesModalConfirmationFilter,
|
|
63
|
+
caseConfirmationStatus: Record<string, CaseConfirmationStatusValue>
|
|
64
|
+
): boolean {
|
|
65
|
+
if (confirmationFilter === 'all') {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const status = caseConfirmationStatus[caseNumber] || DEFAULT_CASE_CONFIRMATION_STATUS;
|
|
70
|
+
|
|
71
|
+
if (confirmationFilter === 'pending') {
|
|
72
|
+
return status.includeConfirmation && !status.isConfirmed;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (confirmationFilter === 'confirmed') {
|
|
76
|
+
return status.includeConfirmation && status.isConfirmed;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return !status.includeConfirmation;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function filterCasesForModal(
|
|
83
|
+
cases: CasesModalCaseItem[],
|
|
84
|
+
preferences: CasesModalPreferences,
|
|
85
|
+
caseConfirmationStatus: Record<string, CaseConfirmationStatusValue>
|
|
86
|
+
): CasesModalCaseItem[] {
|
|
87
|
+
const archiveFilteredCases = preferences.showArchivedOnly
|
|
88
|
+
? cases.filter((entry) => entry.archived && !entry.isReadOnly)
|
|
89
|
+
: cases.filter((entry) => !entry.archived && !entry.isReadOnly);
|
|
90
|
+
|
|
91
|
+
return archiveFilteredCases.filter((entry) =>
|
|
92
|
+
matchesConfirmationFilter(entry.caseNumber, preferences.confirmationFilter, caseConfirmationStatus)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function sortCasesForModal(
|
|
97
|
+
cases: CasesModalCaseItem[],
|
|
98
|
+
sortBy: CasesModalSortBy
|
|
99
|
+
): CasesModalCaseItem[] {
|
|
100
|
+
const next = [...cases];
|
|
101
|
+
|
|
102
|
+
if (sortBy === 'recent') {
|
|
103
|
+
return next.sort((left, right) => {
|
|
104
|
+
const difference = parseTimestamp(right.createdAt) - parseTimestamp(left.createdAt);
|
|
105
|
+
if (difference !== 0) {
|
|
106
|
+
return difference;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return compareCaseNumbersAlphabetically(left.caseNumber, right.caseNumber);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return next.sort((left, right) =>
|
|
114
|
+
compareCaseNumbersAlphabetically(left.caseNumber, right.caseNumber)
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function getCasesForModal(
|
|
119
|
+
cases: CasesModalCaseItem[],
|
|
120
|
+
preferences: CasesModalPreferences,
|
|
121
|
+
caseConfirmationStatus: Record<string, CaseConfirmationStatusValue>
|
|
122
|
+
): CasesModalCaseItem[] {
|
|
123
|
+
return sortCasesForModal(
|
|
124
|
+
filterCasesForModal(cases, preferences, caseConfirmationStatus),
|
|
125
|
+
preferences.sortBy
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -5,6 +5,7 @@ export interface FileConfirmationSummary {
|
|
|
5
5
|
includeConfirmation: boolean;
|
|
6
6
|
isConfirmed: boolean;
|
|
7
7
|
updatedAt: string;
|
|
8
|
+
classType?: 'Bullet' | 'Cartridge Case' | 'Shotshell' | 'Other';
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export interface CaseConfirmationSummary {
|
|
@@ -194,11 +195,20 @@ function normalizeFileConfirmationSummary(value: unknown): FileConfirmationSumma
|
|
|
194
195
|
};
|
|
195
196
|
}
|
|
196
197
|
|
|
197
|
-
|
|
198
|
+
const classType = value.classType;
|
|
199
|
+
const normalizedClassType = typeof classType === 'string' && ['Bullet', 'Cartridge Case', 'Shotshell', 'Other'].includes(classType) ? (classType as 'Bullet' | 'Cartridge Case' | 'Shotshell' | 'Other') : undefined;
|
|
200
|
+
|
|
201
|
+
const summary: FileConfirmationSummary = {
|
|
198
202
|
includeConfirmation: value.includeConfirmation === true,
|
|
199
203
|
isConfirmed: value.isConfirmed === true,
|
|
200
204
|
updatedAt: typeof value.updatedAt === 'string' && value.updatedAt.length > 0 ? value.updatedAt : getIsoNow()
|
|
201
205
|
};
|
|
206
|
+
|
|
207
|
+
if (normalizedClassType) {
|
|
208
|
+
summary.classType = normalizedClassType;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return summary;
|
|
202
212
|
}
|
|
203
213
|
|
|
204
214
|
export function isStaleTimestamp(timestamp: string, maxAgeMs: number): boolean {
|
|
@@ -228,11 +238,17 @@ export function computeCaseConfirmationAggregate(filesById: Record<string, FileC
|
|
|
228
238
|
export function toFileConfirmationSummary(annotationData: AnnotationData | null): FileConfirmationSummary {
|
|
229
239
|
const includeConfirmation = annotationData?.includeConfirmation === true;
|
|
230
240
|
|
|
231
|
-
|
|
241
|
+
const summary: FileConfirmationSummary = {
|
|
232
242
|
includeConfirmation,
|
|
233
243
|
isConfirmed: includeConfirmation && !!annotationData?.confirmationData,
|
|
234
244
|
updatedAt: getIsoNow()
|
|
235
245
|
};
|
|
246
|
+
|
|
247
|
+
if (annotationData?.classType) {
|
|
248
|
+
summary.classType = annotationData.classType;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return summary;
|
|
236
252
|
}
|
|
237
253
|
|
|
238
254
|
export function normalizeConfirmationSummaryDocument(payload: unknown): UserConfirmationSummaryDocument {
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { FileData } from '~/types';
|
|
2
|
+
import type { FileConfirmationSummary } from '~/utils/data';
|
|
3
|
+
|
|
4
|
+
export type FilesModalSortBy = 'recent' | 'filename' | 'confirmation' | 'classType';
|
|
5
|
+
|
|
6
|
+
export type FilesModalConfirmationFilter =
|
|
7
|
+
| 'all'
|
|
8
|
+
| 'pending'
|
|
9
|
+
| 'confirmed'
|
|
10
|
+
| 'none-requested';
|
|
11
|
+
|
|
12
|
+
export type FilesModalClassTypeFilter =
|
|
13
|
+
| 'all'
|
|
14
|
+
| 'Bullet'
|
|
15
|
+
| 'Cartridge Case'
|
|
16
|
+
| 'Shotshell'
|
|
17
|
+
| 'Other';
|
|
18
|
+
|
|
19
|
+
export interface FilesModalPreferences {
|
|
20
|
+
sortBy: FilesModalSortBy;
|
|
21
|
+
confirmationFilter: FilesModalConfirmationFilter;
|
|
22
|
+
classTypeFilter: FilesModalClassTypeFilter;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type FileConfirmationById = Record<string, FileConfirmationSummary>;
|
|
26
|
+
|
|
27
|
+
const DEFAULT_CONFIRMATION_SUMMARY: FileConfirmationSummary = {
|
|
28
|
+
includeConfirmation: false,
|
|
29
|
+
isConfirmed: false,
|
|
30
|
+
updatedAt: '',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function getFileConfirmationState(fileId: string, statusById: FileConfirmationById): FileConfirmationSummary {
|
|
34
|
+
return statusById[fileId] || DEFAULT_CONFIRMATION_SUMMARY;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getConfirmationRank(summary: FileConfirmationSummary): number {
|
|
38
|
+
if (summary.includeConfirmation && !summary.isConfirmed) {
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (summary.includeConfirmation && summary.isConfirmed) {
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return 2;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getClassTypeRank(classType: FileConfirmationSummary['classType']): number {
|
|
50
|
+
if (classType === 'Bullet') {
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (classType === 'Cartridge Case') {
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (classType === 'Shotshell') {
|
|
59
|
+
return 2;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (classType === 'Other') {
|
|
63
|
+
return 3;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return 4;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parseTimestamp(value: string): number {
|
|
70
|
+
const parsed = Date.parse(value);
|
|
71
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function matchesConfirmationFilter(
|
|
75
|
+
summary: FileConfirmationSummary,
|
|
76
|
+
confirmationFilter: FilesModalConfirmationFilter
|
|
77
|
+
): boolean {
|
|
78
|
+
if (confirmationFilter === 'all') {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (confirmationFilter === 'pending') {
|
|
83
|
+
return summary.includeConfirmation && !summary.isConfirmed;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (confirmationFilter === 'confirmed') {
|
|
87
|
+
return summary.includeConfirmation && summary.isConfirmed;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return !summary.includeConfirmation;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function matchesClassTypeFilter(
|
|
94
|
+
summary: FileConfirmationSummary,
|
|
95
|
+
classTypeFilter: FilesModalClassTypeFilter
|
|
96
|
+
): boolean {
|
|
97
|
+
if (classTypeFilter === 'all') {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (classTypeFilter === 'Other') {
|
|
102
|
+
// Treat legacy/unset class types as Other for filtering.
|
|
103
|
+
return summary.classType === 'Other' || !summary.classType;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return summary.classType === classTypeFilter;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function matchesSearch(file: FileData, query: string): boolean {
|
|
110
|
+
const normalized = query.trim().toLowerCase();
|
|
111
|
+
if (!normalized) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return file.originalFilename.toLowerCase().includes(normalized);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function filterFilesForModal(
|
|
119
|
+
files: FileData[],
|
|
120
|
+
preferences: FilesModalPreferences,
|
|
121
|
+
statusById: FileConfirmationById,
|
|
122
|
+
searchQuery: string
|
|
123
|
+
): FileData[] {
|
|
124
|
+
return files.filter((file) => {
|
|
125
|
+
const summary = getFileConfirmationState(file.id, statusById);
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
matchesSearch(file, searchQuery) &&
|
|
129
|
+
matchesConfirmationFilter(summary, preferences.confirmationFilter) &&
|
|
130
|
+
matchesClassTypeFilter(summary, preferences.classTypeFilter)
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function compareFileNames(a: string, b: string): number {
|
|
136
|
+
return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function sortFilesForModal(
|
|
140
|
+
files: FileData[],
|
|
141
|
+
sortBy: FilesModalSortBy,
|
|
142
|
+
statusById: FileConfirmationById
|
|
143
|
+
): FileData[] {
|
|
144
|
+
const next = [...files];
|
|
145
|
+
|
|
146
|
+
if (sortBy === 'recent') {
|
|
147
|
+
return next.sort((left, right) => {
|
|
148
|
+
const difference = parseTimestamp(right.uploadedAt) - parseTimestamp(left.uploadedAt);
|
|
149
|
+
if (difference !== 0) {
|
|
150
|
+
return difference;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return compareFileNames(left.originalFilename, right.originalFilename);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (sortBy === 'filename') {
|
|
158
|
+
return next.sort((left, right) =>
|
|
159
|
+
compareFileNames(left.originalFilename, right.originalFilename)
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (sortBy === 'confirmation') {
|
|
164
|
+
return next.sort((left, right) => {
|
|
165
|
+
const leftSummary = getFileConfirmationState(left.id, statusById);
|
|
166
|
+
const rightSummary = getFileConfirmationState(right.id, statusById);
|
|
167
|
+
const difference = getConfirmationRank(leftSummary) - getConfirmationRank(rightSummary);
|
|
168
|
+
|
|
169
|
+
if (difference !== 0) {
|
|
170
|
+
return difference;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return compareFileNames(left.originalFilename, right.originalFilename);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return next.sort((left, right) => {
|
|
178
|
+
const leftSummary = getFileConfirmationState(left.id, statusById);
|
|
179
|
+
const rightSummary = getFileConfirmationState(right.id, statusById);
|
|
180
|
+
const difference = getClassTypeRank(leftSummary.classType) - getClassTypeRank(rightSummary.classType);
|
|
181
|
+
|
|
182
|
+
if (difference !== 0) {
|
|
183
|
+
return difference;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return compareFileNames(left.originalFilename, right.originalFilename);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function getFilesForModal(
|
|
191
|
+
files: FileData[],
|
|
192
|
+
preferences: FilesModalPreferences,
|
|
193
|
+
statusById: FileConfirmationById,
|
|
194
|
+
searchQuery: string
|
|
195
|
+
): FileData[] {
|
|
196
|
+
return sortFilesForModal(
|
|
197
|
+
filterFilesForModal(files, preferences, statusById, searchQuery),
|
|
198
|
+
preferences.sortBy,
|
|
199
|
+
statusById
|
|
200
|
+
);
|
|
201
|
+
}
|
|
@@ -53,6 +53,10 @@ function extractProxyPath(url: URL): ProxyPathResult {
|
|
|
53
53
|
|
|
54
54
|
try {
|
|
55
55
|
const decodedPath = decodeURIComponent(encodedPath);
|
|
56
|
+
if (decodedPath.includes('?') || decodedPath.includes('#')) {
|
|
57
|
+
return { ok: false, reason: 'bad-encoding' };
|
|
58
|
+
}
|
|
59
|
+
|
|
56
60
|
return { ok: true, path: decodedPath.startsWith('/') ? decodedPath : `/${decodedPath}` };
|
|
57
61
|
} catch {
|
|
58
62
|
return { ok: false, reason: 'bad-encoding' };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@striae-org/striae",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"private": false,
|
|
5
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
6
|
"license": "Apache-2.0",
|
|
@@ -54,10 +54,9 @@
|
|
|
54
54
|
"workers/*/src/*.example.ts",
|
|
55
55
|
"workers/*/src/*.example.js",
|
|
56
56
|
"workers/*/src/*.ts",
|
|
57
|
-
"workers/pdf-worker/scripts/*.js",
|
|
58
|
-
"workers/pdf-worker/src/assets/icon-256.png",
|
|
57
|
+
"workers/pdf-worker/scripts/*.js",
|
|
59
58
|
"!workers/*/src/*worker.ts",
|
|
60
|
-
"workers/pdf-worker/src/assets/generated-assets.ts",
|
|
59
|
+
"workers/pdf-worker/src/assets/generated-assets.example.ts",
|
|
61
60
|
"workers/pdf-worker/src/formats/format-striae.ts",
|
|
62
61
|
"workers/pdf-worker/src/report-types.ts",
|
|
63
62
|
"workers/*/wrangler.jsonc.example",
|