@mwater/visualization 5.6.0 → 5.6.1

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.
Files changed (92) hide show
  1. package/lib/ColorComponent.js +2 -2
  2. package/lib/TranslationsTabComponent.d.ts +34 -0
  3. package/lib/TranslationsTabComponent.js +256 -0
  4. package/lib/dashboards/DashboardComponent.js +1 -1
  5. package/lib/dashboards/ServerDashboardDataSource.d.ts +0 -1
  6. package/lib/dashboards/ServerDashboardDataSource.js +0 -15
  7. package/lib/dashboards/SettingsModalComponent.js +9 -233
  8. package/lib/datagrids/DatagridComponent.js +5 -0
  9. package/lib/datagrids/DatagridViewComponent.js +30 -4
  10. package/lib/maps/BufferLayer.d.ts +0 -13
  11. package/lib/maps/BufferLayer.js +12 -237
  12. package/lib/maps/BufferLayerDesignerComponent.d.ts +1 -1
  13. package/lib/maps/BufferLayerDesignerComponent.js +0 -5
  14. package/lib/maps/ChoroplethLayer.d.ts +1 -16
  15. package/lib/maps/ChoroplethLayer.js +13 -358
  16. package/lib/maps/ClusterLayer.d.ts +0 -9
  17. package/lib/maps/ClusterLayer.js +0 -250
  18. package/lib/maps/DirectMapDataSource.js +1 -38
  19. package/lib/maps/GridLayer.d.ts +0 -15
  20. package/lib/maps/GridLayer.js +0 -212
  21. package/lib/maps/Layer.d.ts +1 -26
  22. package/lib/maps/Layer.js +0 -13
  23. package/lib/maps/MapComponent.d.ts +19 -35
  24. package/lib/maps/MapComponent.js +135 -76
  25. package/lib/maps/MapControlComponent.d.ts +4 -5
  26. package/lib/maps/MapControlComponent.js +5 -12
  27. package/lib/maps/MapDesign.d.ts +8 -0
  28. package/lib/maps/MapDesignerComponent.d.ts +2 -0
  29. package/lib/maps/MapDesignerComponent.js +7 -2
  30. package/lib/maps/MapLayerDataSource.d.ts +0 -4
  31. package/lib/maps/MapLayerViewDesignerComponent.d.ts +3 -1
  32. package/lib/maps/MapLayerViewDesignerComponent.js +5 -1
  33. package/lib/maps/MapLayersDesignerComponent.d.ts +2 -0
  34. package/lib/maps/MapLayersDesignerComponent.js +2 -1
  35. package/lib/maps/MapTranslationsTab.d.ts +15 -0
  36. package/lib/maps/MapTranslationsTab.js +47 -0
  37. package/lib/maps/MapUtils.d.ts +11 -0
  38. package/lib/maps/MapUtils.js +47 -0
  39. package/lib/maps/MapViewComponent.d.ts +1 -1
  40. package/lib/maps/MapViewComponent.js +1 -8
  41. package/lib/maps/MarkersLayer.d.ts +1 -14
  42. package/lib/maps/MarkersLayer.js +71 -252
  43. package/lib/maps/MarkersLayerDesign.d.ts +4 -0
  44. package/lib/maps/MarkersLayerDesignerComponent.d.ts +20 -16
  45. package/lib/maps/MarkersLayerDesignerComponent.js +77 -23
  46. package/lib/maps/ServerMapDataSource.d.ts +0 -1
  47. package/lib/maps/ServerMapDataSource.js +0 -15
  48. package/lib/maps/SwitchableTileUrlLayer.d.ts +0 -2
  49. package/lib/maps/SwitchableTileUrlLayer.js +0 -9
  50. package/lib/maps/TileUrlLayer.d.ts +0 -1
  51. package/lib/maps/TileUrlLayer.js +0 -5
  52. package/lib/maps/VectorMapViewComponent.js +12 -1
  53. package/lib/maps/vectorMaps.d.ts +5 -6
  54. package/lib/maps/vectorMaps.js +13 -9
  55. package/lib/widgets/MapWidget.js +2 -1
  56. package/package.json +2 -2
  57. package/src/ColorComponent.tsx +2 -2
  58. package/src/TranslationsTabComponent.tsx +429 -0
  59. package/src/dashboards/DashboardComponent.tsx +1 -1
  60. package/src/dashboards/ServerDashboardDataSource.ts +0 -19
  61. package/src/dashboards/SettingsModalComponent.tsx +27 -383
  62. package/src/datagrids/DatagridComponent.tsx +6 -0
  63. package/src/datagrids/DatagridViewComponent.tsx +41 -5
  64. package/src/maps/BufferLayer.ts +16 -262
  65. package/src/maps/BufferLayerDesignerComponent.tsx +0 -6
  66. package/src/maps/ChoroplethLayer.ts +16 -393
  67. package/src/maps/ClusterLayer.ts +0 -274
  68. package/src/maps/DirectMapDataSource.ts +2 -49
  69. package/src/maps/GridLayer.ts +0 -224
  70. package/src/maps/Layer.ts +1 -35
  71. package/src/maps/MapComponent.tsx +448 -0
  72. package/src/maps/MapControlComponent.tsx +41 -0
  73. package/src/maps/MapDesign.ts +6 -0
  74. package/src/maps/MapDesignerComponent.tsx +18 -1
  75. package/src/maps/MapLayerDataSource.ts +0 -5
  76. package/src/maps/MapLayerViewDesignerComponent.ts +9 -2
  77. package/src/maps/MapLayersDesignerComponent.ts +4 -1
  78. package/src/maps/MapTranslationsTab.tsx +53 -0
  79. package/src/maps/MapUtils.ts +48 -0
  80. package/src/maps/MapViewComponent.tsx +2 -8
  81. package/src/maps/MarkersLayer.ts +79 -270
  82. package/src/maps/MarkersLayerDesign.ts +6 -0
  83. package/src/maps/MarkersLayerDesignerComponent.tsx +114 -38
  84. package/src/maps/ServerMapDataSource.ts +0 -19
  85. package/src/maps/SwitchableTileUrlLayer.tsx +0 -11
  86. package/src/maps/TileUrlLayer.tsx +0 -6
  87. package/src/maps/VectorMapViewComponent.tsx +13 -2
  88. package/src/maps/vectorMaps.tsx +12 -9
  89. package/src/widgets/MapWidget.tsx +2 -0
  90. package/src/maps/MapComponent.ts +0 -311
  91. package/src/maps/MapControlComponent.ts +0 -46
  92. package/src/maps/RasterMapViewComponent.ts +0 -345
