@mwater/visualization 5.5.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 (275) hide show
  1. package/lib/ColorComponent.js +2 -2
  2. package/lib/MWaterContextComponent.d.ts +1 -1
  3. package/lib/MWaterGlobalFiltersComponent.d.ts +2 -2
  4. package/lib/MWaterGlobalFiltersComponent.js +11 -20
  5. package/lib/MWaterLoaderComponent.d.ts +4 -13
  6. package/lib/MWaterLoaderComponent.js +2 -11
  7. package/lib/TranslationsTabComponent.d.ts +34 -0
  8. package/lib/TranslationsTabComponent.js +256 -0
  9. package/lib/UndoStack.d.ts +2 -1
  10. package/lib/UndoStack.js +12 -6
  11. package/lib/dashboards/DashboardComponent.js +6 -5
  12. package/lib/dashboards/DashboardDesign.d.ts +1 -1
  13. package/lib/dashboards/ServerDashboardDataSource.d.ts +0 -1
  14. package/lib/dashboards/ServerDashboardDataSource.js +0 -25
  15. package/lib/dashboards/SettingsModalComponent.js +9 -233
  16. package/lib/datagrids/DatagridComponent.js +27 -2
  17. package/lib/datagrids/DatagridDesignerComponent.d.ts +2 -3
  18. package/lib/datagrids/DatagridDesignerComponent.js +108 -120
  19. package/lib/datagrids/DatagridViewComponent.js +33 -6
  20. package/lib/datagrids/OrderBysDesignerComponent.d.ts +7 -7
  21. package/lib/datagrids/OrderBysDesignerComponent.js +19 -28
  22. package/lib/index.css +45 -2
  23. package/lib/index.d.ts +5 -5
  24. package/lib/index.js +2 -3
  25. package/lib/layouts/blocks/BlocksDisplayComponent.d.ts +8 -1
  26. package/lib/layouts/blocks/BlocksDisplayComponent.js +46 -4
  27. package/lib/maps/BufferLayer.d.ts +0 -13
  28. package/lib/maps/BufferLayer.js +24 -237
  29. package/lib/maps/BufferLayerDesign.d.ts +1 -1
  30. package/lib/maps/BufferLayerDesignerComponent.d.ts +1 -1
  31. package/lib/maps/BufferLayerDesignerComponent.js +2 -7
  32. package/lib/maps/ChoroplethLayer.d.ts +1 -16
  33. package/lib/maps/ChoroplethLayer.js +25 -358
  34. package/lib/maps/ChoroplethLayerDesign.d.ts +5 -2
  35. package/lib/maps/ChoroplethLayerDesigner.d.ts +10 -32
  36. package/lib/maps/ChoroplethLayerDesigner.js +58 -89
  37. package/lib/maps/ClusterLayer.d.ts +0 -9
  38. package/lib/maps/ClusterLayer.js +0 -250
  39. package/lib/maps/DirectMapDataSource.js +1 -48
  40. package/lib/maps/EditHoverOver.d.ts +4 -3
  41. package/lib/maps/EditHoverOver.js +3 -3
  42. package/lib/maps/GridLayer.d.ts +0 -15
  43. package/lib/maps/GridLayer.js +0 -212
  44. package/lib/maps/HoverContent.js +1 -1
  45. package/lib/maps/Layer.d.ts +1 -26
  46. package/lib/maps/Layer.js +0 -13
  47. package/lib/maps/LeafletMapComponent.js +10 -19
  48. package/lib/maps/MapComponent.d.ts +19 -35
  49. package/lib/maps/MapComponent.js +135 -77
  50. package/lib/maps/MapControlComponent.d.ts +4 -5
  51. package/lib/maps/MapControlComponent.js +5 -12
  52. package/lib/maps/MapDesign.d.ts +8 -0
  53. package/lib/maps/MapDesignerComponent.d.ts +2 -0
  54. package/lib/maps/MapDesignerComponent.js +7 -2
  55. package/lib/maps/MapLayerDataSource.d.ts +0 -4
  56. package/lib/maps/MapLayerViewDesignerComponent.d.ts +3 -1
  57. package/lib/maps/MapLayerViewDesignerComponent.js +5 -1
  58. package/lib/maps/MapLayersDesignerComponent.d.ts +2 -0
  59. package/lib/maps/MapLayersDesignerComponent.js +2 -1
  60. package/lib/maps/MapTranslationsTab.d.ts +15 -0
  61. package/lib/maps/MapTranslationsTab.js +47 -0
  62. package/lib/maps/MapUtils.d.ts +11 -0
  63. package/lib/maps/MapUtils.js +57 -1
  64. package/lib/maps/MapViewComponent.d.ts +1 -1
  65. package/lib/maps/MapViewComponent.js +1 -8
  66. package/lib/maps/MarkersLayer.d.ts +1 -14
  67. package/lib/maps/MarkersLayer.js +89 -254
  68. package/lib/maps/MarkersLayerDesign.d.ts +5 -1
  69. package/lib/maps/MarkersLayerDesignerComponent.d.ts +32 -57
  70. package/lib/maps/MarkersLayerDesignerComponent.js +158 -134
  71. package/lib/maps/ServerMapDataSource.d.ts +0 -1
  72. package/lib/maps/ServerMapDataSource.js +0 -25
  73. package/lib/maps/SwitchableTileUrlLayer.d.ts +0 -2
  74. package/lib/maps/SwitchableTileUrlLayer.js +0 -9
  75. package/lib/maps/TileUrlLayer.d.ts +0 -1
  76. package/lib/maps/TileUrlLayer.js +0 -5
  77. package/lib/maps/VectorMapViewComponent.js +13 -10
  78. package/lib/maps/symbols/font-awesome/asterisk.png +0 -0
  79. package/lib/maps/symbols/font-awesome/ban.png +0 -0
  80. package/lib/maps/symbols/font-awesome/beer.png +0 -0
  81. package/lib/maps/symbols/font-awesome/bell.png +0 -0
  82. package/lib/maps/symbols/font-awesome/bolt.png +0 -0
  83. package/lib/maps/symbols/font-awesome/building.png +0 -0
  84. package/lib/maps/symbols/font-awesome/bullseye.png +0 -0
  85. package/lib/maps/symbols/font-awesome/bus.png +0 -0
  86. package/lib/maps/symbols/font-awesome/caret-up.png +0 -0
  87. package/lib/maps/symbols/font-awesome/certificate.png +0 -0
  88. package/lib/maps/symbols/font-awesome/check-circle.png +0 -0
  89. package/lib/maps/symbols/font-awesome/check.png +0 -0
  90. package/lib/maps/symbols/font-awesome/chevron-circle-down.png +0 -0
  91. package/lib/maps/symbols/font-awesome/chevron-circle-up.png +0 -0
  92. package/lib/maps/symbols/font-awesome/cloud-rain.png +0 -0
  93. package/lib/maps/symbols/font-awesome/cloud.png +0 -0
  94. package/lib/maps/symbols/font-awesome/comment.png +0 -0
  95. package/lib/maps/symbols/font-awesome/crosshairs.png +0 -0
  96. package/lib/maps/symbols/font-awesome/dot-circle-o.png +0 -0
  97. package/lib/maps/symbols/font-awesome/exclamation-circle.png +0 -0
  98. package/lib/maps/symbols/font-awesome/exclamation-triangle.png +0 -0
  99. package/lib/maps/symbols/font-awesome/female.png +0 -0
  100. package/lib/maps/symbols/font-awesome/file.png +0 -0
  101. package/lib/maps/symbols/font-awesome/flag.png +0 -0
  102. package/lib/maps/symbols/font-awesome/flask.png +0 -0
  103. package/lib/maps/symbols/font-awesome/h-square.png +0 -0
  104. package/lib/maps/symbols/font-awesome/home.png +0 -0
  105. package/lib/maps/symbols/font-awesome/info-circle.png +0 -0
  106. package/lib/maps/symbols/font-awesome/male.png +0 -0
  107. package/lib/maps/symbols/font-awesome/medkit.png +0 -0
  108. package/lib/maps/symbols/font-awesome/mobile.png +0 -0
  109. package/lib/maps/symbols/font-awesome/plus-circle.png +0 -0
  110. package/lib/maps/symbols/font-awesome/plus-square.png +0 -0
  111. package/lib/maps/symbols/font-awesome/plus.png +0 -0
  112. package/lib/maps/symbols/font-awesome/square.png +0 -0
  113. package/lib/maps/symbols/font-awesome/star.png +0 -0
  114. package/lib/maps/symbols/font-awesome/thumbs-down.png +0 -0
  115. package/lib/maps/symbols/font-awesome/thumbs-up.png +0 -0
  116. package/lib/maps/symbols/font-awesome/ticket.png +0 -0
  117. package/lib/maps/symbols/font-awesome/times-circle.png +0 -0
  118. package/lib/maps/symbols/font-awesome/times.png +0 -0
  119. package/lib/maps/symbols/font-awesome/tint.png +0 -0
  120. package/lib/maps/symbols/font-awesome/tree.png +0 -0
  121. package/lib/maps/symbols/font-awesome/university.png +0 -0
  122. package/lib/maps/symbols/font-awesome/usd.png +0 -0
  123. package/lib/maps/symbols/font-awesome/user.png +0 -0
  124. package/lib/maps/symbols/font-awesome/users.png +0 -0
  125. package/lib/maps/symbols/font-awesome/wheelchair.png +0 -0
  126. package/lib/maps/symbols/sdf-ize.sh +93 -0
  127. package/lib/maps/vectorMaps.d.ts +6 -6
  128. package/lib/maps/vectorMaps.js +33 -45
  129. package/lib/mwater_table_selection/IndicatorsListComponent.d.ts +4 -2
  130. package/lib/mwater_table_selection/IndicatorsListComponent.js +103 -34
  131. package/lib/mwater_table_selection/MWaterCalculatedDataSourcesListComponent.d.ts +18 -0
  132. package/lib/mwater_table_selection/MWaterCalculatedDataSourcesListComponent.js +80 -0
  133. package/lib/mwater_table_selection/MWaterCompleteTableSelectComponent.d.ts +26 -0
  134. package/lib/mwater_table_selection/MWaterCompleteTableSelectComponent.js +237 -51
  135. package/lib/mwater_table_selection/MWaterTableSelectComponent.d.ts +2 -2
  136. package/lib/mwater_table_selection/MWaterTableSelectComponent.js +9 -4
  137. package/lib/mwater_table_selection/MWaterWorkflowsSelectComponent.d.ts +19 -0
  138. package/lib/mwater_table_selection/MWaterWorkflowsSelectComponent.js +111 -0
  139. package/lib/quickfilter/QuickfiltersComponent.d.ts +3 -102
  140. package/lib/quickfilter/QuickfiltersComponent.js +53 -110
  141. package/lib/quickfilter/TextLiteralComponent.d.ts +23 -47
  142. package/lib/quickfilter/TextLiteralComponent.js +85 -82
  143. package/lib/widgets/MapWidget.js +6 -3
  144. package/lib/widgets/text/ExprItemEditorComponent.d.ts +3 -8
  145. package/lib/widgets/text/ExprItemEditorComponent.js +36 -33
  146. package/lib/widgets/text/ExprUpdateModalComponent.d.ts +1 -0
  147. package/package.json +3 -4
  148. package/src/ColorComponent.tsx +2 -2
  149. package/src/MWaterContextComponent.tsx +1 -1
  150. package/src/{MWaterGlobalFiltersComponent.ts → MWaterGlobalFiltersComponent.tsx} +32 -33
  151. package/src/{MWaterLoaderComponent.ts → MWaterLoaderComponent.tsx} +17 -18
  152. package/src/TranslationsTabComponent.tsx +429 -0
  153. package/src/UndoStack.ts +14 -6
  154. package/src/dashboards/DashboardComponent.tsx +6 -5
  155. package/src/dashboards/DashboardDesign.ts +1 -1
  156. package/src/dashboards/ServerDashboardDataSource.ts +0 -31
  157. package/src/dashboards/SettingsModalComponent.tsx +27 -383
  158. package/src/datagrids/DatagridComponent.tsx +36 -2
  159. package/src/datagrids/DatagridDesignerComponent.tsx +241 -229
  160. package/src/datagrids/DatagridViewComponent.tsx +44 -7
  161. package/src/datagrids/OrderBysDesignerComponent.tsx +61 -70
  162. package/src/index.css +45 -2
  163. package/src/index.ts +5 -11
  164. package/src/layouts/blocks/BlocksDisplayComponent.tsx +60 -5
  165. package/src/maps/BufferLayer.ts +30 -263
  166. package/src/maps/BufferLayerDesign.ts +1 -1
  167. package/src/maps/BufferLayerDesignerComponent.tsx +2 -7
  168. package/src/maps/ChoroplethLayer.ts +30 -394
  169. package/src/maps/ChoroplethLayerDesign.ts +5 -2
  170. package/src/maps/ChoroplethLayerDesigner.tsx +169 -165
  171. package/src/maps/ClusterLayer.ts +0 -274
  172. package/src/maps/DirectMapDataSource.ts +2 -61
  173. package/src/maps/EditHoverOver.tsx +9 -5
  174. package/src/maps/GridLayer.ts +0 -224
  175. package/src/maps/HoverContent.tsx +1 -1
  176. package/src/maps/Layer.ts +1 -35
  177. package/src/maps/LeafletMapComponent.tsx +10 -19
  178. package/src/maps/MapComponent.tsx +448 -0
  179. package/src/maps/MapControlComponent.tsx +41 -0
  180. package/src/maps/MapDesign.ts +6 -0
  181. package/src/maps/MapDesignerComponent.tsx +18 -1
  182. package/src/maps/MapLayerDataSource.ts +0 -5
  183. package/src/maps/MapLayerViewDesignerComponent.ts +9 -2
  184. package/src/maps/MapLayersDesignerComponent.ts +4 -1
  185. package/src/maps/MapTranslationsTab.tsx +53 -0
  186. package/src/maps/MapUtils.ts +61 -1
  187. package/src/maps/MapViewComponent.tsx +2 -8
  188. package/src/maps/MarkersLayer.ts +101 -275
  189. package/src/maps/MarkersLayerDesign.ts +7 -1
  190. package/src/maps/MarkersLayerDesignerComponent.tsx +436 -0
  191. package/src/maps/ServerMapDataSource.ts +0 -31
  192. package/src/maps/SwitchableTileUrlLayer.tsx +0 -11
  193. package/src/maps/TileUrlLayer.tsx +0 -6
  194. package/src/maps/VectorMapViewComponent.tsx +15 -15
  195. package/src/maps/symbols/font-awesome/asterisk.png +0 -0
  196. package/src/maps/symbols/font-awesome/ban.png +0 -0
  197. package/src/maps/symbols/font-awesome/beer.png +0 -0
  198. package/src/maps/symbols/font-awesome/bell.png +0 -0
  199. package/src/maps/symbols/font-awesome/bolt.png +0 -0
  200. package/src/maps/symbols/font-awesome/building.png +0 -0
  201. package/src/maps/symbols/font-awesome/bullseye.png +0 -0
  202. package/src/maps/symbols/font-awesome/bus.png +0 -0
  203. package/src/maps/symbols/font-awesome/caret-up.png +0 -0
  204. package/src/maps/symbols/font-awesome/certificate.png +0 -0
  205. package/src/maps/symbols/font-awesome/check-circle.png +0 -0
  206. package/src/maps/symbols/font-awesome/check.png +0 -0
  207. package/src/maps/symbols/font-awesome/chevron-circle-down.png +0 -0
  208. package/src/maps/symbols/font-awesome/chevron-circle-up.png +0 -0
  209. package/src/maps/symbols/font-awesome/cloud-rain.png +0 -0
  210. package/src/maps/symbols/font-awesome/cloud.png +0 -0
  211. package/src/maps/symbols/font-awesome/comment.png +0 -0
  212. package/src/maps/symbols/font-awesome/crosshairs.png +0 -0
  213. package/src/maps/symbols/font-awesome/dot-circle-o.png +0 -0
  214. package/src/maps/symbols/font-awesome/exclamation-circle.png +0 -0
  215. package/src/maps/symbols/font-awesome/exclamation-triangle.png +0 -0
  216. package/src/maps/symbols/font-awesome/female.png +0 -0
  217. package/src/maps/symbols/font-awesome/file.png +0 -0
  218. package/src/maps/symbols/font-awesome/flag.png +0 -0
  219. package/src/maps/symbols/font-awesome/flask.png +0 -0
  220. package/src/maps/symbols/font-awesome/h-square.png +0 -0
  221. package/src/maps/symbols/font-awesome/home.png +0 -0
  222. package/src/maps/symbols/font-awesome/info-circle.png +0 -0
  223. package/src/maps/symbols/font-awesome/male.png +0 -0
  224. package/src/maps/symbols/font-awesome/medkit.png +0 -0
  225. package/src/maps/symbols/font-awesome/mobile.png +0 -0
  226. package/src/maps/symbols/font-awesome/plus-circle.png +0 -0
  227. package/src/maps/symbols/font-awesome/plus-square.png +0 -0
  228. package/src/maps/symbols/font-awesome/plus.png +0 -0
  229. package/src/maps/symbols/font-awesome/square.png +0 -0
  230. package/src/maps/symbols/font-awesome/star.png +0 -0
  231. package/src/maps/symbols/font-awesome/thumbs-down.png +0 -0
  232. package/src/maps/symbols/font-awesome/thumbs-up.png +0 -0
  233. package/src/maps/symbols/font-awesome/ticket.png +0 -0
  234. package/src/maps/symbols/font-awesome/times-circle.png +0 -0
  235. package/src/maps/symbols/font-awesome/times.png +0 -0
  236. package/src/maps/symbols/font-awesome/tint.png +0 -0
  237. package/src/maps/symbols/font-awesome/tree.png +0 -0
  238. package/src/maps/symbols/font-awesome/university.png +0 -0
  239. package/src/maps/symbols/font-awesome/usd.png +0 -0
  240. package/src/maps/symbols/font-awesome/user.png +0 -0
  241. package/src/maps/symbols/font-awesome/users.png +0 -0
  242. package/src/maps/symbols/font-awesome/wheelchair.png +0 -0
  243. package/src/maps/symbols/sdf-ize.sh +93 -0
  244. package/src/maps/vectorMaps.tsx +32 -53
  245. package/src/mwater_table_selection/IndicatorsListComponent.tsx +165 -37
  246. package/src/mwater_table_selection/MWaterCalculatedDataSourcesListComponent.tsx +111 -0
  247. package/src/mwater_table_selection/MWaterCompleteTableSelectComponent.tsx +373 -37
  248. package/src/mwater_table_selection/MWaterTableSelectComponent.tsx +12 -8
  249. package/src/mwater_table_selection/MWaterWorkflowsSelectComponent.tsx +159 -0
  250. package/src/quickfilter/{QuickfiltersComponent.ts → QuickfiltersComponent.tsx} +165 -158
  251. package/src/quickfilter/TextLiteralComponent.tsx +197 -0
  252. package/src/widgets/MapWidget.tsx +11 -1
  253. package/src/widgets/text/ExprItemEditorComponent.tsx +83 -77
  254. package/src/widgets/text/ExprUpdateModalComponent.tsx +1 -0
  255. package/test/UndoStackTests.ts +52 -1
  256. package/.storybook/config.js +0 -7
  257. package/.storybook/head.html +0 -3
  258. package/.storybook/webpack.config.js +0 -15
  259. package/src/maps/BingLayer.ts +0 -146
  260. package/src/maps/MapComponent.ts +0 -312
  261. package/src/maps/MapControlComponent.ts +0 -46
  262. package/src/maps/MarkersLayerDesignerComponent.ts +0 -374
  263. package/src/maps/RasterMapViewComponent.ts +0 -345
  264. package/src/quickfilter/TextLiteralComponent.ts +0 -165
  265. package/stories/UpdateableComponent.js +0 -29
  266. package/stories/consoles.js +0 -202
  267. package/stories/dashboards.js +0 -217
  268. package/stories/datagridDesign.js +0 -114
  269. package/stories/datagrids.js +0 -69
  270. package/stories/dates.js +0 -80
  271. package/stories/exprcomponent.js +0 -43
  272. package/stories/index.js +0 -18
  273. package/stories/leaflet.js +0 -59
  274. package/stories/maps.js +0 -24
  275. package/stories/pivotChart.js +0 -235
