mxrb 0.1.0

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 (237) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.de-DE.md +108 -0
  4. data/README.md +172 -0
  5. data/README.pt-BR.md +109 -0
  6. data/bin/mxrb +1613 -0
  7. data/docs/README.md +10 -0
  8. data/docs/de-DE/README.md +24 -0
  9. data/docs/de-DE/architectural-patterns.md +26 -0
  10. data/docs/de-DE/architectural-standard.md +48 -0
  11. data/docs/de-DE/architecture.md +76 -0
  12. data/docs/de-DE/compiler.md +162 -0
  13. data/docs/de-DE/conventions.md +19 -0
  14. data/docs/de-DE/design-system.md +52 -0
  15. data/docs/de-DE/entity-dsl.md +53 -0
  16. data/docs/de-DE/native-runtime-quality-report.md +74 -0
  17. data/docs/de-DE/oql-sql.md +211 -0
  18. data/docs/de-DE/platform-api-integration.md +36 -0
  19. data/docs/de-DE/platform-operations.md +243 -0
  20. data/docs/de-DE/project-structure.md +32 -0
  21. data/docs/de-DE/ruby-first-roadmap.md +116 -0
  22. data/docs/de-DE/scaffolds.md +59 -0
  23. data/docs/de-DE/semantic-refactoring.md +56 -0
  24. data/docs/de-DE/team-server.md +33 -0
  25. data/docs/de-DE/validation-matrix.md +64 -0
  26. data/docs/de-DE/vetclinic-acceptance.md +24 -0
  27. data/docs/de-DE/writing.md +138 -0
  28. data/docs/en-US/README.md +24 -0
  29. data/docs/en-US/architectural-patterns.md +151 -0
  30. data/docs/en-US/architectural-standard.md +89 -0
  31. data/docs/en-US/architecture.md +170 -0
  32. data/docs/en-US/compiler.md +157 -0
  33. data/docs/en-US/conventions.md +82 -0
  34. data/docs/en-US/design-system.md +163 -0
  35. data/docs/en-US/entity-dsl.md +53 -0
  36. data/docs/en-US/native-runtime-quality-report.md +73 -0
  37. data/docs/en-US/oql-sql.md +201 -0
  38. data/docs/en-US/platform-api-integration.md +36 -0
  39. data/docs/en-US/platform-operations.md +244 -0
  40. data/docs/en-US/project-structure.md +49 -0
  41. data/docs/en-US/ruby-first-roadmap.md +186 -0
  42. data/docs/en-US/scaffolds.md +57 -0
  43. data/docs/en-US/semantic-refactoring.md +198 -0
  44. data/docs/en-US/team-server.md +32 -0
  45. data/docs/en-US/validation-matrix.md +257 -0
  46. data/docs/en-US/vetclinic-acceptance.md +23 -0
  47. data/docs/en-US/writing.md +655 -0
  48. data/docs/pt-BR/README.md +24 -0
  49. data/docs/pt-BR/architectural-patterns.md +25 -0
  50. data/docs/pt-BR/architectural-standard.md +533 -0
  51. data/docs/pt-BR/architecture.md +300 -0
  52. data/docs/pt-BR/compiler.md +162 -0
  53. data/docs/pt-BR/conventions.md +19 -0
  54. data/docs/pt-BR/design-system.md +65 -0
  55. data/docs/pt-BR/entity-dsl.md +116 -0
  56. data/docs/pt-BR/native-runtime-quality-report.md +73 -0
  57. data/docs/pt-BR/oql-sql.md +208 -0
  58. data/docs/pt-BR/platform-api-integration.md +36 -0
  59. data/docs/pt-BR/platform-operations.md +260 -0
  60. data/docs/pt-BR/project-structure.md +32 -0
  61. data/docs/pt-BR/ruby-first-roadmap.md +239 -0
  62. data/docs/pt-BR/scaffolds.md +104 -0
  63. data/docs/pt-BR/semantic-refactoring.md +55 -0
  64. data/docs/pt-BR/team-server.md +59 -0
  65. data/docs/pt-BR/validation-matrix.md +86 -0
  66. data/docs/pt-BR/vetclinic-acceptance.md +50 -0
  67. data/docs/pt-BR/writing.md +177 -0
  68. data/examples/sudoku_evaluation.rb +14 -0
  69. data/examples/sudoku_functional_test.rb +28 -0
  70. data/lib/mxrb/architecture/graph.rb +124 -0
  71. data/lib/mxrb/architecture/validator.rb +98 -0
  72. data/lib/mxrb/benchmark.rb +33 -0
  73. data/lib/mxrb/compare.rb +390 -0
  74. data/lib/mxrb/compiler/adapter.rb +96 -0
  75. data/lib/mxrb/compiler/artifact_document_compiler.rb +102 -0
  76. data/lib/mxrb/compiler/artifact_materializer.rb +53 -0
  77. data/lib/mxrb/compiler/client_model_materializer.rb +42 -0
  78. data/lib/mxrb/compiler/code_action_document_compiler.rb +64 -0
  79. data/lib/mxrb/compiler/code_action_materializer.rb +35 -0
  80. data/lib/mxrb/compiler/code_action_type_compiler.rb +32 -0
  81. data/lib/mxrb/compiler/combo_box_bundle_compiler.rb +314 -0
  82. data/lib/mxrb/compiler/compatibility_analyzer.rb +169 -0
  83. data/lib/mxrb/compiler/constants_materializer.rb +111 -0
  84. data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +290 -0
  85. data/lib/mxrb/compiler/database_connector_action_compiler.rb +280 -0
  86. data/lib/mxrb/compiler/deployment_asset_copier.rb +60 -0
  87. data/lib/mxrb/compiler/deployment_bootstrapper.rb +208 -0
  88. data/lib/mxrb/compiler/deployment_materializer.rb +43 -0
  89. data/lib/mxrb/compiler/deployment_metadata.rb +64 -0
  90. data/lib/mxrb/compiler/domain_document_compiler.rb +153 -0
  91. data/lib/mxrb/compiler/domain_model_materializer.rb +35 -0
  92. data/lib/mxrb/compiler/domain_security_compiler.rb +82 -0
  93. data/lib/mxrb/compiler/gallery_bundle_compiler.rb +190 -0
  94. data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +281 -0
  95. data/lib/mxrb/compiler/image_bundle_compiler.rb +216 -0
  96. data/lib/mxrb/compiler/java_proxy_generator.rb +509 -0
  97. data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +267 -0
  98. data/lib/mxrb/compiler/legacy_page_builder.rb +287 -0
  99. data/lib/mxrb/compiler/mda.rb +99 -0
  100. data/lib/mxrb/compiler/microflow_document_compiler.rb +85 -0
  101. data/lib/mxrb/compiler/microflow_materializer.rb +48 -0
  102. data/lib/mxrb/compiler/microflow_node_compiler.rb +250 -0
  103. data/lib/mxrb/compiler/model_package.rb +116 -0
  104. data/lib/mxrb/compiler/model_values.rb +67 -0
  105. data/lib/mxrb/compiler/nanoflow_program_compiler.rb +483 -0
  106. data/lib/mxrb/compiler/navigation_document_compiler.rb +93 -0
  107. data/lib/mxrb/compiler/packager.rb +100 -0
  108. data/lib/mxrb/compiler/page_bundle_builder.rb +56 -0
  109. data/lib/mxrb/compiler/page_bundle_compiler.rb +2004 -0
  110. data/lib/mxrb/compiler/page_document_compiler.rb +77 -0
  111. data/lib/mxrb/compiler/portable_packager.rb +356 -0
  112. data/lib/mxrb/compiler/project_jar_archive.rb +84 -0
  113. data/lib/mxrb/compiler/project_jar_builder.rb +121 -0
  114. data/lib/mxrb/compiler/project_materializer.rb +139 -0
  115. data/lib/mxrb/compiler/project_model_orderer.rb +52 -0
  116. data/lib/mxrb/compiler/runtime_data_types.rb +31 -0
  117. data/lib/mxrb/compiler/runtime_model_schema.rb +90 -0
  118. data/lib/mxrb/compiler/schemas/runtime-10.24.0.73019.json +1052 -0
  119. data/lib/mxrb/compiler/schemas/runtime-11.json +1215 -0
  120. data/lib/mxrb/compiler/schemas/runtime-6.10.8.json +1148 -0
  121. data/lib/mxrb/compiler/schemas/runtime-7.17.0.json +1144 -0
  122. data/lib/mxrb/compiler/schemas/runtime-7.5.0.json +1215 -0
  123. data/lib/mxrb/compiler/schemas/runtime-9.6.1.29396.json +868 -0
  124. data/lib/mxrb/compiler/schemas/system-model-10.24.0.73019.b64 +1135 -0
  125. data/lib/mxrb/compiler/schemas/system-model-11.12.1.b64 +815 -0
  126. data/lib/mxrb/compiler/schemas/system-model-6.10.8.b64 +497 -0
  127. data/lib/mxrb/compiler/schemas/system-model-7.17.0.b64 +519 -0
  128. data/lib/mxrb/compiler/schemas/system-model-7.5.0.b64 +504 -0
  129. data/lib/mxrb/compiler/schemas/system-model-9.6.1.29396.b64 +816 -0
  130. data/lib/mxrb/compiler/security_materializer.rb +104 -0
  131. data/lib/mxrb/compiler/settings_document_compiler.rb +45 -0
  132. data/lib/mxrb/compiler/settings_materializer.rb +25 -0
  133. data/lib/mxrb/compiler/source_model.rb +128 -0
  134. data/lib/mxrb/compiler/system_model_seed.rb +88 -0
  135. data/lib/mxrb/compiler/system_queue_materializer.rb +54 -0
  136. data/lib/mxrb/compiler/system_text_materializer.rb +49 -0
  137. data/lib/mxrb/compiler/translation_materializer.rb +95 -0
  138. data/lib/mxrb/compiler/web_bundle_builder.rb +155 -0
  139. data/lib/mxrb/compiler/web_list_data_source.rb +139 -0
  140. data/lib/mxrb/compiler/web_operation_compiler.rb +614 -0
  141. data/lib/mxrb/compiler/web_shell_materializer.rb +194 -0
  142. data/lib/mxrb/compiler/widget_package_extractor.rb +63 -0
  143. data/lib/mxrb/doctor.rb +116 -0
  144. data/lib/mxrb/dsl/builder.rb +1636 -0
  145. data/lib/mxrb/errors.rb +18 -0
  146. data/lib/mxrb/evaluation.rb +131 -0
  147. data/lib/mxrb/exporter.rb +1716 -0
  148. data/lib/mxrb/frontend/migrator.rb +775 -0
  149. data/lib/mxrb/functional.rb +307 -0
  150. data/lib/mxrb/github/annotator.rb +161 -0
  151. data/lib/mxrb/initializer.rb +257 -0
  152. data/lib/mxrb/integrity/validator.rb +136 -0
  153. data/lib/mxrb/io/bson_codec.rb +145 -0
  154. data/lib/mxrb/io/mpr_file.rb +558 -0
  155. data/lib/mxrb/io/mxunit_codec.rb +42 -0
  156. data/lib/mxrb/marketplace.rb +380 -0
  157. data/lib/mxrb/model/association.rb +98 -0
  158. data/lib/mxrb/model/attribute.rb +100 -0
  159. data/lib/mxrb/model/connector.rb +17 -0
  160. data/lib/mxrb/model/design_materializer.rb +133 -0
  161. data/lib/mxrb/model/design_migration.rb +126 -0
  162. data/lib/mxrb/model/design_system.rb +111 -0
  163. data/lib/mxrb/model/domain_model.rb +51 -0
  164. data/lib/mxrb/model/entity.rb +180 -0
  165. data/lib/mxrb/model/menu.rb +38 -0
  166. data/lib/mxrb/model/microflow.rb +100 -0
  167. data/lib/mxrb/model/module.rb +142 -0
  168. data/lib/mxrb/model/navigation.rb +127 -0
  169. data/lib/mxrb/model/page.rb +410 -0
  170. data/lib/mxrb/model/project.rb +187 -0
  171. data/lib/mxrb/model/unit.rb +48 -0
  172. data/lib/mxrb/module_initializer.rb +75 -0
  173. data/lib/mxrb/official_marketplace/content_api.rb +292 -0
  174. data/lib/mxrb/official_marketplace/dependency_resolver.rb +329 -0
  175. data/lib/mxrb/official_marketplace/lifecycle.rb +367 -0
  176. data/lib/mxrb/official_marketplace/module_package_importer.rb +318 -0
  177. data/lib/mxrb/official_marketplace/widget_package_installer.rb +414 -0
  178. data/lib/mxrb/official_marketplace.rb +720 -0
  179. data/lib/mxrb/oql/analyzer.rb +155 -0
  180. data/lib/mxrb/oql/index_advisor.rb +85 -0
  181. data/lib/mxrb/oql/plan_analyzer.rb +185 -0
  182. data/lib/mxrb/oql/server.rb +132 -0
  183. data/lib/mxrb/oql/sql_server_plan_analyzer.rb +190 -0
  184. data/lib/mxrb/oql/sql_server_workload_analyzer.rb +80 -0
  185. data/lib/mxrb/oql/workload_analyzer.rb +175 -0
  186. data/lib/mxrb/oql/workload_baseline.rb +60 -0
  187. data/lib/mxrb/oql.rb +424 -0
  188. data/lib/mxrb/project_lifecycle.rb +76 -0
  189. data/lib/mxrb/protocols/plan.rb +86 -0
  190. data/lib/mxrb/protocols.rb +123 -0
  191. data/lib/mxrb/runtime/database_workspace.rb +677 -0
  192. data/lib/mxrb/runtime/docker_executor.rb +41 -0
  193. data/lib/mxrb/runtime/docker_workspace.rb +29 -0
  194. data/lib/mxrb/runtime/executor.rb +169 -0
  195. data/lib/mxrb/runtime/java_locator.rb +26 -0
  196. data/lib/mxrb/runtime/native.rb +474 -0
  197. data/lib/mxrb/runtime/sql_server_database.rb +146 -0
  198. data/lib/mxrb/runtime/toolchain.rb +82 -0
  199. data/lib/mxrb/scaffold/cli.rb +113 -0
  200. data/lib/mxrb/scaffold/generator.rb +246 -0
  201. data/lib/mxrb/scaffold/help.rb +80 -0
  202. data/lib/mxrb/scaffold/page_templates.rb +54 -0
  203. data/lib/mxrb/scaffold/recipes.rb +198 -0
  204. data/lib/mxrb/scaffold/registry.rb +82 -0
  205. data/lib/mxrb/scaffold/templates.rb +723 -0
  206. data/lib/mxrb/scaffold/transaction.rb +84 -0
  207. data/lib/mxrb/schema/tables.rb +55 -0
  208. data/lib/mxrb/semantic/analyzer.rb +473 -0
  209. data/lib/mxrb/semantic/batch_plan.rb +68 -0
  210. data/lib/mxrb/semantic/domain_mutator.rb +419 -0
  211. data/lib/mxrb/semantic/embedder.rb +51 -0
  212. data/lib/mxrb/semantic/extractor.rb +290 -0
  213. data/lib/mxrb/semantic/index.rb +623 -0
  214. data/lib/mxrb/semantic/inliner.rb +216 -0
  215. data/lib/mxrb/semantic/mover.rb +153 -0
  216. data/lib/mxrb/semantic/onnx_embedder.rb +34 -0
  217. data/lib/mxrb/semantic/remover.rb +70 -0
  218. data/lib/mxrb/semantic/renamer.rb +211 -0
  219. data/lib/mxrb/semantic/tfidf_embedder.rb +46 -0
  220. data/lib/mxrb/semantic/vec_store.rb +75 -0
  221. data/lib/mxrb/team_server.rb +390 -0
  222. data/lib/mxrb/templates/project/10.24.0.73019.json +32 -0
  223. data/lib/mxrb/templates/project/11.12.1.json +32 -0
  224. data/lib/mxrb/templates/project/6.10.8.json +50 -0
  225. data/lib/mxrb/templates/project/7.17.0.json +50 -0
  226. data/lib/mxrb/templates/project/7.5.0.json +50 -0
  227. data/lib/mxrb/templates/project/9.6.1.29396.json +50 -0
  228. data/lib/mxrb/version.rb +5 -0
  229. data/lib/mxrb/widget_package.rb +338 -0
  230. data/lib/mxrb/widget_synchronizer.rb +31 -0
  231. data/lib/mxrb/writer.rb +3775 -0
  232. data/lib/mxrb.rb +188 -0
  233. data/marketplace/catalog.json +10 -0
  234. data/marketplace/modules/shared-kernel/domain/README.md +4 -0
  235. data/marketplace/modules/shared-kernel/module.rb +5 -0
  236. data/marketplace/modules/shared-kernel/mxrb-module.json +9 -0
  237. metadata +447 -0
