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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.de-DE.md +108 -0
- data/README.md +172 -0
- data/README.pt-BR.md +109 -0
- data/bin/mxrb +1613 -0
- data/docs/README.md +10 -0
- data/docs/de-DE/README.md +24 -0
- data/docs/de-DE/architectural-patterns.md +26 -0
- data/docs/de-DE/architectural-standard.md +48 -0
- data/docs/de-DE/architecture.md +76 -0
- data/docs/de-DE/compiler.md +162 -0
- data/docs/de-DE/conventions.md +19 -0
- data/docs/de-DE/design-system.md +52 -0
- data/docs/de-DE/entity-dsl.md +53 -0
- data/docs/de-DE/native-runtime-quality-report.md +74 -0
- data/docs/de-DE/oql-sql.md +211 -0
- data/docs/de-DE/platform-api-integration.md +36 -0
- data/docs/de-DE/platform-operations.md +243 -0
- data/docs/de-DE/project-structure.md +32 -0
- data/docs/de-DE/ruby-first-roadmap.md +116 -0
- data/docs/de-DE/scaffolds.md +59 -0
- data/docs/de-DE/semantic-refactoring.md +56 -0
- data/docs/de-DE/team-server.md +33 -0
- data/docs/de-DE/validation-matrix.md +64 -0
- data/docs/de-DE/vetclinic-acceptance.md +24 -0
- data/docs/de-DE/writing.md +138 -0
- data/docs/en-US/README.md +24 -0
- data/docs/en-US/architectural-patterns.md +151 -0
- data/docs/en-US/architectural-standard.md +89 -0
- data/docs/en-US/architecture.md +170 -0
- data/docs/en-US/compiler.md +157 -0
- data/docs/en-US/conventions.md +82 -0
- data/docs/en-US/design-system.md +163 -0
- data/docs/en-US/entity-dsl.md +53 -0
- data/docs/en-US/native-runtime-quality-report.md +73 -0
- data/docs/en-US/oql-sql.md +201 -0
- data/docs/en-US/platform-api-integration.md +36 -0
- data/docs/en-US/platform-operations.md +244 -0
- data/docs/en-US/project-structure.md +49 -0
- data/docs/en-US/ruby-first-roadmap.md +186 -0
- data/docs/en-US/scaffolds.md +57 -0
- data/docs/en-US/semantic-refactoring.md +198 -0
- data/docs/en-US/team-server.md +32 -0
- data/docs/en-US/validation-matrix.md +257 -0
- data/docs/en-US/vetclinic-acceptance.md +23 -0
- data/docs/en-US/writing.md +655 -0
- data/docs/pt-BR/README.md +24 -0
- data/docs/pt-BR/architectural-patterns.md +25 -0
- data/docs/pt-BR/architectural-standard.md +533 -0
- data/docs/pt-BR/architecture.md +300 -0
- data/docs/pt-BR/compiler.md +162 -0
- data/docs/pt-BR/conventions.md +19 -0
- data/docs/pt-BR/design-system.md +65 -0
- data/docs/pt-BR/entity-dsl.md +116 -0
- data/docs/pt-BR/native-runtime-quality-report.md +73 -0
- data/docs/pt-BR/oql-sql.md +208 -0
- data/docs/pt-BR/platform-api-integration.md +36 -0
- data/docs/pt-BR/platform-operations.md +260 -0
- data/docs/pt-BR/project-structure.md +32 -0
- data/docs/pt-BR/ruby-first-roadmap.md +239 -0
- data/docs/pt-BR/scaffolds.md +104 -0
- data/docs/pt-BR/semantic-refactoring.md +55 -0
- data/docs/pt-BR/team-server.md +59 -0
- data/docs/pt-BR/validation-matrix.md +86 -0
- data/docs/pt-BR/vetclinic-acceptance.md +50 -0
- data/docs/pt-BR/writing.md +177 -0
- data/examples/sudoku_evaluation.rb +14 -0
- data/examples/sudoku_functional_test.rb +28 -0
- data/lib/mxrb/architecture/graph.rb +124 -0
- data/lib/mxrb/architecture/validator.rb +98 -0
- data/lib/mxrb/benchmark.rb +33 -0
- data/lib/mxrb/compare.rb +390 -0
- data/lib/mxrb/compiler/adapter.rb +96 -0
- data/lib/mxrb/compiler/artifact_document_compiler.rb +102 -0
- data/lib/mxrb/compiler/artifact_materializer.rb +53 -0
- data/lib/mxrb/compiler/client_model_materializer.rb +42 -0
- data/lib/mxrb/compiler/code_action_document_compiler.rb +64 -0
- data/lib/mxrb/compiler/code_action_materializer.rb +35 -0
- data/lib/mxrb/compiler/code_action_type_compiler.rb +32 -0
- data/lib/mxrb/compiler/combo_box_bundle_compiler.rb +314 -0
- data/lib/mxrb/compiler/compatibility_analyzer.rb +169 -0
- data/lib/mxrb/compiler/constants_materializer.rb +111 -0
- data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +290 -0
- data/lib/mxrb/compiler/database_connector_action_compiler.rb +280 -0
- data/lib/mxrb/compiler/deployment_asset_copier.rb +60 -0
- data/lib/mxrb/compiler/deployment_bootstrapper.rb +208 -0
- data/lib/mxrb/compiler/deployment_materializer.rb +43 -0
- data/lib/mxrb/compiler/deployment_metadata.rb +64 -0
- data/lib/mxrb/compiler/domain_document_compiler.rb +153 -0
- data/lib/mxrb/compiler/domain_model_materializer.rb +35 -0
- data/lib/mxrb/compiler/domain_security_compiler.rb +82 -0
- data/lib/mxrb/compiler/gallery_bundle_compiler.rb +190 -0
- data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +281 -0
- data/lib/mxrb/compiler/image_bundle_compiler.rb +216 -0
- data/lib/mxrb/compiler/java_proxy_generator.rb +509 -0
- data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +267 -0
- data/lib/mxrb/compiler/legacy_page_builder.rb +287 -0
- data/lib/mxrb/compiler/mda.rb +99 -0
- data/lib/mxrb/compiler/microflow_document_compiler.rb +85 -0
- data/lib/mxrb/compiler/microflow_materializer.rb +48 -0
- data/lib/mxrb/compiler/microflow_node_compiler.rb +250 -0
- data/lib/mxrb/compiler/model_package.rb +116 -0
- data/lib/mxrb/compiler/model_values.rb +67 -0
- data/lib/mxrb/compiler/nanoflow_program_compiler.rb +483 -0
- data/lib/mxrb/compiler/navigation_document_compiler.rb +93 -0
- data/lib/mxrb/compiler/packager.rb +100 -0
- data/lib/mxrb/compiler/page_bundle_builder.rb +56 -0
- data/lib/mxrb/compiler/page_bundle_compiler.rb +2004 -0
- data/lib/mxrb/compiler/page_document_compiler.rb +77 -0
- data/lib/mxrb/compiler/portable_packager.rb +356 -0
- data/lib/mxrb/compiler/project_jar_archive.rb +84 -0
- data/lib/mxrb/compiler/project_jar_builder.rb +121 -0
- data/lib/mxrb/compiler/project_materializer.rb +139 -0
- data/lib/mxrb/compiler/project_model_orderer.rb +52 -0
- data/lib/mxrb/compiler/runtime_data_types.rb +31 -0
- data/lib/mxrb/compiler/runtime_model_schema.rb +90 -0
- data/lib/mxrb/compiler/schemas/runtime-10.24.0.73019.json +1052 -0
- data/lib/mxrb/compiler/schemas/runtime-11.json +1215 -0
- data/lib/mxrb/compiler/schemas/runtime-6.10.8.json +1148 -0
- data/lib/mxrb/compiler/schemas/runtime-7.17.0.json +1144 -0
- data/lib/mxrb/compiler/schemas/runtime-7.5.0.json +1215 -0
- data/lib/mxrb/compiler/schemas/runtime-9.6.1.29396.json +868 -0
- data/lib/mxrb/compiler/schemas/system-model-10.24.0.73019.b64 +1135 -0
- data/lib/mxrb/compiler/schemas/system-model-11.12.1.b64 +815 -0
- data/lib/mxrb/compiler/schemas/system-model-6.10.8.b64 +497 -0
- data/lib/mxrb/compiler/schemas/system-model-7.17.0.b64 +519 -0
- data/lib/mxrb/compiler/schemas/system-model-7.5.0.b64 +504 -0
- data/lib/mxrb/compiler/schemas/system-model-9.6.1.29396.b64 +816 -0
- data/lib/mxrb/compiler/security_materializer.rb +104 -0
- data/lib/mxrb/compiler/settings_document_compiler.rb +45 -0
- data/lib/mxrb/compiler/settings_materializer.rb +25 -0
- data/lib/mxrb/compiler/source_model.rb +128 -0
- data/lib/mxrb/compiler/system_model_seed.rb +88 -0
- data/lib/mxrb/compiler/system_queue_materializer.rb +54 -0
- data/lib/mxrb/compiler/system_text_materializer.rb +49 -0
- data/lib/mxrb/compiler/translation_materializer.rb +95 -0
- data/lib/mxrb/compiler/web_bundle_builder.rb +155 -0
- data/lib/mxrb/compiler/web_list_data_source.rb +139 -0
- data/lib/mxrb/compiler/web_operation_compiler.rb +614 -0
- data/lib/mxrb/compiler/web_shell_materializer.rb +194 -0
- data/lib/mxrb/compiler/widget_package_extractor.rb +63 -0
- data/lib/mxrb/doctor.rb +116 -0
- data/lib/mxrb/dsl/builder.rb +1636 -0
- data/lib/mxrb/errors.rb +18 -0
- data/lib/mxrb/evaluation.rb +131 -0
- data/lib/mxrb/exporter.rb +1716 -0
- data/lib/mxrb/frontend/migrator.rb +775 -0
- data/lib/mxrb/functional.rb +307 -0
- data/lib/mxrb/github/annotator.rb +161 -0
- data/lib/mxrb/initializer.rb +257 -0
- data/lib/mxrb/integrity/validator.rb +136 -0
- data/lib/mxrb/io/bson_codec.rb +145 -0
- data/lib/mxrb/io/mpr_file.rb +558 -0
- data/lib/mxrb/io/mxunit_codec.rb +42 -0
- data/lib/mxrb/marketplace.rb +380 -0
- data/lib/mxrb/model/association.rb +98 -0
- data/lib/mxrb/model/attribute.rb +100 -0
- data/lib/mxrb/model/connector.rb +17 -0
- data/lib/mxrb/model/design_materializer.rb +133 -0
- data/lib/mxrb/model/design_migration.rb +126 -0
- data/lib/mxrb/model/design_system.rb +111 -0
- data/lib/mxrb/model/domain_model.rb +51 -0
- data/lib/mxrb/model/entity.rb +180 -0
- data/lib/mxrb/model/menu.rb +38 -0
- data/lib/mxrb/model/microflow.rb +100 -0
- data/lib/mxrb/model/module.rb +142 -0
- data/lib/mxrb/model/navigation.rb +127 -0
- data/lib/mxrb/model/page.rb +410 -0
- data/lib/mxrb/model/project.rb +187 -0
- data/lib/mxrb/model/unit.rb +48 -0
- data/lib/mxrb/module_initializer.rb +75 -0
- data/lib/mxrb/official_marketplace/content_api.rb +292 -0
- data/lib/mxrb/official_marketplace/dependency_resolver.rb +329 -0
- data/lib/mxrb/official_marketplace/lifecycle.rb +367 -0
- data/lib/mxrb/official_marketplace/module_package_importer.rb +318 -0
- data/lib/mxrb/official_marketplace/widget_package_installer.rb +414 -0
- data/lib/mxrb/official_marketplace.rb +720 -0
- data/lib/mxrb/oql/analyzer.rb +155 -0
- data/lib/mxrb/oql/index_advisor.rb +85 -0
- data/lib/mxrb/oql/plan_analyzer.rb +185 -0
- data/lib/mxrb/oql/server.rb +132 -0
- data/lib/mxrb/oql/sql_server_plan_analyzer.rb +190 -0
- data/lib/mxrb/oql/sql_server_workload_analyzer.rb +80 -0
- data/lib/mxrb/oql/workload_analyzer.rb +175 -0
- data/lib/mxrb/oql/workload_baseline.rb +60 -0
- data/lib/mxrb/oql.rb +424 -0
- data/lib/mxrb/project_lifecycle.rb +76 -0
- data/lib/mxrb/protocols/plan.rb +86 -0
- data/lib/mxrb/protocols.rb +123 -0
- data/lib/mxrb/runtime/database_workspace.rb +677 -0
- data/lib/mxrb/runtime/docker_executor.rb +41 -0
- data/lib/mxrb/runtime/docker_workspace.rb +29 -0
- data/lib/mxrb/runtime/executor.rb +169 -0
- data/lib/mxrb/runtime/java_locator.rb +26 -0
- data/lib/mxrb/runtime/native.rb +474 -0
- data/lib/mxrb/runtime/sql_server_database.rb +146 -0
- data/lib/mxrb/runtime/toolchain.rb +82 -0
- data/lib/mxrb/scaffold/cli.rb +113 -0
- data/lib/mxrb/scaffold/generator.rb +246 -0
- data/lib/mxrb/scaffold/help.rb +80 -0
- data/lib/mxrb/scaffold/page_templates.rb +54 -0
- data/lib/mxrb/scaffold/recipes.rb +198 -0
- data/lib/mxrb/scaffold/registry.rb +82 -0
- data/lib/mxrb/scaffold/templates.rb +723 -0
- data/lib/mxrb/scaffold/transaction.rb +84 -0
- data/lib/mxrb/schema/tables.rb +55 -0
- data/lib/mxrb/semantic/analyzer.rb +473 -0
- data/lib/mxrb/semantic/batch_plan.rb +68 -0
- data/lib/mxrb/semantic/domain_mutator.rb +419 -0
- data/lib/mxrb/semantic/embedder.rb +51 -0
- data/lib/mxrb/semantic/extractor.rb +290 -0
- data/lib/mxrb/semantic/index.rb +623 -0
- data/lib/mxrb/semantic/inliner.rb +216 -0
- data/lib/mxrb/semantic/mover.rb +153 -0
- data/lib/mxrb/semantic/onnx_embedder.rb +34 -0
- data/lib/mxrb/semantic/remover.rb +70 -0
- data/lib/mxrb/semantic/renamer.rb +211 -0
- data/lib/mxrb/semantic/tfidf_embedder.rb +46 -0
- data/lib/mxrb/semantic/vec_store.rb +75 -0
- data/lib/mxrb/team_server.rb +390 -0
- data/lib/mxrb/templates/project/10.24.0.73019.json +32 -0
- data/lib/mxrb/templates/project/11.12.1.json +32 -0
- data/lib/mxrb/templates/project/6.10.8.json +50 -0
- data/lib/mxrb/templates/project/7.17.0.json +50 -0
- data/lib/mxrb/templates/project/7.5.0.json +50 -0
- data/lib/mxrb/templates/project/9.6.1.29396.json +50 -0
- data/lib/mxrb/version.rb +5 -0
- data/lib/mxrb/widget_package.rb +338 -0
- data/lib/mxrb/widget_synchronizer.rb +31 -0
- data/lib/mxrb/writer.rb +3775 -0
- data/lib/mxrb.rb +188 -0
- data/marketplace/catalog.json +10 -0
- data/marketplace/modules/shared-kernel/domain/README.md +4 -0
- data/marketplace/modules/shared-kernel/module.rb +5 -0
- data/marketplace/modules/shared-kernel/mxrb-module.json +9 -0
- 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
|