@@ -0,0 +1,429 @@
1
+ import _ from "lodash"
2
+ import React, { useMemo, useRef, useState } from "react"
3
+ import { languages } from "./languages"
4
+ import { default as ReactSelect } from "react-select"
5
+ import { LocalizedString } from "@mwater/expressions"
6
+ import produce from "immer"
7
+ import FileSaver from "file-saver"
8
+ import * as localizeUtils from "ez-localize/lib/utils"
9
+ import { canAutoTranslate, translateStrings } from "./autotranslate"
10
+ import { FormGroup } from "@mwater/react-library/lib/bootstrap"
11
+
12
+ export interface TranslationsTabComponentProps {
13
+ /** Base locale of the design (e.g. "en") */
14
+ locale: string
15
+
16
+ /** Other locales the design is translated into */
17
+ otherLocales: string[]
18
+
19
+ /** Translation mappings per locale. Maps locale to { originalString: translatedString } */
20
+ translations: { [locale: string]: { [key: string]: string } }
21
+
22
+ /** All strings that need translation */
23
+ translatableStrings: string[]
24
+
25
+ /** Called when base locale changes */
26
+ onLocaleChange: (locale: string) => void
27
+
28
+ /** Called when other locales change */
29
+ onOtherLocalesChange: (locales: string[]) => void
30
+
31
+ /** Called when translations change */
32
+ onTranslationsChange: (translations: { [locale: string]: { [key: string]: string } }) => void
33
+
34
+ /** Custom filename for download. Defaults to "Translations.xlsx" */
35
+ downloadFilename?: string
36
+
37
+ /** Custom description for base language section */
38
+ baseLanguageDescription?: string
39
+ }
40
+
41
+ /**
42
+ * Reusable translations tab component for managing localization of designs
43
+ * (dashboards, maps, etc.) that have locale, otherLocales, and translations properties.
44
+ */
45
+ export function TranslationsTabComponent(props: TranslationsTabComponentProps) {
46
+ const {
47
+ locale,
48
+ otherLocales,
49
+ translations,
50
+ translatableStrings,
51
+ onLocaleChange,
52
+ onOtherLocalesChange,
53
+ onTranslationsChange,
54
+ downloadFilename = "Translations.xlsx",
55
+ baseLanguageDescription = T`This is the base language.`
56
+ } = props
57
+
58
+ const fileInputRef = useRef<HTMLInputElement>(null)
59
+
60
+ const localeOptions = useMemo(() => {
61
+ return _.sortBy(
62
+ _.map(languages, (language) => ({
63
+ value: language.code,
64
+ label: `${language.en} (${language.name})`
65
+ })),
66
+ 'label'
67
+ )
68
+ }, [])
69
+
70
+ // Get available languages that aren't already selected
71
+ const availableLocaleOptions = useMemo(() => {
72
+ const selectedLocales = new Set([locale, ...otherLocales])
73
+ return localeOptions.filter(opt => !selectedLocales.has(opt.value))
74
+ }, [localeOptions, locale, otherLocales])
75
+
76
+ // Calculate percentage of strings translated for each locale
77
+ const translationPercentages = useMemo(() => {
78
+ const percentages: { [locale: string]: number } = {}
79
+ const totalStrings = translatableStrings.length
80
+
81
+ for (const loc of otherLocales) {
82
+ const translatedCount = translatableStrings.filter(str =>
83
+ translations?.[loc]?.[str] != null
84
+ ).length
85
+
86
+ // Round down to nearest percent
87
+ percentages[loc] = (totalStrings > 0) ? Math.floor((translatedCount / totalStrings) * 100) : 0
88
+ }
89
+
90
+ return percentages
91
+ }, [translations, otherLocales, translatableStrings])
92
+
93
+ const handleAddLocale = (selectedLocale: any) => {
94
+ const newOtherLocales = [...otherLocales, selectedLocale.value]
95
+ onOtherLocalesChange(newOtherLocales)
96
+ // Note: We don't initialize translations here because calling both callbacks
97
+ // in sequence would cause a race condition where the second overwrites the first.
98
+ // The code handles undefined translations via optional chaining.
99
+ }
100
+
101
+ const handleRemoveLocale = (localeToRemove: string) => {
102
+ const newOtherLocales = otherLocales.filter(loc => loc !== localeToRemove)
103
+ onOtherLocalesChange(newOtherLocales)
104
+ // Note: We don't remove translations here for the same reason as above.
105
+ // Orphaned translations are harmless and will be cleaned up on next save.
106
+ }
107
+
108
+ // Convert translations to LocalizedString format
109
+ const getLocalizedStrings = (): LocalizedString[] => {
110
+ const localizedStrings: LocalizedString[] = []
111
+ for (const str of translatableStrings) {
112
+ const localizedString: LocalizedString = { _base: locale }
113
+ localizedString[locale] = str
114
+
115
+ // Only add translations for other locales if they exist
116
+ for (const otherLocale of otherLocales) {
117
+ if (translations?.[otherLocale]?.[str]) {
118
+ localizedString[otherLocale] = translations[otherLocale][str]
119
+ }
120
+ }
121
+
122
+ localizedStrings.push(localizedString)
123
+ }
124
+
125
+ return localizedStrings
126
+ }
127
+
128
+ // Convert LocalizedString format back to translations
129
+ const updateFromLocalizedStrings = (localizedStrings: LocalizedString[]) => {
130
+ const newTranslations: { [locale: string]: { [key: string]: string } } = {}
131
+
132
+ // Initialize translations object
133
+ for (const loc of otherLocales) {
134
+ newTranslations[loc] = {}
135
+ }
136
+
137
+ // Add all translations
138
+ for (const str of localizedStrings) {
139
+ for (const loc of otherLocales) {
140
+ if (str[loc]) {
141
+ newTranslations[loc][str[str._base]] = str[loc]
142
+ }
143
+ }
144
+ }
145
+
146
+ return newTranslations
147
+ }
148
+
149
+ const handleDownload = () => {
150
+ // Get strings in LocalizedString format
151
+ const strings = getLocalizedStrings()
152
+
153
+ // Create xlsx base64 using all locales (primary + other)
154
+ const locales = [{ code: locale, name: locale }]
155
+ .concat(otherLocales.map(code => ({ code, name: code })))
156
+
157
+ const base64 = localizeUtils.exportXlsx(locales, strings)
158
+
159
+ // Download
160
+ FileSaver.saveAs(
161
+ b64toBlob(base64, "application/octet-stream"),
162
+ downloadFilename
163
+ )
164
+ }
165
+
166
+ const handleUpload = () => {
167
+ fileInputRef.current?.click()
168
+ }
169
+
170
+ const handleUploadChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
171
+ const reader = new FileReader()
172
+
173
+ reader.onload = (file) => {
174
+ if (!file.target?.result) {
175
+ return
176
+ }
177
+
178
+ const base64 = (file.target.result as string).split(",")[1]
179
+
180
+ try {
181
+ // Create locales array for import
182
+ const locales = [{ code: locale, name: locale }]
183
+ .concat(otherLocales.map(code => ({ code, name: code })))
184
+
185
+ // Import updates
186
+ const updates = localizeUtils.importXlsx(locales, base64)
187
+
188
+ // If nothing localized
189
+ if (updates.length === 0) {
190
+ alert(T`No translation data found in file`)
191
+ return
192
+ }
193
+
194
+ // Convert back to translations format and update
195
+ const newTranslations = updateFromLocalizedStrings(updates)
196
+ onTranslationsChange(newTranslations)
197
+
198
+ alert(T`${updates.length} translations applied`)
199
+ } catch (error) {
200
+ console.error("Invalid xlsx file:", error)
201
+ alert(T`Invalid xlsx file`)
202
+ }
203
+ }
204
+
205
+ if (evt.target.files?.[0]) {
206
+ reader.readAsDataURL(evt.target.files[0])
207
+ }
208
+ }
209
+
210
+ const handleTranslationsChangeForLocale = (loc: string, newLocaleTranslations: { [key: string]: string }) => {
211
+ const newTranslations = { ...translations }
212
+ newTranslations[loc] = newLocaleTranslations
213
+ onTranslationsChange(newTranslations)
214
+ }
215
+
216
+ return (
217
+ <>
218
+ <FormGroup
219
+ label={T`Base Language`}
220
+ labelMuted={true}
221
+ help={baseLanguageDescription}
222
+ >
223
+ <ReactSelect
224
+ value={_.findWhere(localeOptions, { value: locale }) || null}
225
+ options={localeOptions}
226
+ onChange={(selected: any) => onLocaleChange(selected.value)}
227
+ />
228
+ </FormGroup>
229
+ <FormGroup
230
+ label={T`Additional Languages`}
231
+ labelMuted={true}
232
+ help={T`Add languages to translate into`}
233
+ >
234
+ {/* Show current additional languages */}
235
+ <table>
236
+ <tbody>
237
+ {otherLocales.map(loc => {
238
+ const localeOption = _.findWhere(localeOptions, { value: loc })
239
+ if (!localeOption) {
240
+ return null
241
+ }
242
+
243
+ return (
244
+ <tr key={loc}>
245
+ <td style={{ paddingRight: 10 }}>{localeOption.label}</td>
246
+ <td className={translationPercentages[loc] === 100 ? "text-success" : "text-warning"} style={{ textAlign: "right" }}>
247
+ {T`${translationPercentages[loc]}% translated`}
248
+ </td>
249
+ <td>
250
+ {translationPercentages[loc] < 100 && (
251
+ <AutoTranslateLink
252
+ locale={loc}
253
+ baseLocale={locale}
254
+ strings={translatableStrings}
255
+ existingTranslations={translations?.[loc] || {}}
256
+ onTranslationsChange={(newLocaleTranslations) => handleTranslationsChangeForLocale(loc, newLocaleTranslations)}
257
+ />
258
+ )}
259
+ </td>
260
+ <td>
261
+ <button
262
+ type="button"
263
+ className="btn btn-sm btn-link"
264
+ onClick={() => handleRemoveLocale(loc)}
265
+ >
266
+ <i className="fa fa-times" />
267
+ </button>
268
+ </td>
269
+ </tr>
270
+ )
271
+ })}
272
+ </tbody>
273
+ </table>
274
+
275
+ {/* Add new language dropdown */}
276
+ {availableLocaleOptions.length > 0 && (
277
+ <div className="mt-3">
278
+ <ReactSelect
279
+ value={null}
280
+ options={availableLocaleOptions}
281
+ onChange={handleAddLocale}
282
+ placeholder={T`Add language...`}
283
+ />
284
+ </div>
285
+ )}
286
+ </FormGroup>
287
+
288
+ {/* Add translation management section if there are additional languages */}
289
+ {otherLocales.length > 0 && (
290
+ <>
291
+ <FormGroup
292
+ label={T`Manage Translations`}
293
+ labelMuted={true}
294
+ help={T`Download and re-upload an Excel spreadsheet of text to translate:`}
295
+ >
296
+ <p>{T`Download and re-upload an Excel spreadsheet of text to translate:`}</p>
297
+
298
+ <div>
299
+ <button
300
+ type="button"
301
+ className="btn btn-secondary"
302
+ onClick={handleDownload}
303
+ >
304
+ <i className="fas fa-download me-2" />{T`Download XLSX`}
305
+ </button>
306
+ </div>
307
+ <div className="text-muted mt-2">
308
+ {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.`}
309
+ </div>
310
+
311
+ <br />
312
+ <p>
313
+ {T`Once translation is complete, upload the file back using the button below:`}
314
+ </p>
315
+ <div>
316
+ <button
317
+ type="button"
318
+ className="btn btn-secondary"
319
+ onClick={handleUpload}
320
+ >
321
+ <i className="fas fa-upload me-2" />{T`Upload Translated XLSX`}
322
+ </button>
323
+ </div>
324
+ <input
325
+ type="file"
326
+ ref={fileInputRef}
327
+ style={{ display: "none" }}
328
+ onChange={handleUploadChange}
329
+ accept=".xlsx"
330
+ />
331
+ </FormGroup>
332
+ </>
333
+ )}
334
+ </>
335
+ )
336
+ }
337
+
338
+ interface AutoTranslateLinkProps {
339
+ locale: string
340
+ baseLocale: string
341
+ strings: string[]
342
+ existingTranslations: { [key: string]: string }
343
+ onTranslationsChange: (newTranslations: { [key: string]: string }) => void
344
+ }
345
+
346
+ /**
347
+ * Button that auto-translates untranslated strings using the translation service
348
+ */
349
+ function AutoTranslateLink(props: AutoTranslateLinkProps) {
350
+ const { locale, baseLocale, strings, existingTranslations, onTranslationsChange } = props
351
+ const [isTranslating, setIsTranslating] = useState(false)
352
+
353
+ const untranslatedStrings = useMemo(() => {
354
+ return strings.filter(str => !existingTranslations[str])
355
+ }, [strings, existingTranslations])
356
+
357
+ const handleClick = async () => {
358
+ if (isTranslating) {
359
+ return
360
+ }
361
+
362
+ setIsTranslating(true)
363
+ try {
364
+ const translatedStrings = await translateStrings(untranslatedStrings, baseLocale, locale)
365
+
366
+ const newTranslations = { ...existingTranslations }
367
+ for (let i = 0; i < untranslatedStrings.length; i++) {
368
+ newTranslations[untranslatedStrings[i]] = translatedStrings[i]
369
+ }
370
+
371
+ onTranslationsChange(newTranslations)
372
+ } catch (error) {
373
+ console.error("Error translating strings:", error)
374
+ alert(T`Error translating strings`)
375
+ } finally {
376
+ setIsTranslating(false)
377
+ }
378
+ }
379
+
380
+ if (untranslatedStrings.length === 0) {
381
+ return null
382
+ }
383
+
384
+ if (!canAutoTranslate(locale)) {
385
+ return null
386
+ }
387
+
388
+ return (
389
+ <button
390
+ type="button"
391
+ className="btn btn-sm btn-link"
392
+ onClick={handleClick}
393
+ disabled={isTranslating}
394
+ style={{ marginLeft: 5, padding: "0 5px" }}
395
+ >
396
+ {isTranslating ? (
397
+ <span>
398
+ <i className="fa fa-spinner fa-spin" />
399
+ {" "}
400
+ {T`Translating...`}
401
+ </span>
402
+ ) : (
403
+ T`Autotranslate`
404
+ )}
405
+ </button>
406
+ )
407
+ }
408
+
409
+ /**
410
+ * Helper function for base64 to blob conversion
411
+ */
412
+ function b64toBlob(b64Data: string, contentType: string = "", sliceSize: number = 512) {
413
+ const byteCharacters = atob(b64Data)
414
+ const byteArrays = []
415
+
416
+ for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
417
+ const slice = byteCharacters.slice(offset, offset + sliceSize)
418
+ const byteNumbers = new Array(slice.length)
419
+
420
+ for (let i = 0; i < slice.length; i++) {
421
+ byteNumbers[i] = slice.charCodeAt(i)
422
+ }
423
+
424
+ const byteArray = new Uint8Array(byteNumbers)
425
+ byteArrays.push(byteArray)
426
+ }
427
+
428
+ return new Blob(byteArrays, { type: contentType })
429
+ }
package/src/UndoStack.ts CHANGED
@@ -4,24 +4,32 @@ import _ from "lodash"
4
4
  export default class UndoStack {
5
5
  undoStack: any[]
6
6
  redoStack: any[]
7
+ maxDepth?: number
7
8
 
8
- constructor(undoStack?: any, redoStack?: any) {
9
+ constructor(undoStack?: any, redoStack?: any, maxDepth?: number) {
9
10
  this.undoStack = undoStack || []
10
11
  this.redoStack = redoStack || []
12
+ this.maxDepth = maxDepth
11
13
  }
12
14
 
13
15
  // Add a value to the stack
14
16
  push(value: any) {
15
17
  // No trivial pushes
16
- if (_.isEqual(this.getValue(), value)) {
18
+ if (JSON.stringify(this.getValue()) === JSON.stringify(value)) {
17
19
  return this
18
20
  }
19
21
 
20
- const undoStack = this.undoStack.slice()
22
+ let undoStack = this.undoStack.slice()
21
23
  undoStack.push(value)
24
+
25
+ // Limit stack depth if maxDepth is set
26
+ if (this.maxDepth && undoStack.length > this.maxDepth) {
27
+ undoStack = undoStack.slice(undoStack.length - this.maxDepth)
28
+ }
29
+
22
30
  const redoStack: any = []
23
31
 
24
- return new UndoStack(undoStack, redoStack)
32
+ return new UndoStack(undoStack, redoStack, this.maxDepth)
25
33
  }
26
34
 
27
35
  canUndo() {
@@ -39,7 +47,7 @@ export default class UndoStack {
39
47
 
40
48
  const undoStack = _.initial(this.undoStack)
41
49
 
42
- return new UndoStack(undoStack, redoStack)
50
+ return new UndoStack(undoStack, redoStack, this.maxDepth)
43
51
  }
44
52
 
45
53
  redo() {
@@ -49,7 +57,7 @@ export default class UndoStack {
49
57
 
50
58
  const redoStack = _.initial(this.redoStack)
51
59
 
52
- return new UndoStack(undoStack, redoStack)
60
+ return new UndoStack(undoStack, redoStack, this.maxDepth)
53
61
  }
54
62
 
55
63
  // Get the current value
@@ -289,7 +289,7 @@ export default class DashboardComponent extends React.Component<DashboardCompone
289
289
  {this.state.locale}
290
290
  </a>
291
291
  <ul className="dropdown-menu dropdown-menu-end">
292
- {[this.props.design.locale || "en", ...this.props.design.otherLocales].map(locale =>
292
+ {[...new Set([this.props.design.locale || "en", ...this.props.design.otherLocales])].map(locale =>
293
293
  <li key={locale}>
294
294
  <a
295
295
  className="dropdown-item"
@@ -304,23 +304,23 @@ export default class DashboardComponent extends React.Component<DashboardCompone
304
304
  : undefined}
305
305
  <a key="print" className="btn btn-link btn-sm" onClick={this.handlePrint}>
306
306
  <span className="fas fa-print"/>
307
- <span className="hide-600px"> {T`Print`}</span>
307
+ <span className="hide-800px"> {T`Print`}</span>
308
308
  </a>
309
309
  <a key="refresh" className="btn btn-link btn-sm" onClick={this.handleRefreshData}>
310
310
  <span className="fas fa-sync"/>
311
- <span className="hide-600px"> {T`Refresh`}</span>
311
+ <span className="hide-800px"> {T`Refresh`}</span>
312
312
  </a>
313
313
  {this.state.hideQuickfilters && this.props.design.quickfilters && this.props.design.quickfilters.length > 0
314
314
  ? <a key="showQuickfilters" className="btn btn-link btn-sm" onClick={this.handleShowQuickfilters}>
315
315
  <span className="fa fa-filter"/>
316
- <span className="hide-600px"> {T`Show Quickfilters`}</span>
316
+ <span className="hide-800px"> {T`Show Quickfilters`}</span>
317
317
  </a>
318
318
  : undefined}
319
319
 
320
320
  {this.state.editing
321
321
  ? <a key="settings" className="btn btn-link btn-sm" onClick={this.handleSettings}>
322
322
  <span className="fas fa-cog"/>
323
- <span className="hide-600px"> {T`Settings`}</span>
323
+ <span className="hide-800px"> {T`Settings`}</span>
324
324
  </a>
325
325
  : undefined}
326
326
  {this.state.editing ? this.renderStyle() : undefined}
@@ -435,6 +435,7 @@ export default class DashboardComponent extends React.Component<DashboardCompone
435
435
  <div style={{
436
436
  display: "grid",
437
437
  gridTemplateRows: this.props.hideTitleBar ? "auto 1fr" : "auto auto 1fr",
438
+ gridTemplateColumns: "minmax(0, 1fr)",
438
439
  height: "100%"
439
440
  }}>
440
441
  {!this.props.hideTitleBar ? this.renderTitleBar() : undefined}
@@ -39,6 +39,6 @@ export interface DashboardDesign {
39
39
  /** true to enable implicit filtering (see ImplicitFilterBuilder). Defaults to true for older dashboards. */
40
40
  implicitFiltersEnabled?: boolean
41
41
 
42
- /** array of global filters. See below. */
42
+ /** Array of global filters */
43
43
  globalFilters?: GlobalFilter[]
44
44
  }
@@ -318,25 +318,6 @@ class ServerWidgetLayerDataSource implements MapLayerDataSource {
318
318
  return this.createUrl(filters, "png")
319
319
  }
320
320
 
321
- // Get the url for the interactivity tiles with the specified filters applied
322
- // Called with (design, filters) where design is the layer design and filters are filters to apply. Returns URL
323
- getUtfGridUrl(design: any, filters: JsonQLFilter[]) {
324
- // Handle special cases
325
- if (this.options.layerView.type === "MWaterServer") {
326
- return this.createLegacyUrl(this.options.layerView.design, "grid.json", filters)
327
- }
328
-
329
- // Create layer
330
- const layer = LayerFactory.createLayer(this.options.layerView.type)
331
-
332
- // If layer has tiles url directly available
333
- if (layer.getLayerDefinitionType() === "TileUrl") {
334
- return layer.getUtfGridUrl(this.options.layerView.design, filters)
335
- }
336
-
337
- return this.createUrl(filters, "grid.json")
338
- }
339
-
340
321
  /** Get the url for vector tile source with an expiry time. Only for layers of type "VectorTile"
341
322
  * @param createdAfter ISO 8601 timestamp requiring that tile source on server is created after specified datetime
342
323
  */
@@ -407,12 +388,6 @@ class ServerWidgetLayerDataSource implements MapLayerDataSource {
407
388
 
408
389
  let url = `${this.options.apiUrl}maps/tiles/{z}/{x}/{y}.${extension}?` + querystring.stringify(query)
409
390
 
410
- // Add subdomains: {s} will be substituted with "a", "b" or "c" in leaflet for api.mwater.co only.
411
- // Used to speed queries
412
- if (url.match(/^https:\/\/api\.mwater\.co\//)) {
413
- url = url.replace(/^https:\/\/api\.mwater\.co\//, "https://{s}-api.mwater.co/")
414
- }
415
-
416
391
  return url
417
392
  }
418
393
 
@@ -421,12 +396,6 @@ class ServerWidgetLayerDataSource implements MapLayerDataSource {
421
396
  let where
422
397
  let url = `${this.options.apiUrl}maps/tiles/{z}/{x}/{y}.${extension}?type=${design.type}&radius=1000`
423
398
 
424
- // Add subdomains: {s} will be substituted with "a", "b" or "c" in leaflet for api.mwater.co only.
425
- // Used to speed queries
426
- if (url.match(/^https:\/\/api\.mwater\.co\//)) {
427
- url = url.replace(/^https:\/\/api\.mwater\.co\//, "https://{s}-api.mwater.co/")
428
- }
429
-
430
399
  if (this.options.client) {
431
400
  url += `&client=${this.options.client}`
432
401
  }