@@ -1,20 +1,16 @@
1
1
  import _ from "lodash"
2
- import React, { useMemo, useRef, useState } from "react"
3
- import { languages } from "../languages"
2
+ import React, { useMemo } from "react"
4
3
  import * as ui from "@mwater/react-library/lib/bootstrap"
5
- import { default as ReactSelect } from "react-select"
6
4
  import * as DashboardUtils from "./DashboardUtils"
7
5
  import ActionCancelModalComponent from "@mwater/react-library/lib/ActionCancelModalComponent"
8
6
  import QuickfiltersDesignComponent from "../quickfilter/QuickfiltersDesignComponent"
9
7
  import FiltersDesignerComponent from "../FiltersDesignerComponent"
10
- import { DataSource, LocalizedString, Schema } from "@mwater/expressions"
8
+ import { DataSource, Schema } from "@mwater/expressions"
11
9
  import { DashboardDesign } from "./DashboardDesign"
12
10
  import { GlobalFiltersElementFactoryContext } from "../MWaterContextComponent"
13
11
  import produce from "immer"
14
12
  import TabbedComponent from "@mwater/react-library/lib/TabbedComponent"
15
- import FileSaver from "file-saver"
16
- import * as localizeUtils from "ez-localize/lib/utils"
17
- import { canAutoTranslate, translateStrings } from "../autotranslate"
13
+ import { TranslationsTabComponent } from "../TranslationsTabComponent"
18
14
 
