mxrb 0.1.2 → 0.1.4
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 +4 -4
- data/bin/mxrb +458 -10
- data/docs/de-DE/README.md +1 -0
- data/docs/de-DE/oql-sql.md +42 -0
- data/docs/de-DE/project-structure.md +201 -2
- data/docs/de-DE/runtime-ruby.md +104 -0
- data/docs/de-DE/runtime-schema-migrations.md +44 -0
- data/docs/de-DE/validation-matrix.md +42 -4
- data/docs/en-US/README.md +1 -0
- data/docs/en-US/oql-sql.md +41 -0
- data/docs/en-US/project-structure.md +192 -2
- data/docs/en-US/runtime-ruby.md +100 -0
- data/docs/en-US/runtime-schema-migrations.md +41 -0
- data/docs/en-US/validation-matrix.md +39 -4
- data/docs/pt-BR/README.md +1 -0
- data/docs/pt-BR/entity-dsl.md +8 -0
- data/docs/pt-BR/oql-sql.md +41 -0
- data/docs/pt-BR/project-structure.md +197 -2
- data/docs/pt-BR/runtime-ruby.md +108 -0
- data/docs/pt-BR/runtime-schema-migrations.md +40 -0
- data/docs/pt-BR/validation-matrix.md +42 -5
- data/docs/pt-BR/writing.md +28 -0
- data/lib/mxrb/cli/help.rb +426 -0
- data/lib/mxrb/cli/release.rb +137 -0
- data/lib/mxrb/compiler/compatibility_analyzer.rb +1 -1
- data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +8 -1
- data/lib/mxrb/compiler/domain_document_compiler.rb +3 -0
- data/lib/mxrb/compiler/gallery_bundle_compiler.rb +11 -1
- data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +76 -19
- data/lib/mxrb/compiler/legacy_custom_widget_compiler.rb +159 -0
- data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +43 -7
- data/lib/mxrb/compiler/legacy_page_builder.rb +1248 -23
- data/lib/mxrb/compiler/packager.rb +27 -13
- data/lib/mxrb/compiler/page_bundle_compiler.rb +13 -4
- data/lib/mxrb/compiler/portable_packager.rb +16 -11
- data/lib/mxrb/compiler/web_bundle_builder.rb +5 -1
- data/lib/mxrb/compiler/web_shell_materializer.rb +42 -2
- data/lib/mxrb/compiler/widget_package_extractor.rb +3 -0
- data/lib/mxrb/domain_diagram/lifecycle.rb +272 -0
- data/lib/mxrb/domain_diagram.rb +409 -0
- data/lib/mxrb/dsl/builder.rb +62 -11
- data/lib/mxrb/environment.rb +226 -0
- data/lib/mxrb/exporter.rb +265 -45
- data/lib/mxrb/http/server.rb +107 -0
- data/lib/mxrb/initializer.rb +80 -9
- data/lib/mxrb/integrity/validator.rb +32 -7
- data/lib/mxrb/io/bson_codec.rb +42 -1
- data/lib/mxrb/io/mpr_file.rb +146 -3
- data/lib/mxrb/model/attribute.rb +3 -0
- data/lib/mxrb/model/entity.rb +44 -2
- data/lib/mxrb/model/microflow.rb +3 -1
- data/lib/mxrb/model/module.rb +71 -0
- data/lib/mxrb/model/page.rb +126 -15
- data/lib/mxrb/model/project.rb +3 -0
- data/lib/mxrb/modeler/catalog.rb +178 -0
- data/lib/mxrb/official_marketplace/module_package_importer.rb +23 -12
- data/lib/mxrb/official_marketplace/widget_package_installer.rb +15 -11
- data/lib/mxrb/official_marketplace.rb +9 -3
- data/lib/mxrb/oql/reverse_translator.rb +394 -0
- data/lib/mxrb/oql/server.rb +4 -8
- data/lib/mxrb/oql.rb +32 -4
- data/lib/mxrb/progress.rb +242 -0
- data/lib/mxrb/project_lifecycle.rb +5 -2
- data/lib/mxrb/ruby_app/exporter.rb +1749 -0
- data/lib/mxrb/ruby_app/preset.rb +506 -0
- data/lib/mxrb/ruby_app/session_manager.rb +122 -0
- data/lib/mxrb/ruby_app.rb +1361 -0
- data/lib/mxrb/runtime/access_control.rb +408 -0
- data/lib/mxrb/runtime/native.rb +1015 -93
- data/lib/mxrb/runtime/scheduler.rb +396 -0
- data/lib/mxrb/runtime/schema_migrator.rb +598 -0
- data/lib/mxrb/runtime/shared_store.rb +335 -0
- data/lib/mxrb/runtime/sqlite_store.rb +729 -0
- data/lib/mxrb/scaffold/generator.rb +11 -3
- data/lib/mxrb/team_server.rb +10 -7
- data/lib/mxrb/uml/activity_diagram.rb +118 -0
- data/lib/mxrb/uml/class_diagram.rb +118 -0
- data/lib/mxrb/uml/sequence_diagram.rb +112 -0
- data/lib/mxrb/uml/server.rb +146 -0
- data/lib/mxrb/uml/support.rb +35 -0
- data/lib/mxrb/version.rb +1 -1
- data/lib/mxrb/web_ui/assets/abnfDiagram-N423BO3Z-C4JD3cEC.js +1 -0
- data/lib/mxrb/web_ui/assets/arc-3Z53kgFp.js +1 -0
- data/lib/mxrb/web_ui/assets/architecture-TIHT7OUA-BStBTLea.js +1 -0
- data/lib/mxrb/web_ui/assets/architectureDiagram-T3A2C74G-DBwqnM4J.js +36 -0
- data/lib/mxrb/web_ui/assets/array-BifhSqXX.js +1 -0
- data/lib/mxrb/web_ui/assets/blockDiagram-VBNYF7ZC-DLtuHt-j.js +132 -0
- data/lib/mxrb/web_ui/assets/c4Diagram-5PPSVZJV-ConSk_pe.js +10 -0
- data/lib/mxrb/web_ui/assets/channel-BhPvD7Dt.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-2GRJ4B5K-D2HpWsf_.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-4I5QYGJK-CiIdNwaF.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-5RXB4S5H-Bu_AaEkE.js +231 -0
- data/lib/mxrb/web_ui/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- data/lib/mxrb/web_ui/assets/chunk-6Q2QTUOP-wQkNmt_X.js +88 -0
- data/lib/mxrb/web_ui/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-GF5L2VYU-G8MmiBzU.js +206 -0
- data/lib/mxrb/web_ui/assets/chunk-I66GZJ75-CulcrLPh.js +127 -0
- data/lib/mxrb/web_ui/assets/chunk-JQJVKLGR-ukjzcdlL.js +156 -0
- data/lib/mxrb/web_ui/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-KBJHAD2P-Wm35lYov.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-KEIR6QF5-BICK3FdT.js +161 -0
- data/lib/mxrb/web_ui/assets/chunk-NSK5VX7P-D2nYrCY4.js +2 -0
- data/lib/mxrb/web_ui/assets/chunk-QR6OTTB3-BrdIzg4Q.js +62 -0
- data/lib/mxrb/web_ui/assets/chunk-RYQCIY6F-CCfZMcW6.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-UBXNYLIW-BPEVYsFK.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-W5SLKNZC-2E6OPhA4.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-WRU74C26-CBIvOyWp.js +70 -0
- data/lib/mxrb/web_ui/assets/chunk-XXDRQBXY-pH58XAyl.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- data/lib/mxrb/web_ui/assets/classDiagram-JCYQIIEL-xiqo4D89.js +1 -0
- data/lib/mxrb/web_ui/assets/classDiagram-v2-OCEON4UE-xiqo4D89.js +1 -0
- data/lib/mxrb/web_ui/assets/cose-bilkent-JH36ORCC-t_flPpii.js +1 -0
- data/lib/mxrb/web_ui/assets/cynefin-VYW2F7L2-BIqktTEv.js +1 -0
- data/lib/mxrb/web_ui/assets/cynefinDiagram-MW4NZA55-C2d--lut.js +62 -0
- data/lib/mxrb/web_ui/assets/cytoscape.esm-B-NFISlW.js +321 -0
- data/lib/mxrb/web_ui/assets/dagre-Buvkdvvj.js +1 -0
- data/lib/mxrb/web_ui/assets/dagre-VZM6K2ZE-CSz2_tb5.js +4 -0
- data/lib/mxrb/web_ui/assets/defaultLocale-BFoDCU3G.js +1 -0
- data/lib/mxrb/web_ui/assets/diagram-7IWD3JNH-DCt7u7V_.js +30 -0
- data/lib/mxrb/web_ui/assets/diagram-B4RE2ZJO-DqZBqkMu.js +3 -0
- data/lib/mxrb/web_ui/assets/diagram-LBJQPF4R-ClIYbIsI.js +24 -0
- data/lib/mxrb/web_ui/assets/diagram-Q27KOJAE-DFxhvthJ.js +24 -0
- data/lib/mxrb/web_ui/assets/diagram-UB23O5K3-BSUTXptv.js +41 -0
- data/lib/mxrb/web_ui/assets/dist-D2qOEbeJ.js +1 -0
- data/lib/mxrb/web_ui/assets/domain-mGOZ1KCG.css +1 -0
- data/lib/mxrb/web_ui/assets/domain-qsjXhy4v.js +1 -0
- data/lib/mxrb/web_ui/assets/ebnfDiagram-BXEA7PRR-B5reICEi.js +1 -0
- data/lib/mxrb/web_ui/assets/erDiagram-JOGREHBK-Co5Mxn2p.js +85 -0
- data/lib/mxrb/web_ui/assets/eventmodeling-45OFAUF4-C1ylhSks.js +1 -0
- data/lib/mxrb/web_ui/assets/flowDiagram-UKHOOZJN-CHXaJIaS.js +1 -0
- data/lib/mxrb/web_ui/assets/ganttDiagram-PKOTCBZU-DPPPaWaz.js +292 -0
- data/lib/mxrb/web_ui/assets/gitGraph-TEB2WS4Q-qS77ywA9.js +1 -0
- data/lib/mxrb/web_ui/assets/gitGraphDiagram-DS77QQ5N-DpAv6-BT.js +106 -0
- data/lib/mxrb/web_ui/assets/graphlib-DS17s2tU.js +1 -0
- data/lib/mxrb/web_ui/assets/info-DKCQHKI2-C9VD1Y4e.js +1 -0
- data/lib/mxrb/web_ui/assets/infoDiagram-6WML65LV-Bk1kbcCy.js +2 -0
- data/lib/mxrb/web_ui/assets/init-C-OQMol4.js +1 -0
- data/lib/mxrb/web_ui/assets/ishikawaDiagram-WSZJBQD7-0gcISBFR.js +70 -0
- data/lib/mxrb/web_ui/assets/journeyDiagram-NVQOT4AX-Dt0RQnlh.js +139 -0
- data/lib/mxrb/web_ui/assets/jsx-runtime-DV0a5kSb.js +9 -0
- data/lib/mxrb/web_ui/assets/kanban-definition-27J2QSJJ-DIPQM7w9.js +89 -0
- data/lib/mxrb/web_ui/assets/katex-CXMH3UgJ.js +257 -0
- data/lib/mxrb/web_ui/assets/line-C0rYD-tL.js +1 -0
- data/lib/mxrb/web_ui/assets/linear-BPPxWorj.js +1 -0
- data/lib/mxrb/web_ui/assets/map-BaFkSB1l.js +1 -0
- data/lib/mxrb/web_ui/assets/mermaid-parser.core-B_UPzTxa.js +7 -0
- data/lib/mxrb/web_ui/assets/mindmap-definition-FAOFIHXS-CdJRSMuk.js +96 -0
- data/lib/mxrb/web_ui/assets/modeler-BNqHthwk.css +1 -0
- data/lib/mxrb/web_ui/assets/modeler-CQmqflbg.js +1 -0
- data/lib/mxrb/web_ui/assets/ordinal-BDEzSJ7C.js +1 -0
- data/lib/mxrb/web_ui/assets/packet-7NZHBO7P-CwQ4NSBs.js +1 -0
- data/lib/mxrb/web_ui/assets/path-fybaL0A-.js +1 -0
- data/lib/mxrb/web_ui/assets/pegDiagram-VL7TDLO6-D40S7ppx.js +1 -0
- data/lib/mxrb/web_ui/assets/pie-RZYD4A2V-CJroHMKh.js +1 -0
- data/lib/mxrb/web_ui/assets/pieDiagram-7S7Q4E2Y-QB54__BV.js +39 -0
- data/lib/mxrb/web_ui/assets/quadrantDiagram-CIZ2JOQS-DBhDS6ag.js +7 -0
- data/lib/mxrb/web_ui/assets/radar-I7S5WNFK-DdIUwd2d.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-3IZDKUUU-B1Lw6Xyq.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-abnf-AHOZXSZD-CQgQoeak.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-ebnf-EBAXGLYW-D7qEQ2x8.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-peg-LSFZ7HO6-C005Vxbb.js +1 -0
- data/lib/mxrb/web_ui/assets/railroadDiagram-AXF67PYL-BBP2qk7M.js +1 -0
- data/lib/mxrb/web_ui/assets/requirementDiagram-LRYGKXZP-D2cLsAoI.js +84 -0
- data/lib/mxrb/web_ui/assets/rough.esm-Dy-Kn_BL.js +1 -0
- data/lib/mxrb/web_ui/assets/sankeyDiagram-W5VNT64P-Bpi3FiP2.js +40 -0
- data/lib/mxrb/web_ui/assets/sequenceDiagram-SI44F4Z6-DCRcDlVe.js +162 -0
- data/lib/mxrb/web_ui/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
- data/lib/mxrb/web_ui/assets/src-CvxfUak2.js +1 -0
- data/lib/mxrb/web_ui/assets/stateDiagram-OKZ733FA-C2GCEoxl.js +1 -0
- data/lib/mxrb/web_ui/assets/stateDiagram-v2-UEYNNEHI-D2gWgedh.js +1 -0
- data/lib/mxrb/web_ui/assets/swimlanes-SLNWSIFB-CRI9irEW.js +2 -0
- data/lib/mxrb/web_ui/assets/swimlanesDiagram-ULZ7WXOC-C5Wb_-hZ.js +8 -0
- data/lib/mxrb/web_ui/assets/theme-_mxc7BdY.css +1 -0
- data/lib/mxrb/web_ui/assets/timeline-definition-Z64GVDOM-B1-mNg-h.js +120 -0
- data/lib/mxrb/web_ui/assets/treeView-QDETBFTQ-7CTvIwKq.js +1 -0
- data/lib/mxrb/web_ui/assets/treemap-6X3UGDF4-CtL22pq5.js +1 -0
- data/lib/mxrb/web_ui/assets/uml-C3KAFGTq.js +51 -0
- data/lib/mxrb/web_ui/assets/uml-Dn-qd01X.css +1 -0
- data/lib/mxrb/web_ui/assets/vennDiagram-T6HMQDX7-BzVnUEAy.js +34 -0
- data/lib/mxrb/web_ui/assets/wardley-OPB4EBWU-JNi-QNag.js +1 -0
- data/lib/mxrb/web_ui/assets/wardleyDiagram-T6FBY63Y-BUwmAQ-B.js +78 -0
- data/lib/mxrb/web_ui/assets/xychartDiagram-ELKLHX3M-DEQS8sKp.js +7 -0
- data/lib/mxrb/web_ui/domain.html +15 -0
- data/lib/mxrb/web_ui/modeler.html +16 -0
- data/lib/mxrb/web_ui/uml.html +31 -0
- data/lib/mxrb/web_ui.rb +89 -0
- data/lib/mxrb/widget_certification.rb +208 -0
- data/lib/mxrb/writer.rb +278 -41
- data/lib/mxrb.rb +38 -8
- metadata +157 -5
|
@@ -17,11 +17,25 @@ module Mxrb
|
|
|
17
17
|
# rendering these elements, not merely serializing their nested settings.
|
|
18
18
|
VISUAL_WIDGET_TYPES = %w[
|
|
19
19
|
Forms$ActionButton Forms$CheckBox Forms$DataGrid Forms$DataView Forms$DatePicker
|
|
20
|
-
Forms$DivContainer Forms$DropDown Forms$DynamicText Forms$
|
|
21
|
-
Forms$
|
|
20
|
+
Forms$DivContainer Forms$DropDown Forms$DynamicText Forms$ImageUploader Forms$ImageViewer
|
|
21
|
+
Forms$Label
|
|
22
|
+
Forms$Header Forms$LayoutGrid Forms$ListView Forms$MenuBar Forms$NavigationTree
|
|
23
|
+
Forms$RadioButtonGroup Forms$ReferenceSelector Forms$ScrollContainer
|
|
24
|
+
Forms$ReferenceSetSelector Forms$Table Forms$TemplateGrid Forms$VerticalSplitPane
|
|
25
|
+
Forms$SidebarToggleButton Forms$SimpleMenuBar
|
|
26
|
+
Forms$MobileBackButton Forms$MobileCancelButton Forms$MobileSaveButton
|
|
22
27
|
Forms$SnippetCallWidget Forms$StaticImageViewer Forms$TabControl Forms$TextArea
|
|
23
28
|
Forms$TextBox Forms$InputReferenceSetSelector CustomWidgets$CustomWidget
|
|
24
29
|
].freeze
|
|
30
|
+
ACTION_BUTTON_TYPES = %w[
|
|
31
|
+
Forms$CancelChangesClientAction Forms$ClosePageClientAction Forms$FormAction
|
|
32
|
+
Forms$MicroflowAction Forms$NoAction Forms$OpenLinkClientAction
|
|
33
|
+
Forms$SaveChangesClientAction
|
|
34
|
+
].freeze
|
|
35
|
+
BOUND_WIDGET_TYPES = %w[
|
|
36
|
+
Forms$CheckBox Forms$DatePicker Forms$DropDown Forms$InputReferenceSetSelector
|
|
37
|
+
Forms$RadioButtonGroup Forms$ReferenceSelector Forms$TextArea Forms$TextBox
|
|
38
|
+
].freeze
|
|
25
39
|
|
|
26
40
|
def initialize(source, web_root, profiles: [:dojo])
|
|
27
41
|
@source = source
|
|
@@ -31,8 +45,8 @@ module Mxrb
|
|
|
31
45
|
end
|
|
32
46
|
|
|
33
47
|
def build
|
|
34
|
-
pages =
|
|
35
|
-
layouts =
|
|
48
|
+
pages = legacy_pages
|
|
49
|
+
layouts = legacy_layouts
|
|
36
50
|
languages.each do |language|
|
|
37
51
|
layouts.each { write_layout(_1, language) }
|
|
38
52
|
pages.each { write_page(_1, language) }
|
|
@@ -48,7 +62,7 @@ module Mxrb
|
|
|
48
62
|
# Performs the same widget audit as #build without writing deployment files.
|
|
49
63
|
def audit
|
|
50
64
|
@unsupported.clear
|
|
51
|
-
(
|
|
65
|
+
(legacy_layouts + legacy_pages).each do |unit|
|
|
52
66
|
name = "#{unit.module_name}.#{unit.document['Name']}"
|
|
53
67
|
audit_unsupported_widgets(unit.document, name)
|
|
54
68
|
descendants(unit.document).select { _1['$Type'] == 'Forms$DataGrid' }.each_with_index do |grid, index|
|
|
@@ -62,10 +76,22 @@ module Mxrb
|
|
|
62
76
|
|
|
63
77
|
private
|
|
64
78
|
|
|
79
|
+
def legacy_pages
|
|
80
|
+
units = @source.units_of('Forms$Page')
|
|
81
|
+
@source.is_a?(SourceModel) ? units.select { @source.web_page?(_1) } : units
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def legacy_layouts
|
|
85
|
+
units = @source.units_of('Forms$Layout')
|
|
86
|
+
@source.is_a?(SourceModel) ? units.select { @source.web_layout?(_1) } : units
|
|
87
|
+
end
|
|
88
|
+
|
|
65
89
|
def write_page(unit, language)
|
|
66
90
|
name = "#{unit.module_name}.#{unit.document['Name']}"
|
|
67
91
|
audit_unsupported_widgets(unit.document, name)
|
|
68
92
|
@widget_sequence = 0
|
|
93
|
+
@widget_ids_by_name = {}
|
|
94
|
+
@templates = []
|
|
69
95
|
rendered = page_widgets(unit.document).map { render_widget(_1, name, language) }.join
|
|
70
96
|
path = File.join(@web_root, 'pages', language, unit.module_name, "#{unit.document['Name']}.page.xml")
|
|
71
97
|
FileUtils.mkdir_p(File.dirname(path))
|
|
@@ -76,36 +102,87 @@ module Mxrb
|
|
|
76
102
|
name = "#{unit.module_name}.#{unit.document['Name']}"
|
|
77
103
|
audit_unsupported_widgets(unit.document, name)
|
|
78
104
|
@widget_sequence = 0
|
|
105
|
+
@widget_ids_by_name = {}
|
|
106
|
+
@templates = []
|
|
79
107
|
rendered = layout_widgets(unit.document).map { render_widget(_1, name, language) }.join
|
|
80
108
|
path = File.join(@web_root, 'pages', language, unit.module_name, "#{unit.document['Name']}.layout.xml")
|
|
81
109
|
FileUtils.mkdir_p(File.dirname(path))
|
|
82
110
|
File.binwrite(path, layout_xml(unit, rendered))
|
|
83
111
|
end
|
|
84
112
|
|
|
85
|
-
def audit_unsupported_widgets(root, page_name)
|
|
86
|
-
visit = lambda do |value|
|
|
113
|
+
def audit_unsupported_widgets(root, page_name, initial_data_view_context: false)
|
|
114
|
+
visit = lambda do |value, data_view_context = initial_data_view_context|
|
|
87
115
|
case value
|
|
88
116
|
when Hash
|
|
89
117
|
type = value['$Type'].to_s
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
elsif type == 'Forms$DivContainer'
|
|
97
|
-
action_type = value.dig('OnClickAction', '$Type').to_s
|
|
98
|
-
@unsupported[page_name] << 'Forms$DivContainer(onClick)' \
|
|
99
|
-
unless action_type.empty? || action_type == 'Forms$NoAction'
|
|
118
|
+
data_view_context ||= %w[Forms$DataView Forms$ListView].include?(type)
|
|
119
|
+
next if type == 'Forms$DataGrid'
|
|
120
|
+
|
|
121
|
+
if type == 'Forms$TemplateGrid'
|
|
122
|
+
audit_template_grid(value, page_name)
|
|
123
|
+
next
|
|
100
124
|
end
|
|
101
125
|
|
|
102
|
-
value
|
|
103
|
-
|
|
126
|
+
audit_widget(value, type, page_name, data_view_context)
|
|
127
|
+
|
|
128
|
+
value.each_value { visit.call(_1, data_view_context) }
|
|
129
|
+
when Array then value.each { visit.call(_1, data_view_context) }
|
|
104
130
|
end
|
|
105
131
|
end
|
|
106
132
|
visit.call(root)
|
|
107
133
|
end
|
|
108
134
|
|
|
135
|
+
def audit_widget(widget, type, page_name, data_view_context)
|
|
136
|
+
if VISUAL_WIDGET_TYPES.include?(type) && !supported_widget_type?(type)
|
|
137
|
+
@unsupported[page_name] << type
|
|
138
|
+
elsif type == 'Forms$DynamicText' && !supported_dynamic_text?(widget, data_view_context)
|
|
139
|
+
@unsupported[page_name] << 'Forms$DynamicText(parameters)'
|
|
140
|
+
elsif type == 'Forms$ActionButton' && !supported_action_button?(widget)
|
|
141
|
+
action_type = widget.dig('Action', '$Type').to_s
|
|
142
|
+
@unsupported[page_name] << (action_type.empty? ? 'Forms$ActionButton(action)' : action_type)
|
|
143
|
+
elsif type == 'Forms$DataView' && !supported_data_view?(widget)
|
|
144
|
+
source_type = widget.dig('DataSource', '$Type').to_s
|
|
145
|
+
@unsupported[page_name] << (source_type.empty? ? 'Forms$DataView(source)' : source_type)
|
|
146
|
+
elsif type == 'Forms$ListView' && !supported_list_view?(widget)
|
|
147
|
+
source_type = widget.dig('DataSource', '$Type').to_s
|
|
148
|
+
@unsupported[page_name] << (source_type.empty? ? 'Forms$ListView(source)' : source_type)
|
|
149
|
+
elsif type == 'Forms$StaticImageViewer' && !supported_static_image?(widget)
|
|
150
|
+
@unsupported[page_name] << type
|
|
151
|
+
elsif type == 'Forms$ImageViewer' && !supported_image_viewer?(widget, data_view_context)
|
|
152
|
+
@unsupported[page_name] << type
|
|
153
|
+
elsif type == 'Forms$ImageUploader' && !data_view_context
|
|
154
|
+
@unsupported[page_name] << type
|
|
155
|
+
elsif type == 'Forms$ReferenceSetSelector' &&
|
|
156
|
+
!supported_reference_set_selector?(widget, data_view_context, page_name)
|
|
157
|
+
@unsupported[page_name] << type
|
|
158
|
+
elsif %w[Forms$MenuBar Forms$NavigationTree Forms$SimpleMenuBar].include?(type) &&
|
|
159
|
+
menu_identifier(widget['MenuSource']).to_s.empty?
|
|
160
|
+
@unsupported[page_name] << type
|
|
161
|
+
elsif type == 'Forms$SnippetCallWidget'
|
|
162
|
+
audit_snippet_call(widget, page_name, data_view_context)
|
|
163
|
+
elsif type == 'CustomWidgets$CustomWidget' && !supported_custom_widget?(widget)
|
|
164
|
+
identifier = widget.dig('Type', 'WidgetId').to_s
|
|
165
|
+
@unsupported[page_name] << (identifier.empty? ? type : identifier)
|
|
166
|
+
elsif BOUND_WIDGET_TYPES.include?(type) && !supported_bound_widget?(widget, data_view_context)
|
|
167
|
+
@unsupported[page_name] << type
|
|
168
|
+
elsif type == 'Forms$DivContainer'
|
|
169
|
+
action_type = widget.dig('OnClickAction', '$Type').to_s
|
|
170
|
+
@unsupported[page_name] << 'Forms$DivContainer(onClick)' \
|
|
171
|
+
unless action_type.empty? || action_type == 'Forms$NoAction'
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def audit_template_grid(widget, page_name)
|
|
176
|
+
compiler = LegacyDataGridCompiler.new(
|
|
177
|
+
@source, page_name, widget, language: 'en_US', sequence: 1
|
|
178
|
+
)
|
|
179
|
+
compiler.html
|
|
180
|
+
@unsupported[page_name].concat(compiler.unsupported)
|
|
181
|
+
audit_unsupported_widgets(
|
|
182
|
+
widget['Contents'] || {}, page_name, initial_data_view_context: true
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
|
|
109
186
|
def page_xml(unit, language, content)
|
|
110
187
|
page = unit.document
|
|
111
188
|
title = translated(page['Title'], language)
|
|
@@ -119,14 +196,14 @@ module Mxrb
|
|
|
119
196
|
"title='#{escape(title)}'#{class_attribute} xmlns:m='http://schemas.mendix.com/forms/1.0'>" \
|
|
120
197
|
"<m:layouts>#{layout.empty? ? '' : "<m:layout path='#{escape(layout)}.layout.xml'></m:layout>"}</m:layouts>" \
|
|
121
198
|
"<m:arguments><m:argument parameterName='#{escape(parameter)}'>#{content}</m:argument></m:arguments>" \
|
|
122
|
-
|
|
199
|
+
"#{templates_xml}</m:page>"
|
|
123
200
|
end
|
|
124
201
|
|
|
125
202
|
def layout_xml(unit, content)
|
|
126
203
|
"\uFEFF<?xml version='1.0' encoding='utf-8'?>" \
|
|
127
204
|
"<m:layout id='#{unit.id}' xmlns='http://www.w3.org/1999/xhtml' " \
|
|
128
205
|
"xmlns:m='http://schemas.mendix.com/forms/1.0'>" \
|
|
129
|
-
"<m:arguments><m:argument>#{content}</m:argument></m:arguments
|
|
206
|
+
"<m:arguments><m:argument>#{content}</m:argument></m:arguments>#{templates_xml}</m:layout>"
|
|
130
207
|
end
|
|
131
208
|
|
|
132
209
|
def render_widget(widget, page_name, language)
|
|
@@ -140,8 +217,58 @@ module Mxrb
|
|
|
140
217
|
"<div data-mx-placeholder='#{escape(identifier)}' class='mx-placeholder'></div>"
|
|
141
218
|
when 'Forms$DivContainer'
|
|
142
219
|
render_container(widget, page_name, language)
|
|
220
|
+
when 'Forms$Table'
|
|
221
|
+
render_table(widget, page_name, language)
|
|
222
|
+
when 'Forms$TemplateGrid'
|
|
223
|
+
render_template_grid(widget, page_name, language)
|
|
224
|
+
when 'Forms$LayoutGrid'
|
|
225
|
+
render_layout_grid(widget, page_name, language)
|
|
226
|
+
when 'Forms$LayoutGridRow'
|
|
227
|
+
render_grid_row(widget, page_name, language)
|
|
228
|
+
when 'Forms$LayoutGridColumn'
|
|
229
|
+
render_grid_column(widget, page_name, language)
|
|
230
|
+
when 'Forms$ScrollContainer'
|
|
231
|
+
render_scroll_container(widget, page_name, language)
|
|
232
|
+
when 'Forms$Header'
|
|
233
|
+
render_header(widget, page_name, language)
|
|
234
|
+
when 'Forms$SidebarToggleButton'
|
|
235
|
+
render_sidebar_toggle(widget, language)
|
|
236
|
+
when 'Forms$NavigationTree', 'Forms$MenuBar', 'Forms$SimpleMenuBar'
|
|
237
|
+
render_navigation(widget)
|
|
238
|
+
when 'Forms$ActionButton'
|
|
239
|
+
render_action_button(widget, language)
|
|
240
|
+
when 'Forms$MobileSaveButton'
|
|
241
|
+
render_mobile_button(widget, language, 'mxui.widget.SaveButton')
|
|
242
|
+
when 'Forms$MobileCancelButton'
|
|
243
|
+
render_mobile_button(widget, language, 'mxui.widget.CancelButton')
|
|
244
|
+
when 'Forms$MobileBackButton'
|
|
245
|
+
render_mobile_button(widget, language, 'mxui.widget.BackButton')
|
|
143
246
|
when 'Forms$DynamicText'
|
|
144
247
|
render_dynamic_text(widget, language)
|
|
248
|
+
when 'Forms$DataView'
|
|
249
|
+
render_data_view(widget, page_name, language)
|
|
250
|
+
when 'Forms$ListView'
|
|
251
|
+
render_list_view(widget, page_name, language)
|
|
252
|
+
when 'Forms$TextBox', 'Forms$TextArea', 'Forms$CheckBox', 'Forms$DatePicker',
|
|
253
|
+
'Forms$DropDown', 'Forms$RadioButtonGroup', 'Forms$ReferenceSelector',
|
|
254
|
+
'Forms$InputReferenceSetSelector'
|
|
255
|
+
render_bound_input(widget, language)
|
|
256
|
+
when 'Forms$Label'
|
|
257
|
+
render_label(widget, language)
|
|
258
|
+
when 'Forms$StaticImageViewer'
|
|
259
|
+
render_static_image(widget)
|
|
260
|
+
when 'Forms$ImageViewer'
|
|
261
|
+
render_image_viewer(widget)
|
|
262
|
+
when 'Forms$ImageUploader'
|
|
263
|
+
render_image_uploader(widget)
|
|
264
|
+
when 'Forms$ReferenceSetSelector'
|
|
265
|
+
render_reference_set_selector(widget, page_name, language)
|
|
266
|
+
when 'Forms$TabControl'
|
|
267
|
+
render_tab_control(widget, page_name, language)
|
|
268
|
+
when 'Forms$SnippetCallWidget'
|
|
269
|
+
render_snippet_call(widget, page_name, language)
|
|
270
|
+
when 'CustomWidgets$CustomWidget'
|
|
271
|
+
render_custom_widget(widget, language)
|
|
145
272
|
when 'Forms$DataGrid'
|
|
146
273
|
render_data_grid(widget, page_name, language)
|
|
147
274
|
else
|
|
@@ -155,15 +282,960 @@ module Mxrb
|
|
|
155
282
|
"<#{tag}#{html_attributes(widget, base: nil)}>#{children}</#{tag}>"
|
|
156
283
|
end
|
|
157
284
|
|
|
285
|
+
def render_table(widget, page_name, language)
|
|
286
|
+
columns = array(widget['ColumnWidths'])
|
|
287
|
+
colgroup = columns.map do |column|
|
|
288
|
+
"<col style='width:#{column['Value'].to_i}%'></col>"
|
|
289
|
+
end.join
|
|
290
|
+
rows = array(widget['Rows']).each_with_index.map do |row, index|
|
|
291
|
+
cells = array(widget['Cells']).select { _1['TopRowIndex'].to_i == index }
|
|
292
|
+
.sort_by { _1['LeftColumnIndex'].to_i }
|
|
293
|
+
content = cells.map { render_table_cell(_1, page_name, language) }.join
|
|
294
|
+
"<tr#{html_attributes(row, base: nil)}>#{content}</tr>"
|
|
295
|
+
end.join
|
|
296
|
+
"<table#{html_attributes(widget, base: 'mx-table')}><colgroup>#{colgroup}</colgroup>" \
|
|
297
|
+
"<tbody>#{rows}</tbody></table>"
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def render_table_cell(cell, page_name, language)
|
|
301
|
+
tag = cell['IsHeader'] == true ? 'th' : 'td'
|
|
302
|
+
spans = {}
|
|
303
|
+
spans['colspan'] = cell['Width'].to_i if cell['Width'].to_i > 1
|
|
304
|
+
spans['rowspan'] = cell['Height'].to_i if cell['Height'].to_i > 1
|
|
305
|
+
span_attributes = spans.map { |key, value| " #{key}='#{value}'" }.join
|
|
306
|
+
content = widget_children(cell).map { render_widget(_1, page_name, language) }.join
|
|
307
|
+
"<#{tag}#{html_attributes(cell, base: nil)}#{span_attributes}>#{content}</#{tag}>"
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def render_template_grid(widget, page_name, language)
|
|
311
|
+
sequence = next_widget_sequence
|
|
312
|
+
compiler = LegacyDataGridCompiler.new(
|
|
313
|
+
@source, page_name, widget, language:, sequence:
|
|
314
|
+
)
|
|
315
|
+
html = compiler.html
|
|
316
|
+
@unsupported[page_name].concat(compiler.unsupported)
|
|
317
|
+
entity = widget.dig('DataSource', 'EntityPath').to_s
|
|
318
|
+
@data_view_stack ||= []
|
|
319
|
+
@data_view_stack << { entity:, label_width: 0, editable: false }
|
|
320
|
+
content = widget_children(widget['Contents'] || {}).map do |child|
|
|
321
|
+
render_widget(child, page_name, language)
|
|
322
|
+
end.join
|
|
323
|
+
@data_view_stack.pop
|
|
324
|
+
@templates << "<m:template widget-id='#{escape(compiler.id)}' name='content'>" \
|
|
325
|
+
"#{content}</m:template>"
|
|
326
|
+
html
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def render_layout_grid(widget, page_name, language)
|
|
330
|
+
render_structural_widget(
|
|
331
|
+
widget, array(widget['Rows']), page_name, language,
|
|
332
|
+
base: 'mx-layoutgrid mx-layoutgrid-fluid'
|
|
333
|
+
)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def render_grid_row(widget, page_name, language)
|
|
337
|
+
render_structural_widget(widget, array(widget['Columns']), page_name, language, base: 'row')
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def render_grid_column(widget, page_name, language)
|
|
341
|
+
classes = ['col', grid_weight_class('md', widget['Weight']),
|
|
342
|
+
grid_weight_class('sm', widget['TabletWeight']),
|
|
343
|
+
grid_weight_class('xs', widget['PhoneWeight'])].compact.join(' ')
|
|
344
|
+
render_structural_widget(widget, widget_children(widget), page_name, language, base: classes)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def render_structural_widget(widget, children, page_name, language, base:)
|
|
348
|
+
content = children.map { render_widget(_1, page_name, language) }.join
|
|
349
|
+
"<div#{html_attributes(widget, base:)}>#{content}</div>"
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def render_scroll_container(widget, page_name, language)
|
|
353
|
+
fixed = widget['ScrollBehavior'] == 'PerRegion'
|
|
354
|
+
sides = [widget['Left'], widget['Right']].compact
|
|
355
|
+
return render_vertical_scroll_container(widget, page_name, language, fixed:) if sides.empty?
|
|
356
|
+
|
|
357
|
+
props = scroll_container_properties(widget, %w[Left CenterRegion Right], fixed:)
|
|
358
|
+
center = render_scroll_region(
|
|
359
|
+
widget['CenterRegion'], 'center', page_name, language, nested: true
|
|
360
|
+
) do
|
|
361
|
+
render_vertical_scroll_container(widget, page_name, language, fixed:)
|
|
362
|
+
end
|
|
363
|
+
content = render_scroll_region(widget['Left'], 'left', page_name, language) + center +
|
|
364
|
+
render_scroll_region(widget['Right'], 'right', page_name, language)
|
|
365
|
+
render_client_container(
|
|
366
|
+
widget, 'mxui.widget.HorizontalScrollContainer', props, content,
|
|
367
|
+
base: scroll_container_classes('horizontal', fixed), widget_classes: false, tabindex: false
|
|
368
|
+
)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def render_vertical_scroll_container(widget, page_name, language, fixed:)
|
|
372
|
+
props = scroll_container_properties(widget, %w[Top CenterRegion Bottom], fixed:)
|
|
373
|
+
content = render_scroll_region(widget['Top'], 'top', page_name, language) +
|
|
374
|
+
render_scroll_region(widget['CenterRegion'], 'middle', page_name, language) +
|
|
375
|
+
render_scroll_region(widget['Bottom'], 'bottom', page_name, language)
|
|
376
|
+
render_client_container(
|
|
377
|
+
widget, 'mxui.widget.VerticalScrollContainer', props, content,
|
|
378
|
+
base: scroll_container_classes('vertical', fixed), widget_classes: false, tabindex: false
|
|
379
|
+
)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def scroll_container_properties(widget, region_names, fixed:)
|
|
383
|
+
config = region_names.filter_map do |name|
|
|
384
|
+
region = widget[name]
|
|
385
|
+
next unless region
|
|
386
|
+
|
|
387
|
+
position = if name == 'CenterRegion'
|
|
388
|
+
region_names.include?('Left') ? 'center' : 'middle'
|
|
389
|
+
else
|
|
390
|
+
name.downcase
|
|
391
|
+
end
|
|
392
|
+
scroll_region_config(region, position)
|
|
393
|
+
end
|
|
394
|
+
{ 'fixed' => fixed, 'config' => config }
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def scroll_region_config(region, position)
|
|
398
|
+
config = { 'position' => position }
|
|
399
|
+
toggle = region['ToggleMode'].to_s
|
|
400
|
+
return config if toggle.empty? || toggle == 'None'
|
|
401
|
+
|
|
402
|
+
config['toggleMode'] = legacy_toggle_mode(toggle)
|
|
403
|
+
config['initiallyOpen'] = toggle.include?('InitiallyOpen') ||
|
|
404
|
+
(!toggle.include?('InitiallyClosed') && toggle.start_with?('ShrinkContent'))
|
|
405
|
+
config
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def legacy_toggle_mode(value)
|
|
409
|
+
return 'shrinkContent' if value.start_with?('ShrinkContent')
|
|
410
|
+
return 'pushContentAside' if value.start_with?('PushContent')
|
|
411
|
+
return 'slideOverContent' if value.start_with?('SlideOverContent')
|
|
412
|
+
|
|
413
|
+
value.sub(/Initially(?:Open|Closed)\z/, '').then { _1[0].to_s.downcase + _1[1..].to_s }
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def render_scroll_region(region, position, page_name, language, nested: false)
|
|
417
|
+
return '' unless region
|
|
418
|
+
|
|
419
|
+
content = if block_given?
|
|
420
|
+
yield
|
|
421
|
+
else
|
|
422
|
+
array(region['Widgets']).map do |child|
|
|
423
|
+
render_widget(child, page_name, language)
|
|
424
|
+
end.join
|
|
425
|
+
end
|
|
426
|
+
base = "mx-layoutcontainer-#{position} mx-scrollcontainer-#{position}"
|
|
427
|
+
region_classes = css_classes(region, base:)
|
|
428
|
+
wrapper = 'mx-layoutcontainer-wrapper mx-scrollcontainer-wrapper'
|
|
429
|
+
wrapper += ' mx-layoutcontainer-nested mx-scrollcontainer-nested' if nested
|
|
430
|
+
style = scroll_region_style(region, position)
|
|
431
|
+
style_attribute = style.empty? ? '' : " style='#{escape(style)}'"
|
|
432
|
+
"<div class='#{escape(region_classes)}'#{style_attribute}>" \
|
|
433
|
+
"<div class='#{wrapper}'>#{content}</div></div>"
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def scroll_region_style(region, position)
|
|
437
|
+
styles = [region['Style'].to_s, region.dig('Appearance', 'Style').to_s].reject(&:empty?)
|
|
438
|
+
if region['SizeMode'] == 'Pixels' && region['Size'].to_i.positive?
|
|
439
|
+
dimension = %w[left right].include?(position) ? 'width' : 'height'
|
|
440
|
+
styles << "#{dimension}:#{region['Size'].to_i}px"
|
|
441
|
+
end
|
|
442
|
+
styles.join(';')
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def scroll_container_classes(orientation, fixed)
|
|
446
|
+
classes = "mx-layoutcontainer mx-scrollcontainer mx-layoutcontainer-#{orientation} " \
|
|
447
|
+
"mx-scrollcontainer-#{orientation}"
|
|
448
|
+
classes += ' mx-layoutcontainer-fixed mx-scrollcontainer-fixed' if fixed
|
|
449
|
+
classes
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def render_header(widget, page_name, language)
|
|
453
|
+
left = array(widget['LeftWidgets']).map { render_widget(_1, page_name, language) }.join
|
|
454
|
+
right = array(widget['RightWidgets']).map { render_widget(_1, page_name, language) }.join
|
|
455
|
+
title = render_client_widget(
|
|
456
|
+
{}, 'mxui.widget.Title', {}, base: nil, widget_classes: false, tabindex: false,
|
|
457
|
+
properties: false
|
|
458
|
+
)
|
|
459
|
+
"<div#{html_attributes(widget, base: 'mx-header')}>" \
|
|
460
|
+
"<div class='mx-header-center'>#{title}</div>" \
|
|
461
|
+
"<div class='mx-header-left'>#{left}</div>" \
|
|
462
|
+
"<div class='mx-header-right'>#{right}</div></div>"
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def render_sidebar_toggle(widget, language)
|
|
466
|
+
props = button_properties(widget, language)
|
|
467
|
+
props['iconClass'] = 'glyphicon-menu-hamburger' if widget['Icon']&.dig('$Type') == 'Forms$GlyphIcon'
|
|
468
|
+
render_type = props['renderType']
|
|
469
|
+
base = render_type == 'button' ? button_style(widget) : nil
|
|
470
|
+
render_client_widget(widget, 'mxui.widget.SidebarToggleButton', props, base:)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def render_navigation(widget)
|
|
474
|
+
type = case widget['$Type']
|
|
475
|
+
when 'Forms$NavigationTree' then 'mxui.widget.NavigationTree'
|
|
476
|
+
when 'Forms$MenuBar' then 'mxui.widget.Navbar'
|
|
477
|
+
else 'mxui.widget.MenuBar'
|
|
478
|
+
end
|
|
479
|
+
props = { 'menuID' => menu_identifier(widget['MenuSource']) }
|
|
480
|
+
props['orientation'] = widget['Orientation'].to_s.downcase if widget['$Type'] == 'Forms$SimpleMenuBar'
|
|
481
|
+
render_client_widget(widget, type, props, base: nil, tabindex: false)
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def menu_identifier(source)
|
|
485
|
+
case source&.fetch('$Type', nil)
|
|
486
|
+
when 'Forms$NavigationSource'
|
|
487
|
+
documents = @source.units_of('Navigation$NavigationDocument').map(&:document)
|
|
488
|
+
profile = documents.flat_map { array(_1['Profiles']) }
|
|
489
|
+
.find { _1['Name'] == source['NavigationProfile'] }
|
|
490
|
+
device_key = "#{source['DeviceType']}Profile"
|
|
491
|
+
profile ||= documents.filter_map { _1[device_key] }.first
|
|
492
|
+
IO::BsonCodec.extract_id(profile&.dig('Menu', '$ID'))
|
|
493
|
+
when 'Forms$MenuDocumentSource'
|
|
494
|
+
qualified = source['Menu'].to_s
|
|
495
|
+
menu = @source.units_of('Menus$MenuDocument').find do |unit|
|
|
496
|
+
"#{unit.module_name}.#{unit.document['Name']}" == qualified
|
|
497
|
+
end
|
|
498
|
+
IO::BsonCodec.extract_id(menu&.document&.dig('ItemCollection', '$ID')) || menu&.id
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def render_action_button(widget, language)
|
|
503
|
+
action = widget['Action'] || {}
|
|
504
|
+
props = button_properties(widget, language)
|
|
505
|
+
case action['$Type']
|
|
506
|
+
when 'Forms$SaveChangesClientAction'
|
|
507
|
+
props.merge!('closeForm' => action.fetch('ClosePage', true), 'needsObject' => true)
|
|
508
|
+
render_client_widget(widget, 'mxui.widget.SaveButton', props)
|
|
509
|
+
when 'Forms$CancelChangesClientAction'
|
|
510
|
+
props.merge!('closeForm' => action.fetch('ClosePage', true), 'needsObject' => true)
|
|
511
|
+
render_client_widget(widget, 'mxui.widget.CancelButton', props)
|
|
512
|
+
when 'Forms$ClosePageClientAction'
|
|
513
|
+
render_client_widget(widget, 'mxui.widget.BackButton', props)
|
|
514
|
+
when 'Forms$OpenLinkClientAction'
|
|
515
|
+
render_link_button(widget, action, props)
|
|
516
|
+
else
|
|
517
|
+
props['action'] = legacy_client_action(action)
|
|
518
|
+
render_client_widget(widget, 'mxui.widget.ActionButton', props)
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
def render_mobile_button(widget, language, type)
|
|
523
|
+
props = button_properties(widget, language)
|
|
524
|
+
unless type == 'mxui.widget.BackButton'
|
|
525
|
+
props.merge!('closeForm' => widget.fetch('ClosePage', true), 'needsObject' => true)
|
|
526
|
+
end
|
|
527
|
+
render_client_widget(widget, type, props)
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def render_data_view(widget, page_name, language)
|
|
531
|
+
id = next_widget_id
|
|
532
|
+
template_index = @templates.length
|
|
533
|
+
@templates << nil
|
|
534
|
+
@data_view_stack ||= []
|
|
535
|
+
@data_view_stack << {
|
|
536
|
+
entity: data_view_entity(widget['DataSource'] || {}),
|
|
537
|
+
label_width: positive_integer(widget['LabelWidth'], 3),
|
|
538
|
+
editable: widget['Editable'] != false
|
|
539
|
+
}
|
|
540
|
+
content = data_view_children(widget, 'Widgets', 'Widget')
|
|
541
|
+
.map { render_widget(_1, page_name, language) }.join
|
|
542
|
+
footer = data_view_children(widget, 'FooterWidgets', 'FooterWidget')
|
|
543
|
+
.map { render_widget(_1, page_name, language) }.join
|
|
544
|
+
@data_view_stack.pop
|
|
545
|
+
@templates[template_index] = data_view_templates(id, content, footer)
|
|
546
|
+
render_client_widget(
|
|
547
|
+
widget, 'mxui.widget.DataView', data_view_properties(widget), id:, base: 'form-horizontal'
|
|
548
|
+
)
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def render_list_view(widget, page_name, language)
|
|
552
|
+
id = next_widget_id
|
|
553
|
+
@widget_ids_by_name ||= {}
|
|
554
|
+
@widget_ids_by_name[widget['Name'].to_s] = id unless widget['Name'].to_s.empty?
|
|
555
|
+
entity = list_view_entity(widget)
|
|
556
|
+
template_index = @templates.length
|
|
557
|
+
@templates << nil
|
|
558
|
+
@data_view_stack ||= []
|
|
559
|
+
@data_view_stack << { entity:, label_width: 0, editable: widget['Editable'] != false }
|
|
560
|
+
content = widget_children(widget).map do |child|
|
|
561
|
+
render_widget(child, page_name, language)
|
|
562
|
+
end.join
|
|
563
|
+
@data_view_stack.pop
|
|
564
|
+
@templates[template_index] = "<m:template widget-id='#{escape(id)}' name='content'>" \
|
|
565
|
+
"#{content}</m:template>"
|
|
566
|
+
props = {
|
|
567
|
+
'datasource' => list_view_source(widget, page_name, entity),
|
|
568
|
+
'page' => positive_integer(widget['PageSize'], 20),
|
|
569
|
+
'hasSearch' => !list_view_search_paths(widget['DataSource'] || {}).empty?,
|
|
570
|
+
'action' => legacy_client_action(widget['ClickAction'] || { '$Type' => 'Forms$NoAction' }),
|
|
571
|
+
'selectable' => list_view_listened_to?(widget['Name'], page_name) ? 'single' : '',
|
|
572
|
+
'templateMap' => { entity => 'content' },
|
|
573
|
+
'readOnly' => widget['Editable'] == false
|
|
574
|
+
}
|
|
575
|
+
render_client_widget(widget, 'mxui.widget.ListView', props, id:, base: nil)
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def list_view_source(widget, page_name, entity)
|
|
579
|
+
source = widget['DataSource'] || {}
|
|
580
|
+
type = source['$Type'] == 'Forms$NewListViewDatabaseSource' ? 'database' : 'xpath'
|
|
581
|
+
props = {
|
|
582
|
+
'friendlyId' => "#{page_name}.#{widget['Name']}", 'type' => type, 'path' => entity
|
|
583
|
+
}
|
|
584
|
+
props['offlineConstraints'] = [] if type == 'database'
|
|
585
|
+
constraint = source['XPathConstraint'].to_s
|
|
586
|
+
props['xpathConstraints'] = constraint unless constraint.empty?
|
|
587
|
+
sort = array(source.dig('SortBar', 'SortItems')).map do |item|
|
|
588
|
+
[relative_list_attribute(item['AttributePath'], entity),
|
|
589
|
+
item['SortOrder'].to_s == 'Descending' ? 'desc' : 'asc']
|
|
590
|
+
end
|
|
591
|
+
props['sort'] = sort unless sort.empty?
|
|
592
|
+
search = list_view_search_paths(source).map do |path|
|
|
593
|
+
relative_list_attribute(path['AttributePath'] || path['Attribute'] || path, entity)
|
|
594
|
+
end.reject(&:empty?)
|
|
595
|
+
props['search'] = search unless search.empty?
|
|
596
|
+
props
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def relative_list_attribute(path, entity)
|
|
600
|
+
path.to_s.delete_prefix("#{entity}.")
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def list_view_entity(widget)
|
|
604
|
+
source = widget['DataSource'] || {}
|
|
605
|
+
entity = source['EntityPath'].to_s
|
|
606
|
+
entity = source.dig('EntityRef', 'Entity').to_s if entity.empty?
|
|
607
|
+
entity
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def list_view_search_paths(source)
|
|
611
|
+
paths = array(source.dig('Search', 'SearchPaths'))
|
|
612
|
+
paths.empty? ? array(source.dig('Search', 'SearchRefs')) : paths
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
def list_view_listened_to?(name, page_name)
|
|
616
|
+
legacy_related_documents(page_name).any? do |document|
|
|
617
|
+
descendants(document).any? do |value|
|
|
618
|
+
value['$Type'] == 'Forms$ListenTargetSource' && value['ListenTarget'] == name
|
|
619
|
+
end
|
|
620
|
+
end
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
def legacy_related_documents(page_name, seen = {})
|
|
624
|
+
unit = (@source.units_of('Forms$Page') + @source.units_of('Forms$Layout')).find do |candidate|
|
|
625
|
+
"#{candidate.module_name}.#{candidate.document['Name']}" == page_name
|
|
626
|
+
end
|
|
627
|
+
return [] unless unit
|
|
628
|
+
|
|
629
|
+
documents = [unit.document]
|
|
630
|
+
descendants(unit.document).select { _1['$Type'] == 'Forms$SnippetCallWidget' }.each do |call|
|
|
631
|
+
qualified = call.dig('FormCall', 'Form').to_s
|
|
632
|
+
next if seen[qualified]
|
|
633
|
+
|
|
634
|
+
seen[qualified] = true
|
|
635
|
+
snippet = snippet_unit(qualified)
|
|
636
|
+
documents.concat(legacy_related_snippet_documents(snippet, seen)) if snippet
|
|
637
|
+
end
|
|
638
|
+
documents
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
def legacy_related_snippet_documents(snippet, seen)
|
|
642
|
+
documents = [snippet.document]
|
|
643
|
+
descendants(snippet.document).select { _1['$Type'] == 'Forms$SnippetCallWidget' }.each do |call|
|
|
644
|
+
qualified = call.dig('FormCall', 'Form').to_s
|
|
645
|
+
next if seen[qualified]
|
|
646
|
+
|
|
647
|
+
seen[qualified] = true
|
|
648
|
+
child = snippet_unit(qualified)
|
|
649
|
+
documents.concat(legacy_related_snippet_documents(child, seen)) if child
|
|
650
|
+
end
|
|
651
|
+
documents
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
def data_view_templates(id, content, footer)
|
|
655
|
+
templates = +"<m:template widget-id='#{escape(id)}' name='content'>#{content}</m:template>"
|
|
656
|
+
templates << "<m:template widget-id='#{escape(id)}' name='footer'>#{footer}</m:template>" unless footer.empty?
|
|
657
|
+
templates
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
def data_view_properties(widget)
|
|
661
|
+
source = widget['DataSource'] || {}
|
|
662
|
+
entity = data_view_entity(source)
|
|
663
|
+
props = {
|
|
664
|
+
'entity' => entity,
|
|
665
|
+
'schema' => IO::BsonCodec.extract_id(widget['$ID']).to_s,
|
|
666
|
+
'datasource' => data_view_source(source, entity)
|
|
667
|
+
}
|
|
668
|
+
props['readOnly'] = true if widget['Editable'] == false
|
|
669
|
+
props['hideFooter'] = true if widget['ShowFooter'] == false
|
|
670
|
+
props
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
def data_view_source(source, entity)
|
|
674
|
+
if source['$Type'] == 'Forms$ListenTargetSource'
|
|
675
|
+
target = source['ListenTarget'].to_s
|
|
676
|
+
return { 'type' => 'listen', 'contextsource' => @widget_ids_by_name&.fetch(target, target) }
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
if source['$Type'] == 'Forms$MicroflowSource'
|
|
680
|
+
return {
|
|
681
|
+
'type' => 'microflow',
|
|
682
|
+
'microflow' => source.dig('MicroflowSettings', 'Microflow').to_s,
|
|
683
|
+
'argMap' => {}
|
|
684
|
+
}
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
path = data_view_path(source)
|
|
688
|
+
{ 'type' => 'direct', 'path' => path.empty? ? entity : path }
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
def data_view_entity(source)
|
|
692
|
+
if source['$Type'] == 'Forms$ListenTargetSource'
|
|
693
|
+
target = source['ListenTarget'].to_s
|
|
694
|
+
list = @source.documents.flat_map { descendants(_1) }.find do |value|
|
|
695
|
+
value['$Type'] == 'Forms$ListView' && value['Name'] == target
|
|
696
|
+
end
|
|
697
|
+
return list_view_entity(list || {})
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
path = source['EntityPath'].to_s
|
|
701
|
+
return path.split('/').last unless path.empty?
|
|
702
|
+
|
|
703
|
+
reference = source['EntityRef'] || {}
|
|
704
|
+
steps = array(reference['Steps'])
|
|
705
|
+
destination = steps.last&.fetch('DestinationEntity', '').to_s
|
|
706
|
+
return destination unless destination.empty?
|
|
707
|
+
|
|
708
|
+
direct = reference['Entity'].to_s
|
|
709
|
+
return direct unless direct.empty?
|
|
710
|
+
|
|
711
|
+
microflow_return_entity(source)
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
def data_view_path(source)
|
|
715
|
+
legacy = source['EntityPath'].to_s
|
|
716
|
+
return legacy unless legacy.empty?
|
|
717
|
+
|
|
718
|
+
reference = source['EntityRef'] || {}
|
|
719
|
+
steps = array(reference['Steps'])
|
|
720
|
+
return reference['Entity'].to_s if steps.empty?
|
|
721
|
+
|
|
722
|
+
steps.flat_map { [_1['Association'], _1['DestinationEntity']] }
|
|
723
|
+
.map(&:to_s).reject(&:empty?).join('/')
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
def microflow_return_entity(source)
|
|
727
|
+
name = source.dig('MicroflowSettings', 'Microflow').to_s
|
|
728
|
+
unit = @source.units_of('Microflows$Microflow').find do |candidate|
|
|
729
|
+
"#{candidate.module_name}.#{candidate.document['Name']}" == name
|
|
730
|
+
end
|
|
731
|
+
unit&.document&.dig('MicroflowReturnType', 'Entity').to_s
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
def data_view_children(widget, plural, singular)
|
|
735
|
+
children = array(widget[plural])
|
|
736
|
+
return children unless children.empty?
|
|
737
|
+
|
|
738
|
+
widget_children(widget[singular])
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
def templates_xml
|
|
742
|
+
templates = Array(@templates).compact.join
|
|
743
|
+
templates.empty? ? '' : "<m:templates>#{templates}</m:templates>"
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
def render_bound_input(widget, language)
|
|
747
|
+
context = Array(@data_view_stack).last
|
|
748
|
+
return '' unless context
|
|
749
|
+
|
|
750
|
+
outer_id = next_widget_id
|
|
751
|
+
type, props = bound_input_contract(widget, language, context)
|
|
752
|
+
input = render_client_widget(
|
|
753
|
+
widget, type, props, base: "col-sm-#{12 - context[:label_width]}", widget_classes: false
|
|
754
|
+
)
|
|
755
|
+
label = translated_input_text(widget, 'LabelTemplate', 'LabelText', language)
|
|
756
|
+
attributes = html_attributes(widget, base: 'form-group', mendix_id: outer_id)
|
|
757
|
+
"<div#{attributes}>" \
|
|
758
|
+
"<label class='control-label col-sm-#{context[:label_width]}'>#{escape(label)}</label>" \
|
|
759
|
+
"#{input}</div>"
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
def bound_input_contract(widget, language, context)
|
|
763
|
+
attribute = attribute_reference(widget)
|
|
764
|
+
props = {
|
|
765
|
+
'insideFormGroup' => true,
|
|
766
|
+
'attributePath' => bound_attribute_path(widget, context),
|
|
767
|
+
'readOnlyStyle' => read_only_style(widget)
|
|
768
|
+
}
|
|
769
|
+
props['readOnly'] = true if widget['Editable'] == 'Never' || !context[:editable]
|
|
770
|
+
props.merge!(required_properties(widget, language))
|
|
771
|
+
case widget['$Type']
|
|
772
|
+
when 'Forms$TextBox' then text_box_contract(widget, attribute, language, props)
|
|
773
|
+
when 'Forms$TextArea' then text_area_contract(widget, language, props)
|
|
774
|
+
when 'Forms$CheckBox' then ['mxui.widget.BoolSelect', props]
|
|
775
|
+
when 'Forms$DatePicker' then date_picker_contract(widget, language, props)
|
|
776
|
+
when 'Forms$DropDown' then ['mxui.widget.EnumSelect', props]
|
|
777
|
+
when 'Forms$RadioButtonGroup' then radio_button_contract(widget, attribute, language, props)
|
|
778
|
+
when 'Forms$ReferenceSelector' then reference_selector_contract(widget, props)
|
|
779
|
+
when 'Forms$InputReferenceSetSelector' then reference_set_selector_contract(widget, props)
|
|
780
|
+
end
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
def bound_attribute_path(widget, context)
|
|
784
|
+
return reference_attribute_path(widget, context) if %w[
|
|
785
|
+
Forms$ReferenceSelector Forms$InputReferenceSetSelector
|
|
786
|
+
].include?(widget['$Type'])
|
|
787
|
+
|
|
788
|
+
"#{context[:entity]}/#{attribute_reference(widget).split('.').last}"
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
def text_box_contract(widget, attribute, language, props)
|
|
792
|
+
props['placeholder'] = translated_input_text(widget, 'PlaceholderTemplate', 'Placeholder', language)
|
|
793
|
+
type = attribute_definition(attribute)&.dig('NewType') ||
|
|
794
|
+
attribute_definition(attribute)&.dig('Type') || {}
|
|
795
|
+
input_type = if widget['IsPasswordBox'] == true
|
|
796
|
+
'mxui.widget.PasswordInput'
|
|
797
|
+
elsif numeric_attribute_type?(type)
|
|
798
|
+
'mxui.widget.NumberInput'
|
|
799
|
+
else
|
|
800
|
+
'mxui.widget.TextInput'
|
|
801
|
+
end
|
|
802
|
+
max_length = positive_integer(widget['MaxLengthCode'], type['Length'].to_i)
|
|
803
|
+
props['maxLength'] = max_length if input_type != 'mxui.widget.NumberInput' && max_length.positive?
|
|
804
|
+
props['mask'] = widget['InputMask'].to_s unless widget['InputMask'].to_s.empty?
|
|
805
|
+
[input_type, props]
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
def text_area_contract(widget, language, props)
|
|
809
|
+
props['placeholder'] = translated_input_text(widget, 'PlaceholderTemplate', 'Placeholder', language)
|
|
810
|
+
props['rows'] = positive_integer(widget['NumberOfLines'], 5)
|
|
811
|
+
max_length = positive_integer(widget['MaxLengthCode'], 0)
|
|
812
|
+
props['maxLength'] = max_length if max_length.positive?
|
|
813
|
+
['mxui.widget.TextArea', props]
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
def date_picker_contract(widget, language, props)
|
|
817
|
+
props['placeholder'] = translated_input_text(widget, 'PlaceholderTemplate', 'Placeholder', language)
|
|
818
|
+
format = widget.dig('FormattingInfo', 'DateFormat').to_s.downcase
|
|
819
|
+
props['selector'] = %w[date time].include?(format) ? format : 'datetime'
|
|
820
|
+
custom = widget.dig('FormattingInfo', 'CustomDateFormat').to_s
|
|
821
|
+
props['format'] = custom unless custom.empty?
|
|
822
|
+
['mxui.widget.DateInput', props]
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
def radio_button_contract(widget, attribute, language, props)
|
|
826
|
+
props['horizontal'] = widget.fetch('Orientation', 'Horizontal') != 'Vertical'
|
|
827
|
+
props['options'] = enumeration_options(attribute, language)
|
|
828
|
+
['mxui.widget.RadioButtonGroup', props]
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
def reference_selector_contract(widget, props)
|
|
832
|
+
props['datasource'] = selector_source(widget)
|
|
833
|
+
settings = widget['GotoFormSettings'] || {}
|
|
834
|
+
props['gotoPageSettings'] = page_settings(settings) unless settings['Form'].to_s.empty?
|
|
835
|
+
['mxui.widget.ReferenceSelector', props]
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
def reference_set_selector_contract(widget, props)
|
|
839
|
+
props['datasource'] = selector_source(widget)
|
|
840
|
+
settings = widget['PopupFormSettings'] || {}
|
|
841
|
+
props['selectPageSettings'] = page_settings(settings) unless settings['Form'].to_s.empty?
|
|
842
|
+
['mxui.widget.InputReferenceSetSelector', props]
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
def selector_source(widget)
|
|
846
|
+
source = widget['SelectorSource'] || {}
|
|
847
|
+
target, caption = selector_target(widget)
|
|
848
|
+
if source['$Type'] == 'Forms$SelectorMicroflowSource'
|
|
849
|
+
return {
|
|
850
|
+
'type' => 'microflow',
|
|
851
|
+
'microflow' => source.dig('DataSourceMicroflowSettings', 'Microflow').to_s,
|
|
852
|
+
'path' => target
|
|
853
|
+
}
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
result = {
|
|
857
|
+
'type' => 'xpath', 'path' => target, 'constrainedBy' => [],
|
|
858
|
+
'sort' => [[caption, 'asc']], 'contextAction' => 'keep'
|
|
859
|
+
}
|
|
860
|
+
constraint = source['XPathConstraint'].to_s
|
|
861
|
+
result['constraint'] = constraint unless constraint.empty?
|
|
862
|
+
result
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
def selector_target(widget)
|
|
866
|
+
attribute = attribute_reference(widget).split('/').last.to_s
|
|
867
|
+
parts = attribute.split('.')
|
|
868
|
+
[parts.first(2).join('.'), parts.last.to_s]
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
def reference_attribute_path(widget, context)
|
|
872
|
+
legacy = widget['AttributePath'].to_s
|
|
873
|
+
unless legacy.empty?
|
|
874
|
+
segments = legacy.split('/')
|
|
875
|
+
segments[-1] = segments.last.to_s.split('.').last
|
|
876
|
+
return ([context[:entity]] + segments).join('/')
|
|
877
|
+
end
|
|
878
|
+
|
|
879
|
+
reference = widget.dig('AttributeRef', 'EntityRef') || {}
|
|
880
|
+
steps = array(reference['Steps']).flat_map do |step|
|
|
881
|
+
[step['Association'], step['DestinationEntity']]
|
|
882
|
+
end
|
|
883
|
+
([context[:entity]] + steps + [attribute_reference(widget).split('.').last])
|
|
884
|
+
.map(&:to_s).reject(&:empty?).join('/')
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
def required_properties(widget, language)
|
|
888
|
+
return {} unless widget['Required'] == true
|
|
889
|
+
|
|
890
|
+
{
|
|
891
|
+
'required' => 'true',
|
|
892
|
+
'requiredMsg' => translated_input_text(widget, 'RequiredMessageTemplate', 'RequiredMessage', language)
|
|
893
|
+
}
|
|
894
|
+
end
|
|
895
|
+
|
|
896
|
+
def read_only_style(widget)
|
|
897
|
+
style = widget['ReadOnlyStyle'].to_s.downcase
|
|
898
|
+
%w[control text].include?(style) ? style : 'text'
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
def translated_input_text(widget, template_key, text_key, language)
|
|
902
|
+
template = widget.dig(template_key, 'Template')
|
|
903
|
+
translated(template || widget[text_key], language)
|
|
904
|
+
end
|
|
905
|
+
|
|
906
|
+
def render_label(widget, language)
|
|
907
|
+
tag = widget['RenderMode'].to_s == 'Paragraph' ? 'p' : 'label'
|
|
908
|
+
attributes = html_attributes(widget, base: nil, mendix_id: next_widget_id)
|
|
909
|
+
"<#{tag}#{attributes}>#{escape(translated(widget['Caption'], language))}</#{tag}>"
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
def render_static_image(widget)
|
|
913
|
+
props = { 'url' => image_uri(widget['Image']) }
|
|
914
|
+
action = widget['ClickAction'] || {}
|
|
915
|
+
props['action'] = legacy_client_action(action) unless action['$Type'] == 'Forms$NoAction'
|
|
916
|
+
base = widget['Responsive'] == true ? 'img-responsive' : nil
|
|
917
|
+
render_client_widget(
|
|
918
|
+
widget, 'mxui.widget.StaticImage', props, base:, style: static_image_style(widget)
|
|
919
|
+
)
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
def render_image_viewer(widget)
|
|
923
|
+
source = widget['DataSource'] || {}
|
|
924
|
+
props = {
|
|
925
|
+
'datasource' => { 'type' => 'direct', 'path' => source['EntityPath'].to_s },
|
|
926
|
+
'defaultUrl' => widget['DefaultImage'].to_s.empty? ? '' : image_uri(widget['DefaultImage']),
|
|
927
|
+
'thumb' => widget['ShowAsThumbnail'] == true,
|
|
928
|
+
'width' => image_dimension(widget['Width'], widget['WidthUnit']),
|
|
929
|
+
'height' => image_dimension(widget['Height'], widget['HeightUnit'])
|
|
930
|
+
}
|
|
931
|
+
base = widget['Responsive'] == true ? 'img-responsive' : nil
|
|
932
|
+
render_client_widget(widget, 'mxui.widget.Image', props, base:)
|
|
933
|
+
end
|
|
934
|
+
|
|
935
|
+
def render_image_uploader(widget)
|
|
936
|
+
width, height = widget['ThumbnailSize'].to_s.split(';').map(&:to_i)
|
|
937
|
+
props = {
|
|
938
|
+
'thumbnailWidth' => width.positive? ? width : 100,
|
|
939
|
+
'thumbnailHeight' => height.positive? ? height : 75,
|
|
940
|
+
'maxFileSize' => widget['MaxFileSize'].to_i,
|
|
941
|
+
'restrictions' => widget['AllowedExtensions'].to_s,
|
|
942
|
+
'uploadable' => widget['Editable'] != 'Never'
|
|
943
|
+
}
|
|
944
|
+
render_client_widget(widget, 'mxui.widget.ImageUploader', props, base: nil)
|
|
945
|
+
end
|
|
946
|
+
|
|
947
|
+
def render_reference_set_selector(widget, page_name, language)
|
|
948
|
+
compiler = LegacyDataGridCompiler.new(
|
|
949
|
+
@source, page_name, widget, language:, sequence: next_widget_sequence
|
|
950
|
+
)
|
|
951
|
+
compiler.html.tap { @unsupported[page_name].concat(compiler.unsupported) }
|
|
952
|
+
end
|
|
953
|
+
|
|
954
|
+
def image_dimension(value, unit)
|
|
955
|
+
number = value.to_i
|
|
956
|
+
return '' unless number.positive?
|
|
957
|
+
|
|
958
|
+
case unit
|
|
959
|
+
when 'Pixels' then "#{number}px"
|
|
960
|
+
when 'Percentage' then "#{number}%"
|
|
961
|
+
else ''
|
|
962
|
+
end
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
def static_image_style(widget)
|
|
966
|
+
dimensions = [%w[Width WidthUnit width], %w[Height HeightUnit height]].filter_map do |value, unit, name|
|
|
967
|
+
amount = widget[value].to_i
|
|
968
|
+
suffix = { 'Pixels' => 'px', 'Percentage' => '%' }[widget[unit].to_s]
|
|
969
|
+
"#{name}:#{amount}#{suffix}" if amount.positive? && suffix
|
|
970
|
+
end
|
|
971
|
+
[widget['Style'].to_s, *dimensions].reject(&:empty?).join(';')
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
def render_tab_control(widget, page_name, language)
|
|
975
|
+
id = next_widget_id
|
|
976
|
+
tabs = array(widget['TabPages'])
|
|
977
|
+
selected = default_tab_index(widget, tabs)
|
|
978
|
+
pages = tabs.each_with_index.map do |tab, index|
|
|
979
|
+
tab_id = next_widget_id
|
|
980
|
+
props = {
|
|
981
|
+
'title' => translated(tab['Caption'], language), 'delayLoading' => index != selected,
|
|
982
|
+
'tabName' => tab['Name'].to_s
|
|
983
|
+
}
|
|
984
|
+
[tab, tab_id, render_client_widget(
|
|
985
|
+
tab, 'mxui.widget.TabContent', props, id: tab_id, base: nil,
|
|
986
|
+
widget_classes: false, tabindex: false
|
|
987
|
+
)]
|
|
988
|
+
end
|
|
989
|
+
@templates << "<m:template widget-id='#{escape(id)}' name='pages'>" \
|
|
990
|
+
"#{pages.map(&:last).join}</m:template>"
|
|
991
|
+
pages.each do |tab, tab_id, _html|
|
|
992
|
+
content_index = @templates.length
|
|
993
|
+
@templates << nil
|
|
994
|
+
content = array(tab['Widgets']).map { render_widget(_1, page_name, language) }.join
|
|
995
|
+
@templates[content_index] = "<m:template widget-id='#{escape(tab_id)}' " \
|
|
996
|
+
"name='content'>#{content}</m:template>"
|
|
997
|
+
end
|
|
998
|
+
render_client_widget(widget, 'mxui.widget.TabContainer', {}, id:, base: nil)
|
|
999
|
+
end
|
|
1000
|
+
|
|
1001
|
+
def default_tab_index(widget, tabs)
|
|
1002
|
+
pointer = IO::BsonCodec.extract_id(widget['DefaultPagePointer'])
|
|
1003
|
+
tabs.index { IO::BsonCodec.extract_id(_1['$ID']) == pointer } || 0
|
|
1004
|
+
end
|
|
1005
|
+
|
|
1006
|
+
def render_snippet_call(widget, page_name, language)
|
|
1007
|
+
qualified = widget.dig('FormCall', 'Form').to_s
|
|
1008
|
+
snippet = snippet_unit(qualified)
|
|
1009
|
+
@snippet_stack ||= []
|
|
1010
|
+
return '' unless snippet && !@snippet_stack.include?(qualified)
|
|
1011
|
+
|
|
1012
|
+
@snippet_stack << qualified
|
|
1013
|
+
widget_children(snippet.document).map { render_widget(_1, page_name, language) }.join
|
|
1014
|
+
ensure
|
|
1015
|
+
@snippet_stack.pop if @snippet_stack&.last == qualified
|
|
1016
|
+
end
|
|
1017
|
+
|
|
1018
|
+
def audit_snippet_call(widget, page_name, data_view_context)
|
|
1019
|
+
qualified = widget.dig('FormCall', 'Form').to_s
|
|
1020
|
+
snippet = snippet_unit(qualified)
|
|
1021
|
+
@snippet_audit_stack ||= []
|
|
1022
|
+
if !snippet || @snippet_audit_stack.include?(qualified)
|
|
1023
|
+
@unsupported[page_name] << 'Forms$SnippetCallWidget'
|
|
1024
|
+
return
|
|
1025
|
+
end
|
|
1026
|
+
|
|
1027
|
+
@snippet_audit_stack << qualified
|
|
1028
|
+
audit_unsupported_widgets(
|
|
1029
|
+
snippet.document, page_name, initial_data_view_context: data_view_context
|
|
1030
|
+
)
|
|
1031
|
+
ensure
|
|
1032
|
+
@snippet_audit_stack.pop if @snippet_audit_stack&.last == qualified
|
|
1033
|
+
end
|
|
1034
|
+
|
|
1035
|
+
def snippet_unit(qualified)
|
|
1036
|
+
@source.units_of('Forms$Snippet').find do |unit|
|
|
1037
|
+
"#{unit.module_name}.#{unit.document['Name']}" == qualified
|
|
1038
|
+
end
|
|
1039
|
+
end
|
|
1040
|
+
|
|
1041
|
+
def render_custom_widget(widget, language)
|
|
1042
|
+
compiler = LegacyCustomWidgetCompiler.new(@source, widget, language:)
|
|
1043
|
+
return '' unless compiler.supported?
|
|
1044
|
+
|
|
1045
|
+
render_client_widget(widget, compiler.widget_id, compiler.properties_hash, base: nil)
|
|
1046
|
+
end
|
|
1047
|
+
|
|
1048
|
+
def button_properties(widget, language)
|
|
1049
|
+
caption = translated(widget.dig('CaptionTemplate', 'Template'), language)
|
|
1050
|
+
render_type = widget['RenderType'].to_s.downcase
|
|
1051
|
+
props = {
|
|
1052
|
+
'caption' => { 'text' => caption },
|
|
1053
|
+
'renderType' => render_type.empty? ? 'button' : render_type
|
|
1054
|
+
}
|
|
1055
|
+
icon = image_uri(widget.dig('Icon', 'Image'))
|
|
1056
|
+
props['iconUrl'] = icon if icon
|
|
1057
|
+
props
|
|
1058
|
+
end
|
|
1059
|
+
|
|
1060
|
+
def render_link_button(widget, action, props)
|
|
1061
|
+
address = action['Address'] || {}
|
|
1062
|
+
props['action'] = {
|
|
1063
|
+
'Web' => 'open', 'Email' => 'email', 'Call' => 'call', 'Text' => 'text'
|
|
1064
|
+
}.fetch(action['LinkType'].to_s, action['LinkType'].to_s.downcase)
|
|
1065
|
+
props['address'] = address['Value'].to_s
|
|
1066
|
+
render_client_widget(widget, 'mxui.widget.LinkButton', props)
|
|
1067
|
+
end
|
|
1068
|
+
|
|
1069
|
+
def render_client_widget(widget, type, props, **options)
|
|
1070
|
+
attributes = client_widget_attributes(widget, type, props, **options)
|
|
1071
|
+
serialized = attributes.map { |key, value| "#{key}='#{escape(value)}'" }.join(' ')
|
|
1072
|
+
"<div #{serialized}></div>"
|
|
1073
|
+
end
|
|
1074
|
+
|
|
1075
|
+
def render_client_container(widget, type, props, content, **options)
|
|
1076
|
+
attributes = client_widget_attributes(widget, type, props, **options)
|
|
1077
|
+
serialized = attributes.map { |key, value| "#{key}='#{escape(value)}'" }.join(' ')
|
|
1078
|
+
"<div #{serialized}>#{content}</div>"
|
|
1079
|
+
end
|
|
1080
|
+
|
|
1081
|
+
def client_widget_attributes(widget, type, props, **options)
|
|
1082
|
+
id = options.key?(:id) ? options[:id] : next_widget_id
|
|
1083
|
+
@widget_ids_by_name ||= {}
|
|
1084
|
+
@widget_ids_by_name[widget['Name'].to_s] = id unless widget['Name'].to_s.empty?
|
|
1085
|
+
base = options.key?(:base) ? options[:base] : button_style(widget)
|
|
1086
|
+
widget_classes = options.fetch(:widget_classes, true)
|
|
1087
|
+
style = options.fetch(:style, widget['Style'].to_s)
|
|
1088
|
+
style = widget.dig('Appearance', 'Style').to_s if style.empty?
|
|
1089
|
+
tabindex = options.fetch(:tabindex, true)
|
|
1090
|
+
properties = options.fetch(:properties, true)
|
|
1091
|
+
{
|
|
1092
|
+
'data-mendix-id' => id,
|
|
1093
|
+
'data-mendix-type' => type,
|
|
1094
|
+
'data-mendix-props' => properties ? JSON.generate(props)[1..-2] : '',
|
|
1095
|
+
'class' => widget_classes ? css_classes(widget, base:) : base,
|
|
1096
|
+
'style' => style,
|
|
1097
|
+
'tabindex' => tabindex ? widget['TabIndex'].to_i : ''
|
|
1098
|
+
}.reject { |_key, value| value.to_s.empty? }
|
|
1099
|
+
end
|
|
1100
|
+
|
|
1101
|
+
def legacy_client_action(action)
|
|
1102
|
+
case action['$Type']
|
|
1103
|
+
when 'Forms$MicroflowAction'
|
|
1104
|
+
microflow_action(action)
|
|
1105
|
+
when 'Forms$FormAction'
|
|
1106
|
+
settings = action['FormSettings'] || {}
|
|
1107
|
+
{
|
|
1108
|
+
'type' => 'openPage',
|
|
1109
|
+
'hasParameter' => !array(settings['ParameterMappings']).empty?,
|
|
1110
|
+
'params' => page_settings(settings)
|
|
1111
|
+
}
|
|
1112
|
+
when 'Forms$NoAction'
|
|
1113
|
+
{ 'type' => 'doNothing', 'hasParameter' => false, 'params' => {} }
|
|
1114
|
+
end
|
|
1115
|
+
end
|
|
1116
|
+
|
|
1117
|
+
def microflow_action(action)
|
|
1118
|
+
settings = action['MicroflowSettings'] || action
|
|
1119
|
+
name = settings['Microflow'].to_s
|
|
1120
|
+
has_parameter = microflow_has_parameter?(name)
|
|
1121
|
+
params = { 'name' => name }
|
|
1122
|
+
params['validate'] = 'view' unless settings['FormValidations'] == 'None'
|
|
1123
|
+
params['applyTo'] = 'selection' if has_parameter
|
|
1124
|
+
{ 'type' => 'callMicroflow', 'hasParameter' => has_parameter, 'params' => params }
|
|
1125
|
+
end
|
|
1126
|
+
|
|
1127
|
+
def microflow_has_parameter?(qualified_name)
|
|
1128
|
+
unit = @source.units_of('Microflows$Microflow').find do |candidate|
|
|
1129
|
+
"#{candidate.module_name}.#{candidate.document['Name']}" == qualified_name
|
|
1130
|
+
end
|
|
1131
|
+
descendants(unit&.document || {}).any? { _1['$Type'] == 'Microflows$MicroflowParameter' }
|
|
1132
|
+
end
|
|
1133
|
+
|
|
1134
|
+
def page_settings(settings)
|
|
1135
|
+
form = settings['Form'].to_s
|
|
1136
|
+
location = settings['Location'].to_s
|
|
1137
|
+
result = {
|
|
1138
|
+
'path' => "#{form.tr('.', '/')}.page.xml",
|
|
1139
|
+
'location' => %w[Popup ModalPopup].include?(location) ? 'modal' : 'content'
|
|
1140
|
+
}
|
|
1141
|
+
target = @source.units_of('Forms$Page').find do |unit|
|
|
1142
|
+
"#{unit.module_name}.#{unit.document['Name']}" == form
|
|
1143
|
+
end
|
|
1144
|
+
result['resizable'] = true if target&.document&.fetch('PopupResizable', false) == true
|
|
1145
|
+
result
|
|
1146
|
+
end
|
|
1147
|
+
|
|
1148
|
+
def image_uri(reference)
|
|
1149
|
+
module_name, collection_name, image_name = reference.to_s.split('.', 3)
|
|
1150
|
+
unit = @source.units_of('Images$ImageCollection').find do |candidate|
|
|
1151
|
+
candidate.module_name == module_name && candidate.document['Name'] == collection_name
|
|
1152
|
+
end
|
|
1153
|
+
image = array(unit&.document&.fetch('Images', nil)).find { _1['Name'] == image_name }
|
|
1154
|
+
unless image
|
|
1155
|
+
return system_image_uri(collection_name, image_name) if module_name == 'System'
|
|
1156
|
+
|
|
1157
|
+
return
|
|
1158
|
+
end
|
|
1159
|
+
|
|
1160
|
+
"img/#{module_name}$#{image_name}.#{image_format(image)}"
|
|
1161
|
+
end
|
|
1162
|
+
|
|
1163
|
+
def system_image_uri(collection_name, image_name)
|
|
1164
|
+
return if collection_name != 'Images' || image_name.to_s.empty?
|
|
1165
|
+
|
|
1166
|
+
extension = %w[Error Running Completed Module].include?(image_name) ? 'gif' : 'png'
|
|
1167
|
+
"img/System$#{image_name}.#{extension}"
|
|
1168
|
+
end
|
|
1169
|
+
|
|
1170
|
+
def button_style(widget)
|
|
1171
|
+
style = widget['ButtonStyle'].to_s.downcase
|
|
1172
|
+
"btn-#{style.empty? ? 'default' : style}"
|
|
1173
|
+
end
|
|
1174
|
+
|
|
158
1175
|
def render_dynamic_text(widget, language)
|
|
159
1176
|
content = widget['Content'] || {}
|
|
160
1177
|
text = translated(content['Template'], language)
|
|
161
1178
|
text = translated(content['Fallback'], language) if text.empty?
|
|
1179
|
+
parameters = array(content['Parameters'])
|
|
1180
|
+
unless parameters.empty?
|
|
1181
|
+
return render_client_widget(
|
|
1182
|
+
widget, 'mxui.widget.DynamicText',
|
|
1183
|
+
{ 'content' => dynamic_text_template(text, parameters) }, base: 'mx-text'
|
|
1184
|
+
)
|
|
1185
|
+
end
|
|
1186
|
+
|
|
162
1187
|
tag = widget['RenderMode'].to_s == 'Paragraph' ? 'p' : 'span'
|
|
163
1188
|
attributes = html_attributes(widget, base: 'mx-text', mendix_id: next_widget_id)
|
|
164
1189
|
"<#{tag}#{attributes}>#{escape(text)}</#{tag}>"
|
|
165
1190
|
end
|
|
166
1191
|
|
|
1192
|
+
def dynamic_text_template(text, parameters)
|
|
1193
|
+
values = parameters.each_with_index.to_h do |parameter, index|
|
|
1194
|
+
[index.to_s, dynamic_text_attribute_path(parameter)]
|
|
1195
|
+
end
|
|
1196
|
+
formats = parameters.each_with_index.to_h do |parameter, index|
|
|
1197
|
+
[index.to_s, dynamic_text_format(parameter)]
|
|
1198
|
+
end
|
|
1199
|
+
elements = text.split(/(\{\d+\})/).filter_map do |part|
|
|
1200
|
+
next if part.empty?
|
|
1201
|
+
|
|
1202
|
+
match = part.match(/\A\{(\d+)\}\z/)
|
|
1203
|
+
match ? match[1].to_i - 1 : part
|
|
1204
|
+
end
|
|
1205
|
+
{ 'elements' => elements, 'parameters' => values, 'formats' => formats }
|
|
1206
|
+
end
|
|
1207
|
+
|
|
1208
|
+
def dynamic_text_attribute_path(parameter)
|
|
1209
|
+
reference = parameter['AttributeRef'] || {}
|
|
1210
|
+
attribute = attribute_reference(parameter)
|
|
1211
|
+
steps = array(reference.dig('EntityRef', 'Steps')).flat_map do |step|
|
|
1212
|
+
[step['Association'], step['DestinationEntity']]
|
|
1213
|
+
end
|
|
1214
|
+
return (steps + [attribute.split('.').last]).reject(&:empty?).join('/') unless steps.empty?
|
|
1215
|
+
|
|
1216
|
+
entity = Array(@data_view_stack).last&.fetch(:entity, '').to_s
|
|
1217
|
+
relative_list_attribute(attribute, entity)
|
|
1218
|
+
end
|
|
1219
|
+
|
|
1220
|
+
def dynamic_text_format(parameter)
|
|
1221
|
+
formatting = parameter['FormattingInfo'] || {}
|
|
1222
|
+
type = attribute_definition(attribute_reference(parameter))&.dig('NewType', '$Type') ||
|
|
1223
|
+
attribute_definition(attribute_reference(parameter))&.dig('Type', '$Type')
|
|
1224
|
+
date = formatting['DateFormat'].to_s.downcase
|
|
1225
|
+
custom = formatting['CustomDateFormat'].to_s
|
|
1226
|
+
if type == 'DomainModels$DateTimeAttributeType'
|
|
1227
|
+
return { 'type' => 'custom', 'pattern' => custom } unless custom.empty?
|
|
1228
|
+
return { 'type' => date } if %w[date time datetime].include?(date)
|
|
1229
|
+
end
|
|
1230
|
+
|
|
1231
|
+
return {} unless numeric_attribute_type?('$Type' => type)
|
|
1232
|
+
|
|
1233
|
+
{
|
|
1234
|
+
'groupDigits' => formatting['GroupDigits'] == true,
|
|
1235
|
+
'decimalPrecision' => positive_integer(formatting['DecimalPrecision'], 2)
|
|
1236
|
+
}
|
|
1237
|
+
end
|
|
1238
|
+
|
|
167
1239
|
def render_data_grid(widget, page_name, language)
|
|
168
1240
|
compiler = LegacyDataGridCompiler.new(
|
|
169
1241
|
@source, page_name, widget, language:, sequence: next_widget_sequence
|
|
@@ -212,7 +1284,160 @@ module Mxrb
|
|
|
212
1284
|
end
|
|
213
1285
|
|
|
214
1286
|
def supported_widget_type?(type)
|
|
215
|
-
%w[
|
|
1287
|
+
%w[
|
|
1288
|
+
Forms$ActionButton Forms$CheckBox Forms$DataGrid Forms$DataView Forms$DatePicker
|
|
1289
|
+
Forms$DivContainer Forms$DropDown Forms$DynamicText Forms$InputReferenceSetSelector
|
|
1290
|
+
Forms$Header Forms$ImageUploader Forms$ImageViewer Forms$Label Forms$LayoutGrid Forms$ListView
|
|
1291
|
+
Forms$MenuBar Forms$NavigationTree
|
|
1292
|
+
Forms$MobileBackButton Forms$MobileCancelButton Forms$MobileSaveButton
|
|
1293
|
+
Forms$RadioButtonGroup Forms$ReferenceSelector Forms$ReferenceSetSelector
|
|
1294
|
+
Forms$SnippetCallWidget
|
|
1295
|
+
Forms$ScrollContainer Forms$SidebarToggleButton Forms$SimpleMenuBar
|
|
1296
|
+
Forms$StaticImageViewer Forms$Table Forms$TabControl Forms$TemplateGrid
|
|
1297
|
+
Forms$TextArea Forms$TextBox
|
|
1298
|
+
CustomWidgets$CustomWidget
|
|
1299
|
+
].include?(type)
|
|
1300
|
+
end
|
|
1301
|
+
|
|
1302
|
+
def supported_action_button?(widget)
|
|
1303
|
+
action = widget['Action'] || {}
|
|
1304
|
+
return false unless ACTION_BUTTON_TYPES.include?(action['$Type'])
|
|
1305
|
+
return false if action['$Type'] == 'Forms$OpenLinkClientAction' && action.dig('Address', 'IsDynamic') == true
|
|
1306
|
+
|
|
1307
|
+
true
|
|
1308
|
+
end
|
|
1309
|
+
|
|
1310
|
+
def supported_data_view?(widget)
|
|
1311
|
+
source = widget['DataSource'] || {}
|
|
1312
|
+
return !data_view_entity(source).empty? if source['$Type'] == 'Forms$ListenTargetSource'
|
|
1313
|
+
return !data_view_entity(source).empty? if source['$Type'] == 'Forms$MicroflowSource'
|
|
1314
|
+
return false unless source['$Type'] == 'Forms$DataViewSource'
|
|
1315
|
+
|
|
1316
|
+
!data_view_entity(source).empty?
|
|
1317
|
+
end
|
|
1318
|
+
|
|
1319
|
+
def supported_list_view?(widget)
|
|
1320
|
+
%w[Forms$ListViewXPathSource Forms$NewListViewDatabaseSource].include?(
|
|
1321
|
+
widget.dig('DataSource', '$Type')
|
|
1322
|
+
) &&
|
|
1323
|
+
!list_view_entity(widget).empty?
|
|
1324
|
+
end
|
|
1325
|
+
|
|
1326
|
+
def supported_dynamic_text?(widget, data_view_context)
|
|
1327
|
+
parameters = array(widget.dig('Content', 'Parameters'))
|
|
1328
|
+
return true if parameters.empty?
|
|
1329
|
+
return false unless data_view_context
|
|
1330
|
+
|
|
1331
|
+
parameters.all? do |parameter|
|
|
1332
|
+
!attribute_reference(parameter).empty? && parameter['Expression'].to_s.empty?
|
|
1333
|
+
end
|
|
1334
|
+
end
|
|
1335
|
+
|
|
1336
|
+
def supported_static_image?(widget)
|
|
1337
|
+
return false unless image_uri(widget['Image'])
|
|
1338
|
+
|
|
1339
|
+
%w[Forms$MicroflowAction Forms$NoAction].include?(widget.dig('ClickAction', '$Type'))
|
|
1340
|
+
end
|
|
1341
|
+
|
|
1342
|
+
def supported_image_viewer?(widget, data_view_context)
|
|
1343
|
+
return false unless data_view_context
|
|
1344
|
+
return false unless widget.dig('DataSource', '$Type') == 'Forms$ImageViewerSource'
|
|
1345
|
+
return false if widget.dig('DataSource', 'EntityPath').to_s.empty?
|
|
1346
|
+
return false unless widget['DefaultImage'].to_s.empty? || image_uri(widget['DefaultImage'])
|
|
1347
|
+
|
|
1348
|
+
widget.dig('OnClickBehavior', '$Type').to_s == 'Forms$OnClickNothing'
|
|
1349
|
+
end
|
|
1350
|
+
|
|
1351
|
+
def supported_reference_set_selector?(widget, data_view_context, page_name)
|
|
1352
|
+
path = widget.dig('DataSource', 'EntityPath').to_s
|
|
1353
|
+
return false unless data_view_context && widget.dig('DataSource', '$Type') == 'Forms$ReferenceSetSource'
|
|
1354
|
+
return false unless path.split('/').size == 2
|
|
1355
|
+
|
|
1356
|
+
compiler = LegacyDataGridCompiler.new(
|
|
1357
|
+
@source, page_name, widget, language: 'en_US', sequence: 1
|
|
1358
|
+
)
|
|
1359
|
+
compiler.html
|
|
1360
|
+
compiler.unsupported.empty?
|
|
1361
|
+
end
|
|
1362
|
+
|
|
1363
|
+
def supported_custom_widget?(widget)
|
|
1364
|
+
LegacyCustomWidgetCompiler.new(@source, widget, language: 'en_US').supported?
|
|
1365
|
+
end
|
|
1366
|
+
|
|
1367
|
+
def supported_bound_widget?(widget, data_view_context)
|
|
1368
|
+
return false unless data_view_context && !attribute_reference(widget).empty?
|
|
1369
|
+
return true unless %w[
|
|
1370
|
+
Forms$ReferenceSelector Forms$InputReferenceSetSelector
|
|
1371
|
+
].include?(widget['$Type'])
|
|
1372
|
+
|
|
1373
|
+
source = widget['SelectorSource'] || {}
|
|
1374
|
+
return false unless %w[
|
|
1375
|
+
Forms$NewSelectorDatabaseSource Forms$SelectorDatabaseSource Forms$SelectorMicroflowSource
|
|
1376
|
+
Forms$SelectorXPathSource
|
|
1377
|
+
].include?(source['$Type'])
|
|
1378
|
+
return false if selector_target(widget).first.empty?
|
|
1379
|
+
|
|
1380
|
+
source['$Type'] != 'Forms$SelectorMicroflowSource' ||
|
|
1381
|
+
!source.dig('DataSourceMicroflowSettings', 'Microflow').to_s.empty?
|
|
1382
|
+
end
|
|
1383
|
+
|
|
1384
|
+
def attribute_reference(widget)
|
|
1385
|
+
(widget['AttributePath'] || widget.dig('AttributeRef', 'Attribute')).to_s
|
|
1386
|
+
end
|
|
1387
|
+
|
|
1388
|
+
def attribute_definition(reference)
|
|
1389
|
+
module_name, entity_name, attribute_name = reference.to_s.split('.', 3)
|
|
1390
|
+
return unless module_name && entity_name && attribute_name
|
|
1391
|
+
|
|
1392
|
+
domain = domain_document(module_name)
|
|
1393
|
+
entity = array(domain&.fetch('Entities', nil)).find do |candidate|
|
|
1394
|
+
(candidate['Name'] || candidate['UnqualifiedName']) == entity_name
|
|
1395
|
+
end
|
|
1396
|
+
array(entity&.fetch('Attributes', nil)).find { _1['Name'] == attribute_name }
|
|
1397
|
+
end
|
|
1398
|
+
|
|
1399
|
+
def domain_document(module_name)
|
|
1400
|
+
application = @source.units_of('DomainModels$DomainModel').find do |unit|
|
|
1401
|
+
unit.module_name == module_name
|
|
1402
|
+
end
|
|
1403
|
+
return application.document if application
|
|
1404
|
+
return unless module_name == 'System' && @source.respond_to?(:version)
|
|
1405
|
+
|
|
1406
|
+
@system_domain_document ||= SystemModelSeed.for(@source.version).domain_document
|
|
1407
|
+
rescue CompilationError
|
|
1408
|
+
nil
|
|
1409
|
+
end
|
|
1410
|
+
|
|
1411
|
+
def numeric_attribute_type?(type)
|
|
1412
|
+
%w[
|
|
1413
|
+
DomainModels$AutoNumberAttributeType DomainModels$DecimalAttributeType
|
|
1414
|
+
DomainModels$IntegerAttributeType DomainModels$LongAttributeType
|
|
1415
|
+
].include?(type['$Type'])
|
|
1416
|
+
end
|
|
1417
|
+
|
|
1418
|
+
def enumeration_options(attribute, language)
|
|
1419
|
+
type = attribute_definition(attribute)&.dig('NewType') ||
|
|
1420
|
+
attribute_definition(attribute)&.dig('Type') || {}
|
|
1421
|
+
qualified_name = type['Enumeration'].to_s
|
|
1422
|
+
module_name, name = qualified_name.split('.', 2)
|
|
1423
|
+
enumeration = @source.units_of('Enumerations$Enumeration').find do |unit|
|
|
1424
|
+
unit.module_name == module_name && unit.document['Name'] == name
|
|
1425
|
+
end
|
|
1426
|
+
array(enumeration&.document&.fetch('Values', nil)).map do |value|
|
|
1427
|
+
{ 'key' => value['Name'].to_s, 'caption' => translated(value['Caption'], language) }
|
|
1428
|
+
end
|
|
1429
|
+
end
|
|
1430
|
+
|
|
1431
|
+
def positive_integer(value, fallback)
|
|
1432
|
+
number = Integer(value || 0)
|
|
1433
|
+
number.positive? ? number : fallback
|
|
1434
|
+
rescue ArgumentError, TypeError
|
|
1435
|
+
fallback
|
|
1436
|
+
end
|
|
1437
|
+
|
|
1438
|
+
def grid_weight_class(size, weight)
|
|
1439
|
+
value = Integer(weight || -1)
|
|
1440
|
+
"col-#{size}-#{value}" if (1..12).cover?(value)
|
|
216
1441
|
end
|
|
217
1442
|
|
|
218
1443
|
def layout_argument_id(parameter)
|
|
@@ -274,7 +1499,7 @@ module Mxrb
|
|
|
274
1499
|
end
|
|
275
1500
|
|
|
276
1501
|
def frozen_unsupported
|
|
277
|
-
@unsupported.transform_values { _1.reject(&:empty?).uniq.sort }.freeze
|
|
1502
|
+
@unsupported.transform_values { _1.reject(&:empty?).uniq.sort }.reject { |_key, value| value.empty? }.freeze
|
|
278
1503
|
end
|
|
279
1504
|
|
|
280
1505
|
def escape(value)
|