@@ -0,0 +1,2004 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Mxrb
6
+ module Compiler
7
+ PageBundle = Data.define(:qualified_name, :source, :unsupported_widgets, :unsupported_custom_widgets)
8
+
9
+ # Converts the web-page subset of the source model into a Runtime-loadable ES module.
10
+ # rubocop:disable Metrics, Style/MultilineBlockChain
11
+ class PageBundleCompiler
12
+ include ModelValues
13
+
14
+ def initialize(source)
15
+ @source = source
16
+ @unsupported = []
17
+ @unsupported_custom = []
18
+ @data_view_scopes = []
19
+ @list_scopes = []
20
+ @snippet_scopes = []
21
+ @snippet_stack = []
22
+ @snippet_documents = []
23
+ @uses_conditional = false
24
+ @uses_dynamic_class = false
25
+ end
26
+
27
+ def compile(unit)
28
+ prepare_compile(unit, 'p')
29
+ build_bundle(module_source(page_content))
30
+ end
31
+
32
+ def compile_layout(unit)
33
+ prepare_compile(unit, 'l')
34
+ @layout_mode = true
35
+ widgets = array(unit.document.dig('Content', 'Widgets'))
36
+ build_bundle(layout_module_source(children(widgets)))
37
+ end
38
+
39
+ private
40
+
41
+ def prepare_compile(unit, prefix)
42
+ @unit = unit
43
+ @qualified_name = "#{unit.module_name}.#{unit.document['Name']}"
44
+ @scope_prefix = prefix
45
+ @layout_mode = false
46
+ @data_view_scopes = []
47
+ @list_scopes = []
48
+ @snippet_scopes = []
49
+ @snippet_stack = []
50
+ @snippet_documents = []
51
+ @generic_widgets = {}
52
+ @nanoflow_programs = NanoflowProgramCompiler.new(@source)
53
+ end
54
+
55
+ def build_bundle(source)
56
+ PageBundle.new(
57
+ qualified_name: @qualified_name, source:,
58
+ unsupported_widgets: @unsupported.uniq.sort.freeze,
59
+ unsupported_custom_widgets: @unsupported_custom.uniq.sort.freeze
60
+ )
61
+ end
62
+
63
+ def arguments = array(@unit.document.dig('FormCall', 'Arguments'))
64
+
65
+ def page_content
66
+ entries = arguments.map do |argument|
67
+ [slot_name(argument['Parameter']), content_function(array(argument['Widgets']))]
68
+ end
69
+ duplicate = entries.map(&:first).tally.find { |_name, count| count > 1 }&.first
70
+ raise CompilationError, "duplicate page slot #{duplicate.inspect}" if duplicate
71
+
72
+ entries.to_h
73
+ end
74
+
75
+ def slot_name(parameter)
76
+ name = parameter.to_s
77
+ valid = name.split('.').length >= 2 && name.split('.').all? { present_identifier?(_1) }
78
+ raise CompilationError, "invalid page slot #{parameter.inspect}" unless valid
79
+
80
+ name
81
+ end
82
+
83
+ def content_function(widgets)
84
+ "renderKey => React.createElement(PageFragment, { renderKey }, #{children(widgets)})"
85
+ end
86
+
87
+ def children(widgets) = "[#{widgets.map { render_widget(_1) }.join(', ')}]"
88
+
89
+ def render_widget(widget)
90
+ rendered = case widget['$Type']
91
+ when 'Forms$DivContainer' then render_container(widget)
92
+ when 'Forms$LayoutGrid' then render_layout_grid(widget)
93
+ when 'Forms$LayoutGridRow' then render_grid_row(widget)
94
+ when 'Forms$LayoutGridColumn' then render_grid_column(widget)
95
+ when 'Forms$Table' then render_table(widget)
96
+ when 'Forms$DynamicText' then render_text(widget)
97
+ when 'Forms$Title' then render_title(widget)
98
+ when 'Forms$ActionButton' then render_action_button(widget)
99
+ when 'Forms$DataView' then render_data_view(widget)
100
+ when 'Forms$ListView' then render_list_view(widget)
101
+ when 'Forms$TextBox' then render_text_box(widget)
102
+ when 'Forms$TextArea' then render_text_area(widget)
103
+ when 'Forms$DatePicker' then render_date_picker(widget)
104
+ when 'Forms$CheckBox' then render_check_box(widget)
105
+ when 'Forms$RadioButtonGroup' then render_radio_button_group(widget)
106
+ when 'Forms$FileManager' then render_file_manager(widget)
107
+ when 'Forms$GroupBox' then render_group_box(widget)
108
+ when 'Forms$SnippetCallWidget' then render_snippet_call(widget)
109
+ when 'Forms$Label' then render_label(widget)
110
+ when 'Forms$TabControl' then render_tab_control(widget)
111
+ when 'Forms$StaticImageViewer' then render_static_image(widget)
112
+ when 'Forms$ScrollContainer' then render_scroll_container(widget)
113
+ when 'Forms$Placeholder' then render_placeholder(widget)
114
+ when 'Forms$SidebarToggleButton' then render_sidebar_toggle(widget)
115
+ when 'Forms$Header' then render_header(widget)
116
+ when 'Forms$NavigationTree' then render_menu(widget, 'NavigationTree')
117
+ when 'Forms$MenuBar' then render_menu(widget, 'MenuBar')
118
+ when 'Forms$SimpleMenuBar' then render_menu(widget, 'SimpleMenuBar')
119
+ when 'CustomWidgets$CustomWidget' then render_custom_widget(widget)
120
+ else render_unsupported(widget)
121
+ end
122
+ rendered = wrap_dynamic_classes(widget, rendered)
123
+ wrap_conditional_visibility(widget, rendered)
124
+ end
125
+
126
+ def render_layout_grid(widget)
127
+ render_element('div', widget, array(widget['Rows']), 'mx-layoutgrid mx-layoutgrid-fluid')
128
+ end
129
+
130
+ def render_grid_row(widget)
131
+ render_element('div', widget, array(widget['Columns']), 'row')
132
+ end
133
+
134
+ def render_grid_column(widget)
135
+ classes = ['col', grid_weight_class('md', widget['Weight']),
136
+ grid_weight_class('sm', widget['TabletWeight']),
137
+ grid_weight_class('xs', widget['PhoneWeight'])].compact.join(' ')
138
+ render_element('div', widget, array(widget['Widgets']), classes)
139
+ end
140
+
141
+ def render_table(widget)
142
+ cells = array(widget['Cells']).group_by { integer_or(_1['TopRowIndex'], 0) }
143
+ rows = array(widget['Rows']).map.with_index do |row, index|
144
+ content = cells.fetch(index, []).sort_by { integer_or(_1['LeftColumnIndex'], 0) }
145
+ .map { render_table_cell(_1) }
146
+ props = common_props(row).merge(className: css_class(row))
147
+ "React.createElement(\"tr\", #{js_props(props)}, [#{content.join(', ')}])"
148
+ end
149
+ props = common_props(widget).merge(
150
+ className: ['mx-table', css_class(widget)].reject(&:empty?).join(' ')
151
+ )
152
+ body = "React.createElement(\"tbody\", null, [#{rows.join(', ')}])"
153
+ "React.createElement(\"table\", #{js_props(props)}, #{body})"
154
+ end
155
+
156
+ def render_table_cell(cell)
157
+ tag = cell['IsHeader'] == true ? 'th' : 'td'
158
+ props = common_props(cell).merge(
159
+ className: css_class(cell),
160
+ colSpan: positive_integer(cell['Width'], 1), rowSpan: positive_integer(cell['Height'], 1),
161
+ 'data-column-index': integer_or(cell['LeftColumnIndex'], 0)
162
+ )
163
+ content = children(array(cell['Widgets']))
164
+ "React.createElement(#{JSON.generate(tag)}, #{js_props(props)}, #{content})"
165
+ end
166
+
167
+ def integer_or(value, fallback)
168
+ Integer(value)
169
+ rescue ArgumentError, TypeError
170
+ fallback
171
+ end
172
+
173
+ def render_scroll_container(widget)
174
+ @uses_layout_widgets = true
175
+ props = common_props(widget).merge(
176
+ '$widgetId': widget_key(widget), class: css_class(widget),
177
+ scrollPerRegion: widget['ScrollBehavior'] == 'PerRegion',
178
+ layoutMode: widget['LayoutMode'].to_s.downcase,
179
+ top: scroll_region(widget['Top']), bottom: scroll_region(widget['Bottom']),
180
+ left: scroll_region(widget['Left']), right: scroll_region(widget['Right']),
181
+ center: scroll_region(widget['CenterRegion'])
182
+ )
183
+ "React.createElement($ScrollContainer, #{js_literal(props)})"
184
+ end
185
+
186
+ def scroll_region(region)
187
+ return { enabled: false } unless region
188
+
189
+ toggle_mode = region['ToggleMode'].to_s
190
+ {
191
+ enabled: true, content: raw_js(children(array(region['Widgets']))),
192
+ sizeMode: region['SizeMode'].to_s.downcase, sizeValue: region['Size'],
193
+ class: region.dig('Appearance', 'Class').to_s,
194
+ toggleMode: scroll_toggle_mode(toggle_mode),
195
+ initiallyOpen: !toggle_mode.include?('InitiallyClosed')
196
+ }
197
+ end
198
+
199
+ def scroll_toggle_mode(value)
200
+ return 'none' if value.empty? || value == 'None'
201
+ return 'shrink' if value.start_with?('ShrinkContent')
202
+ return 'push' if value.start_with?('PushContent')
203
+ return 'slide' if value.start_with?('SlideOverContent')
204
+
205
+ raise CompilationError, "unsupported scroll container toggle mode: #{value}"
206
+ end
207
+
208
+ def render_placeholder(widget)
209
+ @uses_layout_widgets = true
210
+ key = widget_key(widget)
211
+ identifier = "#{@qualified_name}.#{widget['Name']}"
212
+ "React.createElement($Placeholder, #{js_props(
213
+ common_props(widget).merge('$widgetId': key),
214
+ expressions: { content: "PlaceholderProperty({ id: #{JSON.generate(identifier)} })" }
215
+ )})"
216
+ end
217
+
218
+ def render_sidebar_toggle(widget)
219
+ @uses_layout_widgets = true
220
+ key = widget_key(widget)
221
+ caption = translated_text(widget.dig('CaptionTemplate', 'Template'))
222
+ tooltip = translated_text(widget['Tooltip'])
223
+ props = common_props(widget).merge(
224
+ '$widgetId': key, buttonId: key, class: css_class(widget),
225
+ renderType: 'button', buttonClass: button_style(widget)
226
+ )
227
+ "React.createElement($SidebarToggle, #{js_props(props, expressions: {
228
+ caption: "TextProperty({ value: #{JSON.generate(caption)} })",
229
+ tooltip: "TextProperty({ value: #{JSON.generate(tooltip)} })"
230
+ })})"
231
+ end
232
+
233
+ def render_header(widget)
234
+ @uses_layout_widgets = true
235
+ props = common_props(widget).merge(
236
+ '$widgetId': widget_key(widget), class: css_class(widget), content: [],
237
+ leftWidgets: array(widget['LeftWidgets']).map { render_widget(_1) },
238
+ rightWidgets: array(widget['RightWidgets']).map { render_widget(_1) }
239
+ )
240
+ "React.createElement($Header, #{js_props(props)})"
241
+ end
242
+
243
+ def render_menu(widget, component)
244
+ items = menu_items(widget['MenuSource'])
245
+ return render_unsupported(widget) unless items
246
+
247
+ @uses_layout_widgets = true
248
+ props = common_props(widget).merge(
249
+ '$widgetId': widget_key(widget), name: widget['Name'].to_s, class: css_class(widget),
250
+ menu: items.map.with_index { |item, index| compile_menu_item(item, widget, [index]) }
251
+ )
252
+ props[:orientation] = widget['Orientation'].to_s.downcase if component == 'SimpleMenuBar'
253
+ "React.createElement($#{component}, #{js_literal(props)})"
254
+ end
255
+
256
+ def menu_items(source)
257
+ case source&.fetch('$Type', nil)
258
+ when 'Forms$MenuDocumentSource'
259
+ qualified = source['Menu'].to_s
260
+ unit = @source.units_of('Menus$MenuDocument').find do |candidate|
261
+ "#{candidate.module_name}.#{candidate.document['Name']}" == qualified
262
+ end
263
+ array(unit.document.dig('ItemCollection', 'Items')) if unit
264
+ when 'Forms$NavigationSource'
265
+ profile = @source.units_of('Navigation$NavigationDocument').flat_map do |unit|
266
+ array(unit.document['Profiles'])
267
+ end.find { _1['Name'] == source['NavigationProfile'] }
268
+ array(profile.dig('Menu', 'Items')) if profile
269
+ end
270
+ end
271
+
272
+ def compile_menu_item(item, widget, path)
273
+ result = {
274
+ caption: raw_js("TextProperty({ value: #{JSON.generate(translated_text(item['Caption']))} })")
275
+ }
276
+ action = menu_action_config(item['Action'] || {})
277
+ result[:action] = raw_js("ActionProperty(#{js_literal(action)})") if action
278
+ icon = menu_icon_property(item['Icon'])
279
+ result[:icon] = raw_js(icon) if icon
280
+ nested_items = array(item['Items'])
281
+ unless nested_items.empty?
282
+ result[:items] = nested_items.map.with_index do |child, index|
283
+ compile_menu_item(child, widget, path + [index])
284
+ end
285
+ end
286
+ result
287
+ end
288
+
289
+ def menu_action_config(action)
290
+ payload = open_page_config(action) || menu_microflow_config(action)
291
+ return unless payload
292
+
293
+ { action: payload, skipClientValidation: true }
294
+ end
295
+
296
+ def menu_microflow_config(action)
297
+ return unless action['$Type'] == 'Forms$MicroflowAction'
298
+
299
+ name = action.dig('MicroflowSettings', 'Microflow').to_s
300
+ return unless present_identifier?(name) && microflow_argument_map(action['MicroflowSettings'] || {}) == {}
301
+
302
+ {
303
+ type: 'callMicroflow', argMap: {},
304
+ config: {
305
+ operationId: WebOperationCompiler.menu_operation_id(action), validate: 'view',
306
+ allowedRoles: allowed_roles_for('Microflows$Microflow', name)
307
+ },
308
+ disabledDuringExecution: action.fetch('DisabledDuringExecution', true)
309
+ }
310
+ end
311
+
312
+ def menu_icon_property(icon)
313
+ reference = icon&.fetch('Image', '').to_s
314
+ name = reference.split('.').last
315
+ return if name.to_s.empty?
316
+
317
+ css = name.gsub(/([a-z\d])([A-Z])/, '\\1-\\2').tr('_', '-').downcase
318
+ "WebIconProperty({ icon: { type: \"icon\", iconClass: \"mx-icon-filled mx-icon-#{css}\" } })"
319
+ end
320
+
321
+ def grid_weight_class(size, weight)
322
+ value = Integer(weight || -1)
323
+ "col-#{size}-#{value}" if (1..12).cover?(value)
324
+ end
325
+
326
+ def render_element(tag, widget, widgets, base_class)
327
+ props = common_props(widget).merge(
328
+ className: [base_class, css_class(widget)].reject(&:empty?).join(' '),
329
+ children: widgets.map { render_widget(_1) }
330
+ )
331
+ "React.createElement(#{JSON.generate(tag)}, #{js_props(props)})"
332
+ end
333
+
334
+ def render_custom_widget(widget)
335
+ grid = DataGridBundleCompiler.new(
336
+ @source, @qualified_name, widget,
337
+ render_widgets: ->(widgets, scope, entity) { render_scoped_widgets(widgets, scope, entity) }
338
+ )
339
+ if grid.supported?
340
+ @uses_data_grid = true
341
+ return grid.render
342
+ end
343
+
344
+ gallery = GalleryBundleCompiler.new(@source, @qualified_name, widget)
345
+ if gallery.supported?
346
+ @uses_gallery = true
347
+ nano_reference = nanoflow_reference(gallery.data_source.nanoflow_name) if gallery.data_source.nanoflow?
348
+ return render_unsupported(widget) if gallery.data_source.nanoflow? && !nano_reference
349
+
350
+ @list_scopes << { scope: gallery.widget_key, entity: gallery.entity_name }
351
+ content = children(gallery.content_widgets)
352
+ @list_scopes.pop
353
+ return gallery.render(content, nanoflow_reference: nano_reference)
354
+ end
355
+
356
+ object_scope = current_object_scope
357
+ image = ImageBundleCompiler.new(
358
+ @source, @qualified_name, widget,
359
+ scope: object_scope&.fetch(:scope, nil), entity: object_scope&.fetch(:entity, nil),
360
+ key_prefix: @scope_prefix,
361
+ action_property: lambda do |action|
362
+ config = client_action_config(widget, action)
363
+ "ActionProperty(#{js_literal(config)})" if config
364
+ end
365
+ )
366
+ if image.supported?
367
+ @uses_image = true
368
+ @uses_custom_image = true
369
+ return image.render
370
+ end
371
+
372
+ combo = ComboBoxBundleCompiler.new(
373
+ @source, @qualified_name, widget,
374
+ scope: scope_name(@data_view_scopes.last), entity: @data_view_scopes.last&.fetch(:entity, nil)
375
+ )
376
+ if combo.supported?
377
+ @uses_form_widgets = true
378
+ @uses_combo_box = true
379
+ return combo.render
380
+ end
381
+
382
+ generic = GenericWidgetBundleCompiler.new(
383
+ @source, @qualified_name, widget,
384
+ scope: object_scope&.fetch(:scope, nil), entity: object_scope&.fetch(:entity, nil),
385
+ render_widgets: ->(widgets) { raw_js(children(widgets)) }, key_prefix: @scope_prefix,
386
+ action_property: lambda do |action|
387
+ config = if nanoflow_action?(action)
388
+ nanoflow_action_config(action)
389
+ else
390
+ client_action_config(widget, action)
391
+ end
392
+ "ActionProperty(#{js_literal(config)})" if config
393
+ end
394
+ )
395
+ return render_unsupported(widget) unless generic.supported?
396
+
397
+ @generic_widgets[generic.component_name] = generic.module_path
398
+ generic.render
399
+ end
400
+
401
+ def render_scoped_widgets(widgets, scope, entity)
402
+ @list_scopes << { scope:, entity: }
403
+ children(widgets)
404
+ ensure
405
+ @list_scopes.pop
406
+ end
407
+
408
+ def render_container(widget)
409
+ action = widget['OnClickAction'] || {}
410
+ action_config = container_action_config(widget, action)
411
+ return render_action_container(widget, action_config) if action_config
412
+
413
+ props = common_props(widget).merge(
414
+ className: css_class(widget), children: array(widget['Widgets']).map { render_widget(_1) }
415
+ )
416
+ handler = action_handler(action) if action['$Type'] && action['$Type'] != 'Forms$NoAction'
417
+ props[:role] = 'button' if handler
418
+ "React.createElement(#{JSON.generate(render_mode(widget))}, " \
419
+ "#{js_props(props, expressions: { onClick: handler }.compact)})"
420
+ end
421
+
422
+ def render_action_container(widget, action_config)
423
+ @uses_form_widgets = true
424
+ @uses_container = true
425
+ key = widget_key(widget)
426
+ props = common_props(widget).merge(
427
+ '$widgetId': key, class: css_class(widget), renderMode: render_mode(widget),
428
+ content: array(widget['Widgets']).map { render_widget(_1) }
429
+ )
430
+ "React.createElement($Container, #{js_props(props, expressions: {
431
+ onClick: "ActionProperty(#{js_literal(action_config)})"
432
+ })})"
433
+ end
434
+
435
+ def container_action_config(widget, action)
436
+ return nanoflow_action_config(action) if nanoflow_action?(action)
437
+
438
+ client_action_config(widget, action)
439
+ end
440
+
441
+ def render_text(widget)
442
+ return render_bound_text(widget) if bound_text_attributes(widget)
443
+
444
+ props = common_props(widget).merge(className: css_class(widget))
445
+ caption = translated_text(widget.dig('Content', 'Template'))
446
+ "React.createElement(#{JSON.generate(text_mode(widget))}, #{js_props(props)}, #{JSON.generate(caption)})"
447
+ end
448
+
449
+ def render_title(widget)
450
+ props = common_props(widget).merge(className: css_class(widget))
451
+ "React.createElement(\"h1\", #{js_props(props)}, #{JSON.generate(page_title)})"
452
+ end
453
+
454
+ def render_group_box(widget)
455
+ @uses_form_widgets = true
456
+ @uses_group_box = true
457
+ collapsible = case widget['Collapsible']
458
+ when 'YesInitiallyCollapsed' then 'yes'
459
+ when 'YesInitiallyExpanded' then 'expanded'
460
+ else 'no'
461
+ end
462
+ props = common_props(widget).merge(
463
+ '$widgetId': widget_key(widget), class: css_class(widget), id: widget_key(widget),
464
+ renderMode: render_mode('RenderMode' => widget['HeaderMode']), collapsible:,
465
+ content: array(widget['Widgets']).map { render_widget(_1) }
466
+ )
467
+ caption = translated_text(widget.dig('CaptionTemplate', 'Template'))
468
+ "React.createElement($GroupBox, #{js_props(props, expressions: {
469
+ header: "TextProperty({ value: #{JSON.generate(caption)} })"
470
+ })})"
471
+ end
472
+
473
+ def render_snippet_call(widget)
474
+ qualified = widget.dig('FormCall', 'Form').to_s
475
+ snippet = snippet_index[qualified]
476
+ return render_unsupported(widget) unless snippet && !@snippet_stack.include?(qualified)
477
+
478
+ @snippet_stack << qualified
479
+ @snippet_documents << snippet.document
480
+ @snippet_scopes << snippet_scope_map(snippet.document)
481
+ rendered = children(array(snippet.document['Widgets']))
482
+ @snippet_scopes.pop
483
+ @snippet_documents.pop
484
+ @snippet_stack.pop
485
+ "React.createElement(React.Fragment, #{js_props(common_props(widget))}, #{rendered})"
486
+ ensure
487
+ if @snippet_stack.last == qualified
488
+ @snippet_scopes.pop
489
+ @snippet_documents.pop
490
+ @snippet_stack.pop
491
+ end
492
+ end
493
+
494
+ def snippet_scope_map(document)
495
+ scope = current_object_scope
496
+ return {} unless scope
497
+
498
+ object_parameters = array(document['Parameters']).select do |parameter|
499
+ parameter.dig('ParameterType', '$Type') == 'DataTypes$ObjectType'
500
+ end
501
+ object_parameters.to_h { [_1['Name'].to_s, scope] }
502
+ end
503
+
504
+ def snippet_index
505
+ @snippet_index ||= @source.units_of('Forms$Snippet').to_h do |unit|
506
+ ["#{unit.module_name}.#{unit.document['Name']}", unit]
507
+ end
508
+ end
509
+
510
+ def render_bound_text(widget)
511
+ scope = current_object_scope
512
+ attributes = bound_text_attributes(widget)
513
+ parameters = array(widget.dig('Content', 'Parameters'))
514
+ @uses_bound_text = true
515
+ expressions = attributes.map.with_index do |attribute, index|
516
+ ["value#{index + 1}".to_sym, bound_text_value(scope, attribute, parameters[index])]
517
+ end.to_h
518
+ "React.createElement($MxrbFormattedText, #{js_props(bound_text_props(widget), expressions:)})"
519
+ end
520
+
521
+ def bound_text_value(scope, attribute, parameter)
522
+ entity, _, name = attribute.rpartition('.')
523
+ attribute_property(scope.fetch(:scope), entity, name, path: attribute_reference_path(parameter))
524
+ end
525
+
526
+ def attribute_reference_path(parameter)
527
+ steps = array(parameter&.dig('AttributeRef', 'EntityRef', 'Steps'))
528
+ steps.flat_map { [_1['Association'], _1['DestinationEntity']] }
529
+ .select { present_identifier?(_1) }.join('/')
530
+ end
531
+
532
+ def bound_text_props(widget)
533
+ common_props(widget).merge(
534
+ '$widgetId': widget_key(widget), class: css_class(widget), renderMode: text_mode(widget),
535
+ template: translated_text(widget.dig('Content', 'Template'))
536
+ )
537
+ end
538
+
539
+ def bound_text_attributes(widget)
540
+ return unless current_object_scope
541
+
542
+ parameters = array(widget.dig('Content', 'Parameters'))
543
+ return if parameters.empty?
544
+
545
+ attributes = parameters.map { text_parameter_attribute(_1) }
546
+ attributes if attributes.all?
547
+ end
548
+
549
+ def text_parameter_attribute(parameter)
550
+ attribute = parameter.dig('AttributeRef', 'Attribute').to_s
551
+ return attribute if qualified_attribute?(attribute)
552
+
553
+ match = parameter['Expression'].to_s.match(
554
+ %r{\A(?:toString\()?\$currentObject/([A-Za-z_]\w*)\)?\z}
555
+ )
556
+ entity = current_object_scope&.fetch(:entity, '').to_s
557
+ return unless match && present_identifier?(entity)
558
+
559
+ "#{entity}.#{match[1]}"
560
+ end
561
+
562
+ def qualified_attribute?(attribute)
563
+ entity, separator, name = attribute.rpartition('.')
564
+ separator == '.' && present_identifier?(entity) && present_identifier?(name)
565
+ end
566
+
567
+ def current_object_scope
568
+ scope = @list_scopes.last || @data_view_scopes.last
569
+ return scope if scope.is_a?(Hash)
570
+ return unless scope
571
+
572
+ { scope:, entity: '' }
573
+ end
574
+
575
+ def scope_name(scope) = scope.is_a?(Hash) ? scope[:scope] : scope
576
+
577
+ def wrap_conditional_visibility(widget, rendered)
578
+ condition = conditional_visibility(widget['ConditionalVisibilitySettings'])
579
+ return rendered unless condition
580
+
581
+ @uses_conditional = true
582
+ attributes, predicate = condition
583
+ expressions = attributes.map.with_index do |attribute, index|
584
+ entity, _, name = attribute.rpartition('.')
585
+ ["value#{index + 1}".to_sym,
586
+ attribute_property(current_object_scope.fetch(:scope), entity, name)]
587
+ end.to_h.merge(test: "props => #{predicate}")
588
+ visibility_key = "#{widget_key(widget)}$visibility"
589
+ props = common_props(widget).merge(key: visibility_key, '$widgetId': visibility_key)
590
+ "React.createElement($MxrbConditional, #{js_props(props, expressions:)}, #{rendered})"
591
+ end
592
+
593
+ def conditional_visibility(settings)
594
+ expression = settings&.fetch('Expression', '').to_s.strip
595
+ scope = current_object_scope
596
+ return if expression.empty? || scope.nil? || !present_identifier?(scope[:entity])
597
+
598
+ attributes = []
599
+ predicate = visibility_logical(expression, attributes)
600
+ [attributes, predicate] if predicate
601
+ end
602
+
603
+ def visibility_logical(expression, attributes)
604
+ parts = expression.split(/\s+or\s+/i)
605
+ return logical_predicate(parts, attributes, '||') if parts.length > 1
606
+
607
+ parts = expression.split(/\s+and\s+/i)
608
+ return logical_predicate(parts, attributes, '&&') if parts.length > 1
609
+
610
+ visibility_atom(expression, attributes)
611
+ end
612
+
613
+ def logical_predicate(parts, attributes, operator)
614
+ predicates = parts.map { visibility_logical(_1.strip, attributes) }
615
+ return unless predicates.all?
616
+
617
+ "(#{predicates.join(" #{operator} ")})"
618
+ end
619
+
620
+ def visibility_atom(expression, attributes)
621
+ match = expression.strip.match(
622
+ %r{\A\$currentObject/([A-Za-z_]\w*)(?:\s*(=|!=)\s*(.+))?\z}
623
+ )
624
+ return unless match
625
+
626
+ attribute = "#{current_object_scope.fetch(:entity)}.#{match[1]}"
627
+ index = attributes.index(attribute) || attributes.length.tap { attributes << attribute }
628
+ value = "mxrbValue(props.value#{index + 1})"
629
+ return "Boolean(#{value})" unless match[2]
630
+
631
+ comparison = visibility_comparison(value, match[3].strip)
632
+ return unless comparison
633
+
634
+ match[2] == '!=' ? "!(#{comparison})" : comparison
635
+ end
636
+
637
+ def visibility_comparison(value, expected)
638
+ case expected
639
+ when 'empty' then "(#{value} == null || #{value} === \"\")"
640
+ when 'true', 'false' then "#{value} === #{expected}"
641
+ when /\A-?\d+(?:\.\d+)?\z/ then "Number(#{value}) === #{expected}"
642
+ when /\A[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)+\z/
643
+ "String(#{value}) === #{JSON.generate(expected.split('.').last)}"
644
+ when /\A'(.*)'\z/m then "String(#{value}) === #{JSON.generate(Regexp.last_match(1))}"
645
+ end
646
+ end
647
+
648
+ def wrap_dynamic_classes(widget, rendered)
649
+ expression = widget.dig('Appearance', 'DynamicClasses').to_s.strip
650
+ scope = current_object_scope
651
+ return rendered if expression.empty? || scope.nil? || !present_identifier?(scope[:entity])
652
+
653
+ attributes = []
654
+ resolver = dynamic_class_expression(expression, attributes)
655
+ return rendered unless resolver
656
+
657
+ @uses_dynamic_class = true
658
+ expressions = attributes.map.with_index do |attribute, index|
659
+ entity, _, name = attribute.rpartition('.')
660
+ ["value#{index + 1}".to_sym, attribute_property(scope.fetch(:scope), entity, name)]
661
+ end.to_h.merge(resolveClass: "props => String(#{resolver} || '').trim()")
662
+ class_key = "#{widget_key(widget)}$class"
663
+ props = common_props(widget).merge(key: class_key, '$widgetId': class_key)
664
+ "React.createElement($MxrbDynamicClass, #{js_props(props, expressions:)}, #{rendered})"
665
+ end
666
+
667
+ def dynamic_class_expression(expression, attributes)
668
+ terms = split_dynamic_class_terms(expression)
669
+ compiled = terms.map { dynamic_class_term(_1, attributes) }
670
+ return unless compiled.all?
671
+
672
+ compiled.join(' + ')
673
+ end
674
+
675
+ def split_dynamic_class_terms(expression)
676
+ parts = []
677
+ start = 0
678
+ depth = 0
679
+ quote = nil
680
+ expression.each_char.with_index do |character, index|
681
+ if quote
682
+ quote = nil if character == quote && expression[index - 1] != '\\'
683
+ elsif ["'", '"'].include?(character)
684
+ quote = character
685
+ elsif character == '('
686
+ depth += 1
687
+ elsif character == ')'
688
+ depth -= 1
689
+ elsif character == '+' && depth.zero?
690
+ parts << expression[start...index].strip
691
+ start = index + 1
692
+ end
693
+ end
694
+ parts << expression[start..].to_s.strip
695
+ parts.reject(&:empty?)
696
+ end
697
+
698
+ def dynamic_class_term(term, attributes)
699
+ stripped = unwrap_dynamic_class_parentheses(term.strip)
700
+ quoted = quoted_dynamic_class(stripped)
701
+ return quoted if quoted
702
+
703
+ if (match = stripped.match(%r{\A\$currentObject/([A-Za-z_]\w*)\z}))
704
+ return "String(#{dynamic_class_value(match[1], attributes)} ?? '')"
705
+ end
706
+
707
+ dynamic_class_conditional(stripped, attributes)
708
+ end
709
+
710
+ def quoted_dynamic_class(term)
711
+ match = term.match(/\A'(.*)'\z/m) || term.match(/\A"(.*)"\z/m)
712
+ JSON.generate(match[1]) if match
713
+ end
714
+
715
+ def unwrap_dynamic_class_parentheses(term)
716
+ return term unless term.start_with?('(') && term.end_with?(')')
717
+
718
+ term[1...-1].strip
719
+ end
720
+
721
+ def dynamic_class_conditional(term, attributes)
722
+ match = term.match(/\Aif\s+(.+?)\s+then\s+(.+?)\s+else\s+(.+)\z/m)
723
+ return unless match
724
+
725
+ predicate = visibility_logical(match[1].strip, attributes)
726
+ accepted = dynamic_class_expression(match[2].strip, attributes)
727
+ rejected = dynamic_class_expression(match[3].strip, attributes)
728
+ return unless predicate && accepted && rejected
729
+
730
+ "(#{predicate} ? #{accepted} : #{rejected})"
731
+ end
732
+
733
+ def dynamic_class_value(name, attributes)
734
+ attribute = "#{current_object_scope.fetch(:entity)}.#{name}"
735
+ index = attributes.index(attribute) || attributes.length.tap { attributes << attribute }
736
+ "mxrbValue(props.value#{index + 1})"
737
+ end
738
+
739
+ def render_action_button(widget)
740
+ action = widget['Action'] || {}
741
+ return render_data_action_button(widget, action) if data_action?(action)
742
+ return render_nanoflow_action_button(widget, action) if nanoflow_action?(action)
743
+ return render_client_action_button(widget, action) if client_action_config(widget, action)
744
+
745
+ caption = translated_text(widget.dig('CaptionTemplate', 'Template'))
746
+ classes = ['btn', 'mx-button', button_style(widget), css_class(widget)].reject(&:empty?).join(' ')
747
+ props = common_props(widget).merge(type: 'button', className: classes)
748
+ handler = action_handler(action)
749
+ props[:disabled] = true unless handler
750
+ "React.createElement(\"button\", #{js_props(props, expressions: { onClick: handler }.compact)}, " \
751
+ "#{JSON.generate(caption)})"
752
+ end
753
+
754
+ def render_client_action_button(widget, action)
755
+ @uses_form_widgets = true
756
+ key = widget_key(widget)
757
+ caption = translated_text(widget.dig('CaptionTemplate', 'Template'))
758
+ "React.createElement($ActionButton, #{js_props(nanoflow_button_props(widget, key), expressions: {
759
+ caption: "TextProperty({ value: #{JSON.generate(caption)} })",
760
+ tooltip: 'TextProperty({ value: "" })',
761
+ action: "ActionProperty(#{js_literal(client_action_config(widget, action))})"
762
+ })})"
763
+ end
764
+
765
+ def client_action_config(widget, action)
766
+ payload = close_page_config(action) || open_link_config(action) || open_page_config(action) ||
767
+ create_object_config(widget, action) || sign_out_config(action) ||
768
+ microflow_config(widget, action)
769
+ return unless payload
770
+
771
+ { action: payload, abortOnServerValidation: true }
772
+ end
773
+
774
+ def open_page_config(action)
775
+ return unless action['$Type'] == 'Forms$FormAction'
776
+
777
+ form = action.dig('FormSettings', 'Form').to_s
778
+ return unless present_identifier?(form)
779
+ return unless parameter_mappings(action['FormSettings'] || {}).empty?
780
+
781
+ config = {
782
+ name: "#{form.tr('.', '/')}.page.xml", location: 'content',
783
+ allowedRoles: allowed_roles_for('Forms$Page', form)
784
+ }
785
+ {
786
+ type: 'openPage', argMap: {}, config:,
787
+ disabledDuringExecution: action.fetch('DisabledDuringExecution', true)
788
+ }
789
+ end
790
+
791
+ def sign_out_config(action)
792
+ return unless action['$Type'] == 'Forms$SignOutClientAction'
793
+
794
+ {
795
+ type: 'signOut', argMap: {}, config: { namedUser: true },
796
+ disabledDuringExecution: action.fetch('DisabledDuringExecution', true)
797
+ }
798
+ end
799
+
800
+ def create_object_config(widget, action)
801
+ return unless action['$Type'] == 'Forms$CreateObjectClientAction'
802
+
803
+ entity = action.dig('EntityRef', 'Entity').to_s
804
+ form = action.dig('PageSettings', 'Form').to_s
805
+ parameter = target_page_parameter(form, entity)
806
+ target = page_unit(form)
807
+ return unless present_identifier?(entity) && target && parameter
808
+
809
+ {
810
+ type: 'createObject', argMap: {},
811
+ config: {
812
+ entity:, operationId: WebOperationCompiler.operation_id(@qualified_name, widget['Name']),
813
+ pageSettings: {
814
+ name: "#{form.tr('.', '/')}.page.xml", location: page_location(target),
815
+ resizable: target.document.fetch('PopupResizable', true),
816
+ allowedRoles: allowed_roles_for('Forms$Page', form)
817
+ },
818
+ allowedRoles: allowed_roles_for('Forms$Page', form),
819
+ objectParameter: "param#{parameter}"
820
+ },
821
+ disabledDuringExecution: action.fetch('DisabledDuringExecution', true)
822
+ }
823
+ end
824
+
825
+ def page_location(unit)
826
+ layout_name = unit.document.dig('FormCall', 'Form').to_s
827
+ target_layout = @source.units_of('Forms$Layout').find do |candidate|
828
+ "#{candidate.module_name}.#{candidate.document['Name']}" == layout_name
829
+ end
830
+ popup_layout_document?(target_layout&.document) ? 'modal' : 'content'
831
+ end
832
+
833
+ def allowed_roles_for(type, qualified_name)
834
+ unit = @source.units_of(type).find do |candidate|
835
+ "#{candidate.module_name}.#{candidate.document['Name']}" == qualified_name
836
+ end
837
+ return [] unless unit
838
+
839
+ module_roles = array(unit.document.fetch('AllowedModuleRoles', nil)).map(&:to_s)
840
+ security = @source.documents('Security$ProjectSecurity').first || {}
841
+ user_roles = array(security.fetch('UserRoles', nil))
842
+ if security.fetch('SecurityLevel', nil) == 'CheckNothing'
843
+ return user_roles.map { _1['Name'].to_s }.reject(&:empty?)
844
+ end
845
+
846
+ user_roles.select do |role|
847
+ (array(role['ModuleRoles']).map(&:to_s) & module_roles).any?
848
+ end.map { _1['Name'].to_s }
849
+ end
850
+
851
+ def close_page_config(action)
852
+ return unless action['$Type'] == 'Forms$ClosePageClientAction'
853
+
854
+ { type: 'closePage', argMap: {}, config: {},
855
+ disabledDuringExecution: action.fetch('DisabledDuringExecution', true) }
856
+ end
857
+
858
+ def open_link_config(action)
859
+ return unless action['$Type'] == 'Forms$OpenLinkClientAction'
860
+
861
+ address = action['Address'] || {}
862
+ config = { schema: action['LinkType'].to_s.downcase }
863
+ arg_map = {}
864
+ if address['IsDynamic'] == true
865
+ attribute = address.dig('AttributeRef', 'Attribute').to_s
866
+ scope = scope_name(@data_view_scopes.last)
867
+ return unless scope && attribute.include?('.')
868
+
869
+ config[:addressAttribute] = attribute.sub(/\.([^.]+)\z/, '/\\1')
870
+ arg_map[:'$object'] = { widget: scope, source: 'object' }
871
+ else
872
+ config[:address] = address['Value'].to_s
873
+ end
874
+ { type: 'openLink', argMap: arg_map, config:,
875
+ disabledDuringExecution: action.fetch('DisabledDuringExecution', true) }
876
+ end
877
+
878
+ def microflow_config(widget, action)
879
+ return unless action['$Type'] == 'Forms$MicroflowAction'
880
+
881
+ settings = action['MicroflowSettings'] || {}
882
+ return unless present_identifier?(settings['Microflow'])
883
+
884
+ arg_map = microflow_argument_map(settings)
885
+ return unless arg_map
886
+
887
+ {
888
+ type: 'callMicroflow', argMap: arg_map,
889
+ config: { operationId: WebOperationCompiler.operation_id(@qualified_name, widget['Name']) },
890
+ disabledDuringExecution: action.fetch('DisabledDuringExecution', true)
891
+ }
892
+ end
893
+
894
+ def microflow_argument_map(settings)
895
+ pairs = parameter_mappings(settings).map { microflow_argument(_1) }
896
+ return pairs.to_h if pairs.any? && pairs.all?
897
+ return unless pairs.empty?
898
+
899
+ inferred_microflow_argument_map(settings['Microflow'])
900
+ end
901
+
902
+ def inferred_microflow_argument_map(qualified_name)
903
+ scope = current_object_scope
904
+ return {} unless scope && present_identifier?(scope[:entity])
905
+
906
+ inferred_microflow_parameters(qualified_name, scope[:entity]).to_h do |parameter|
907
+ [parameter['Name'].to_sym, { widget: scope[:scope], source: 'object' }]
908
+ end
909
+ end
910
+
911
+ def inferred_microflow_parameters(qualified_name, entity)
912
+ flow = @source.units_of('Microflows$Microflow').find do |unit|
913
+ "#{unit.module_name}.#{unit.document['Name']}" == qualified_name
914
+ end
915
+ array(flow&.document&.dig('ObjectCollection', 'Objects')).select do |object|
916
+ object['$Type'] == 'Microflows$MicroflowParameter' &&
917
+ object.dig('VariableType', 'Entity') == entity
918
+ end
919
+ end
920
+
921
+ def microflow_argument(mapping)
922
+ name = mapping['Parameter'].to_s.split('.').last
923
+ expression = mapping['Expression'].to_s
924
+ current_scope = current_object_scope&.fetch(:scope)
925
+ scope = if expression == '$currentObject'
926
+ current_scope
927
+ elsif expression.empty?
928
+ microflow_variable_scope(mapping['Variable'])
929
+ else
930
+ expression
931
+ end
932
+ return unless present_identifier?(name) && scope.to_s.match?(/\A\$[A-Za-z_]\w*\z|\Ap\./)
933
+
934
+ [name.to_sym, { widget: scope, source: 'object' }]
935
+ end
936
+
937
+ def microflow_variable_scope(variable)
938
+ return unless variable
939
+
940
+ snippet = variable['SnippetParameter'].to_s
941
+ unless snippet.empty?
942
+ @snippet_scopes.reverse_each do |mapping|
943
+ return mapping.fetch(snippet).fetch(:scope) if mapping.key?(snippet)
944
+ end
945
+ end
946
+ page = variable['PageParameter'].to_s
947
+ return "$#{page}" if present_identifier?(page)
948
+
949
+ widget = variable['Widget'].to_s
950
+ target = page_widget(widget)
951
+ return widget_key(target) if present_identifier?(widget) && target
952
+
953
+ local = variable['LocalVariable'].to_s
954
+ "$#{local}" if present_identifier?(local)
955
+ end
956
+
957
+ def nanoflow_action?(action)
958
+ action['$Type'] == 'Forms$CallNanoflowClientAction' &&
959
+ present_identifier?(action['Nanoflow']) && nanoflow_argument_map(action) &&
960
+ nanoflow_reference(action['Nanoflow'])
961
+ end
962
+
963
+ def parameter_mappings(action)
964
+ array(action['ParameterMappings']).select { _1.is_a?(Hash) }
965
+ end
966
+
967
+ def render_nanoflow_action_button(widget, action)
968
+ @uses_form_widgets = true
969
+ key = widget_key(widget)
970
+ caption = translated_text(widget.dig('CaptionTemplate', 'Template'))
971
+ "React.createElement($ActionButton, #{js_props(nanoflow_button_props(widget, key), expressions: {
972
+ caption: "TextProperty({ value: #{JSON.generate(caption)} })",
973
+ tooltip: 'TextProperty({ value: "" })',
974
+ action: "ActionProperty(#{js_literal(nanoflow_action_config(action))})"
975
+ })})"
976
+ end
977
+
978
+ def nanoflow_button_props(widget, key)
979
+ common_props(widget).merge(
980
+ '$widgetId': key, buttonId: key, class: css_class(widget), renderType: 'button',
981
+ buttonClass: button_style(widget)
982
+ )
983
+ end
984
+
985
+ def nanoflow_action_config(action)
986
+ nanoflow = raw_js(nanoflow_reference(action['Nanoflow']))
987
+ {
988
+ action: {
989
+ type: 'callNanoflow', argMap: nanoflow_argument_map(action), config: { nanoflow: },
990
+ disabledDuringExecution: action.fetch('DisabledDuringExecution', true)
991
+ },
992
+ abortOnServerValidation: false, skipClientValidation: false
993
+ }
994
+ end
995
+
996
+ def nanoflow_argument_map(action)
997
+ parameter_mappings(action).map { nanoflow_argument(_1) }.then do |pairs|
998
+ pairs.all? ? pairs.to_h : nil
999
+ end
1000
+ end
1001
+
1002
+ def nanoflow_argument(mapping)
1003
+ name = mapping['Parameter'].to_s.split('.').last
1004
+ variable = mapping['Expression'].to_s[/\A\$([A-Za-z_]\w*)\z/, 1]
1005
+ return unless present_identifier?(name) && variable
1006
+
1007
+ scope = current_object_scope
1008
+ page_parameter = array(@unit.document['Parameters']).any? { _1['Name'] == variable }
1009
+ widget = if page_parameter
1010
+ "$#{variable}"
1011
+ elsif scope && variable == scope[:entity].to_s.split('.').last
1012
+ scope[:scope]
1013
+ else
1014
+ "$#{variable}"
1015
+ end
1016
+ expression = {
1017
+ expr: { type: 'variable', variable: },
1018
+ args: { variable.to_sym => { widget:, source: 'object' } }
1019
+ }
1020
+ [name.to_sym, { expression:, kind: 'object' }]
1021
+ end
1022
+
1023
+ def nanoflow_reference(name) = @nanoflow_programs.reference(name.to_s)
1024
+
1025
+ def raw_js(value) = { '$raw' => value }
1026
+
1027
+ def render_data_view(widget)
1028
+ @uses_form_widgets = true
1029
+ scope = widget_key(widget)
1030
+ object = data_view_object_property(widget, scope)
1031
+ return render_unsupported(widget) unless object
1032
+
1033
+ @data_view_scopes << { scope:, entity: data_view_entity(widget) }
1034
+ body = array(widget['Widgets']).map { render_widget(_1) }
1035
+ footer = array(widget['FooterWidgets']).map { render_widget(_1) }
1036
+ @data_view_scopes.pop
1037
+ props = common_props(widget).merge(
1038
+ '$widgetId': scope, class: css_class(widget), body:, footer:,
1039
+ hideFooter: !widget.fetch('ShowFooter', true)
1040
+ )
1041
+ expressions = {
1042
+ object:,
1043
+ emptyMessage: "TextProperty({ value: #{JSON.generate(translated_text(widget['NoEntityMessage']))} })"
1044
+ }
1045
+ "React.createElement($DataView, #{js_props(props, expressions:)})"
1046
+ end
1047
+
1048
+ def data_view_object_property(widget, scope)
1049
+ source = widget['DataSource'] || {}
1050
+ return listen_object_property(source, widget) if source['$Type'] == 'Forms$ListenTargetSource'
1051
+
1052
+ source_scope = data_source_scope(source)
1053
+ path = entity_ref_path(source['EntityRef'])
1054
+ if source_scope
1055
+ if path.empty? && source.dig('SourceVariable', 'PageParameter')
1056
+ return "AssociationObjectProperty({ scope: #{JSON.generate(source_scope[:scope])}, " \
1057
+ 'path: "", editable: true })'
1058
+ end
1059
+ return association_object_property(source_scope[:scope], path, widget['Name'])
1060
+ end
1061
+ current_scope = current_object_scope
1062
+ return association_object_property(current_scope[:scope], path, widget['Name']) if current_scope && !path.empty?
1063
+ return microflow_object_property(source, widget, scope) if source['$Type'] == 'Forms$MicroflowSource'
1064
+ return nanoflow_object_property(source, scope) if source['$Type'] == 'Forms$NanoflowSource'
1065
+
1066
+ nil
1067
+ end
1068
+
1069
+ def listen_object_property(source, widget)
1070
+ target_name = source['ListenTarget'].to_s
1071
+ target = page_widget(target_name)
1072
+ return unless present_identifier?(target_name) && target
1073
+
1074
+ @uses_listen_object = true
1075
+ config = {
1076
+ listenTo: widget_key(target), editable: true,
1077
+ operationId: WebOperationCompiler.operation_id(@qualified_name, widget['Name'])
1078
+ }
1079
+ "ListenObjectProperty(#{js_literal(config)})"
1080
+ end
1081
+
1082
+ def page_widget(name)
1083
+ roots = [*@snippet_documents.reverse, @unit.document]
1084
+ roots.each do |root|
1085
+ widget = all_page_widgets(root).find { _1['Name'] == name }
1086
+ return widget if widget
1087
+ end
1088
+ nil
1089
+ end
1090
+
1091
+ def all_page_widgets(value, result = [])
1092
+ case value
1093
+ when Hash
1094
+ result << value if value['$Type'].to_s.start_with?('Forms$', 'CustomWidgets$')
1095
+ value.each_value { all_page_widgets(_1, result) }
1096
+ when Array then value.each { all_page_widgets(_1, result) }
1097
+ end
1098
+ result
1099
+ end
1100
+
1101
+ def data_view_entity(widget)
1102
+ source = widget['DataSource'] || {}
1103
+ if source['$Type'] == 'Forms$ListenTargetSource'
1104
+ target = page_widget(source['ListenTarget'].to_s)
1105
+ return target ? WebListDataSource.new(@source, target).entity : ''
1106
+ end
1107
+
1108
+ entity = entity_ref_destination(source['EntityRef'])
1109
+ return entity unless entity.empty?
1110
+
1111
+ parameter = source.dig('SourceVariable', 'PageParameter').to_s
1112
+ page_parameter = array(@unit.document['Parameters']).find { _1['Name'] == parameter }
1113
+ page_parameter&.dig('ParameterType', 'Entity').to_s.then do |parameter_entity|
1114
+ parameter_entity.empty? ? flow_return_entity(source) : parameter_entity
1115
+ end
1116
+ end
1117
+
1118
+ def data_source_scope(source)
1119
+ variable = source['SourceVariable'] || {}
1120
+ parameter = variable['PageParameter'].to_s
1121
+ return { scope: "$#{parameter}" } if present_identifier?(parameter)
1122
+
1123
+ snippet_parameter = variable['SnippetParameter'].to_s
1124
+ return unless present_identifier?(snippet_parameter)
1125
+
1126
+ @snippet_scopes.reverse_each do |mapping|
1127
+ return mapping[snippet_parameter] if mapping.key?(snippet_parameter)
1128
+ end
1129
+ nil
1130
+ end
1131
+
1132
+ def association_object_property(source_scope, path, widget_name)
1133
+ @uses_association_object = true
1134
+ config = { scope: source_scope, path:, editable: true }
1135
+ config[:operationId] = WebOperationCompiler.operation_id(@qualified_name, widget_name) unless path.empty?
1136
+ "AssociationObjectProperty(#{js_literal(config)})"
1137
+ end
1138
+
1139
+ def microflow_object_property(source, widget, scope)
1140
+ settings = source['MicroflowSettings'] || source
1141
+ name = settings['Microflow'].to_s
1142
+ arg_map = microflow_argument_map(settings)
1143
+ return unless present_identifier?(name) && arg_map
1144
+
1145
+ @uses_microflow_object = true
1146
+ config = {
1147
+ dataSourceId: scope, operationId: WebOperationCompiler.operation_id(@qualified_name, widget['Name']),
1148
+ editable: true, argMap: arg_map
1149
+ }
1150
+ "MicroflowObjectProperty(#{js_literal(config)})"
1151
+ end
1152
+
1153
+ def nanoflow_object_property(source, scope)
1154
+ reference = nanoflow_reference(source['Nanoflow'])
1155
+ return unless reference && parameter_mappings(source).empty?
1156
+
1157
+ @uses_nanoflow_object = true
1158
+ config = {
1159
+ dataSourceId: scope, editable: true,
1160
+ source: { nanoflow: raw_js(reference) }, argMap: {}
1161
+ }
1162
+ "NanoflowObjectProperty(#{js_literal(config)})"
1163
+ end
1164
+
1165
+ def entity_ref_destination(reference)
1166
+ steps = array(reference&.fetch('Steps', nil))
1167
+ steps.last&.fetch('DestinationEntity', nil).to_s.then do |entity|
1168
+ entity.empty? ? reference&.fetch('Entity', nil).to_s : entity
1169
+ end
1170
+ end
1171
+
1172
+ def entity_ref_path(reference)
1173
+ array(reference&.fetch('Steps', nil)).flat_map do |step|
1174
+ [step['Association'], step['DestinationEntity']]
1175
+ end.map(&:to_s).reject(&:empty?).join('/')
1176
+ end
1177
+
1178
+ def flow_return_entity(source)
1179
+ type, name = if source['$Type'] == 'Forms$MicroflowSource'
1180
+ ['Microflows$Microflow', source.dig('MicroflowSettings', 'Microflow')]
1181
+ elsif source['$Type'] == 'Forms$NanoflowSource'
1182
+ ['Microflows$Nanoflow', source['Nanoflow']]
1183
+ end
1184
+ return '' unless type && present_identifier?(name)
1185
+
1186
+ flow = @source.units_of(type).find { "#{_1.module_name}.#{_1.document['Name']}" == name }
1187
+ flow&.document&.dig('MicroflowReturnType', 'Entity').to_s
1188
+ end
1189
+
1190
+ def render_list_view(widget)
1191
+ data_source = WebListDataSource.new(@source, widget)
1192
+ return render_unsupported(widget) unless data_source.supported? && present_identifier?(data_source.entity)
1193
+
1194
+ key = widget_key(widget)
1195
+ list_value = list_view_property(widget, key, data_source)
1196
+ return render_unsupported(widget) unless list_value
1197
+
1198
+ @uses_list_view = true
1199
+ @list_scopes << { scope: key, entity: data_source.entity }
1200
+ content = children(array(widget['Widgets']))
1201
+ @list_scopes.pop
1202
+ props = common_props(widget).merge(
1203
+ '$widgetId': key, class: css_class(widget), pageSize: positive_integer(widget['PageSize'], 20)
1204
+ )
1205
+ expressions = {
1206
+ listValue: list_value,
1207
+ itemTemplate: "TemplatedWidgetProperty({ children: () => #{content}, " \
1208
+ "dataSourceId: #{JSON.generate(key)}, editable: #{widget['Editable'] == true} })"
1209
+ }
1210
+ "React.createElement($ListView, #{js_props(props, expressions:)})"
1211
+ end
1212
+
1213
+ def list_view_property(widget, key, source)
1214
+ config = { dataSourceId: key }
1215
+ if source.xpath?
1216
+ config.merge!(
1217
+ operationId: WebOperationCompiler.operation_id(@qualified_name, widget['Name']),
1218
+ entity: source.entity, sort: []
1219
+ )
1220
+ return "DatabaseObjectListProperty(#{js_literal(config)})"
1221
+ end
1222
+ if source.association?
1223
+ current = current_object_scope
1224
+ return unless current
1225
+
1226
+ config.merge!(
1227
+ operationId: WebOperationCompiler.operation_id(@qualified_name, widget['Name']),
1228
+ scope: current[:scope], directPath: source.association_path, sort: []
1229
+ )
1230
+ return "AssociationObjectListProperty(#{js_literal(config)})"
1231
+ end
1232
+ if source.microflow?
1233
+ settings = widget.dig('DataSource', 'MicroflowSettings') || widget['DataSource'] || {}
1234
+ arg_map = microflow_argument_map(settings)
1235
+ return unless arg_map
1236
+
1237
+ config.merge!(
1238
+ operationId: WebOperationCompiler.operation_id(@qualified_name, widget['Name']),
1239
+ argMap: arg_map, fetchOnlyWithAllParams: false
1240
+ )
1241
+ return "MicroflowObjectListProperty(#{js_literal(config)})"
1242
+ end
1243
+
1244
+ reference = nanoflow_reference(source.nanoflow_name)
1245
+ return unless reference
1246
+
1247
+ config.merge!(source: { nanoflow: raw_js(reference) }, argMap: {}, fetchOnlyWithAllParams: false)
1248
+ "NanoflowObjectListProperty(#{js_literal(config)})"
1249
+ end
1250
+
1251
+ def positive_integer(value, fallback)
1252
+ number = Integer(value || 0)
1253
+ number.positive? ? number : fallback
1254
+ rescue ArgumentError, TypeError
1255
+ fallback
1256
+ end
1257
+
1258
+ def render_text_box(widget)
1259
+ scope = current_object_scope&.fetch(:scope, nil)
1260
+ attribute = widget.dig('AttributeRef', 'Attribute').to_s
1261
+ entity, separator, name = attribute.rpartition('.')
1262
+ return render_unsupported(widget) unless scope && separator == '.' &&
1263
+ present_identifier?(entity) && present_identifier?(name)
1264
+
1265
+ key = widget_key(widget)
1266
+ caption = translated_text(widget.dig('LabelTemplate', 'Template'))
1267
+ placeholder = translated_text(widget.dig('PlaceholderTemplate', 'Template'))
1268
+ input_props = common_props(widget).merge(
1269
+ '$widgetId': key, isPassword: widget['IsPasswordBox'] == true,
1270
+ mask: widget['InputMask'].to_s, readOnlyStyle: 'text',
1271
+ maxLength: widget['MaxLengthCode'].to_i.positive? ? widget['MaxLengthCode'].to_i : nil,
1272
+ autocomplete: widget['Autocomplete'] == false ? 'off' : 'on',
1273
+ submitWhileEditing: widget['SubmitBehaviour'] == 'OnTyping',
1274
+ submitDelay: widget['SubmitOnInputDelay'].to_i, id: key
1275
+ )
1276
+ input = "React.createElement($TextBox, #{js_props(input_props, expressions: {
1277
+ inputValue: attribute_property(scope, entity, name),
1278
+ placeholder: "TextProperty({ value: #{JSON.generate(placeholder)} })"
1279
+ })})"
1280
+ group_props = {
1281
+ key: "#{key}$formGroup", '$widgetId': "#{key}$formGroup",
1282
+ class: "mx-name-#{widget['Name']} mx-textbox", control: [input],
1283
+ width: 3, orientation: 'horizontal', labelFor: key
1284
+ }
1285
+ "React.createElement($FormGroup, #{js_props(group_props, expressions: {
1286
+ caption: "TextProperty({ value: #{JSON.generate(caption)} })",
1287
+ hasError: 'TextProperty({ value: false })'
1288
+ })})"
1289
+ end
1290
+
1291
+ def render_text_area(widget)
1292
+ scope = current_object_scope&.fetch(:scope, nil)
1293
+ entity, separator, name = widget.dig('AttributeRef', 'Attribute').to_s.rpartition('.')
1294
+ return render_unsupported(widget) unless scope && separator == '.' &&
1295
+ present_identifier?(entity) && present_identifier?(name)
1296
+
1297
+ @uses_form_widgets = true
1298
+ @uses_text_area = true
1299
+ key = widget_key(widget)
1300
+ caption = translated_text(widget.dig('LabelTemplate', 'Template'))
1301
+ placeholder = translated_text(widget.dig('PlaceholderTemplate', 'Template'))
1302
+ max_length = widget['MaxLengthCode'].to_i
1303
+ input_props = common_props(widget).merge(
1304
+ '$widgetId': key, readOnlyStyle: 'text', numberOfLines: positive_integer(widget['NumberOfLines'], 5),
1305
+ autoGrow: widget['AutoGrow'] == true, maxLength: max_length.positive? ? max_length : nil,
1306
+ autocomplete: widget['Autocomplete'] == false ? 'off' : 'on',
1307
+ submitWhileEditing: %w[WhileEditing OnTyping].include?(widget['SubmitBehaviour']),
1308
+ submitDelay: widget['SubmitOnInputDelay'].to_i, id: key
1309
+ )
1310
+ input = "React.createElement($TextArea, #{js_props(input_props, expressions: {
1311
+ inputValue: attribute_property(scope, entity, name),
1312
+ placeholder: "TextProperty({ value: #{JSON.generate(placeholder)} })",
1313
+ textTooLongMessage: 'TextProperty({ value: "Text is too long" })',
1314
+ counterMessage: 'TextProperty({ value: "" })'
1315
+ })})"
1316
+ render_form_group(widget, key, caption, input, 'mx-textarea')
1317
+ end
1318
+
1319
+ def render_radio_button_group(widget)
1320
+ scope = current_object_scope&.fetch(:scope, nil)
1321
+ entity, separator, name = widget.dig('AttributeRef', 'Attribute').to_s.rpartition('.')
1322
+ return render_unsupported(widget) unless scope && separator == '.' &&
1323
+ present_identifier?(entity) && present_identifier?(name)
1324
+
1325
+ @uses_form_widgets = true
1326
+ @uses_radio_button_group = true
1327
+ key = widget_key(widget)
1328
+ input = "React.createElement($RadioButtonGroup, #{js_props(
1329
+ common_props(widget).merge('$widgetId': key, readOnlyStyle: 'text', id: key,
1330
+ ariaRequired: false, tabIndex: widget['TabIndex'].to_i),
1331
+ expressions: { value: attribute_property(scope, entity, name) }
1332
+ )})"
1333
+ caption = translated_text(widget.dig('LabelTemplate', 'Template'))
1334
+ render_form_group(widget, key, caption, input, 'mx-radiogroup')
1335
+ end
1336
+
1337
+ def render_file_manager(widget)
1338
+ scope = current_object_scope&.fetch(:scope, nil)
1339
+ return render_unsupported(widget) unless scope
1340
+
1341
+ @uses_form_widgets = true
1342
+ @uses_file_manager = true
1343
+ key = widget_key(widget)
1344
+ config = { scope:, path: '', isEditable: true, allowUpload: widget['Type'] != 'Download' }
1345
+ props = common_props(widget).merge(
1346
+ '$widgetId': key, class: css_class(widget), id: key,
1347
+ widgetType: widget['Type'].to_s.downcase.then { _1.empty? ? 'both' : _1 },
1348
+ extensions: widget['AllowedExtensions'].to_s,
1349
+ maxFileSize: positive_integer(widget['MaxFileSize'], 200),
1350
+ showInBrowser: widget['ShowFileInBrowser'] == true
1351
+ )
1352
+ "React.createElement($FileManager, #{js_props(props, expressions: {
1353
+ content: "DynamicFileProperty(#{js_literal(config)})"
1354
+ })})"
1355
+ end
1356
+
1357
+ def render_form_group(widget, key, caption, input, widget_class)
1358
+ group_props = {
1359
+ key: "#{key}$formGroup", '$widgetId': "#{key}$formGroup",
1360
+ class: "mx-name-#{widget['Name']} #{widget_class}", control: [input],
1361
+ width: 3, orientation: 'horizontal', labelFor: key
1362
+ }
1363
+ "React.createElement($FormGroup, #{js_props(group_props, expressions: {
1364
+ caption: "TextProperty({ value: #{JSON.generate(caption)} })",
1365
+ hasError: 'TextProperty({ value: false })'
1366
+ })})"
1367
+ end
1368
+
1369
+ def render_date_picker(widget)
1370
+ scope = current_object_scope&.fetch(:scope, nil)
1371
+ entity, separator, name = widget.dig('AttributeRef', 'Attribute').to_s.rpartition('.')
1372
+ return render_unsupported(widget) unless scope && separator == '.' &&
1373
+ present_identifier?(entity) && present_identifier?(name)
1374
+
1375
+ @uses_form_widgets = true
1376
+ @uses_date_picker = true
1377
+ key = widget_key(widget)
1378
+ caption = translated_text(widget.dig('LabelTemplate', 'Template'))
1379
+ placeholder = translated_text(widget.dig('PlaceholderTemplate', 'Template'))
1380
+ date_format = widget.dig('FormattingInfo', 'DateFormat').to_s
1381
+ mode = date_format.casecmp('time').zero? ? 'time' : 'date'
1382
+ formatting = mode == 'time' ? { timeFormat: { type: 'time' } } : { dateFormat: { type: 'date' } }
1383
+ input_props = common_props(widget).merge(
1384
+ '$widgetId': key, mode:, showCalendarButton: widget.fetch('ShowCalendarButton', true),
1385
+ readOnlyStyle: 'text', id: key
1386
+ )
1387
+ input = "React.createElement($DatePicker, #{js_props(input_props, expressions: {
1388
+ inputValue: attribute_property(scope, entity, name, formatting:),
1389
+ placeholder: "TextProperty({ value: #{JSON.generate(placeholder)} })",
1390
+ buttonLabel: 'TextProperty({ value: "Show date picker" })'
1391
+ })})"
1392
+ group_props = {
1393
+ key: "#{key}$formGroup", '$widgetId': "#{key}$formGroup",
1394
+ class: "mx-name-#{widget['Name']} mx-datepicker", control: [input],
1395
+ width: 3, orientation: 'horizontal', labelFor: key
1396
+ }
1397
+ "React.createElement($FormGroup, #{js_props(group_props, expressions: {
1398
+ caption: "TextProperty({ value: #{JSON.generate(caption)} })",
1399
+ hasError: 'TextProperty({ value: false })'
1400
+ })})"
1401
+ end
1402
+
1403
+ def render_check_box(widget)
1404
+ scope = current_object_scope&.fetch(:scope, nil)
1405
+ entity, separator, name = widget.dig('AttributeRef', 'Attribute').to_s.rpartition('.')
1406
+ return render_unsupported(widget) unless scope && separator == '.' &&
1407
+ present_identifier?(entity) && present_identifier?(name)
1408
+
1409
+ @uses_form_widgets = true
1410
+ key = widget_key(widget)
1411
+ input = "React.createElement($CheckBox, #{js_props(
1412
+ common_props(widget).merge('$widgetId': key, readOnlyStyle: 'text', id: key),
1413
+ expressions: { value: attribute_property(scope, entity, name) }
1414
+ )})"
1415
+ group_props = {
1416
+ key: "#{key}$formGroup", '$widgetId': "#{key}$formGroup",
1417
+ class: "mx-name-#{widget['Name']} mx-checkbox", control: [input],
1418
+ width: 3, orientation: 'horizontal', labelFor: key
1419
+ }
1420
+ caption = translated_text(widget.dig('LabelTemplate', 'Template'))
1421
+ "React.createElement($FormGroup, #{js_props(group_props, expressions: {
1422
+ caption: "TextProperty({ value: #{JSON.generate(caption)} })",
1423
+ hasError: 'TextProperty({ value: false })'
1424
+ })})"
1425
+ end
1426
+
1427
+ def render_label(widget)
1428
+ @uses_form_widgets = true
1429
+ key = widget_key(widget)
1430
+ props = common_props(widget).merge('$widgetId': key, class: css_class(widget), id: key)
1431
+ caption = translated_text(widget['Caption'])
1432
+ "React.createElement($Label, #{js_props(props, expressions: {
1433
+ caption: "TextProperty({ value: #{JSON.generate(caption)} })"
1434
+ })})"
1435
+ end
1436
+
1437
+ def render_tab_control(widget)
1438
+ @uses_tab_container = true
1439
+ key = widget_key(widget)
1440
+ tabs = array(widget['TabPages'])
1441
+ compiled = tabs.map do |tab|
1442
+ {
1443
+ name: tab['Name'].to_s,
1444
+ caption: raw_js("TextProperty({ value: #{JSON.generate(translated_text(tab['Caption']))} })"),
1445
+ isDelayed: false, refreshOnShow: tab['RefreshOnShow'] == true,
1446
+ content: raw_js(children(array(tab['Widgets'])))
1447
+ }
1448
+ end
1449
+ props = common_props(widget).merge(
1450
+ '$widgetId': key, class: css_class(widget), widgetId: key,
1451
+ defaultTab: default_tab_index(widget, tabs), tabs: compiled
1452
+ )
1453
+ "React.createElement($TabContainer, #{js_literal(props)})"
1454
+ end
1455
+
1456
+ def default_tab_index(widget, tabs)
1457
+ pointer = IO::BsonCodec.extract_id(widget['DefaultPagePointer'])
1458
+ index = tabs.index { IO::BsonCodec.extract_id(_1['$ID']) == pointer }
1459
+ index || 0
1460
+ end
1461
+
1462
+ def render_static_image(widget)
1463
+ uri = image_uri(widget['Image'])
1464
+ return render_unsupported(widget) unless uri
1465
+
1466
+ @uses_image = true
1467
+ ImageBundleCompiler.render_static(
1468
+ widget_key(widget), css_class(widget), uri,
1469
+ width: widget['Width'], width_unit: widget['WidthUnit'], height: widget['Height'],
1470
+ height_unit: widget['HeightUnit'], responsive: widget['Responsive'] == true
1471
+ )
1472
+ end
1473
+
1474
+ def image_uri(reference)
1475
+ module_name, collection_name, image_name = reference.to_s.split('.', 3)
1476
+ unit = @source.units_of('Images$ImageCollection').find do |candidate|
1477
+ candidate.module_name == module_name && candidate.document['Name'] == collection_name
1478
+ end
1479
+ return unless unit
1480
+
1481
+ image = array(unit.document.fetch('Images', nil)).find { _1['Name'] == image_name }
1482
+ return unless image
1483
+
1484
+ "img/#{[module_name, collection_name, image_name].join('$')}.#{image_format(image)}"
1485
+ end
1486
+
1487
+ def attribute_property(scope, entity, attribute, path: '', formatting: {})
1488
+ config = {
1489
+ scope:, path:, entity:, attribute:,
1490
+ onChange: { type: 'doNothing', argMap: {}, config: {}, disabledDuringExecution: true },
1491
+ isList: false, validation: nil, formatting:
1492
+ }
1493
+ "AttributeProperty(#{js_literal(config)})"
1494
+ end
1495
+
1496
+ def data_action?(action)
1497
+ current_object_scope && %w[
1498
+ Forms$SaveChangesClientAction Forms$CancelChangesClientAction Forms$DeleteClientAction
1499
+ ]
1500
+ .include?(action['$Type'])
1501
+ end
1502
+
1503
+ def render_data_action_button(widget, action)
1504
+ type = {
1505
+ 'Forms$SaveChangesClientAction' => 'saveChanges',
1506
+ 'Forms$CancelChangesClientAction' => 'cancelChanges',
1507
+ 'Forms$DeleteClientAction' => 'deleteObject'
1508
+ }.fetch(action['$Type'])
1509
+ scope = current_object_scope.fetch(:scope)
1510
+ key = widget_key(widget)
1511
+ object_argument = %w[saveChanges deleteObject].include?(type)
1512
+ action_config = {
1513
+ action: {
1514
+ type:, argMap: object_argument ? { '$object': { widget: scope, source: 'object' } } : {},
1515
+ config: {
1516
+ operationId: WebOperationCompiler.operation_id(@qualified_name, widget['Name']),
1517
+ closePage: action.fetch('ClosePage', true)
1518
+ },
1519
+ disabledDuringExecution: action.fetch('DisabledDuringExecution', true)
1520
+ },
1521
+ abortOnServerValidation: true
1522
+ }
1523
+ props = common_props(widget).merge(
1524
+ '$widgetId': key, buttonId: key, class: css_class(widget), renderType: 'button',
1525
+ buttonClass: button_style(widget)
1526
+ )
1527
+ caption = translated_text(widget.dig('CaptionTemplate', 'Template'))
1528
+ "React.createElement($ActionButton, #{js_props(props, expressions: {
1529
+ caption: "TextProperty({ value: #{JSON.generate(caption)} })",
1530
+ tooltip: 'TextProperty({ value: "" })', action: "ActionProperty(#{js_literal(action_config)})"
1531
+ })})"
1532
+ end
1533
+
1534
+ def button_style(widget)
1535
+ style = widget['ButtonStyle'].to_s.downcase
1536
+ "btn-#{style.empty? ? 'default' : style}"
1537
+ end
1538
+
1539
+ def action_handler(action)
1540
+ case action['$Type']
1541
+ when 'Forms$NoAction', nil
1542
+ nil
1543
+ when 'Forms$FormAction'
1544
+ open_form_handler(action.dig('FormSettings', 'Form'))
1545
+ when 'Forms$CreateObjectClientAction'
1546
+ create_object_handler(
1547
+ action.dig('EntityRef', 'Entity'), action.dig('PageSettings', 'Form')
1548
+ )
1549
+ else
1550
+ @unsupported << action['$Type'].to_s unless action['$Type'].to_s.empty?
1551
+ nil
1552
+ end
1553
+ end
1554
+
1555
+ def open_form_handler(form)
1556
+ return unless present_identifier?(form)
1557
+
1558
+ "() => window.mx?.ui?.openForm2?.(#{JSON.generate(form)}, {}, undefined, undefined, " \
1559
+ '{ location: "content" })'
1560
+ end
1561
+
1562
+ def create_object_handler(entity, form)
1563
+ return unless present_identifier?(entity) && present_identifier?(form)
1564
+
1565
+ parameter = target_page_parameter(form, entity)
1566
+ return unless parameter
1567
+
1568
+ create_form_handler_source(entity, form, parameter)
1569
+ end
1570
+
1571
+ def create_form_handler_source(entity, form, parameter)
1572
+ source = <<~JS
1573
+ () => window.mx?.data?.create?.({ entity: #{JSON.generate(entity)}, callback: object => {
1574
+ window.mx?.ui?.openForm2?.(#{JSON.generate(form)},
1575
+ { #{JSON.generate(parameter)}: object.getGuid() }, undefined, undefined,
1576
+ { location: "content" });
1577
+ }, error: window.mx?.ui?.onError })
1578
+ JS
1579
+ source.lines.map(&:strip).join(' ')
1580
+ end
1581
+
1582
+ def target_page_parameter(form, entity)
1583
+ parameter = array(page_unit(form)&.document&.fetch('Parameters', nil))
1584
+ .find { object_parameter?(_1, entity) }
1585
+ name = parameter&.fetch('Name', nil)
1586
+ "$#{name}" if present_identifier?(name)
1587
+ end
1588
+
1589
+ def page_unit(qualified_name)
1590
+ @source.units_of('Forms$Page').find do |unit|
1591
+ "#{unit.module_name}.#{unit.document['Name']}" == qualified_name
1592
+ end
1593
+ end
1594
+
1595
+ def object_parameter?(parameter, entity)
1596
+ parameter.dig('ParameterType', '$Type') == 'DataTypes$ObjectType' &&
1597
+ parameter.dig('ParameterType', 'Entity') == entity
1598
+ end
1599
+
1600
+ def present_identifier?(value) = value.to_s.match?(/\A[A-Za-z_][A-Za-z0-9_.]*\z/)
1601
+
1602
+ def render_unsupported(widget)
1603
+ type = widget['$Type'].to_s
1604
+ @unsupported << type
1605
+ @unsupported_custom << custom_widget_identifier(widget) if type == 'CustomWidgets$CustomWidget'
1606
+ props = common_props(widget).merge(
1607
+ className: "#{css_class(widget)} mxrb-unsupported-widget".strip,
1608
+ 'data-mxrb-widget-type': type
1609
+ )
1610
+ "React.createElement(\"div\", #{js_props(props)})"
1611
+ end
1612
+
1613
+ def custom_widget_identifier(widget)
1614
+ type_id = IO::BsonCodec.extract_id(widget.dig('Object', 'TypePointer'))
1615
+ schema = @source.document_index.values.find do |document|
1616
+ IO::BsonCodec.extract_id(document.dig('ObjectType', '$ID')) == type_id
1617
+ end
1618
+ schema&.fetch('WidgetId', nil).to_s.then { _1.empty? ? 'CustomWidgets$CustomWidget' : _1 }
1619
+ end
1620
+
1621
+ def common_props(widget)
1622
+ { key: widget_key(widget), 'data-widget-id': widget_key(widget) }
1623
+ end
1624
+
1625
+ def widget_key(widget)
1626
+ name = widget['Name'].to_s
1627
+ encoded = widget_id(widget)[0, 12]
1628
+ "#{@scope_prefix || 'p'}.#{@qualified_name}.#{name.empty? ? encoded : name}"
1629
+ end
1630
+
1631
+ def widget_id(widget)
1632
+ identifier = widget['$ID']
1633
+ return identifier.data.unpack1('H*') if identifier.respond_to?(:data)
1634
+
1635
+ identifier&.dig('$binary', 'base64').to_s.gsub(/[^A-Za-z0-9]/, '')
1636
+ end
1637
+
1638
+ def css_class(widget)
1639
+ name = widget['Name'].to_s
1640
+ appearance = widget.dig('Appearance', 'Class').to_s
1641
+ named = "mx-name-#{name}" unless name.empty?
1642
+ [named, widget['Class'], appearance].map(&:to_s).reject(&:empty?).uniq.join(' ')
1643
+ end
1644
+
1645
+ def render_mode(widget) = widget['RenderMode'].to_s.downcase.then { _1.empty? ? 'div' : _1 }
1646
+
1647
+ def text_mode(widget)
1648
+ { 'Text' => 'span', 'Paragraph' => 'p', 'Heading1' => 'h1', 'Heading2' => 'h2',
1649
+ 'Heading3' => 'h3', 'Heading4' => 'h4', 'Heading5' => 'h5', 'Heading6' => 'h6' }
1650
+ .merge('H1' => 'h1', 'H2' => 'h2', 'H3' => 'h3', 'H4' => 'h4', 'H5' => 'h5', 'H6' => 'h6')
1651
+ .fetch(widget['RenderMode'].to_s, 'span')
1652
+ end
1653
+
1654
+ def translated_text(text)
1655
+ array(text&.fetch('Items', nil)).find { _1['LanguageCode'] == 'en_US' }&.fetch('Text', '') ||
1656
+ array(text&.fetch('Items', nil)).first&.fetch('Text', '') || ''
1657
+ end
1658
+
1659
+ def js_props(props, expressions: {})
1660
+ pairs = props.map do |key, value|
1661
+ rendered = value.is_a?(Array) ? "[#{value.join(', ')}]" : JSON.generate(value)
1662
+ "#{JSON.generate(key)}: #{rendered}"
1663
+ end
1664
+ pairs.concat(expressions.map { |key, value| "#{JSON.generate(key)}: #{value}" })
1665
+ "{ #{pairs.join(', ')} }"
1666
+ end
1667
+
1668
+ def js_literal(value)
1669
+ return value['$raw'] if value.is_a?(Hash) && value.key?('$raw')
1670
+
1671
+ case value
1672
+ when Hash
1673
+ "{ #{value.map { |key, item| "#{JSON.generate(key)}: #{js_literal(item)}" }.join(', ')} }"
1674
+ when Array then "[#{value.map { js_literal(_1) }.join(', ')}]"
1675
+ else JSON.generate(value)
1676
+ end
1677
+ end
1678
+
1679
+ def module_source(content)
1680
+ rendered_content = render_content(content)
1681
+ content_source = if parent_layout_name.empty?
1682
+ rendered_content
1683
+ else
1684
+ "Object.assign({}, parentContent, #{rendered_content})"
1685
+ end
1686
+ <<~JS
1687
+ import React from "react";
1688
+ import { PageFragment } from "mendix/PageFragment";
1689
+ #{parent_layout_import}
1690
+ #{widget_imports}
1691
+ #{@nanoflow_programs.declarations}
1692
+
1693
+ export const title = #{JSON.generate(page_title)};
1694
+ export const classes = #{JSON.generate(page_classes)};
1695
+ export const autofocus = "off";
1696
+ #{cancel_changes_export}
1697
+ export const style = {};
1698
+ export const parameters = #{JSON.generate(page_parameters)};
1699
+ export const content = #{content_source};
1700
+ JS
1701
+ end
1702
+
1703
+ def cancel_changes_export
1704
+ return '' unless popup_page?
1705
+
1706
+ "export const cancelChangesOperationId = #{JSON.generate(cancel_changes_operation_id)};"
1707
+ end
1708
+
1709
+ def cancel_changes_operation_id
1710
+ WebOperationCompiler.operation_id(@qualified_name, '$cancelChanges')
1711
+ end
1712
+
1713
+ def popup_page?
1714
+ !@layout_mode && popup_layout_document?(layout&.document)
1715
+ end
1716
+
1717
+ def popup_layout_document?(document)
1718
+ return false unless document
1719
+
1720
+ name = document['Name'].to_s
1721
+ return true if name.match?(/popup/i)
1722
+
1723
+ document['CanvasWidth'].to_i.positive? && document['CanvasWidth'].to_i <= 800 &&
1724
+ %w[Forms$NavigationTree Forms$MenuBar Forms$SimpleMenuBar Forms$Header].none? do |type|
1725
+ nested_widgets(document, type).any?
1726
+ end
1727
+ end
1728
+
1729
+ def nested_widgets(value, type, result = [])
1730
+ case value
1731
+ when Hash
1732
+ result << value if value['$Type'] == type
1733
+ value.each_value { nested_widgets(_1, type, result) }
1734
+ when Array then value.each { nested_widgets(_1, type, result) }
1735
+ end
1736
+ result
1737
+ end
1738
+
1739
+ def layout_module_source(rendered)
1740
+ parent = parent_layout_name.empty? ? '{}' : 'parentContent'
1741
+ <<~JS
1742
+ import React from "react";
1743
+ #{parent_layout_import}
1744
+ #{widget_imports}
1745
+ #{@nanoflow_programs.declarations}
1746
+
1747
+ export const content = Object.assign({}, #{parent}, {
1748
+ "Main": #{rendered}
1749
+ });
1750
+ JS
1751
+ end
1752
+
1753
+ def parent_layout_import
1754
+ return '' if parent_layout_name.empty?
1755
+
1756
+ "import { content as parentContent } from \"../layouts/#{parent_layout_name}.js\";"
1757
+ end
1758
+
1759
+ def parent_layout_name
1760
+ reference = if @layout_mode
1761
+ @unit.document.dig('Content', 'LayoutCall', 'Form')
1762
+ else
1763
+ @unit.document.dig('FormCall', 'Form')
1764
+ end.to_s
1765
+ return '' unless present_qualified_name?(reference)
1766
+
1767
+ layout = @source.units_of('Forms$Layout').find do |unit|
1768
+ "#{unit.module_name}.#{unit.document['Name']}" == reference
1769
+ end
1770
+ layout && @source.web_layout?(layout) ? reference : ''
1771
+ end
1772
+
1773
+ def present_qualified_name?(value)
1774
+ value.split('.').length >= 2 && value.split('.').all? { present_identifier?(_1) }
1775
+ end
1776
+
1777
+ def page_parameters
1778
+ array(@unit.document['Parameters']).filter_map do |parameter|
1779
+ name = parameter['Name']
1780
+ type = parameter['ParameterType'] || {}
1781
+ next unless present_identifier?(name) && type['$Type'] == 'DataTypes$ObjectType'
1782
+
1783
+ ["$#{name}", { kind: 'object' }]
1784
+ end.to_h
1785
+ end
1786
+
1787
+ def widget_imports
1788
+ return '' unless @uses_data_grid || @uses_form_widgets || @uses_gallery || @uses_bound_text ||
1789
+ @uses_tab_container || @uses_image || @uses_conditional || @uses_dynamic_class ||
1790
+ @uses_list_view || @generic_widgets&.any? || @uses_layout_widgets
1791
+
1792
+ imports = ['import { asPluginWidgets } from "mendix";']
1793
+ widgets = []
1794
+ if @uses_layout_widgets
1795
+ imports.concat([
1796
+ 'import { PlaceholderProperty } from "mendix/PlaceholderProperty";',
1797
+ 'import { TextProperty } from "mendix/TextProperty";',
1798
+ 'import { ActionProperty } from "mendix/ActionProperty";',
1799
+ 'import { WebIconProperty } from "mendix/WebIconProperty";',
1800
+ 'import { ScrollContainer } from "mendix/widgets/web/ScrollContainer";',
1801
+ 'import { Placeholder } from "mendix/widgets/web/Placeholder";',
1802
+ 'import { SidebarToggle } from "mendix/widgets/web/SidebarToggle";',
1803
+ 'import { Header } from "mendix/widgets/web/Header";',
1804
+ 'import { NavigationTree } from "mendix/widgets/web/NavigationTree";',
1805
+ 'import { MenuBar } from "mendix/widgets/web/MenuBar";',
1806
+ 'import { SimpleMenuBar } from "mendix/widgets/web/SimpleMenuBar";'
1807
+ ])
1808
+ widgets.concat(%w[ScrollContainer Placeholder SidebarToggle Header NavigationTree MenuBar SimpleMenuBar])
1809
+ end
1810
+ (@generic_widgets || {}).sort.each do |name, path|
1811
+ imports.concat([
1812
+ "import * as #{name}WidgetModule from \"../widgets/#{path}.mjs\";",
1813
+ "const #{name} = Object.getOwnPropertyDescriptor(#{name}WidgetModule, " \
1814
+ "#{JSON.generate(name)})?.value || " \
1815
+ "Object.getOwnPropertyDescriptor(#{name}WidgetModule, \"default\")?.value;"
1816
+ ])
1817
+ widgets << name
1818
+ end
1819
+ if @generic_widgets&.any?
1820
+ imports.concat([
1821
+ 'import { AttributeProperty } from "mendix/AttributeProperty";',
1822
+ 'import { ExpressionProperty } from "mendix/ExpressionProperty";',
1823
+ 'import { ListExpressionProperty } from "mendix/ListExpressionProperty";',
1824
+ 'import { DatabaseObjectListProperty } from "mendix/DatabaseObjectListProperty";'
1825
+ ])
1826
+ end
1827
+ if @uses_data_grid
1828
+ imports.concat([
1829
+ 'import { DatabaseObjectListProperty } from "mendix/DatabaseObjectListProperty";',
1830
+ 'import { AttributeProperty } from "mendix/AttributeProperty";',
1831
+ 'import { ExpressionProperty } from "mendix/ExpressionProperty";',
1832
+ 'import { ListExpressionProperty } from "mendix/ListExpressionProperty";',
1833
+ 'import { TemplatedWidgetProperty } from "mendix/TemplatedWidgetProperty";',
1834
+ 'import Datagrid from "../widgets/com/mendix/widget/web/datagrid/Datagrid.mjs";'
1835
+ ])
1836
+ widgets << 'Datagrid'
1837
+ end
1838
+ if @uses_form_widgets
1839
+ imports.concat([
1840
+ 'import { AssociationObjectProperty } from "mendix/AssociationObjectProperty";',
1841
+ 'import { AttributeProperty } from "mendix/AttributeProperty";',
1842
+ 'import { ActionProperty } from "mendix/ActionProperty";',
1843
+ 'import { TextProperty } from "mendix/TextProperty";',
1844
+ 'import { DataView } from "mendix/widgets/web/DataView";',
1845
+ 'import { TextBox } from "mendix/widgets/web/TextBox";',
1846
+ 'import { CheckBox } from "mendix/widgets/web/CheckBox";',
1847
+ 'import { Label } from "mendix/widgets/web/Label";',
1848
+ 'import { Container } from "mendix/widgets/web/Container";',
1849
+ 'import { FormGroup } from "mendix/widgets/web/FormGroup";',
1850
+ 'import { ActionButton } from "mendix/widgets/web/ActionButton";'
1851
+ ])
1852
+ widgets.concat(%w[DataView TextBox CheckBox Label FormGroup ActionButton])
1853
+ if @uses_date_picker
1854
+ imports << 'import { DatePicker } from "mendix/widgets/web/DatePicker";'
1855
+ widgets << 'DatePicker'
1856
+ end
1857
+ if @uses_text_area
1858
+ imports << 'import { TextArea } from "mendix/widgets/web/TextArea";'
1859
+ widgets << 'TextArea'
1860
+ end
1861
+ if @uses_radio_button_group
1862
+ imports << 'import { RadioButtonGroup } from "mendix/widgets/web/RadioButtonGroup";'
1863
+ widgets << 'RadioButtonGroup'
1864
+ end
1865
+ if @uses_group_box
1866
+ imports << 'import { GroupBox } from "mendix/widgets/web/GroupBox";'
1867
+ widgets << 'GroupBox'
1868
+ end
1869
+ if @uses_file_manager
1870
+ imports.concat([
1871
+ 'import { DynamicFileProperty } from "mendix/DynamicFileProperty";',
1872
+ 'import { FileManager } from "mendix/widgets/web/FileManager";'
1873
+ ])
1874
+ widgets << 'FileManager'
1875
+ end
1876
+ imports << 'import { MicroflowObjectProperty } from "mendix/MicroflowObjectProperty";' \
1877
+ if @uses_microflow_object
1878
+ imports << 'import { ListenObjectProperty } from "mendix/ListenObjectProperty";' \
1879
+ if @uses_listen_object
1880
+ widgets << 'Container' if @uses_container
1881
+ end
1882
+ if @uses_combo_box
1883
+ imports.concat([
1884
+ 'import { AssociationProperty } from "mendix/AssociationProperty";',
1885
+ 'import { DatabaseObjectListProperty } from "mendix/DatabaseObjectListProperty";',
1886
+ 'import { MicroflowObjectListProperty } from "mendix/MicroflowObjectListProperty";',
1887
+ 'import { ListAttributeProperty } from "mendix/ListAttributeProperty";',
1888
+ 'import { ListAssociationProperty } from "mendix/ListAssociationProperty";',
1889
+ 'import { ListExpressionProperty } from "mendix/ListExpressionProperty";',
1890
+ 'import { SelectionProperty } from "mendix/SelectionProperty";',
1891
+ 'import { ExpressionProperty } from "mendix/ExpressionProperty";',
1892
+ 'import Combobox from "../widgets/com/mendix/widget/web/combobox/Combobox.mjs";'
1893
+ ])
1894
+ widgets << 'Combobox'
1895
+ end
1896
+ imports << 'import { NanoflowObjectProperty } from "mendix/NanoflowObjectProperty";' \
1897
+ if @uses_nanoflow_object
1898
+ if @uses_list_view
1899
+ imports.concat([
1900
+ 'import { DatabaseObjectListProperty } from "mendix/DatabaseObjectListProperty";',
1901
+ 'import { AssociationObjectListProperty } from "mendix/AssociationObjectListProperty";',
1902
+ 'import { MicroflowObjectListProperty } from "mendix/MicroflowObjectListProperty";',
1903
+ 'import { NanoflowObjectListProperty } from "mendix/NanoflowObjectListProperty";',
1904
+ 'import { TemplatedWidgetProperty } from "mendix/TemplatedWidgetProperty";',
1905
+ 'import { ListView } from "mendix/widgets/web/ListView";'
1906
+ ])
1907
+ widgets << 'ListView'
1908
+ end
1909
+ if @uses_gallery
1910
+ imports.concat([
1911
+ 'import { DatabaseObjectListProperty } from "mendix/DatabaseObjectListProperty";',
1912
+ 'import { MicroflowObjectListProperty } from "mendix/MicroflowObjectListProperty";',
1913
+ 'import { NanoflowObjectListProperty } from "mendix/NanoflowObjectListProperty";',
1914
+ 'import { SelectionProperty } from "mendix/SelectionProperty";',
1915
+ 'import { TemplatedWidgetProperty } from "mendix/TemplatedWidgetProperty";',
1916
+ 'import { ExpressionProperty } from "mendix/ExpressionProperty";',
1917
+ 'import { Gallery } from "../widgets/com/mendix/widget/web/gallery/Gallery.mjs";'
1918
+ ])
1919
+ widgets << 'Gallery'
1920
+ end
1921
+ if @uses_tab_container
1922
+ imports.concat([
1923
+ 'import { TextProperty } from "mendix/TextProperty";',
1924
+ 'import { TabContainer } from "mendix/widgets/web/TabContainer";'
1925
+ ])
1926
+ widgets << 'TabContainer'
1927
+ end
1928
+ if @uses_image
1929
+ imports.concat([
1930
+ 'import { ExpressionProperty } from "mendix/ExpressionProperty";',
1931
+ 'import { WebStaticImageProperty } from "mendix/WebStaticImageProperty";'
1932
+ ])
1933
+ imports << if @uses_custom_image
1934
+ 'import { Image } from "../widgets/com/mendix/widget/web/image/Image.mjs";'
1935
+ else
1936
+ 'import { Image } from "mendix/widgets/web/Image";'
1937
+ end
1938
+ widgets << 'Image'
1939
+ end
1940
+ if @uses_bound_text
1941
+ imports.concat([
1942
+ 'import { AttributeProperty } from "mendix/AttributeProperty";',
1943
+ 'const MxrbFormattedText = ({ template, renderMode, class: className, ...props }) => ' \
1944
+ 'React.createElement(renderMode, { className }, Object.keys(props)' \
1945
+ '.filter(key => /^value\\d+$/.test(key))' \
1946
+ '.sort((left, right) => Number(left.slice(5)) - Number(right.slice(5)))' \
1947
+ '.reduce((text, key, index) => text.split(`{${index + 1}}`)' \
1948
+ '.join(props[key]?.displayValue ?? " "), template));',
1949
+ 'MxrbFormattedText.displayName = "MxrbFormattedText";'
1950
+ ])
1951
+ widgets << 'MxrbFormattedText'
1952
+ end
1953
+ if @uses_conditional
1954
+ imports.concat([
1955
+ 'import { AttributeProperty } from "mendix/AttributeProperty";',
1956
+ 'const mxrbValue = property => property?.value ?? property?.displayValue;',
1957
+ 'const MxrbConditional = ({ test, children, ...props }) => ' \
1958
+ 'test(props) ? children : null;',
1959
+ 'MxrbConditional.displayName = "MxrbConditional";'
1960
+ ])
1961
+ widgets << 'MxrbConditional'
1962
+ end
1963
+ if @uses_dynamic_class
1964
+ imports.concat([
1965
+ 'import { AttributeProperty } from "mendix/AttributeProperty";',
1966
+ 'const mxrbValue = property => property?.value ?? property?.displayValue;',
1967
+ 'const MxrbDynamicClass = ({ resolveClass, children, ...props }) => {' \
1968
+ ' const dynamicClass = resolveClass(props);' \
1969
+ ' const currentClass = children.props.class || children.props.className || "";' \
1970
+ ' const mergedClass = [currentClass, dynamicClass].filter(Boolean).join(" ").trim();' \
1971
+ ' const classProp = typeof children.type === "string" ?' \
1972
+ ' { className: mergedClass } : { class: mergedClass };' \
1973
+ ' return React.cloneElement(children, classProp); };',
1974
+ 'MxrbDynamicClass.displayName = "MxrbDynamicClass";'
1975
+ ])
1976
+ widgets << 'MxrbDynamicClass'
1977
+ end
1978
+ imports.uniq.push(
1979
+ "const { #{widgets.map { "$#{_1}" }.join(', ')} } = asPluginWidgets({ #{widgets.join(', ')} });"
1980
+ ).join("\n")
1981
+ end
1982
+
1983
+ def page_title = translated_text(@unit.document['Title'])
1984
+
1985
+ def page_classes
1986
+ [layout&.document&.dig('Appearance', 'Class'), @unit.document.dig('Appearance', 'Class')]
1987
+ .map(&:to_s).reject(&:empty?).join(' ')
1988
+ end
1989
+
1990
+ def render_content(content)
1991
+ JSON.pretty_generate(content).sub(/\A\{/, '{').sub(/\}\z/, '}')
1992
+ .gsub(/"(renderKey =>[^\n]+)"/) { Regexp.last_match(1).gsub('\\"', '"') }
1993
+ end
1994
+
1995
+ def layout
1996
+ qualified = @unit.document.dig('FormCall', 'Form').to_s
1997
+ @source.units_of('Forms$Layout').find do |unit|
1998
+ "#{unit.module_name}.#{unit.document['Name']}" == qualified
1999
+ end
2000
+ end
2001
+ end
2002
+ # rubocop:enable Metrics, Style/MultilineBlockChain
2003
+ end
2004
+ end