19
15
  export interface SettingsModalComponentProps {
20
16
  onDesignChange: (design: DashboardDesign) => void
@@ -225,388 +221,36 @@ interface LanguageTabProps {
225
221
  schema: Schema
226
222
  }
227
223
 
224
+ /** Wrapper around TranslationsTabComponent for dashboard-specific usage */
228
225
  function LanguageTab({ design, onDesignChange, schema }: LanguageTabProps) {
229
- const fileInputRef = useRef<HTMLInputElement>(null)
230
-
231
- const locale = design.locale || "en"
232
-
233
- const localeOptions = useMemo(() => {
234
- return _.sortBy(
235
- _.map(languages, (language) => ({
236
- value: language.code,
237
- label: `${language.en} (${language.name})`
238
- })),
239
- 'label'
240
- )
241
- }, [languages])
242
-
243
- // Get available languages that aren't already selected
244
- const availableLocaleOptions = useMemo(() => {
245
- const selectedLocales = new Set([design.locale, ...(design.otherLocales || [])])
246
- return localeOptions.filter(opt => !selectedLocales.has(opt.value))
247
- }, [localeOptions, design.locale, design.otherLocales])
248
-
249
- const translatableStrings = useMemo(() => DashboardUtils.getTranslatableStringsFromDashboard(design, schema), [design, schema])
250
-
251
- // Calculate percentage of strings translated for each locale
252
- const translationPercentages = useMemo(() => {
253
- const percentages: { [locale: string]: number } = {}
254
- const totalStrings = translatableStrings.length
255
-
256
- for (const locale of design.otherLocales || []) {
257
- const translatedCount = translatableStrings.filter(str =>
258
- design.translations?.[locale]?.[str] != null
259
- ).length
260
-
261
- // Round down to nearest percent
262
- percentages[locale] = (totalStrings > 0) ? Math.floor((translatedCount / totalStrings) * 100) : 0
263
- }
264
-
265
- return percentages
266
- }, [design.translations, design.otherLocales, translatableStrings])
267
-
268
- const handleAddLocale = (locale: any) => {
269
- onDesignChange(produce(design, draft => {
270
- draft.otherLocales = [...(draft.otherLocales || []), locale.value]
271
- // Initialize empty translations object if needed
272
- if (!draft.translations) {
273
- draft.translations = {}
274
- }
275
- if (!draft.translations[locale.value]) {
276
- draft.translations[locale.value] = {}
277
- }
278
- }))
279
- }
280
-
281
- const handleRemoveLocale = (localeToRemove: string) => {
282
- onDesignChange(produce(design, draft => {
283
- draft.otherLocales = (draft.otherLocales || []).filter(locale => locale !== localeToRemove)
284
- // Remove translations for this locale
285
- if (draft.translations) {
286
- delete draft.translations[localeToRemove]
287
- }
288
- }))
289
- }
290
-
291
- // Convert dashboard translations to LocalizedString format
292
- const getLocalizedStrings = (): LocalizedString[] => {
293
- // For each string, create a localized string
294
- const localizedStrings: LocalizedString[] = []
295
- for (const str of translatableStrings) {
296
- const localizedString: LocalizedString = { _base: locale }
297
- localizedString[locale] = str
298
-
299
- // Only add translations for other locales if they exist
300
- for (const otherLocale of design.otherLocales || []) {
301
- if (design.translations?.[otherLocale]?.[str]) {
302
- localizedString[otherLocale] = design.translations?.[otherLocale]?.[str]
303
- }
304
- }
305
-
306
- localizedStrings.push(localizedString)
307
- }
308
-
309
- return localizedStrings
310
- }
311
-
312
- // Convert LocalizedString format back to dashboard translations
313
- const updateFromLocalizedStrings = (localizedStrings: LocalizedString[]) => {
314
- const newTranslations: { [locale: string]: { [key: string]: string } } = {}
315
-
316
- // Get all locales except base
317
- const locales = design.otherLocales || []
318
-
319
- // Initialize translations object
320
- for (const locale of locales) {
321
- newTranslations[locale] = {}
322
- }
323
-
324
- // Add all translations
325
- for (const str of localizedStrings) {
326
- for (const locale of locales) {
327
- if (str[locale]) {
328
- newTranslations[locale][str[str._base]] = str[locale]
329
- }
330
- }
331
- }
332
-
333
- return newTranslations
334
- }
335
-
336
- const handleDownload = () => {
337
- // Get strings in LocalizedString format
338
- const strings = getLocalizedStrings()
339
-
340
- // Create xlsx base64 using all locales (primary + other)
341
- const locales = [{ code: locale, name: locale }]
342
- .concat((design.otherLocales || []).map(code => ({ code, name: code })))
343
-
344
- const base64 = localizeUtils.exportXlsx(locales, strings)
345
-
346
- // Download
347
- FileSaver.saveAs(
348
- b64toBlob(base64, "application/octet-stream"),
349
- "Dashboard Translations.xlsx"
350
- )
351
- }
352
-
353
- const handleUpload = () => {
354
- fileInputRef.current?.click()
355
- }
356
-
357
- const handleUploadChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
358
- const reader = new FileReader()
226
+ const translatableStrings = useMemo(
227
+ () => DashboardUtils.getTranslatableStringsFromDashboard(design, schema),
228
+ [design, schema]
229
+ )
359
230
 
360
- reader.onload = (file) => {
361
- if (!file.target?.result) {
362
- return
231
+ return (
232
+ <TranslationsTabComponent
233
+ locale={design.locale || "en"}
234
+ otherLocales={design.otherLocales || []}
235
+ translations={design.translations || {}}
236
+ translatableStrings={translatableStrings}
237
+ onLocaleChange={(locale) =>
238
+ onDesignChange(produce(design, draft => {
239
+ draft.locale = locale
240
+ }))
363
241
  }
364
-
365
- const base64 = (file.target.result as string).split(",")[1]
366
-
367
- try {
368
- // Create locales array for import
369
- const locales = [{ code: locale, name: locale }]
370
- .concat((design.otherLocales || []).map(code => ({ code, name: code })))
371
-
372
- // Import updates
373
- const updates = localizeUtils.importXlsx(locales, base64)
374
-
375
- // If nothing localized
376
- if (updates.length === 0) {
377
- alert(T`No translation data found in file`)
378
- return
379
- }
380
-
381
- // Convert back to dashboard format and update
382
- const newTranslations = updateFromLocalizedStrings(updates)
242
+ onOtherLocalesChange={(otherLocales) =>
383
243
  onDesignChange(produce(design, draft => {
384
- draft.translations = newTranslations
244
+ draft.otherLocales = otherLocales
385
245
  }))
386
-
387
- alert(T`${updates.length} translations applied`)
388
- } catch (error) {
389
- console.error("Invalid xlsx file:", error)
390
- alert(T`Invalid xlsx file`)
391
246
  }
392
- }
393
-
394
- if (evt.target.files?.[0]) {
395
- reader.readAsDataURL(evt.target.files[0])
396
- }
397
- }
398
-
399
- return (
400
- <>
401
- <h4>{T`Base Language`}</h4>
402
- <div className="mb-2">
403
- {T`This is the language that the dashboard is written in.`}
404
- </div>
405
-
406
- <ReactSelect
407
- value={_.findWhere(localeOptions, { value: design.locale || "en" }) || null}
408
- options={localeOptions}
409
- onChange={(locale: any) =>
410
- onDesignChange(produce(design, draft => {
411
- draft.locale = locale.value
412
- }))
413
- }
414
- />
415
-
416
- <h4 className="mt-4">{T`Additional Languages`}</h4>
417
- <div className="mb-2">
418
- {T`Add languages that this dashboard will be translated into`}
419
- </div>
420
-
421
- {/* Show current additional languages */}
422
- <table>
423
- <tbody>
424
- {(design.otherLocales || []).map(locale => {
425
- const localeOption = _.findWhere(localeOptions, { value: locale })
426
- if (!localeOption) {
427
- return null
428
- }
429
-
430
- return (
431
- <tr key={locale}>
432
- <td style={{ paddingRight: 10 }}>{localeOption.label}</td>
433
- <td className={translationPercentages[locale] === 100 ? "text-success" : "text-warning"} style={{ textAlign: "right" }}>
434
- {T`${translationPercentages[locale]}% translated`}
435
- </td>
436
- <td>
437
- {translationPercentages[locale] < 100 && (
438
- <AutoTranslateLink
439
- locale={locale}
440
- baseLocale={design.locale || "en"}
441
- strings={translatableStrings}
442
- existingTranslations={design.translations?.[locale] || {}}
443
- onTranslationsChange={(newTranslations) => {
444
- onDesignChange(produce(design, draft => {
445
- if (!draft.translations) {
446
- draft.translations = {}
447
- }
448
- draft.translations[locale] = newTranslations
449
- }))
450
- }}
451
- />
452
- )}
453
- </td>
454
- <td>
455
- <button
456
- type="button"
457
- className="btn btn-sm btn-link"
458
- onClick={() => handleRemoveLocale(locale)}
459
- >
460
- <i className="fa fa-times" />
461
- </button>
462
- </td>
463
- </tr>
464
- )
465
- })}
466
- </tbody>
467
- </table>
468
-
469
- {/* Add new language dropdown */}
470
- {availableLocaleOptions.length > 0 && (
471
- <div className="mt-3">
472
- <ReactSelect
473
- value={null}
474
- options={availableLocaleOptions}
475
- onChange={handleAddLocale}
476
- placeholder={T`Add language...`}
477
- />
478
- </div>
479
- )}
480
-
481
- {/* Add translation management section if there are additional languages */}
482
- {(design.otherLocales || []).length > 0 && (
483
- <>
484
- <h4 className="mt-4">{T`Manage Translations`}</h4>
485
- <p>{T`Download and re-upload an Excel spreadsheet of text to translate:`}</p>
486
-
487
- <div>
488
- <button
489
- type="button"
490
- className="btn btn-secondary"
491
- onClick={handleDownload}
492
- >
493
- <i className="fas fa-download me-2"/>{T`Download XLSX`}
494
- </button>
495
- </div>
496
- <div className="text-muted mt-2">
497
- {T`This creates a spreadsheet that can be sent to a translator. Please do not change the first column or first row of the spreadsheet.`}
498
- </div>
499
-
500
- <br />
501
- <p>
502
- {T`Once translation is complete, upload the file back using the button below:`}
503
- </p>
504
- <div>
505
- <button
506
- type="button"
507
- className="btn btn-secondary"
508
- onClick={handleUpload}
509
- >
510
- <i className="fas fa-upload me-2"/>{T`Upload Translated XLSX`}
511
- </button>
512
- </div>
513
- <input
514
- type="file"
515
- ref={fileInputRef}
516
- style={{ display: "none" }}
517
- onChange={handleUploadChange}
518
- accept=".xlsx"
519
- />
520
- </>
521
- )}
522
- </>
523
- )
524
- }
525
-
526
- /** Helper function for base64 to blob conversion */
527
- function b64toBlob(b64Data: string, contentType: string = "", sliceSize: number = 512) {
528
- const byteCharacters = atob(b64Data)
529
- const byteArrays = []
530
-
531
- for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
532
- const slice = byteCharacters.slice(offset, offset + sliceSize)
533
- const byteNumbers = new Array(slice.length)
534
-
535
- for (let i = 0; i < slice.length; i++) {
536
- byteNumbers[i] = slice.charCodeAt(i)
537
- }
538
-
539
- const byteArray = new Uint8Array(byteNumbers)
540
- byteArrays.push(byteArray)
541
- }
542
-
543
- return new Blob(byteArrays, { type: contentType })
544
- }
545
-
546
- interface AutoTranslateLinkProps {
547
- locale: string
548
- baseLocale: string
549
- strings: string[]
550
- existingTranslations: { [key: string]: string }
551
- onTranslationsChange: (newTranslations: { [key: string]: string }) => void
552
- }
553
-
554
- function AutoTranslateLink(props: AutoTranslateLinkProps) {
555
- const { locale, baseLocale, strings, existingTranslations, onTranslationsChange } = props
556
- const [isTranslating, setIsTranslating] = useState(false)
557
-
558
- const untranslatedStrings = useMemo(() => {
559
- return strings.filter(str => !existingTranslations[str])
560
- }, [strings, existingTranslations])
561
-
562
- const handleClick = async () => {
563
- if (isTranslating) {
564
- return
565
- }
566
-
567
- setIsTranslating(true)
568
- try {
569
- const translatedStrings = await translateStrings(untranslatedStrings, baseLocale, locale)
570
-
571
- const newTranslations = { ...existingTranslations }
572
- for (let i = 0; i < untranslatedStrings.length; i++) {
573
- newTranslations[untranslatedStrings[i]] = translatedStrings[i]
247
+ onTranslationsChange={(translations) =>
248
+ onDesignChange(produce(design, draft => {
249
+ draft.translations = translations
250
+ }))
574
251
  }
575
-
576
- onTranslationsChange(newTranslations)
577
- } catch (error) {
578
- console.error("Error translating strings:", error)
579
- alert(T`Error translating strings`)
580
- } finally {
581
- setIsTranslating(false)
582
- }
583
- }
584
-
585
- if (untranslatedStrings.length === 0) {
586
- return null
587
- }
588
-
589
- if (!canAutoTranslate(locale)) {
590
- return null
591
- }
592
-
593
- return (
594
- <button
595
- type="button"
596
- className="btn btn-sm btn-link"
597
- onClick={handleClick}
598
- disabled={isTranslating}
599
- style={{ marginLeft: 5, padding: "0 5px" }}
600
- >
601
- {isTranslating ? (
602
- <span>
603
- <i className="fa fa-spinner fa-spin" />
604
- {" "}
605
- {T`Translating...`}
606
- </span>
607
- ) : (
608
- T`Autotranslate`
609
- )}
610
- </button>
252
+ downloadFilename="Dashboard Translations.xlsx"
253
+ baseLanguageDescription={T`This is the language that the dashboard is written in.`}
254
+ />
611
255
  )
612
256
  }
@@ -126,6 +126,12 @@ export default forwardRef<DatagridComponentRef, DatagridComponentProps>(function
126
126
  }
127
127
  let activeFilters = filters || []
128
128
 
129
+ // Clean before counting
130
+ const cleanedDesign = new DatagridUtils(schema).cleanDesign(design)
131
+ if (new DatagridUtils(schema).validateDesign(cleanedDesign)) {
132
+ return
133
+ }
134
+
129
135
  // Compile quickfilters
130
136
  activeFilters = activeFilters.concat(getQuickfilterFilters())
131
137
  datagridDataSource.countRows(design,
@@ -82,6 +82,11 @@ const DatagridViewComponent = forwardRef<DatagridViewComponentRef, DatagridViewC
82
82
  const [rows, setRows] = useState<any[]>([])
83
83
  const [entirelyLoaded, setEntirelyLoaded] = useState(false)
84
84
  const loadStateRef = useRef<LoadState | null>(null)
85
+
86
+ // Track which design key the current rows were loaded with to avoid rendering stale data
87
+ // with a mismatched column structure (e.g. after column reorder)
88
+ const [loadedDesignKey, setLoadedDesignKey] = useState<string | null>(null)
89
+ const currentDesignKey = getDatagridReloadKey(props.design)
85
90
 
86
91
  // Get row header width based on selection and showRowNumbers
87
92
  const rowHeaderWidth = props.selectedRows
@@ -154,7 +159,7 @@ const DatagridViewComponent = forwardRef<DatagridViewComponentRef, DatagridViewC
154
159
 
155
160
  /** Render row headers with optional selection checkboxes */
156
161
  const renderRowHeader = useCallback((options: RenderRowHeaderProps) => {
157
- const rowHeaderStyle = {
162
+ const rowHeaderStyle: React.CSSProperties = {
158
163
  width: options.width,
159
164
  height: options.height,
160
165
  display: "flex",
@@ -165,9 +170,20 @@ const DatagridViewComponent = forwardRef<DatagridViewComponentRef, DatagridViewC
165
170
  padding: "0 8px"
166
171
  }
167
172
 
168
- const rowId = options.row < rows.length ? rows[options.row].id : null
173
+ // If design key doesn't match, rows are stale - show loading state
174
+ if (loadedDesignKey !== currentDesignKey || options.row >= rows.length) {
175
+ return (
176
+ <div style={rowHeaderStyle}>
177
+ {props.design.showRowNumbers ? (
178
+ <div style={{ flex: 1, textAlign: "right" }}>{options.row + 1}</div>
179
+ ) : null}
180
+ </div>
181
+ )
182
+ }
183
+
184
+ const rowId = rows[options.row].id
169
185
  const isSelected = rowId != null && props.selectedRows ? props.selectedRows.isSelected(rowId) : false
170
- const isSubtable = options.row < rows.length ? rows[options.row].subtable >= 0 : false
186
+ const isSubtable = rows[options.row].subtable >= 0
171
187
 
172
188
  return (
173
189
  <div style={rowHeaderStyle}>
@@ -185,7 +201,7 @@ const DatagridViewComponent = forwardRef<DatagridViewComponentRef, DatagridViewC
185
201
  ) : null}
186
202
  </div>
187
203
  )
188
- }, [rows, props.selectedRows, props.onSelectedRowsChange, props.design.showRowNumbers, handleRowSelect])
204
+ }, [rows, props.selectedRows, props.onSelectedRowsChange, props.design.showRowNumbers, handleRowSelect, loadedDesignKey, currentDesignKey])
189
205
 
190
206
  const loadMoreRows = useStableCallback((callback?: () => void) => {
191
207
  const loadState: LoadState = {
@@ -217,6 +233,7 @@ const DatagridViewComponent = forwardRef<DatagridViewComponentRef, DatagridViewC
217
233
  loadStateRef.current = null
218
234
  setRows(prev => [...prev, ...newRows])
219
235
  setEntirelyLoaded(newRows.length < (props.pageSize || 100))
236
+ setLoadedDesignKey(getDatagridReloadKey(loadState.design))
220
237
  callback?.()
221
238
  }
222
239
  }
@@ -226,6 +243,7 @@ const DatagridViewComponent = forwardRef<DatagridViewComponentRef, DatagridViewC
226
243
  function reload() {
227
244
  setRows([])
228
245
  setEntirelyLoaded(false)
246
+ setLoadedDesignKey(null)
229
247
  }
230
248
 
231
249
  // Reset rows when design, filters or refreshKey changes
@@ -341,6 +359,11 @@ const DatagridViewComponent = forwardRef<DatagridViewComponentRef, DatagridViewC
341
359
  })
