@striae-org/striae 4.1.0 → 4.2.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/.env.example +8 -0
- package/app/components/actions/case-export/core-export.ts +14 -8
- package/app/components/actions/case-export/data-processing.ts +1 -0
- package/app/components/actions/case-export/download-handlers.ts +7 -0
- package/app/components/actions/case-export/metadata-helpers.ts +2 -1
- package/app/components/actions/case-import/confirmation-import.ts +12 -2
- package/app/components/actions/case-import/orchestrator.ts +78 -32
- package/app/components/actions/case-import/storage-operations.ts +97 -8
- package/app/components/actions/case-import/zip-processing.ts +159 -86
- package/app/components/actions/case-manage.ts +430 -8
- package/app/components/actions/confirm-export.ts +9 -2
- package/app/components/actions/image-manage.ts +77 -44
- package/app/components/audit/user-audit-viewer.tsx +19 -8
- package/app/components/audit/user-audit.module.css +21 -0
- package/app/components/audit/viewer/audit-entries-list.tsx +7 -0
- package/app/components/audit/viewer/audit-filters-panel.tsx +1 -0
- package/app/components/audit/viewer/audit-viewer-utils.ts +2 -0
- package/app/components/audit/viewer/use-audit-viewer-data.ts +21 -1
- package/app/components/canvas/box-annotations/box-annotations.module.css +22 -18
- package/app/components/canvas/box-annotations/box-annotations.tsx +15 -0
- package/app/components/canvas/canvas.module.css +64 -54
- package/app/components/canvas/canvas.tsx +14 -16
- package/app/components/canvas/confirmation/confirmation.module.css +1 -0
- package/app/components/canvas/confirmation/confirmation.tsx +6 -12
- package/app/components/navbar/case-modals/archive-case-modal.module.css +110 -0
- package/app/components/navbar/case-modals/archive-case-modal.tsx +129 -0
- package/app/components/navbar/case-modals/open-case-modal.module.css +81 -0
- package/app/components/navbar/case-modals/open-case-modal.tsx +120 -0
- package/app/components/navbar/case-modals/rename-case-modal.module.css +81 -0
- package/app/components/navbar/case-modals/rename-case-modal.tsx +107 -0
- package/app/components/navbar/navbar.module.css +447 -0
- package/app/components/navbar/navbar.tsx +377 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +1 -0
- package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +15 -16
- package/app/components/sidebar/case-export/case-export.module.css +1 -0
- package/app/components/sidebar/case-export/case-export.tsx +8 -46
- package/app/components/sidebar/case-import/case-import.module.css +23 -0
- package/app/components/sidebar/case-import/case-import.tsx +64 -16
- package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +20 -1
- package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +15 -0
- package/app/components/sidebar/cases/case-sidebar.tsx +25 -519
- package/app/components/sidebar/cases/cases-modal.module.css +1 -0
- package/app/components/sidebar/cases/cases-modal.tsx +6 -8
- package/app/components/sidebar/cases/cases.module.css +62 -21
- package/app/components/sidebar/files/files-modal.module.css +1 -0
- package/app/components/sidebar/files/files-modal.tsx +12 -13
- package/app/components/sidebar/notes/notes-editor-modal.module.css +49 -0
- package/app/components/sidebar/notes/notes-editor-modal.tsx +66 -0
- package/app/components/sidebar/notes/notes-modal.tsx +7 -8
- package/app/components/sidebar/notes/notes-sidebar.tsx +199 -113
- package/app/components/sidebar/notes/notes.module.css +153 -0
- package/app/components/sidebar/sidebar-container.tsx +15 -28
- package/app/components/sidebar/sidebar.module.css +5 -69
- package/app/components/sidebar/sidebar.tsx +24 -125
- package/app/components/sidebar/upload/image-upload-zone.module.css +13 -13
- package/app/components/user/inactivity-warning.module.css +1 -0
- package/app/components/user/inactivity-warning.tsx +15 -2
- package/app/components/user/manage-profile.tsx +23 -10
- package/app/hooks/useOverlayDismiss.ts +52 -4
- package/app/routes/auth/login.tsx +785 -774
- package/app/routes/striae/striae.module.css +10 -3
- package/app/routes/striae/striae.tsx +469 -30
- package/app/services/audit/audit.service.ts +173 -27
- package/app/services/audit/builders/audit-event-builders-case-file.ts +43 -0
- package/app/services/audit/builders/audit-event-builders-workflow.ts +2 -0
- package/app/services/audit/builders/index.ts +1 -0
- package/app/types/audit.ts +3 -1
- package/app/types/case.ts +29 -0
- package/app/types/import.ts +3 -0
- package/app/utils/data/permissions.ts +16 -1
- package/app/utils/forensics/audit-export-signature.ts +5 -1
- package/app/utils/forensics/confirmation-signature.ts +3 -0
- package/app/utils/forensics/export-verification.ts +497 -22
- package/package.json +3 -3
- package/scripts/deploy-primershear-emails.sh +2 -1
- package/worker-configuration.d.ts +1 -1
- package/workers/audit-worker/worker-configuration.d.ts +7448 -11323
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/worker-configuration.d.ts +7448 -11323
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/worker-configuration.d.ts +7447 -11322
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/worker-configuration.d.ts +7447 -11322
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/src/formats/format-striae.ts +8 -7
- package/workers/pdf-worker/worker-configuration.d.ts +7448 -11323
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/worker-configuration.d.ts +7448 -11323
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/public/.well-known/keybase.txt +0 -56
|
@@ -7,6 +7,17 @@ import { canUploadFile, getCaseData, updateCaseData, deleteFileAnnotations } fro
|
|
|
7
7
|
import type { CaseData, FileData, ImageUploadResponse } from '~/types';
|
|
8
8
|
import { auditService } from '~/services/audit';
|
|
9
9
|
|
|
10
|
+
export interface DeleteFileResult {
|
|
11
|
+
imageMissing: boolean;
|
|
12
|
+
fileName: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DeleteFileOptions {
|
|
16
|
+
skipValidation?: boolean;
|
|
17
|
+
skipCaseDataUpdate?: boolean;
|
|
18
|
+
suppressAudit?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
10
21
|
export const fetchFiles = async (
|
|
11
22
|
user: User,
|
|
12
23
|
caseNumber: string,
|
|
@@ -114,7 +125,13 @@ export const uploadFile = async (
|
|
|
114
125
|
}
|
|
115
126
|
};
|
|
116
127
|
|
|
117
|
-
export const deleteFile = async (
|
|
128
|
+
export const deleteFile = async (
|
|
129
|
+
user: User,
|
|
130
|
+
caseNumber: string,
|
|
131
|
+
fileId: string,
|
|
132
|
+
deleteReason: string = 'User-requested deletion via file list',
|
|
133
|
+
options: DeleteFileOptions = {}
|
|
134
|
+
): Promise<DeleteFileResult> => {
|
|
118
135
|
const startTime = Date.now();
|
|
119
136
|
|
|
120
137
|
// Get file info for audit logging (outside try block so it's available in catch)
|
|
@@ -123,7 +140,9 @@ export const deleteFile = async (user: User, caseNumber: string, fileId: string,
|
|
|
123
140
|
|
|
124
141
|
try {
|
|
125
142
|
// Get the case data using centralized function
|
|
126
|
-
const caseData = await getCaseData(user, caseNumber
|
|
143
|
+
const caseData = await getCaseData(user, caseNumber, {
|
|
144
|
+
skipValidation: options.skipValidation === true
|
|
145
|
+
});
|
|
127
146
|
if (!caseData) {
|
|
128
147
|
throw new Error('Case not found');
|
|
129
148
|
}
|
|
@@ -133,6 +152,7 @@ export const deleteFile = async (user: User, caseNumber: string, fileId: string,
|
|
|
133
152
|
const fileSize = 0; // We don't store file size, so use 0
|
|
134
153
|
|
|
135
154
|
let imageDeleteFailed = false;
|
|
155
|
+
let imageMissing = false;
|
|
136
156
|
let imageDeleteError = '';
|
|
137
157
|
|
|
138
158
|
// Attempt to delete image file
|
|
@@ -145,6 +165,7 @@ export const deleteFile = async (user: User, caseNumber: string, fileId: string,
|
|
|
145
165
|
if (imageResponse.status === 404) {
|
|
146
166
|
// Image already doesn't exist - proceed with data cleanup
|
|
147
167
|
console.warn(`Image ${fileId} not found (404) - proceeding with data cleanup`);
|
|
168
|
+
imageMissing = true;
|
|
148
169
|
} else {
|
|
149
170
|
// Other errors should still fail the operation
|
|
150
171
|
imageDeleteFailed = true;
|
|
@@ -160,64 +181,76 @@ export const deleteFile = async (user: User, caseNumber: string, fileId: string,
|
|
|
160
181
|
// Clean up data files regardless of image deletion success/404
|
|
161
182
|
// Try to delete notes file using centralized function
|
|
162
183
|
try {
|
|
163
|
-
await deleteFileAnnotations(user, caseNumber, fileId
|
|
184
|
+
await deleteFileAnnotations(user, caseNumber, fileId, {
|
|
185
|
+
skipValidation: options.skipValidation === true
|
|
186
|
+
});
|
|
164
187
|
} catch (error) {
|
|
165
188
|
// Ignore 404 errors - notes file might not exist
|
|
166
189
|
console.log('Notes file deletion result:', error);
|
|
167
190
|
}
|
|
168
191
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
192
|
+
if (options.skipCaseDataUpdate !== true) {
|
|
193
|
+
// Update case data.json to remove file reference using centralized function
|
|
194
|
+
const updatedData: CaseData = {
|
|
195
|
+
...caseData,
|
|
196
|
+
files: (caseData.files || []).filter((f: FileData) => f.id !== fileId)
|
|
197
|
+
};
|
|
174
198
|
|
|
175
|
-
|
|
199
|
+
await updateCaseData(user, caseNumber, updatedData);
|
|
200
|
+
}
|
|
176
201
|
|
|
177
202
|
// Log successful file deletion
|
|
178
203
|
const endTime = Date.now();
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
204
|
+
if (options.suppressAudit !== true) {
|
|
205
|
+
try {
|
|
206
|
+
await auditService.logFileDeletion(
|
|
207
|
+
user,
|
|
208
|
+
fileName,
|
|
209
|
+
fileSize,
|
|
210
|
+
deleteReason,
|
|
211
|
+
caseNumber,
|
|
212
|
+
fileId,
|
|
213
|
+
fileToDelete?.originalFilename
|
|
214
|
+
);
|
|
215
|
+
} catch (auditError) {
|
|
216
|
+
console.error('Failed to log file deletion:', auditError);
|
|
217
|
+
}
|
|
191
218
|
}
|
|
192
219
|
|
|
193
220
|
console.log(`✅ File deleted: ${fileName} (${endTime - startTime}ms)`);
|
|
221
|
+
return {
|
|
222
|
+
imageMissing,
|
|
223
|
+
fileName
|
|
224
|
+
};
|
|
194
225
|
|
|
195
226
|
} catch (error) {
|
|
196
227
|
// Log failed file deletion
|
|
197
228
|
const endTime = Date.now();
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
229
|
+
if (options.suppressAudit !== true) {
|
|
230
|
+
try {
|
|
231
|
+
await auditService.logEvent({
|
|
232
|
+
userId: user.uid,
|
|
233
|
+
userEmail: user.email || '',
|
|
234
|
+
action: 'file-delete',
|
|
235
|
+
result: 'failure',
|
|
236
|
+
fileName: fileName, // Now uses the original filename
|
|
237
|
+
fileType: 'unknown',
|
|
238
|
+
validationErrors: [error instanceof Error ? error.message : 'Unknown error'],
|
|
239
|
+
caseNumber,
|
|
240
|
+
fileDetails: {
|
|
241
|
+
fileId: fileId,
|
|
242
|
+
fileSize: 0,
|
|
243
|
+
deleteReason: 'Failed deletion attempt',
|
|
244
|
+
originalFileName: fileToDelete?.originalFilename
|
|
245
|
+
},
|
|
246
|
+
performanceMetrics: {
|
|
247
|
+
processingTimeMs: endTime - startTime,
|
|
248
|
+
fileSizeBytes: 0
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
} catch (auditError) {
|
|
252
|
+
console.error('Failed to log file deletion failure:', auditError);
|
|
253
|
+
}
|
|
221
254
|
}
|
|
222
255
|
|
|
223
256
|
console.error('Error in deleteFile:', error);
|
|
@@ -58,6 +58,8 @@ export const UserAuditViewer = ({ isOpen, onClose, caseNumber, title }: UserAudi
|
|
|
58
58
|
error,
|
|
59
59
|
setError,
|
|
60
60
|
auditTrail,
|
|
61
|
+
isArchivedReadOnlyCase,
|
|
62
|
+
bundledAuditWarning,
|
|
61
63
|
loadAuditData
|
|
62
64
|
} = useAuditViewerData({
|
|
63
65
|
isOpen,
|
|
@@ -84,8 +86,8 @@ export const UserAuditViewer = ({ isOpen, onClose, caseNumber, title }: UserAudi
|
|
|
84
86
|
});
|
|
85
87
|
|
|
86
88
|
const {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
requestClose,
|
|
90
|
+
overlayProps
|
|
89
91
|
} = useOverlayDismiss({
|
|
90
92
|
isOpen,
|
|
91
93
|
onClose
|
|
@@ -98,11 +100,8 @@ export const UserAuditViewer = ({ isOpen, onClose, caseNumber, title }: UserAudi
|
|
|
98
100
|
return (
|
|
99
101
|
<div
|
|
100
102
|
className={styles.overlay}
|
|
101
|
-
onMouseDown={handleOverlayMouseDown}
|
|
102
|
-
onKeyDown={handleOverlayKeyDown}
|
|
103
|
-
role="button"
|
|
104
|
-
tabIndex={0}
|
|
105
103
|
aria-label="Close audit trail dialog"
|
|
104
|
+
{...overlayProps}
|
|
106
105
|
>
|
|
107
106
|
<div className={styles.modal}>
|
|
108
107
|
<AuditViewerHeader
|
|
@@ -111,7 +110,7 @@ export const UserAuditViewer = ({ isOpen, onClose, caseNumber, title }: UserAudi
|
|
|
111
110
|
onExportCSV={handleExportCSV}
|
|
112
111
|
onExportJSON={handleExportJSON}
|
|
113
112
|
onGenerateReport={handleGenerateReport}
|
|
114
|
-
onClose={
|
|
113
|
+
onClose={requestClose}
|
|
115
114
|
/>
|
|
116
115
|
|
|
117
116
|
<div className={styles.content}>
|
|
@@ -133,6 +132,14 @@ export const UserAuditViewer = ({ isOpen, onClose, caseNumber, title }: UserAudi
|
|
|
133
132
|
|
|
134
133
|
{!loading && !error && (
|
|
135
134
|
<>
|
|
135
|
+
{isArchivedReadOnlyCase && (
|
|
136
|
+
<div className={bundledAuditWarning ? styles.archivedWarning : styles.archivedNotice}>
|
|
137
|
+
<p>
|
|
138
|
+
{bundledAuditWarning || 'Viewing bundled audit trail data from this imported archived case package.'}
|
|
139
|
+
</p>
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
|
|
136
143
|
{/* User Information Section */}
|
|
137
144
|
{user && (
|
|
138
145
|
<AuditUserInfoCard user={user} userData={userData} userBadgeId={userBadgeId} />
|
|
@@ -183,7 +190,11 @@ export const UserAuditViewer = ({ isOpen, onClose, caseNumber, title }: UserAudi
|
|
|
183
190
|
|
|
184
191
|
{auditEntries.length === 0 && !loading && !error && (
|
|
185
192
|
<div className={styles.noData}>
|
|
186
|
-
<p>
|
|
193
|
+
<p>
|
|
194
|
+
{isArchivedReadOnlyCase
|
|
195
|
+
? 'No bundled audit trail entries are available for this imported archived case.'
|
|
196
|
+
: 'No audit trail available. Your activities will appear here as you use Striae.'}
|
|
197
|
+
</p>
|
|
187
198
|
</div>
|
|
188
199
|
)}
|
|
189
200
|
</div>
|
|
@@ -101,6 +101,27 @@
|
|
|
101
101
|
padding: 20px;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
.archivedNotice,
|
|
105
|
+
.archivedWarning {
|
|
106
|
+
margin-bottom: 16px;
|
|
107
|
+
padding: 12px 14px;
|
|
108
|
+
border-radius: 6px;
|
|
109
|
+
border: 1px solid;
|
|
110
|
+
font-size: 0.9rem;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.archivedNotice {
|
|
114
|
+
background: color-mix(in lab, var(--primary) 8%, transparent);
|
|
115
|
+
border-color: color-mix(in lab, var(--primary) 35%, transparent);
|
|
116
|
+
color: var(--textBody);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.archivedWarning {
|
|
120
|
+
background: color-mix(in lab, var(--warning) 12%, transparent);
|
|
121
|
+
border-color: color-mix(in lab, var(--warning) 40%, transparent);
|
|
122
|
+
color: var(--text);
|
|
123
|
+
}
|
|
124
|
+
|
|
104
125
|
/* Loading & Error States */
|
|
105
126
|
.loading,
|
|
106
127
|
.error {
|
|
@@ -44,6 +44,13 @@ export const AuditEntriesList = ({ entries }: AuditEntriesListProps) => {
|
|
|
44
44
|
</div>
|
|
45
45
|
)}
|
|
46
46
|
|
|
47
|
+
{entry.action === 'confirmation-import' && entry.details.reviewerBadgeId && (
|
|
48
|
+
<div className={styles.detailRow}>
|
|
49
|
+
<span className={styles.detailLabel}>Reviewer Badge/ID:</span>
|
|
50
|
+
<span className={styles.badgeTag}>{entry.details.reviewerBadgeId}</span>
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
|
|
47
54
|
{entry.result === 'failure' && entry.details.validationErrors.length > 0 && (
|
|
48
55
|
<div className={styles.detailRow}>
|
|
49
56
|
<span className={styles.detailLabel}>Error:</span>
|
|
@@ -259,6 +259,7 @@ export const AuditFiltersPanel = ({
|
|
|
259
259
|
<option value="case-create">Case Create</option>
|
|
260
260
|
<option value="case-rename">Case Rename</option>
|
|
261
261
|
<option value="case-delete">Case Delete</option>
|
|
262
|
+
<option value="case-archive">Case Archive</option>
|
|
262
263
|
<option value="case-export">Case Export</option>
|
|
263
264
|
<option value="case-import">Case Import</option>
|
|
264
265
|
</optgroup>
|
|
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|
|
2
2
|
import type { User } from 'firebase/auth';
|
|
3
3
|
import { auditService } from '~/services/audit';
|
|
4
4
|
import { type AuditTrail, type UserData, type ValidationAuditEntry, type WorkflowPhase } from '~/types';
|
|
5
|
-
import { getUserData } from '~/utils/data';
|
|
5
|
+
import { getCaseData, getUserData } from '~/utils/data';
|
|
6
6
|
import type { DateRangeFilter } from './types';
|
|
7
7
|
|
|
8
8
|
const isWorkflowPhase = (phase: unknown): phase is WorkflowPhase =>
|
|
@@ -82,6 +82,8 @@ export const useAuditViewerData = ({
|
|
|
82
82
|
const [loading, setLoading] = useState(false);
|
|
83
83
|
const [error, setError] = useState<string>('');
|
|
84
84
|
const [auditTrail, setAuditTrail] = useState<AuditTrail | null>(null);
|
|
85
|
+
const [isArchivedReadOnlyCase, setIsArchivedReadOnlyCase] = useState(false);
|
|
86
|
+
const [bundledAuditWarning, setBundledAuditWarning] = useState<string>('');
|
|
85
87
|
|
|
86
88
|
const loadUserData = useCallback(async () => {
|
|
87
89
|
if (!user) {
|
|
@@ -103,11 +105,27 @@ export const useAuditViewerData = ({
|
|
|
103
105
|
|
|
104
106
|
setLoading(true);
|
|
105
107
|
setError('');
|
|
108
|
+
setBundledAuditWarning('');
|
|
106
109
|
|
|
107
110
|
try {
|
|
108
111
|
const { startDate, endDate } = buildAuditDateQuery(dateRange, customStartDate, customEndDate);
|
|
109
112
|
|
|
113
|
+
if (effectiveCaseNumber) {
|
|
114
|
+
const caseData = await getCaseData(user, effectiveCaseNumber);
|
|
115
|
+
const archivedReadOnlyCase = Boolean(caseData?.isReadOnly && caseData.archived === true);
|
|
116
|
+
setIsArchivedReadOnlyCase(archivedReadOnlyCase);
|
|
117
|
+
|
|
118
|
+
if (archivedReadOnlyCase && !caseData?.bundledAuditTrail?.entries?.length) {
|
|
119
|
+
setBundledAuditWarning(
|
|
120
|
+
'This imported archived case does not include bundled audit trail data. No audit entries are available for this case.'
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
setIsArchivedReadOnlyCase(false);
|
|
125
|
+
}
|
|
126
|
+
|
|
110
127
|
const entries = await auditService.getAuditEntriesForUser(user.uid, {
|
|
128
|
+
requestingUser: user,
|
|
111
129
|
caseNumber: effectiveCaseNumber,
|
|
112
130
|
startDate,
|
|
113
131
|
endDate,
|
|
@@ -161,6 +179,8 @@ export const useAuditViewerData = ({
|
|
|
161
179
|
error,
|
|
162
180
|
setError,
|
|
163
181
|
auditTrail,
|
|
182
|
+
isArchivedReadOnlyCase,
|
|
183
|
+
bundledAuditWarning,
|
|
164
184
|
loadAuditData
|
|
165
185
|
};
|
|
166
186
|
};
|
|
@@ -59,16 +59,17 @@
|
|
|
59
59
|
|
|
60
60
|
.annotationLabel {
|
|
61
61
|
position: absolute;
|
|
62
|
-
bottom: -
|
|
62
|
+
bottom: -30px;
|
|
63
63
|
left: 0;
|
|
64
64
|
background: rgba(0, 0, 0, 0.8);
|
|
65
65
|
color: white;
|
|
66
|
-
padding:
|
|
67
|
-
font-size:
|
|
68
|
-
|
|
66
|
+
padding: 4px 10px;
|
|
67
|
+
font-size: 13px;
|
|
68
|
+
line-height: 1.3;
|
|
69
|
+
border-radius: 4px;
|
|
69
70
|
white-space: nowrap;
|
|
70
71
|
pointer-events: none;
|
|
71
|
-
max-width:
|
|
72
|
+
max-width: 260px;
|
|
72
73
|
overflow: hidden;
|
|
73
74
|
text-overflow: ellipsis;
|
|
74
75
|
}
|
|
@@ -82,33 +83,33 @@
|
|
|
82
83
|
background: white;
|
|
83
84
|
border: 1px solid #ccc;
|
|
84
85
|
border-radius: 6px;
|
|
85
|
-
padding:
|
|
86
|
+
padding: 16px;
|
|
86
87
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
87
|
-
min-width:
|
|
88
|
+
min-width: 250px;
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
.labelDialogTitle {
|
|
91
|
-
font-size:
|
|
92
|
+
font-size: 15px;
|
|
92
93
|
font-weight: 600;
|
|
93
|
-
margin-bottom:
|
|
94
|
+
margin-bottom: 8px;
|
|
94
95
|
color: #333;
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
.labelDialogNote {
|
|
98
|
-
font-size:
|
|
99
|
+
font-size: 13px;
|
|
99
100
|
color: #666;
|
|
100
|
-
margin-bottom:
|
|
101
|
+
margin-bottom: 12px;
|
|
101
102
|
font-style: italic;
|
|
102
|
-
line-height: 1.
|
|
103
|
+
line-height: 1.4;
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
.labelInput {
|
|
106
107
|
width: 100%;
|
|
107
|
-
padding:
|
|
108
|
+
padding: 8px 10px;
|
|
108
109
|
border: 1px solid #ccc;
|
|
109
110
|
border-radius: 4px;
|
|
110
111
|
font-size: 14px;
|
|
111
|
-
margin-bottom:
|
|
112
|
+
margin-bottom: 12px;
|
|
112
113
|
box-sizing: border-box;
|
|
113
114
|
}
|
|
114
115
|
|
|
@@ -120,18 +121,21 @@
|
|
|
120
121
|
|
|
121
122
|
.labelDialogButtons {
|
|
122
123
|
display: flex;
|
|
123
|
-
gap:
|
|
124
|
+
gap: 10px;
|
|
124
125
|
justify-content: flex-end;
|
|
126
|
+
margin-top: 8px;
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
.labelConfirmButton,
|
|
128
130
|
.labelCancelButton {
|
|
129
|
-
padding:
|
|
131
|
+
padding: 10px 18px;
|
|
130
132
|
border: none;
|
|
131
133
|
border-radius: 4px;
|
|
132
|
-
font-size:
|
|
134
|
+
font-size: 14px;
|
|
135
|
+
font-weight: 500;
|
|
133
136
|
cursor: pointer;
|
|
134
137
|
transition: background-color 0.2s ease;
|
|
138
|
+
min-width: 88px;
|
|
135
139
|
}
|
|
136
140
|
|
|
137
141
|
.labelConfirmButton {
|
|
@@ -167,4 +171,4 @@
|
|
|
167
171
|
|
|
168
172
|
.readOnlyAnnotation:hover::after {
|
|
169
173
|
display: none !important; /* Hide the delete button for read-only annotations */
|
|
170
|
-
}
|
|
174
|
+
}
|
|
@@ -89,6 +89,7 @@ export const BoxAnnotations = ({
|
|
|
89
89
|
|
|
90
90
|
// Ref to track if component is mounted to prevent state updates after unmount
|
|
91
91
|
const isMountedRef = useRef(true);
|
|
92
|
+
const labelInputRef = useRef<HTMLInputElement>(null);
|
|
92
93
|
|
|
93
94
|
useEffect(() => {
|
|
94
95
|
return () => {
|
|
@@ -96,6 +97,19 @@ export const BoxAnnotations = ({
|
|
|
96
97
|
};
|
|
97
98
|
}, []);
|
|
98
99
|
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (!labelDialog.isVisible) return;
|
|
102
|
+
|
|
103
|
+
const focusFrame = window.requestAnimationFrame(() => {
|
|
104
|
+
labelInputRef.current?.focus();
|
|
105
|
+
labelInputRef.current?.select();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return () => {
|
|
109
|
+
window.cancelAnimationFrame(focusFrame);
|
|
110
|
+
};
|
|
111
|
+
}, [labelDialog.isVisible]);
|
|
112
|
+
|
|
99
113
|
// Memoized function to get relative coordinates (more stable reference)
|
|
100
114
|
const getRelativeCoordinates = useCallback((e: React.MouseEvent): { x: number; y: number } => {
|
|
101
115
|
const imageElement = imageRef.current;
|
|
@@ -602,6 +616,7 @@ export const BoxAnnotations = ({
|
|
|
602
616
|
}
|
|
603
617
|
</div>
|
|
604
618
|
<input
|
|
619
|
+
ref={labelInputRef}
|
|
605
620
|
type="text"
|
|
606
621
|
value={labelDialog.label}
|
|
607
622
|
onChange={handleLabelChange}
|