@xcelsior/ui-spreadsheets 1.0.5 → 1.0.6
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/dist/index.d.mts +4 -18
- package/dist/index.d.ts +4 -18
- package/dist/index.js +117 -94
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +77 -54
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/CommentModals.tsx +28 -11
- package/src/components/Spreadsheet.tsx +2 -7
- package/src/components/SpreadsheetCell.tsx +222 -195
- package/src/hooks/useSpreadsheetComments.ts +6 -12
package/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
1
2
|
import { cn } from '../utils';
|
|
2
3
|
import type { CellComment } from '../types';
|
|
3
4
|
|
|
@@ -6,28 +7,43 @@ import type { CellComment } from '../types';
|
|
|
6
7
|
export interface AddCommentModalProps {
|
|
7
8
|
/** Whether the modal is open */
|
|
8
9
|
isOpen: boolean;
|
|
9
|
-
/** Current comment text */
|
|
10
|
-
commentText: string;
|
|
11
10
|
/** Column label for the cell (optional) */
|
|
12
11
|
columnLabel?: string;
|
|
13
|
-
/** Callback to
|
|
14
|
-
|
|
15
|
-
/** Callback to add the comment */
|
|
16
|
-
onAdd: () => void;
|
|
12
|
+
/** Callback to add the comment with the text */
|
|
13
|
+
onAdd: (text: string) => void;
|
|
17
14
|
/** Callback to close/cancel */
|
|
18
15
|
onClose: () => void;
|
|
19
16
|
}
|
|
20
17
|
|
|
21
18
|
export function AddCommentModal({
|
|
22
19
|
isOpen,
|
|
23
|
-
commentText,
|
|
24
20
|
columnLabel,
|
|
25
|
-
onCommentTextChange,
|
|
26
21
|
onAdd,
|
|
27
22
|
onClose,
|
|
28
23
|
}: AddCommentModalProps) {
|
|
24
|
+
const [commentText, setCommentText] = useState('');
|
|
25
|
+
|
|
26
|
+
// Reset text when modal opens/closes
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!isOpen) {
|
|
29
|
+
setCommentText('');
|
|
30
|
+
}
|
|
31
|
+
}, [isOpen]);
|
|
32
|
+
|
|
29
33
|
if (!isOpen) return null;
|
|
30
34
|
|
|
35
|
+
const handleAdd = () => {
|
|
36
|
+
if (commentText.trim()) {
|
|
37
|
+
onAdd(commentText);
|
|
38
|
+
setCommentText('');
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const handleClose = () => {
|
|
43
|
+
setCommentText('');
|
|
44
|
+
onClose();
|
|
45
|
+
};
|
|
46
|
+
|
|
31
47
|
return (
|
|
32
48
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
|
33
49
|
<div className="bg-white rounded-lg shadow-xl p-6 w-96 max-w-full mx-4">
|
|
@@ -36,21 +52,22 @@ export function AddCommentModal({
|
|
|
36
52
|
</h3>
|
|
37
53
|
<textarea
|
|
38
54
|
value={commentText}
|
|
39
|
-
onChange={(e) =>
|
|
55
|
+
onChange={(e) => setCommentText(e.target.value)}
|
|
40
56
|
placeholder="Enter your comment..."
|
|
41
57
|
className="w-full h-24 p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none"
|
|
58
|
+
autoFocus
|
|
42
59
|
/>
|
|
43
60
|
<div className="flex justify-end gap-2 mt-4">
|
|
44
61
|
<button
|
|
45
62
|
type="button"
|
|
46
|
-
onClick={
|
|
63
|
+
onClick={handleClose}
|
|
47
64
|
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
|
48
65
|
>
|
|
49
66
|
Cancel
|
|
50
67
|
</button>
|
|
51
68
|
<button
|
|
52
69
|
type="button"
|
|
53
|
-
onClick={
|
|
70
|
+
onClick={handleAdd}
|
|
54
71
|
disabled={!commentText.trim()}
|
|
55
72
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
56
73
|
>
|
|
@@ -178,8 +178,6 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
178
178
|
cellHasComments,
|
|
179
179
|
commentModalCell,
|
|
180
180
|
setCommentModalCell,
|
|
181
|
-
commentText,
|
|
182
|
-
setCommentText,
|
|
183
181
|
viewCommentsCell,
|
|
184
182
|
setViewCommentsCell,
|
|
185
183
|
handleAddCellComment,
|
|
@@ -1054,7 +1052,6 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1054
1052
|
{/* Add Cell Comment Modal */}
|
|
1055
1053
|
<AddCommentModal
|
|
1056
1054
|
isOpen={commentModalCell !== null}
|
|
1057
|
-
commentText={commentText}
|
|
1058
1055
|
columnLabel={
|
|
1059
1056
|
commentModalCell
|
|
1060
1057
|
? commentModalCell.columnId === ROW_INDEX_COLUMN_ID
|
|
@@ -1062,13 +1059,11 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1062
1059
|
: columns.find((c) => c.id === commentModalCell.columnId)?.label
|
|
1063
1060
|
: undefined
|
|
1064
1061
|
}
|
|
1065
|
-
|
|
1066
|
-
onAdd={() =>
|
|
1062
|
+
onAdd={(text) =>
|
|
1067
1063
|
commentModalCell !== null &&
|
|
1068
|
-
handleAddCellComment(commentModalCell.rowId, commentModalCell.columnId)
|
|
1064
|
+
handleAddCellComment(commentModalCell.rowId, commentModalCell.columnId, text)
|
|
1069
1065
|
}
|
|
1070
1066
|
onClose={() => {
|
|
1071
|
-
setCommentText('');
|
|
1072
1067
|
setCommentModalCell(null);
|
|
1073
1068
|
}}
|
|
1074
1069
|
/>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
|
-
import { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { useState, useRef, useEffect, memo } from 'react';
|
|
3
3
|
import { HiOutlineClipboardCopy, HiOutlineClipboardCheck } from 'react-icons/hi';
|
|
4
4
|
import { AiFillHighlight } from 'react-icons/ai';
|
|
5
5
|
import { FaComment, FaRegComment } from 'react-icons/fa';
|
|
@@ -27,38 +27,38 @@ const cellPaddingNormal = 'px-2 py-1';
|
|
|
27
27
|
* />
|
|
28
28
|
* ```
|
|
29
29
|
*/
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}) => {
|
|
30
|
+
const SpreadsheetCell: React.FC<SpreadsheetCellProps> = ({
|
|
31
|
+
value,
|
|
32
|
+
column,
|
|
33
|
+
row,
|
|
34
|
+
rowIndex,
|
|
35
|
+
rowId,
|
|
36
|
+
isEditable = false,
|
|
37
|
+
isEditing = false,
|
|
38
|
+
isFocused = false,
|
|
39
|
+
isRowSelected = false,
|
|
40
|
+
isRowHovered = false,
|
|
41
|
+
highlightColor,
|
|
42
|
+
hasComments = false,
|
|
43
|
+
unresolvedCommentCount = 0,
|
|
44
|
+
isCopied = false,
|
|
45
|
+
compactMode = false,
|
|
46
|
+
isPinned = false,
|
|
47
|
+
pinSide,
|
|
48
|
+
leftOffset = 0,
|
|
49
|
+
rightOffset = 0,
|
|
50
|
+
onClick,
|
|
51
|
+
onChange,
|
|
52
|
+
onConfirm,
|
|
53
|
+
onCancel,
|
|
54
|
+
onCopyDown,
|
|
55
|
+
onCopyToSelected,
|
|
56
|
+
onHighlight,
|
|
57
|
+
onAddComment,
|
|
58
|
+
onViewComments,
|
|
59
|
+
hasSelectedRows = false,
|
|
60
|
+
className,
|
|
61
|
+
}) => {
|
|
62
62
|
const [localValue, setLocalValue] = useState(value);
|
|
63
63
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
64
64
|
const selectRef = useRef<HTMLSelectElement>(null);
|
|
@@ -126,53 +126,53 @@ export const SpreadsheetCell: React.FC<SpreadsheetCellProps> = ({
|
|
|
126
126
|
const renderEditInput = () => {
|
|
127
127
|
if (column.type === 'select' && column.options) {
|
|
128
128
|
return (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
value={localValue ?? ''}
|
|
132
|
-
onChange={(e) => {
|
|
133
|
-
setLocalValue(e.target.value);
|
|
134
|
-
onChange?.(e.target.value);
|
|
135
|
-
onConfirm?.();
|
|
136
|
-
}}
|
|
137
|
-
onKeyDown={handleKeyDown}
|
|
138
|
-
onBlur={() => onConfirm?.()}
|
|
139
|
-
className={cn(
|
|
140
|
-
'w-full border border-gray-300 rounded text-xs focus:outline-none focus:ring-1 focus:ring-blue-500',
|
|
141
|
-
compactMode ? 'px-1 py-0.5' : 'px-2 py-1'
|
|
142
|
-
)}
|
|
143
|
-
>
|
|
144
|
-
{column.options.map((option) => (
|
|
145
|
-
<option key={option} value={option}>
|
|
146
|
-
{option}
|
|
147
|
-
</option>
|
|
148
|
-
))}
|
|
149
|
-
</select>
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return (
|
|
154
|
-
<input
|
|
155
|
-
ref={inputRef}
|
|
156
|
-
type={column.type === 'number' ? 'number' : 'text'}
|
|
157
|
-
step={column.type === 'number' ? '0.01' : undefined}
|
|
129
|
+
<select
|
|
130
|
+
ref={selectRef}
|
|
158
131
|
value={localValue ?? ''}
|
|
159
132
|
onChange={(e) => {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
? ''
|
|
164
|
-
: parseFloat(e.target.value)
|
|
165
|
-
: e.target.value;
|
|
166
|
-
setLocalValue(newValue);
|
|
167
|
-
onChange?.(newValue);
|
|
133
|
+
setLocalValue(e.target.value);
|
|
134
|
+
onChange?.(e.target.value);
|
|
135
|
+
onConfirm?.();
|
|
168
136
|
}}
|
|
169
137
|
onKeyDown={handleKeyDown}
|
|
170
138
|
onBlur={() => onConfirm?.()}
|
|
171
139
|
className={cn(
|
|
172
|
-
|
|
173
|
-
|
|
140
|
+
'w-full border border-gray-300 rounded text-xs focus:outline-none focus:ring-1 focus:ring-blue-500',
|
|
141
|
+
compactMode ? 'px-1 py-0.5' : 'px-2 py-1'
|
|
174
142
|
)}
|
|
175
|
-
|
|
143
|
+
>
|
|
144
|
+
{column.options.map((option) => (
|
|
145
|
+
<option key={option} value={option}>
|
|
146
|
+
{option}
|
|
147
|
+
</option>
|
|
148
|
+
))}
|
|
149
|
+
</select>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<input
|
|
155
|
+
ref={inputRef}
|
|
156
|
+
type={column.type === 'number' ? 'number' : 'text'}
|
|
157
|
+
step={column.type === 'number' ? '0.01' : undefined}
|
|
158
|
+
value={localValue ?? ''}
|
|
159
|
+
onChange={(e) => {
|
|
160
|
+
const newValue =
|
|
161
|
+
column.type === 'number'
|
|
162
|
+
? e.target.value === ''
|
|
163
|
+
? ''
|
|
164
|
+
: parseFloat(e.target.value)
|
|
165
|
+
: e.target.value;
|
|
166
|
+
setLocalValue(newValue);
|
|
167
|
+
onChange?.(newValue);
|
|
168
|
+
}}
|
|
169
|
+
onKeyDown={handleKeyDown}
|
|
170
|
+
onBlur={() => onConfirm?.()}
|
|
171
|
+
className={cn(
|
|
172
|
+
'w-full border border-gray-300 rounded text-xs focus:outline-none focus:ring-1 focus:ring-blue-500 bg-yellow-50',
|
|
173
|
+
compactMode ? 'px-1 py-0.5' : 'px-2 py-1'
|
|
174
|
+
)}
|
|
175
|
+
/>
|
|
176
176
|
);
|
|
177
177
|
};
|
|
178
178
|
|
|
@@ -198,136 +198,163 @@ export const SpreadsheetCell: React.FC<SpreadsheetCellProps> = ({
|
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
return (
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
201
|
+
<td
|
|
202
|
+
onClick={onClick}
|
|
203
|
+
onKeyDown={handleCellKeyDown}
|
|
204
|
+
data-cell-id={`${rowId}-${column.id}`}
|
|
205
|
+
className={cn(
|
|
206
|
+
'border border-gray-200 text-xs group cursor-pointer transition-colors',
|
|
207
|
+
cellPadding,
|
|
208
|
+
column.align === 'right' && 'text-right',
|
|
209
|
+
column.align === 'center' && 'text-center',
|
|
210
|
+
isCopied && 'animate-pulse',
|
|
211
|
+
isFocused && 'ring-2 ring-blue-500 ring-inset',
|
|
212
|
+
isPinned ? 'z-20' : 'z-0',
|
|
213
|
+
className
|
|
214
|
+
)}
|
|
215
|
+
style={{
|
|
216
|
+
backgroundColor: getBackgroundColor(),
|
|
217
|
+
minWidth: column.minWidth || column.width,
|
|
218
|
+
...positionStyles,
|
|
219
|
+
}}
|
|
220
|
+
>
|
|
221
|
+
{isEditing ? (
|
|
222
|
+
renderEditInput()
|
|
223
|
+
) : (
|
|
224
|
+
<div className="flex items-center gap-1 relative">
|
|
225
|
+
{/* Main content */}
|
|
226
|
+
<div
|
|
227
|
+
className={cn(
|
|
228
|
+
'flex-1 truncate',
|
|
229
|
+
isEditable &&
|
|
230
|
+
'cursor-text hover:bg-gray-50 px-0.5 rounded min-h-[18px] flex items-center bg-yellow-50/50'
|
|
231
|
+
)}
|
|
232
|
+
title={String(value ?? '')}
|
|
233
|
+
>
|
|
234
|
+
{renderContent()}
|
|
235
|
+
</div>
|
|
236
236
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
237
|
+
{/* Action buttons - show on hover, except comment indicator which is always visible */}
|
|
238
|
+
<div className="flex items-center gap-0.5 shrink-0">
|
|
239
|
+
{/* Copy down button - hover only */}
|
|
240
|
+
{value !== null && value !== undefined && value !== '' && onCopyDown && (
|
|
241
|
+
<button
|
|
242
|
+
type="button"
|
|
243
|
+
onClick={(e) => {
|
|
244
|
+
e.stopPropagation();
|
|
245
|
+
onCopyDown();
|
|
246
|
+
}}
|
|
247
|
+
className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 bg-gray-100 hover:bg-gray-200 rounded"
|
|
248
|
+
title="Copy value down to rows below"
|
|
249
|
+
>
|
|
250
|
+
<HiOutlineClipboardCopy className="h-2.5 w-2.5 text-gray-500" />
|
|
251
|
+
</button>
|
|
252
|
+
)}
|
|
253
253
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
254
|
+
{/* Copy to selected button - hover only */}
|
|
255
|
+
{hasSelectedRows &&
|
|
256
|
+
value !== null &&
|
|
257
|
+
value !== undefined &&
|
|
258
|
+
value !== '' &&
|
|
259
|
+
onCopyToSelected && (
|
|
260
|
+
<button
|
|
261
|
+
type="button"
|
|
262
|
+
onClick={(e) => {
|
|
263
|
+
e.stopPropagation();
|
|
264
|
+
onCopyToSelected();
|
|
265
|
+
}}
|
|
266
|
+
className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 bg-green-100 hover:bg-green-200 rounded"
|
|
267
|
+
title="Copy to selected rows"
|
|
268
|
+
>
|
|
269
|
+
<HiOutlineClipboardCheck className="h-2.5 w-2.5 text-green-600" />
|
|
270
|
+
</button>
|
|
271
|
+
)}
|
|
272
272
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
273
|
+
{/* Highlight button - hover only */}
|
|
274
|
+
{onHighlight && (
|
|
275
|
+
<button
|
|
276
|
+
type="button"
|
|
277
|
+
onClick={(e) => {
|
|
278
|
+
e.stopPropagation();
|
|
279
|
+
onHighlight();
|
|
280
|
+
}}
|
|
281
|
+
className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 bg-gray-100 hover:bg-gray-200 rounded"
|
|
282
|
+
title="Highlight cell"
|
|
283
|
+
>
|
|
284
|
+
<AiFillHighlight
|
|
285
|
+
className={cn(
|
|
286
|
+
'h-2.5 w-2.5',
|
|
287
|
+
highlightColor ? 'text-amber-500' : 'text-gray-500'
|
|
288
|
+
)}
|
|
289
|
+
/>
|
|
290
|
+
</button>
|
|
291
|
+
)}
|
|
292
292
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
293
|
+
{/* Comment button - always visible when has comments, hover only when adding */}
|
|
294
|
+
{hasComments && onViewComments ? (
|
|
295
|
+
<button
|
|
296
|
+
type="button"
|
|
297
|
+
onClick={(e) => {
|
|
298
|
+
e.stopPropagation();
|
|
299
|
+
onViewComments();
|
|
300
|
+
}}
|
|
301
|
+
className="p-0.5 bg-amber-100 hover:bg-amber-200 rounded transition-colors flex items-center gap-0.5"
|
|
302
|
+
title={`${unresolvedCommentCount} comment(s) - click to view`}
|
|
303
|
+
>
|
|
304
|
+
<FaComment className="h-2.5 w-2.5 text-amber-500" />
|
|
305
|
+
{unresolvedCommentCount > 0 && (
|
|
306
|
+
<span className="text-[9px] font-medium text-amber-600">
|
|
307
307
|
{unresolvedCommentCount > 99
|
|
308
|
-
|
|
309
|
-
|
|
308
|
+
? '99+'
|
|
309
|
+
: unresolvedCommentCount}
|
|
310
310
|
</span>
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
</div>
|
|
311
|
+
)}
|
|
312
|
+
</button>
|
|
313
|
+
) : onAddComment ? (
|
|
314
|
+
<button
|
|
315
|
+
type="button"
|
|
316
|
+
onClick={(e) => {
|
|
317
|
+
e.stopPropagation();
|
|
318
|
+
onAddComment();
|
|
319
|
+
}}
|
|
320
|
+
className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 bg-gray-100 hover:bg-gray-200 rounded"
|
|
321
|
+
title="Add comment"
|
|
322
|
+
>
|
|
323
|
+
<FaRegComment className="h-2.5 w-2.5 text-gray-500" />
|
|
324
|
+
</button>
|
|
325
|
+
) : null}
|
|
327
326
|
</div>
|
|
328
|
-
|
|
329
|
-
|
|
327
|
+
</div>
|
|
328
|
+
)}
|
|
329
|
+
</td>
|
|
330
330
|
);
|
|
331
331
|
};
|
|
332
332
|
|
|
333
333
|
SpreadsheetCell.displayName = 'SpreadsheetCell';
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Memoized SpreadsheetCell to prevent unnecessary re-renders
|
|
337
|
+
* Only re-renders when props that affect this specific cell change
|
|
338
|
+
*/
|
|
339
|
+
export const MemoizedSpreadsheetCell = memo(SpreadsheetCell, (prevProps, nextProps) => {
|
|
340
|
+
// Quick bail-out checks for most common changes
|
|
341
|
+
if (prevProps.isEditing !== nextProps.isEditing) return false;
|
|
342
|
+
if (prevProps.isFocused !== nextProps.isFocused) return false;
|
|
343
|
+
if (prevProps.value !== nextProps.value) return false;
|
|
344
|
+
if (prevProps.isRowSelected !== nextProps.isRowSelected) return false;
|
|
345
|
+
if (prevProps.isRowHovered !== nextProps.isRowHovered) return false;
|
|
346
|
+
if (prevProps.highlightColor !== nextProps.highlightColor) return false;
|
|
347
|
+
if (prevProps.hasComments !== nextProps.hasComments) return false;
|
|
348
|
+
if (prevProps.unresolvedCommentCount !== nextProps.unresolvedCommentCount) return false;
|
|
349
|
+
if (prevProps.isCopied !== nextProps.isCopied) return false;
|
|
350
|
+
if (prevProps.isPinned !== nextProps.isPinned) return false;
|
|
351
|
+
if (prevProps.leftOffset !== nextProps.leftOffset) return false;
|
|
352
|
+
if (prevProps.rightOffset !== nextProps.rightOffset) return false;
|
|
353
|
+
|
|
354
|
+
// All relevant props are equal, skip re-render
|
|
355
|
+
return true;
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
MemoizedSpreadsheetCell.displayName = 'MemoizedSpreadsheetCell';
|
|
359
|
+
|
|
360
|
+
export { MemoizedSpreadsheetCell as SpreadsheetCell };
|
|
@@ -17,15 +17,13 @@ export interface UseSpreadsheetCommentsReturn {
|
|
|
17
17
|
// Add comment modal state
|
|
18
18
|
commentModalCell: CellPosition | null;
|
|
19
19
|
setCommentModalCell: (cell: CellPosition | null) => void;
|
|
20
|
-
commentText: string;
|
|
21
|
-
setCommentText: (text: string) => void;
|
|
22
20
|
|
|
23
21
|
// View comments modal state
|
|
24
22
|
viewCommentsCell: CellPosition | null;
|
|
25
23
|
setViewCommentsCell: (cell: CellPosition | null) => void;
|
|
26
24
|
|
|
27
25
|
// Actions
|
|
28
|
-
handleAddCellComment: (rowId: string | number, columnId: string) => void;
|
|
26
|
+
handleAddCellComment: (rowId: string | number, columnId: string, text: string) => void;
|
|
29
27
|
handleToggleCommentResolved: (commentId: string) => void;
|
|
30
28
|
|
|
31
29
|
// Utility
|
|
@@ -41,7 +39,6 @@ export function useSpreadsheetComments({
|
|
|
41
39
|
|
|
42
40
|
// Modal states
|
|
43
41
|
const [commentModalCell, setCommentModalCell] = useState<CellPosition | null>(null);
|
|
44
|
-
const [commentText, setCommentText] = useState('');
|
|
45
42
|
const [viewCommentsCell, setViewCommentsCell] = useState<CellPosition | null>(null);
|
|
46
43
|
|
|
47
44
|
// Use external comments if provided, otherwise use internal
|
|
@@ -75,12 +72,12 @@ export function useSpreadsheetComments({
|
|
|
75
72
|
|
|
76
73
|
// Add a cell comment
|
|
77
74
|
const handleAddCellComment = useCallback(
|
|
78
|
-
(rowId: string | number, columnId: string) => {
|
|
79
|
-
if (!
|
|
75
|
+
(rowId: string | number, columnId: string, text: string) => {
|
|
76
|
+
if (!text.trim()) return;
|
|
80
77
|
|
|
81
78
|
if (onAddCellComment) {
|
|
82
79
|
// Controlled mode
|
|
83
|
-
onAddCellComment(rowId, columnId,
|
|
80
|
+
onAddCellComment(rowId, columnId, text);
|
|
84
81
|
} else {
|
|
85
82
|
// Uncontrolled mode
|
|
86
83
|
setCellCommentsInternal((prev) => [
|
|
@@ -89,16 +86,15 @@ export function useSpreadsheetComments({
|
|
|
89
86
|
id: `comment-${Date.now()}`,
|
|
90
87
|
rowId,
|
|
91
88
|
columnId,
|
|
92
|
-
text
|
|
89
|
+
text,
|
|
93
90
|
timestamp: new Date(),
|
|
94
91
|
resolved: false,
|
|
95
92
|
},
|
|
96
93
|
]);
|
|
97
94
|
}
|
|
98
|
-
setCommentText('');
|
|
99
95
|
setCommentModalCell(null);
|
|
100
96
|
},
|
|
101
|
-
[
|
|
97
|
+
[onAddCellComment]
|
|
102
98
|
);
|
|
103
99
|
|
|
104
100
|
// Toggle comment resolved status
|
|
@@ -117,8 +113,6 @@ export function useSpreadsheetComments({
|
|
|
117
113
|
// Add comment modal state
|
|
118
114
|
commentModalCell,
|
|
119
115
|
setCommentModalCell,
|
|
120
|
-
commentText,
|
|
121
|
-
setCommentText,
|
|
122
116
|
|
|
123
117
|
// View comments modal state
|
|
124
118
|
viewCommentsCell,
|