342
360
 
343
361
  const handleCopy = useStableCallback((rowIndex: number, colIndex: number) => {
362
+ // If design key doesn't match, rows are stale - don't copy
363
+ if (loadedDesignKey !== currentDesignKey || rowIndex >= rows.length) {
364
+ return
365
+ }
366
+
344
367
  const row = rows[rowIndex]
345
368
  const col = props.design.columns[colIndex]
346
369
  const value = row[`c${colIndex}`]
@@ -369,6 +392,13 @@ const DatagridViewComponent = forwardRef<DatagridViewComponentRef, DatagridViewC
369
392
  })
370
393
 
371
394
  const renderCell = useStableCallback((options: RenderCellProps) => {
395
+ // If rows were loaded with a different design key, show loading state to avoid
396
+ // rendering stale data with mismatched column structure (e.g. after column reorder)
397
+ if (loadedDesignKey !== currentDesignKey) {
398
+ _.defer(() => loadMoreRows())
399
+ return <div style={{ padding: 8 }}><i className="fa fa-spinner fa-spin text-muted" /></div>
400
+ }
401
+
372
402
  if (options.row >= rows.length) {
373
403
  _.defer(() => loadMoreRows())
374
404
  return <div style={{ padding: 8 }}><i className="fa fa-spinner fa-spin text-muted" /></div>
@@ -446,7 +476,8 @@ const DatagridViewComponent = forwardRef<DatagridViewComponentRef, DatagridViewC
446
476
  })
447
477
 
448
478
  const renderCellEditor = useStableCallback((options: RenderCellEditorProps) => {
449
- if (options.row >= rows.length) {
479
+ // If design key doesn't match, rows are stale - don't render editor
480
+ if (loadedDesignKey !== currentDesignKey || options.row >= rows.length) {
450
481
  return <div />
451
482
  }
452
483
 
@@ -478,6 +509,11 @@ const DatagridViewComponent = forwardRef<DatagridViewComponentRef, DatagridViewC
478
509
  return false
479
510
  }
480
511
 
512
+ // If design key doesn't match, rows are stale - don't allow edit
513
+ if (loadedDesignKey !== currentDesignKey || options.row >= rows.length) {
514
+ return false
515
+ }
516
+
481
517
  const rowId = rows[options.row].id
482
518
  if (rowId == null) {
483
519
  return false