@archbase/components 4.0.2 → 4.0.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archbase/components",
3
- "version": "4.0.2",
3
+ "version": "4.0.3",
4
4
  "description": "UI Components for Archbase React v3 - Form editors, data visualization, and business components",
5
5
  "author": "Edson Martins <edsonmartins2005@gmail.com>",
6
6
  "license": "MIT",
@@ -38,9 +38,6 @@
38
38
  "react-dom": "^18.3.0 || ^19.2.0"
39
39
  },
40
40
  "dependencies": {
41
- "@archbase/core": "4.0.2",
42
- "@archbase/data": "4.0.2",
43
- "@archbase/layout": "4.0.2",
44
41
  "@fortune-sheet/core": "^1.0.4",
45
42
  "@fortune-sheet/react": "^1.0.4",
46
43
  "@pdfme/ui": "^5.3.6",
@@ -123,7 +120,10 @@
123
120
  "yet-another-react-lightbox": "^3.21.0",
124
121
  "react-photo-album": "^3.0.0",
125
122
  "react-image-picker-editor": "^1.1.1",
126
- "react-pro-sidebar": "^1.1.0"
123
+ "react-pro-sidebar": "^1.1.0",
124
+ "@archbase/core": "4.0.3",
125
+ "@archbase/layout": "4.0.3",
126
+ "@archbase/data": "4.0.3"
127
127
  },
