@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.
- package/lib/ColorComponent.js +2 -2
- package/lib/TranslationsTabComponent.d.ts +34 -0
- package/lib/TranslationsTabComponent.js +256 -0
- package/lib/dashboards/DashboardComponent.js +1 -1
- package/lib/dashboards/ServerDashboardDataSource.d.ts +0 -1
- package/lib/dashboards/ServerDashboardDataSource.js +0 -15
- package/lib/dashboards/SettingsModalComponent.js +9 -233
- package/lib/datagrids/DatagridComponent.js +5 -0
- package/lib/datagrids/DatagridViewComponent.js +30 -4
- package/lib/maps/BufferLayer.d.ts +0 -13
- package/lib/maps/BufferLayer.js +12 -237
- package/lib/maps/BufferLayerDesignerComponent.d.ts +1 -1
- package/lib/maps/BufferLayerDesignerComponent.js +0 -5
- package/lib/maps/ChoroplethLayer.d.ts +1 -16
- package/lib/maps/ChoroplethLayer.js +13 -358
- package/lib/maps/ClusterLayer.d.ts +0 -9
- package/lib/maps/ClusterLayer.js +0 -250
- package/lib/maps/DirectMapDataSource.js +1 -38
- package/lib/maps/GridLayer.d.ts +0 -15
- package/lib/maps/GridLayer.js +0 -212
- package/lib/maps/Layer.d.ts +1 -26
- package/lib/maps/Layer.js +0 -13
- package/lib/maps/MapComponent.d.ts +19 -35
- package/lib/maps/MapComponent.js +135 -76
- package/lib/maps/MapControlComponent.d.ts +4 -5
- package/lib/maps/MapControlComponent.js +5 -12
- package/lib/maps/MapDesign.d.ts +8 -0
- package/lib/maps/MapDesignerComponent.d.ts +2 -0
- package/lib/maps/MapDesignerComponent.js +7 -2
- package/lib/maps/MapLayerDataSource.d.ts +0 -4
- package/lib/maps/MapLayerViewDesignerComponent.d.ts +3 -1
- package/lib/maps/MapLayerViewDesignerComponent.js +5 -1
- package/lib/maps/MapLayersDesignerComponent.d.ts +2 -0
- package/lib/maps/MapLayersDesignerComponent.js +2 -1
- package/lib/maps/MapTranslationsTab.d.ts +15 -0
- package/lib/maps/MapTranslationsTab.js +47 -0
- package/lib/maps/MapUtils.d.ts +11 -0
- package/lib/maps/MapUtils.js +47 -0
- package/lib/maps/MapViewComponent.d.ts +1 -1
- package/lib/maps/MapViewComponent.js +1 -8
- package/lib/maps/MarkersLayer.d.ts +1 -14
- package/lib/maps/MarkersLayer.js +71 -252
- package/lib/maps/MarkersLayerDesign.d.ts +4 -0
- package/lib/maps/MarkersLayerDesignerComponent.d.ts +20 -16
- package/lib/maps/MarkersLayerDesignerComponent.js +77 -23
- package/lib/maps/ServerMapDataSource.d.ts +0 -1
- package/lib/maps/ServerMapDataSource.js +0 -15
- package/lib/maps/SwitchableTileUrlLayer.d.ts +0 -2
- package/lib/maps/SwitchableTileUrlLayer.js +0 -9
- package/lib/maps/TileUrlLayer.d.ts +0 -1
- package/lib/maps/TileUrlLayer.js +0 -5
- package/lib/maps/VectorMapViewComponent.js +12 -1
- package/lib/maps/vectorMaps.d.ts +5 -6
- package/lib/maps/vectorMaps.js +13 -9
- package/lib/widgets/MapWidget.js +2 -1
- package/package.json +2 -2
- package/src/ColorComponent.tsx +2 -2
- package/src/TranslationsTabComponent.tsx +429 -0
- package/src/dashboards/DashboardComponent.tsx +1 -1
- package/src/dashboards/ServerDashboardDataSource.ts +0 -19
- package/src/dashboards/SettingsModalComponent.tsx +27 -383
- package/src/datagrids/DatagridComponent.tsx +6 -0
- package/src/datagrids/DatagridViewComponent.tsx +41 -5
- package/src/maps/BufferLayer.ts +16 -262
- package/src/maps/BufferLayerDesignerComponent.tsx +0 -6
- package/src/maps/ChoroplethLayer.ts +16 -393
- package/src/maps/ClusterLayer.ts +0 -274
- package/src/maps/DirectMapDataSource.ts +2 -49
- package/src/maps/GridLayer.ts +0 -224
- package/src/maps/Layer.ts +1 -35
- package/src/maps/MapComponent.tsx +448 -0
- package/src/maps/MapControlComponent.tsx +41 -0
- package/src/maps/MapDesign.ts +6 -0
- package/src/maps/MapDesignerComponent.tsx +18 -1
- package/src/maps/MapLayerDataSource.ts +0 -5
- package/src/maps/MapLayerViewDesignerComponent.ts +9 -2
- package/src/maps/MapLayersDesignerComponent.ts +4 -1
- package/src/maps/MapTranslationsTab.tsx +53 -0
- package/src/maps/MapUtils.ts +48 -0
- package/src/maps/MapViewComponent.tsx +2 -8
- package/src/maps/MarkersLayer.ts +79 -270
- package/src/maps/MarkersLayerDesign.ts +6 -0
- package/src/maps/MarkersLayerDesignerComponent.tsx +114 -38
- package/src/maps/ServerMapDataSource.ts +0 -19
- package/src/maps/SwitchableTileUrlLayer.tsx +0 -11
- package/src/maps/TileUrlLayer.tsx +0 -6
- package/src/maps/VectorMapViewComponent.tsx +13 -2
- package/src/maps/vectorMaps.tsx +12 -9
- package/src/widgets/MapWidget.tsx +2 -0
- package/src/maps/MapComponent.ts +0 -311
- package/src/maps/MapControlComponent.ts +0 -46
- package/src/maps/RasterMapViewComponent.ts +0 -345
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
import _ from "lodash"
|
|
2
|
-
import React, { useMemo
|
|
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,
|
|
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
|
|
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
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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.
|
|
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
|
-
|
|
395
|
-
|
|
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
|
-
|
|
577
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|