128
128
  "devDependencies": {
129
129
  "@types/d3": "^7.4.3",
@@ -51,7 +51,15 @@ export interface ArchbaseImageEditProps<T, ID> extends ImageProps {
51
51
  /** Referência para o componente interno */
52
52
  innerRef?: React.RefObject<HTMLInputElement> | undefined;
53
53
  /** Cor de fundo da imagem */
54
- imageBackgroundColor?: string
54
+ imageBackgroundColor?: string;
55
+ /** Callback quando o estado de processamento muda (útil para desabilitar botões enquanto processa) */
56
+ onProcessingChange?: (isProcessing: boolean) => void;
57
+ /** Largura máxima da imagem em pixels (redimensiona automaticamente se exceder) */
58
+ maxWidth?: number;
59
+ /** Altura máxima da imagem em pixels (redimensiona automaticamente se exceder) */
60
+ maxHeight?: number;
61
+ /** Tamanho máximo da imagem em KB (recomprime se exceder) */
62
+ maxSizeKb?: number;
55
63
  }
56
64
 
57
65
  export function ArchbaseImageEdit<T, ID>({
@@ -75,6 +83,10 @@ export function ArchbaseImageEdit<T, ID>({
75
83
  innerRef,
76
84
  variant,
77
85
  imageBackgroundColor,
86
+ onProcessingChange,
87
+ maxWidth,
88
+ maxHeight,
89
+ maxSizeKb,
78
90
  ...otherProps
79
91
  }: ArchbaseImageEditProps<T, ID>) {
80
92
  // 🔄 MIGRAÇÃO V1/V2: Hook de compatibilidade
@@ -120,7 +132,8 @@ export function ArchbaseImageEdit<T, ID>({
120
132
  }
121
133
  }
122
134
 
123
- if (isBase64(initialValue) && !disabledBase64Convertion) {
135
+ const wasBase64 = isBase64(initialValue) && !disabledBase64Convertion;
136
+ if (wasBase64) {
124
137
  initialValue = atob(initialValue);
125
138
  }
126
139
 
@@ -199,15 +212,27 @@ useEffect(() => {
199
212
  const changedValue = image;
200
213
  setValue((_prev) => changedValue);
201
214
 
202
- if (dataSource && !dataSource.isBrowsing() && dataField && dataSource.getFieldValue(dataField) !== changedValue) {
215
+ if (dataSource && !dataSource.isBrowsing() && dataField) {
216
+ // ✅ CORRIGIDO: Normalizar valores para comparação
217
+ const currentFieldValue = dataSource.getFieldValue(dataField);
218
+
219
+ // Preparar valor para salvar
203
220
  let valueToSave: string | undefined;
204
221
  if (!changedValue) {
205
222
  valueToSave = undefined;
206
223
  } else {
207
224
  valueToSave = disabledBase64Convertion ? changedValue : btoa(changedValue);
208
225
  }
209
- // 🔄 MIGRAÇÃO V1/V2: Usar handleValueChange do padrão de compatibilidade
210
- v1v2Compatibility.handleValueChange(valueToSave);
226
+
227
+ // ✅ Normalizar ambos os valores para comparação (null, undefined, '' → undefined)
228
+ const normalizedCurrent = currentFieldValue || undefined;
229
+ const normalizedNew = valueToSave || undefined;
230
+
231
+ // Só atualiza se realmente mudou
232
+ if (normalizedCurrent !== normalizedNew) {
233
+ // 🔄 MIGRAÇÃO V1/V2: Usar handleValueChange do padrão de compatibilidade
234
+ v1v2Compatibility.handleValueChange(valueToSave);
235
+ }
211
236
  }
212
237
  if (onChangeImage) {
213
238
  onChangeImage(image);
@@ -234,6 +259,7 @@ useEffect(() => {
234
259
  <ArchbaseImagePickerEditor
235
260
  imageSrcProp={value}
236
261
  variant={variant}
262
+ onProcessingChange={onProcessingChange}
237
263
  config={{
238
264
  borderRadius: radius,
239
265
  width,
@@ -247,6 +273,9 @@ useEffect(() => {
247
273
  hideAddBtn: isReadOnly(),
248
274
  onChangeImage: handleChangeImage,
249
275
  imageBackgroundColor,
276
+ maxWidth,
277
+ maxHeight,
278
+ maxSizeKb,
250
279
  }}
251
280
  />
252
281
  </Input.Wrapper>
@@ -156,6 +156,90 @@ function extractFormat(dataUri: string | null | undefined): string {
156
156
  return 'png';
157
157
  }
158
158
 
159
+ /**
160
+ * Redimensiona uma imagem se exceder os limites de tamanho
161
+ * @param dataUri Data URI da imagem original
162
+ * @param maxWidth Largura máxima em pixels
163
+ * @param maxHeight Altura máxima em pixels
164
+ * @param maxSizeKb Tamanho máximo em KB
165
+ * @param quality Qualidade inicial da compressão (0-100)
166
+ * @returns Promise com o data URI redimensionado
167
+ */
168
+ async function resizeImageIfNeeded(
169
+ dataUri: string,
170
+ maxWidth?: number,
171
+ maxHeight?: number,
172
+ maxSizeKb?: number,
173
+ quality: number = 80
174
+ ): Promise<string> {
175
+ return new Promise((resolve) => {
176
+ // Se não há limites definidos, retorna como está
177
+ if (!maxWidth && !maxHeight && !maxSizeKb) {
178
+ resolve(dataUri);
179
+ return;
180
+ }
181
+
182
+ const img = new Image();
183
+ img.onload = () => {
184
+ let targetWidth = img.width;
185
+ let targetHeight = img.height;
186
+
187
+ // Calcular novo tamanho se exceder limites
188
+ if (maxWidth && img.width > maxWidth) {
189
+ const ratio = maxWidth / img.width;
190
+ targetWidth = maxWidth;
191
+ targetHeight = Math.round(img.height * ratio);
192
+ }
193
+ if (maxHeight && targetHeight > maxHeight) {
194
+ const ratio = maxHeight / targetHeight;
195
+ targetHeight = maxHeight;
196
+ targetWidth = Math.round(targetWidth * ratio);
197
+ }
198
+
199
+ // Se não precisa redimensionar e não há limite de tamanho, retorna original
200
+ if (targetWidth === img.width && targetHeight === img.height && !maxSizeKb) {
201
+ resolve(dataUri);
202
+ return;
203
+ }
204
+
205
+ // Criar canvas para redimensionar
206
+ const canvas = document.createElement('canvas');
207
+ canvas.width = targetWidth;
208
+ canvas.height = targetHeight;
209
+ const ctx = canvas.getContext('2d');
210
+ if (!ctx) {
211
+ resolve(dataUri);
212
+ return;
213
+ }
214
+
215
+ ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
216
+
217
+ // Determinar formato de saída
218
+ const format = extractFormat(dataUri);
219
+ const mimeType = format === 'png' ? 'image/png' : 'image/jpeg';
220
+
221
+ // Função para comprimir até atingir o tamanho desejado
222
+ const compressToSize = (currentQuality: number): string => {
223
+ const result = canvas.toDataURL(mimeType, currentQuality / 100);
224
+ if (maxSizeKb) {
225
+ const sizeKb = Math.ceil(((3 / 4) * result.split(',')[1].length) / 1024);
226
+ // Se ainda está grande e qualidade pode ser reduzida
227
+ if (sizeKb > maxSizeKb && currentQuality > 20) {
228
+ return compressToSize(currentQuality - 10);
229
+ }
230
+ }
231
+ return result;
232
+ };
233
+
234
+ resolve(compressToSize(quality));
235
+ };
236
+ img.onerror = () => {
237
+ resolve(dataUri);
238
+ };
239
+ img.src = dataUri;
240
+ });
241
+ }
242
+
159
243
  /**
160
244
  * Props do ArchbaseImagePickerEditor
161
245
  * Mantém compatibilidade total com a versão anterior
@@ -174,6 +258,8 @@ export interface ArchbaseImagePickerEditorProps {
174
258
  imageChanged?: (newDataUri: string | undefined) => void;
175
259
  /** Variante dos botões de ação */
176
260
  variant?: ActionIconVariant;
261
+ /** Callback quando o estado de processamento muda (útil para desabilitar botões enquanto processa) */
262
+ onProcessingChange?: (isProcessing: boolean) => void;
177
263
  }
178
264
 
179
265
  /**
@@ -209,6 +295,7 @@ export const ArchbaseImagePickerEditor = memo(
209
295
  color = '#1e88e5',
210
296
  imageChanged,
211
297
  variant = 'transparent',
298
+ onProcessingChange,
212
299
  }: ArchbaseImagePickerEditorProps) => {
213
300
  const theme = useArchbaseTheme();
214
301
  const { colorScheme } = useMantineColorScheme();
@@ -227,9 +314,19 @@ export const ArchbaseImagePickerEditor = memo(
227
314
  // Ref para a imagem original (antes de normalização) para comparação
228
315
  const originalImagePropRef = useRef<string | undefined>(imageSrcProp);
229
316
 
317
+ // Ref para onProcessingChange para evitar closure stale no debounce
318
+ const onProcessingChangeRef = useRef(onProcessingChange);
319
+ onProcessingChangeRef.current = onProcessingChange;
320
+
230
321
  // Ref para saber se é a primeira renderização
231
322
  const isFirstRender = useRef(true);
232
323
 
324
+ // Ref para saber se já recebemos a imagem inicial (evita que o picker zere antes de carregar)
325
+ const hasReceivedInitialImage = useRef(false);
326
+
327
+ // Timestamp de quando o componente foi montado (para ignorar valores vazios só no início)
328
+ const mountTimeRef = useRef(Date.now());
329
+
233
330
  // Configuração padrão
234
331
  const defaultConfig: ArchbaseImagePickerConf = {
235
332
  objectFit: 'cover',
@@ -256,6 +353,8 @@ export const ArchbaseImagePickerEditor = memo(
256
353
  const debouncedOnChange = useDebouncedCallback((newDataUri: string | undefined) => {
257
354
  // Verificar se a imagem realmente mudou
258
355
  if (lastReportedImageRef.current === newDataUri) {
356
+ // Notificar que o processamento terminou (usando ref para evitar closure stale)
357
+ onProcessingChangeRef.current?.(false);
259
358
  return;
260
359
  }
261
360
 
@@ -270,41 +369,76 @@ export const ArchbaseImagePickerEditor = memo(
270
369
  if (imageChanged) {
271
370
  imageChanged(newDataUri);
272
371
  }
372
+
373
+ // Notificar que o processamento terminou (usando ref para evitar closure stale)
374
+ onProcessingChangeRef.current?.(false);
273
375
  }, 300); // 300ms de debounce
274
376
 
275
377
  // Sincronizar com prop externa
276
378
  useEffect(() => {
277
- // atualizar se a prop original realmente mudou
278
- if (imageSrcProp !== originalImagePropRef.current) {
279
- originalImagePropRef.current = imageSrcProp;
379
+ // Sempre atualizar quando a prop mudar (mesmo que seja a mesma do ref inicial)
380
+ const normalizedSrc = normalizeImageSrc(imageSrcProp);
381
+
382
+ // Marcar que recebemos a imagem inicial quando receber um valor não vazio
383
+ if (normalizedSrc && !hasReceivedInitialImage.current) {
384
+ hasReceivedInitialImage.current = true;
385
+ }
280
386
 
281
- const normalizedSrc = normalizeImageSrc(imageSrcProp);
387
+ // atualizar o estado se o valor normalizado for diferente do atual
388
+ if (normalizedSrc !== imageSrc) {
282
389
  setImageSrc(normalizedSrc);
283
390
  setLoadImage(!!normalizedSrc);
391
+ }
284
392
 
285
- // Atualizar a referência sem disparar callback na sincronização inicial
286
- if (isFirstRender.current) {
287
- lastReportedImageRef.current = normalizedSrc;
288
- isFirstRender.current = false;
289
- }
393
+ // Atualizar a referência
394
+ originalImagePropRef.current = imageSrcProp;
395
+
396
+ // Atualizar a referência sem disparar callback na sincronização inicial
397
+ if (isFirstRender.current) {
398
+ lastReportedImageRef.current = normalizedSrc;
399
+ isFirstRender.current = false;
290
400
  }
291
401
  }, [imageSrcProp]);
292
402
 
293
403
  // Handler quando a imagem muda no editor
294
- const handleImageChanged = useCallback((newDataUri: string) => {
295
- const newValue = newDataUri || undefined;
404
+ const handleImageChanged = useCallback(async (newDataUri: string) => {
405
+ const timeSinceMount = Date.now() - mountTimeRef.current;
296
406
 
297
407
  // Só processa se for diferente do valor atual
298
408
  if (imageSrc === newDataUri) {
299
409
  return;
300
410
  }
301
411
 
302
- setImageSrc(newDataUri || null);
303
- setLoadImage(!!newDataUri);
412
+ // Ignorar valores vazios APENAS nos primeiros 1000ms após montar
413
+ // Isso evita que o react-image-picker-editor zere a imagem antes de carregar
414
+ // Mas permite que o usuário remova a imagem depois
415
+ if (!newDataUri && timeSinceMount < 1000) {
416
+ return;
417
+ }
418
+
419
+ // Notificar que o processamento iniciou
420
+ onProcessingChange?.(true);
421
+
422
+ // Redimensionar se necessário
423
+ let processedDataUri = newDataUri;
424
+ if (newDataUri && (mergedConfig.maxWidth || mergedConfig.maxHeight || mergedConfig.maxSizeKb)) {
425
+ processedDataUri = await resizeImageIfNeeded(
426
+ newDataUri,
427
+ mergedConfig.maxWidth,
428
+ mergedConfig.maxHeight,
429
+ mergedConfig.maxSizeKb,
430
+ mergedConfig.compressInitial ?? 80
431
+ );
432
+ }
433
+
434
+ const newValue = processedDataUri || undefined;
435
+
436
+ setImageSrc(processedDataUri || null);
437
+ setLoadImage(!!processedDataUri);
304
438
 
305
439
  // Usar callback com debounce
306
440
  debouncedOnChange(newValue);
307
- }, [imageSrc, debouncedOnChange]);
441
+ }, [imageSrc, debouncedOnChange, onProcessingChange, mergedConfig.maxWidth, mergedConfig.maxHeight, mergedConfig.maxSizeKb, mergedConfig.compressInitial]);
308
442
 
309
443
  // Calcular tamanho e formato da imagem (apenas para data URIs)
310
444
  const sizeImage = useMemo(() => calculateImageSize(imageSrc), [imageSrc]);
@@ -319,8 +453,39 @@ export const ArchbaseImagePickerEditor = memo(
319
453
  backgroundColor: mergedConfig.imageBackgroundColor ?? (colorScheme === 'dark' ? theme.colors.dark[7] : theme.white),
320
454
  }), [mergedConfig.width, mergedConfig.imageBackgroundColor, colorScheme, theme]);
321
455
 
456
+ // Handler para prevenir submit de formulário e navegação acidental
457
+ const handleContainerClick = useCallback((e: React.MouseEvent) => {
458
+ const target = e.target as HTMLElement;
459
+
460
+ // Prevenir submit de formulário quando botões são clicados
461
+ // Os botões do react-image-picker-editor não têm type="button",
462
+ // então agem como type="submit" quando dentro de um form
463
+ const button = target.closest('button');
464
+ if (button && !button.getAttribute('type')) {
465
+ e.preventDefault();
466
+ }
467
+
468
+ // Prevenir navegação para data URIs em links
469
+ const anchor = target.closest('a');
470
+ if (anchor) {
471
+ const href = anchor.getAttribute('href');
472
+ if (href && href.startsWith('data:')) {
473
+ e.preventDefault();
474
+ // Se for o botão de download, fazer download manualmente
475
+ if (anchor.hasAttribute('download')) {
476
+ const link = document.createElement('a');
477
+ link.href = href;
478
+ link.download = anchor.getAttribute('download') || 'image';
479
+ document.body.appendChild(link);
480
+ link.click();
481
+ document.body.removeChild(link);
482
+ }
483
+ }
484
+ }
485
+ }, []);
486
+
322
487
  return (
323
- <div className="ArchbaseImagePickerEditor" style={containerStyle}>
488
+ <div className="ArchbaseImagePickerEditor" style={containerStyle} onClick={handleContainerClick}>
324
489
  {/* Componente react-image-picker-editor */}
325
490
  <ReactImagePickerEditor
326
491
  config={pickerConfig}
@@ -12,6 +12,12 @@ export interface ArchbaseImagePickerConf {
12
12
  showImageSize?: boolean;
13
13
  onChangeImage?: (image: string|undefined) => void;
14
14
  imageBackgroundColor?: string;
15
+ /** Largura máxima da imagem em pixels (redimensiona automaticamente se exceder) */
16
+ maxWidth?: number;
17
+ /** Altura máxima da imagem em pixels (redimensiona automaticamente se exceder) */
18
+ maxHeight?: number;
19
+ /** Tamanho máximo da imagem em KB (recomprime se exceder) */
20
+ maxSizeKb?: number;
15
21
  }
16
22
 
17
23
  export interface IState {