lcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1676) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/skills/lcp-custom-field/SKILL.md +205 -0
  3. data/.claude/skills/lcp-getting-started/SKILL.md +332 -0
  4. data/.claude/skills/lcp-host-binding/SKILL.md +287 -0
  5. data/.claude/skills/lcp-model/SKILL.md +185 -0
  6. data/.claude/skills/lcp-permissions/SKILL.md +176 -0
  7. data/.claude/skills/lcp-presenter/SKILL.md +194 -0
  8. data/.claude/skills/lcp-workflow/SKILL.md +281 -0
  9. data/CHANGELOG.md +69 -0
  10. data/MIT-LICENSE +20 -0
  11. data/README.md +28 -0
  12. data/Rakefile +17 -0
  13. data/app/assets/javascripts/lcp_ruby/application.js +58 -0
  14. data/app/assets/javascripts/lcp_ruby/controllers/advanced_filter_controller.js +1521 -0
  15. data/app/assets/javascripts/lcp_ruby/controllers/approval_actions_controller.js +24 -0
  16. data/app/assets/javascripts/lcp_ruby/controllers/array_input_controller.js +156 -0
  17. data/app/assets/javascripts/lcp_ruby/controllers/batch_select_controller.js +405 -0
  18. data/app/assets/javascripts/lcp_ruby/controllers/cascading_selects_controller.js +436 -0
  19. data/app/assets/javascripts/lcp_ruby/controllers/char_counter_controller.js +28 -0
  20. data/app/assets/javascripts/lcp_ruby/controllers/clipboard_controller.js +62 -0
  21. data/app/assets/javascripts/lcp_ruby/controllers/conditional_rendering_controller.js +178 -0
  22. data/app/assets/javascripts/lcp_ruby/controllers/confirm_dialog_controller.js +131 -0
  23. data/app/assets/javascripts/lcp_ruby/controllers/custom_fields_manage_controller.js +80 -0
  24. data/app/assets/javascripts/lcp_ruby/controllers/dialog_controller.js +421 -0
  25. data/app/assets/javascripts/lcp_ruby/controllers/dialog_nested_controller.js +182 -0
  26. data/app/assets/javascripts/lcp_ruby/controllers/direct_upload_controller.js +100 -0
  27. data/app/assets/javascripts/lcp_ruby/controllers/drawer_controller.js +205 -0
  28. data/app/assets/javascripts/lcp_ruby/controllers/dropdown_controller.js +160 -0
  29. data/app/assets/javascripts/lcp_ruby/controllers/export_dialog_controller.js +15 -0
  30. data/app/assets/javascripts/lcp_ruby/controllers/field_picker_controller.js +290 -0
  31. data/app/assets/javascripts/lcp_ruby/controllers/file_upload_controller.js +135 -0
  32. data/app/assets/javascripts/lcp_ruby/controllers/filter_auto_submit_controller.js +46 -0
  33. data/app/assets/javascripts/lcp_ruby/controllers/filter_dirty_controller.js +94 -0
  34. data/app/assets/javascripts/lcp_ruby/controllers/form_actions_controller.js +68 -0
  35. data/app/assets/javascripts/lcp_ruby/controllers/form_handling_controller.js +67 -0
  36. data/app/assets/javascripts/lcp_ruby/controllers/import_dialog_controller.js +142 -0
  37. data/app/assets/javascripts/lcp_ruby/controllers/import_mapper_controller.js +322 -0
  38. data/app/assets/javascripts/lcp_ruby/controllers/index_sortable_controller.js +388 -0
  39. data/app/assets/javascripts/lcp_ruby/controllers/inline_create_controller.js +137 -0
  40. data/app/assets/javascripts/lcp_ruby/controllers/kanban_board_controller.js +290 -0
  41. data/app/assets/javascripts/lcp_ruby/controllers/menu_controller.js +19 -0
  42. data/app/assets/javascripts/lcp_ruby/controllers/nested_forms_controller.js +261 -0
  43. data/app/assets/javascripts/lcp_ruby/controllers/responsive_sidebar_controller.js +161 -0
  44. data/app/assets/javascripts/lcp_ruby/controllers/responsive_top_nav_controller.js +394 -0
  45. data/app/assets/javascripts/lcp_ruby/controllers/saved_filters_controller.js +129 -0
  46. data/app/assets/javascripts/lcp_ruby/controllers/search_controller.js +82 -0
  47. data/app/assets/javascripts/lcp_ruby/controllers/selection_controller.js +33 -0
  48. data/app/assets/javascripts/lcp_ruby/controllers/sidebar_controller.js +83 -0
  49. data/app/assets/javascripts/lcp_ruby/controllers/sidebar_toggle_controller.js +66 -0
  50. data/app/assets/javascripts/lcp_ruby/controllers/slider_controller.js +26 -0
  51. data/app/assets/javascripts/lcp_ruby/controllers/submenu_controller.js +55 -0
  52. data/app/assets/javascripts/lcp_ruby/controllers/tom_select_controller.js +168 -0
  53. data/app/assets/javascripts/lcp_ruby/controllers/tree_index_controller.js +106 -0
  54. data/app/assets/javascripts/lcp_ruby/controllers/tree_reparent_controller.js +182 -0
  55. data/app/assets/javascripts/lcp_ruby/controllers/tree_select_controller.js +119 -0
  56. data/app/assets/javascripts/lcp_ruby/controllers/ui_components_controller.js +135 -0
  57. data/app/assets/javascripts/lcp_ruby/controllers/zone_tabs_controller.js +66 -0
  58. data/app/assets/javascripts/lcp_ruby/dev_toolbar.js +494 -0
  59. data/app/assets/javascripts/lcp_ruby/i18n.js.erb +29 -0
  60. data/app/assets/javascripts/lcp_ruby/lucide_init.js +24 -0
  61. data/app/assets/javascripts/lcp_ruby/stimulus_bootstrap.js +7 -0
  62. data/app/assets/javascripts/lcp_ruby/utils.js +119 -0
  63. data/app/assets/javascripts/lcp_ruby/xhr_fetch.js +411 -0
  64. data/app/assets/stylesheets/lcp_ruby/application.css +1940 -0
  65. data/app/assets/stylesheets/lcp_ruby/dev_toolbar.css +355 -0
  66. data/app/controllers/concerns/lcp_ruby/dialog_rendering.rb +167 -0
  67. data/app/controllers/concerns/lcp_ruby/form_action_execution.rb +249 -0
  68. data/app/controllers/concerns/lcp_ruby/page_authorization.rb +105 -0
  69. data/app/controllers/concerns/lcp_ruby/zone_resolution.rb +199 -0
  70. data/app/controllers/lcp_ruby/actions_controller.rb +481 -0
  71. data/app/controllers/lcp_ruby/application_controller.rb +59 -0
  72. data/app/controllers/lcp_ruby/approval_tasks_controller.rb +98 -0
  73. data/app/controllers/lcp_ruby/auth/base_controller.rb +58 -0
  74. data/app/controllers/lcp_ruby/auth/callbacks_controller.rb +103 -0
  75. data/app/controllers/lcp_ruby/auth/passwords_controller.rb +15 -0
  76. data/app/controllers/lcp_ruby/auth/registrations_controller.rb +37 -0
  77. data/app/controllers/lcp_ruby/auth/sessions_controller.rb +112 -0
  78. data/app/controllers/lcp_ruby/custom_fields_controller.rb +370 -0
  79. data/app/controllers/lcp_ruby/dev_toolbar_controller.rb +197 -0
  80. data/app/controllers/lcp_ruby/dialogs_controller.rb +199 -0
  81. data/app/controllers/lcp_ruby/health_controller.rb +23 -0
  82. data/app/controllers/lcp_ruby/impersonation_controller.rb +32 -0
  83. data/app/controllers/lcp_ruby/landing_controller.rb +49 -0
  84. data/app/controllers/lcp_ruby/metrics_controller.rb +17 -0
  85. data/app/controllers/lcp_ruby/resources_controller.rb +2218 -0
  86. data/app/controllers/lcp_ruby/saved_filters_controller.rb +221 -0
  87. data/app/helpers/lcp_ruby/condition_helper.rb +87 -0
  88. data/app/helpers/lcp_ruby/dashboard_helper.rb +45 -0
  89. data/app/helpers/lcp_ruby/dev_toolbar_helper.rb +17 -0
  90. data/app/helpers/lcp_ruby/dialog_helper.rb +27 -0
  91. data/app/helpers/lcp_ruby/display/card_helper.rb +317 -0
  92. data/app/helpers/lcp_ruby/display_helper.rb +63 -0
  93. data/app/helpers/lcp_ruby/display_template_helper.rb +100 -0
  94. data/app/helpers/lcp_ruby/form_helper.rb +1020 -0
  95. data/app/helpers/lcp_ruby/grouped_query_helper.rb +107 -0
  96. data/app/helpers/lcp_ruby/i18n_payload_helper.rb +197 -0
  97. data/app/helpers/lcp_ruby/layout_helper.rb +832 -0
  98. data/app/helpers/lcp_ruby/link_through_helper.rb +29 -0
  99. data/app/helpers/lcp_ruby/oidc_button_helper.rb +59 -0
  100. data/app/helpers/lcp_ruby/search_helper.rb +31 -0
  101. data/app/helpers/lcp_ruby/tree_helper.rb +140 -0
  102. data/app/helpers/lcp_ruby/view_slot_helper.rb +33 -0
  103. data/app/models/lcp_ruby/user.rb +61 -0
  104. data/app/views/layouts/lcp_ruby/application.html.erb +168 -0
  105. data/app/views/layouts/lcp_ruby/auth.html.erb +170 -0
  106. data/app/views/lcp_ruby/auth/callbacks/failure.html.erb +18 -0
  107. data/app/views/lcp_ruby/auth/mailer/reset_password_instructions.html.erb +9 -0
  108. data/app/views/lcp_ruby/auth/passwords/edit.html.erb +34 -0
  109. data/app/views/lcp_ruby/auth/passwords/new.html.erb +24 -0
  110. data/app/views/lcp_ruby/auth/registrations/edit.html.erb +48 -0
  111. data/app/views/lcp_ruby/auth/registrations/new.html.erb +46 -0
  112. data/app/views/lcp_ruby/auth/sessions/new.html.erb +55 -0
  113. data/app/views/lcp_ruby/auth/shared/_links.html.erb +13 -0
  114. data/app/views/lcp_ruby/custom_fields/_manage_row.html.erb +82 -0
  115. data/app/views/lcp_ruby/custom_fields/manage.html.erb +47 -0
  116. data/app/views/lcp_ruby/dialogs/_dialog_composite_frame.html.erb +87 -0
  117. data/app/views/lcp_ruby/dialogs/_dialog_frame.html.erb +60 -0
  118. data/app/views/lcp_ruby/dialogs/_dialog_show_frame.html.erb +14 -0
  119. data/app/views/lcp_ruby/dialogs/_show_secret.html.erb +10 -0
  120. data/app/views/lcp_ruby/dialogs/_success.html.erb +1 -0
  121. data/app/views/lcp_ruby/errors/not_found.html.erb +5 -0
  122. data/app/views/lcp_ruby/menu_renderers/_user_menu.html.erb +51 -0
  123. data/app/views/lcp_ruby/menu_renderers/_user_menu_panel.html.erb +32 -0
  124. data/app/views/lcp_ruby/navigation/_auto_top.html.erb +16 -0
  125. data/app/views/lcp_ruby/navigation/_both_sidebar.html.erb +1 -0
  126. data/app/views/lcp_ruby/navigation/_both_top.html.erb +1 -0
  127. data/app/views/lcp_ruby/navigation/_impersonation_banner.html.erb +14 -0
  128. data/app/views/lcp_ruby/navigation/_item_content.html.erb +23 -0
  129. data/app/views/lcp_ruby/navigation/_link_or_button.html.erb +51 -0
  130. data/app/views/lcp_ruby/navigation/_mobile_header.html.erb +23 -0
  131. data/app/views/lcp_ruby/navigation/_panel_item.html.erb +32 -0
  132. data/app/views/lcp_ruby/navigation/_sidebar.html.erb +38 -0
  133. data/app/views/lcp_ruby/navigation/_sidebar_item.html.erb +38 -0
  134. data/app/views/lcp_ruby/navigation/_sidebar_toggle.html.erb +32 -0
  135. data/app/views/lcp_ruby/navigation/_top.html.erb +26 -0
  136. data/app/views/lcp_ruby/navigation/_top_item.html.erb +112 -0
  137. data/app/views/lcp_ruby/navigation/_widget_item.html.erb +14 -0
  138. data/app/views/lcp_ruby/resources/_action_button.html.erb +138 -0
  139. data/app/views/lcp_ruby/resources/_advanced_filter.html.erb +55 -0
  140. data/app/views/lcp_ruby/resources/_api_status_banner.html.erb +11 -0
  141. data/app/views/lcp_ruby/resources/_association_list.html.erb +132 -0
  142. data/app/views/lcp_ruby/resources/_audit_history.html.erb +66 -0
  143. data/app/views/lcp_ruby/resources/_batch_toolbar.html.erb +54 -0
  144. data/app/views/lcp_ruby/resources/_filter_form.html.erb +57 -0
  145. data/app/views/lcp_ruby/resources/_form.html.erb +73 -0
  146. data/app/views/lcp_ruby/resources/_form_action_button.html.erb +20 -0
  147. data/app/views/lcp_ruby/resources/_form_action_dropdown_item.html.erb +17 -0
  148. data/app/views/lcp_ruby/resources/_form_actions.html.erb +38 -0
  149. data/app/views/lcp_ruby/resources/_form_section.html.erb +235 -0
  150. data/app/views/lcp_ruby/resources/_grid_page.html.erb +17 -0
  151. data/app/views/lcp_ruby/resources/_grouped_index.html.erb +61 -0
  152. data/app/views/lcp_ruby/resources/_inline_create_form.html.erb +50 -0
  153. data/app/views/lcp_ruby/resources/_json_items_list.html.erb +77 -0
  154. data/app/views/lcp_ruby/resources/_json_nested_fields.html.erb +30 -0
  155. data/app/views/lcp_ruby/resources/_kanban_card.html.erb +31 -0
  156. data/app/views/lcp_ruby/resources/_kanban_card_body.html.erb +17 -0
  157. data/app/views/lcp_ruby/resources/_kanban_column.html.erb +33 -0
  158. data/app/views/lcp_ruby/resources/_kanban_column_header.html.erb +10 -0
  159. data/app/views/lcp_ruby/resources/_kanban_index.html.erb +17 -0
  160. data/app/views/lcp_ruby/resources/_nested_field_cell.html.erb +33 -0
  161. data/app/views/lcp_ruby/resources/_nested_fields.html.erb +26 -0
  162. data/app/views/lcp_ruby/resources/_nested_row_content.html.erb +65 -0
  163. data/app/views/lcp_ruby/resources/_scope_filters.html.erb +71 -0
  164. data/app/views/lcp_ruby/resources/_semantic_index_page.html.erb +162 -0
  165. data/app/views/lcp_ruby/resources/_semantic_page.html.erb +121 -0
  166. data/app/views/lcp_ruby/resources/_show_sections.html.erb +128 -0
  167. data/app/views/lcp_ruby/resources/_table_index.html.erb +169 -0
  168. data/app/views/lcp_ruby/resources/_tile_card.html.erb +26 -0
  169. data/app/views/lcp_ruby/resources/_tile_card_body.html.erb +19 -0
  170. data/app/views/lcp_ruby/resources/_tiles_index.html.erb +15 -0
  171. data/app/views/lcp_ruby/resources/_transition_button.html.erb +33 -0
  172. data/app/views/lcp_ruby/resources/_tree_index.html.erb +61 -0
  173. data/app/views/lcp_ruby/resources/_view_switcher.html.erb +24 -0
  174. data/app/views/lcp_ruby/resources/edit.html.erb +9 -0
  175. data/app/views/lcp_ruby/resources/index.html.erb +80 -0
  176. data/app/views/lcp_ruby/resources/new.html.erb +9 -0
  177. data/app/views/lcp_ruby/resources/show.html.erb +37 -0
  178. data/app/views/lcp_ruby/shared/_breadcrumbs.html.erb +15 -0
  179. data/app/views/lcp_ruby/shared/_custom_partial_error.html.erb +3 -0
  180. data/app/views/lcp_ruby/shared/_flash_messages.html.erb +19 -0
  181. data/app/views/lcp_ruby/slots/index/_advanced_filter.html.erb +3 -0
  182. data/app/views/lcp_ruby/slots/index/_collection_actions.html.erb +50 -0
  183. data/app/views/lcp_ruby/slots/index/_manage_all.html.erb +4 -0
  184. data/app/views/lcp_ruby/slots/index/_pagination_footer.html.erb +71 -0
  185. data/app/views/lcp_ruby/slots/index/_predefined_filters.html.erb +8 -0
  186. data/app/views/lcp_ruby/slots/index/_saved_filters.html.erb +87 -0
  187. data/app/views/lcp_ruby/slots/index/_search.html.erb +52 -0
  188. data/app/views/lcp_ruby/slots/index/_search_parameter.html.erb +72 -0
  189. data/app/views/lcp_ruby/slots/index/_sort_dropdown.html.erb +21 -0
  190. data/app/views/lcp_ruby/slots/index/_summary_bar.html.erb +22 -0
  191. data/app/views/lcp_ruby/slots/index/_view_switcher.html.erb +1 -0
  192. data/app/views/lcp_ruby/slots/show/_approval_status.html.erb +8 -0
  193. data/app/views/lcp_ruby/slots/show/_back_to_list.html.erb +1 -0
  194. data/app/views/lcp_ruby/slots/show/_copy_url.html.erb +5 -0
  195. data/app/views/lcp_ruby/slots/show/_single_actions.html.erb +4 -0
  196. data/app/views/lcp_ruby/slots/show/_view_switcher.html.erb +1 -0
  197. data/app/views/lcp_ruby/widgets/_approval_status.html.erb +150 -0
  198. data/app/views/lcp_ruby/widgets/_chart.html.erb +36 -0
  199. data/app/views/lcp_ruby/widgets/_embed.html.erb +13 -0
  200. data/app/views/lcp_ruby/widgets/_kpi_card.html.erb +29 -0
  201. data/app/views/lcp_ruby/widgets/_list.html.erb +17 -0
  202. data/app/views/lcp_ruby/widgets/_presenter_zone.html.erb +84 -0
  203. data/app/views/lcp_ruby/widgets/_record_show_zone.html.erb +11 -0
  204. data/app/views/lcp_ruby/widgets/_text.html.erb +3 -0
  205. data/app/views/lcp_ruby/widgets/_workflow_graph.html.erb +32 -0
  206. data/app/views/lcp_ruby/zones/_custom_zone.html.erb +16 -0
  207. data/app/views/lcp_ruby/zones/_error.html.erb +5 -0
  208. data/app/views/lcp_ruby/zones/_zone_frame.html.erb +25 -0
  209. data/app/views/lcp_ruby/zones/_zone_search.html.erb +20 -0
  210. data/config/locales/cs.yml +859 -0
  211. data/config/locales/en.yml +731 -0
  212. data/config/routes.rb +119 -0
  213. data/docs/README.md +225 -0
  214. data/docs/architecture.md +212 -0
  215. data/docs/feature-catalog.md +763 -0
  216. data/docs/feature-catalog.yml +20911 -0
  217. data/docs/getting-started.md +1187 -0
  218. data/docs/guides/action-buttons.md +537 -0
  219. data/docs/guides/adding-locale.md +353 -0
  220. data/docs/guides/api-backed-models.md +478 -0
  221. data/docs/guides/attachments.md +399 -0
  222. data/docs/guides/auditing.md +333 -0
  223. data/docs/guides/batch-actions.md +342 -0
  224. data/docs/guides/composite-pages.md +1290 -0
  225. data/docs/guides/computed-fields.md +350 -0
  226. data/docs/guides/conditional-rendering.md +832 -0
  227. data/docs/guides/custom-actions.md +238 -0
  228. data/docs/guides/custom-fields.md +439 -0
  229. data/docs/guides/custom-renderers.md +234 -0
  230. data/docs/guides/custom-types.md +274 -0
  231. data/docs/guides/dashboards.md +504 -0
  232. data/docs/guides/debugging/README.md +38 -0
  233. data/docs/guides/debugging/controllers.md +147 -0
  234. data/docs/guides/debugging/data.md +165 -0
  235. data/docs/guides/debugging/metadata.md +143 -0
  236. data/docs/guides/debugging/models.md +126 -0
  237. data/docs/guides/debugging/permissions.md +147 -0
  238. data/docs/guides/debugging/presenters.md +151 -0
  239. data/docs/guides/developer-tools.md +418 -0
  240. data/docs/guides/dialogs.md +392 -0
  241. data/docs/guides/display-types.md +1430 -0
  242. data/docs/guides/eager-loading.md +230 -0
  243. data/docs/guides/event-handlers.md +135 -0
  244. data/docs/guides/export.md +305 -0
  245. data/docs/guides/extensibility.md +761 -0
  246. data/docs/guides/groups.md +220 -0
  247. data/docs/guides/hierarchical-authorization.md +427 -0
  248. data/docs/guides/host-application.md +556 -0
  249. data/docs/guides/host-controller-integration.md +473 -0
  250. data/docs/guides/impersonation.md +83 -0
  251. data/docs/guides/import.md +165 -0
  252. data/docs/guides/inherited-permissions.md +459 -0
  253. data/docs/guides/menu.md +373 -0
  254. data/docs/guides/monitoring.md +254 -0
  255. data/docs/guides/oidc-setup.md +399 -0
  256. data/docs/guides/permission-source.md +205 -0
  257. data/docs/guides/permissions.md +364 -0
  258. data/docs/guides/presenters.md +2324 -0
  259. data/docs/guides/record-aliases.md +303 -0
  260. data/docs/guides/rendering-extension-points.md +280 -0
  261. data/docs/guides/role-source.md +288 -0
  262. data/docs/guides/selectbox.md +516 -0
  263. data/docs/guides/sequences.md +291 -0
  264. data/docs/guides/soft-delete.md +460 -0
  265. data/docs/guides/theming.md +129 -0
  266. data/docs/guides/tiles.md +383 -0
  267. data/docs/guides/tree-structures.md +297 -0
  268. data/docs/guides/userstamps.md +288 -0
  269. data/docs/guides/view-groups.md +259 -0
  270. data/docs/guides/view-slots.md +352 -0
  271. data/docs/guides/virtual-columns.md +810 -0
  272. data/docs/guides/workflow.md +692 -0
  273. data/docs/reference/api-backed-models.md +404 -0
  274. data/docs/reference/api-tokens.md +128 -0
  275. data/docs/reference/auditing.md +277 -0
  276. data/docs/reference/boot_lifecycle.md +188 -0
  277. data/docs/reference/cascading_selects.md +189 -0
  278. data/docs/reference/condition-operators.md +445 -0
  279. data/docs/reference/custom-fields.md +483 -0
  280. data/docs/reference/dialogs.md +286 -0
  281. data/docs/reference/doctor.md +168 -0
  282. data/docs/reference/dynamic-references.md +95 -0
  283. data/docs/reference/eager-loading.md +192 -0
  284. data/docs/reference/engine-configuration.md +989 -0
  285. data/docs/reference/export.md +309 -0
  286. data/docs/reference/forms.md +68 -0
  287. data/docs/reference/groups.md +176 -0
  288. data/docs/reference/host-controller-integration.md +342 -0
  289. data/docs/reference/i18n.md +497 -0
  290. data/docs/reference/i18n_check.md +351 -0
  291. data/docs/reference/import.md +260 -0
  292. data/docs/reference/invariant_check.md +216 -0
  293. data/docs/reference/menu.md +985 -0
  294. data/docs/reference/model-dsl.md +1157 -0
  295. data/docs/reference/models.md +2972 -0
  296. data/docs/reference/monitoring.md +222 -0
  297. data/docs/reference/oidc-bearer.md +269 -0
  298. data/docs/reference/oidc.md +407 -0
  299. data/docs/reference/page_filters.md +328 -0
  300. data/docs/reference/pages.md +1375 -0
  301. data/docs/reference/permission-source.md +185 -0
  302. data/docs/reference/permissions.md +715 -0
  303. data/docs/reference/presenter-dsl.md +1719 -0
  304. data/docs/reference/presenters.md +3627 -0
  305. data/docs/reference/role-source.md +227 -0
  306. data/docs/reference/theme-variables.md +139 -0
  307. data/docs/reference/tree-structures.md +374 -0
  308. data/docs/reference/types.md +470 -0
  309. data/docs/reference/view-groups.md +347 -0
  310. data/docs/reference/view-slots.md +228 -0
  311. data/docs/reference/virtual_forms.md +196 -0
  312. data/docs/reference/workflow-approvals.md +387 -0
  313. data/docs/reference/workflow.md +651 -0
  314. data/examples/crm/Gemfile +9 -0
  315. data/examples/crm/Gemfile.lock +417 -0
  316. data/examples/crm/Rakefile +2 -0
  317. data/examples/crm/app/actions/activity/complete.rb +20 -0
  318. data/examples/crm/app/actions/deal/close_won.rb +20 -0
  319. data/examples/crm/app/assets/config/manifest.js +3 -0
  320. data/examples/crm/app/controllers/application_controller.rb +9 -0
  321. data/examples/crm/app/event_handlers/deal/on_stage_change.rb +17 -0
  322. data/examples/crm/app/lcp_services/computed/weighted_deal_value.rb +13 -0
  323. data/examples/crm/app/lcp_services/data_providers/active_contacts_count.rb +14 -0
  324. data/examples/crm/app/lcp_services/data_providers/open_deals_count.rb +14 -0
  325. data/examples/crm/app/lcp_services/data_providers/pending_activities_count.rb +15 -0
  326. data/examples/crm/app/lcp_services/data_providers/pipeline_value.rb +27 -0
  327. data/examples/crm/app/lcp_services/data_providers/won_deals_count.rb +14 -0
  328. data/examples/crm/app/lcp_services/defaults/thirty_days_out.rb +11 -0
  329. data/examples/crm/app/lcp_services/transforms/titlecase.rb +11 -0
  330. data/examples/crm/app/lcp_services/validators/deal_credit_limit.rb +19 -0
  331. data/examples/crm/app/lcp_services/validators/deal_documents_required.rb +17 -0
  332. data/examples/crm/app/renderers/conditional_badge.rb +39 -0
  333. data/examples/crm/bin/rails +4 -0
  334. data/examples/crm/bin/rake +4 -0
  335. data/examples/crm/config/application.rb +31 -0
  336. data/examples/crm/config/boot.rb +2 -0
  337. data/examples/crm/config/database.yml +12 -0
  338. data/examples/crm/config/environment.rb +2 -0
  339. data/examples/crm/config/initializers/lcp_ruby.rb +45 -0
  340. data/examples/crm/config/lcp_ruby/menu.yml +53 -0
  341. data/examples/crm/config/lcp_ruby/models/activity.rb +43 -0
  342. data/examples/crm/config/lcp_ruby/models/city.rb +19 -0
  343. data/examples/crm/config/lcp_ruby/models/company.rb +65 -0
  344. data/examples/crm/config/lcp_ruby/models/contact.rb +62 -0
  345. data/examples/crm/config/lcp_ruby/models/country.rb +22 -0
  346. data/examples/crm/config/lcp_ruby/models/custom_field_definition.rb +60 -0
  347. data/examples/crm/config/lcp_ruby/models/deal.rb +85 -0
  348. data/examples/crm/config/lcp_ruby/models/deal_category.rb +15 -0
  349. data/examples/crm/config/lcp_ruby/models/gapfree_sequence.rb +17 -0
  350. data/examples/crm/config/lcp_ruby/models/region.rb +15 -0
  351. data/examples/crm/config/lcp_ruby/models/saved_filter.rb +50 -0
  352. data/examples/crm/config/lcp_ruby/pages/activity_quick_log.yml +19 -0
  353. data/examples/crm/config/lcp_ruby/pages/company_detail.yml +127 -0
  354. data/examples/crm/config/lcp_ruby/pages/deals_overview.yml +65 -0
  355. data/examples/crm/config/lcp_ruby/permissions/activity.yml +40 -0
  356. data/examples/crm/config/lcp_ruby/permissions/custom_field_definition.yml +16 -0
  357. data/examples/crm/config/lcp_ruby/permissions/deal.yml +54 -0
  358. data/examples/crm/config/lcp_ruby/permissions/default.yml +16 -0
  359. data/examples/crm/config/lcp_ruby/permissions/gapfree_sequence.yml +6 -0
  360. data/examples/crm/config/lcp_ruby/permissions/saved_filter.yml +27 -0
  361. data/examples/crm/config/lcp_ruby/presenters/activity.rb +140 -0
  362. data/examples/crm/config/lcp_ruby/presenters/activity_quick_form.rb +36 -0
  363. data/examples/crm/config/lcp_ruby/presenters/activity_short.rb +16 -0
  364. data/examples/crm/config/lcp_ruby/presenters/activity_tiles.rb +35 -0
  365. data/examples/crm/config/lcp_ruby/presenters/city.rb +53 -0
  366. data/examples/crm/config/lcp_ruby/presenters/company.rb +147 -0
  367. data/examples/crm/config/lcp_ruby/presenters/company_activities_zone.rb +26 -0
  368. data/examples/crm/config/lcp_ruby/presenters/company_archive.rb +37 -0
  369. data/examples/crm/config/lcp_ruby/presenters/company_contacts_zone.rb +22 -0
  370. data/examples/crm/config/lcp_ruby/presenters/company_deals_zone.rb +36 -0
  371. data/examples/crm/config/lcp_ruby/presenters/company_short.rb +21 -0
  372. data/examples/crm/config/lcp_ruby/presenters/company_show_zone.rb +38 -0
  373. data/examples/crm/config/lcp_ruby/presenters/company_sidebar.rb +15 -0
  374. data/examples/crm/config/lcp_ruby/presenters/company_tiles.rb +35 -0
  375. data/examples/crm/config/lcp_ruby/presenters/contact.rb +120 -0
  376. data/examples/crm/config/lcp_ruby/presenters/contact_quick_form.rb +17 -0
  377. data/examples/crm/config/lcp_ruby/presenters/contact_short.rb +20 -0
  378. data/examples/crm/config/lcp_ruby/presenters/contact_tiles.rb +34 -0
  379. data/examples/crm/config/lcp_ruby/presenters/country.rb +44 -0
  380. data/examples/crm/config/lcp_ruby/presenters/custom_fields.rb +124 -0
  381. data/examples/crm/config/lcp_ruby/presenters/deal.rb +181 -0
  382. data/examples/crm/config/lcp_ruby/presenters/deal_category.rb +46 -0
  383. data/examples/crm/config/lcp_ruby/presenters/deal_overview.rb +6 -0
  384. data/examples/crm/config/lcp_ruby/presenters/deal_pipeline.rb +18 -0
  385. data/examples/crm/config/lcp_ruby/presenters/deal_short.rb +25 -0
  386. data/examples/crm/config/lcp_ruby/presenters/deal_tiles.rb +48 -0
  387. data/examples/crm/config/lcp_ruby/presenters/region.rb +42 -0
  388. data/examples/crm/config/lcp_ruby/presenters/save_filter_dialog.rb +17 -0
  389. data/examples/crm/config/lcp_ruby/presenters/saved_filters.rb +93 -0
  390. data/examples/crm/config/lcp_ruby/views/activities.yml +16 -0
  391. data/examples/crm/config/lcp_ruby/views/cities.yml +10 -0
  392. data/examples/crm/config/lcp_ruby/views/companies.yml +14 -0
  393. data/examples/crm/config/lcp_ruby/views/contacts.yml +16 -0
  394. data/examples/crm/config/lcp_ruby/views/countries.yml +8 -0
  395. data/examples/crm/config/lcp_ruby/views/custom_fields.rb +9 -0
  396. data/examples/crm/config/lcp_ruby/views/deal_categories.yml +8 -0
  397. data/examples/crm/config/lcp_ruby/views/deals.yml +19 -0
  398. data/examples/crm/config/lcp_ruby/views/pipeline.yml +10 -0
  399. data/examples/crm/config/lcp_ruby/views/regions.yml +10 -0
  400. data/examples/crm/config/lcp_ruby/views/saved_filters.yml +7 -0
  401. data/examples/crm/config/locales/cs.yml +338 -0
  402. data/examples/crm/config/locales/en.yml +353 -0
  403. data/examples/crm/config/routes.rb +4 -0
  404. data/examples/crm/config/storage.yml +3 -0
  405. data/examples/crm/config.ru +2 -0
  406. data/examples/crm/db/migrate/20260219104942_create_active_storage_tables.active_storage.rb +57 -0
  407. data/examples/crm/db/schema.rb +245 -0
  408. data/examples/crm/db/seeds.rb +1111 -0
  409. data/examples/crm/erd.md +163 -0
  410. data/examples/hr/Gemfile +9 -0
  411. data/examples/hr/Gemfile.lock +419 -0
  412. data/examples/hr/Rakefile +6 -0
  413. data/examples/hr/app/actions/asset/assign_asset.rb +20 -0
  414. data/examples/hr/app/actions/asset/return_asset.rb +20 -0
  415. data/examples/hr/app/actions/candidate/advance.rb +32 -0
  416. data/examples/hr/app/actions/candidate/hire.rb +20 -0
  417. data/examples/hr/app/actions/candidate/reject_candidate.rb +20 -0
  418. data/examples/hr/app/actions/expense_claim/approve.rb +21 -0
  419. data/examples/hr/app/actions/expense_claim/reject.rb +21 -0
  420. data/examples/hr/app/actions/expense_claim/submit.rb +20 -0
  421. data/examples/hr/app/actions/interview/complete_interview.rb +20 -0
  422. data/examples/hr/app/actions/leave_request/approve.rb +21 -0
  423. data/examples/hr/app/actions/leave_request/cancel.rb +20 -0
  424. data/examples/hr/app/actions/leave_request/reject.rb +21 -0
  425. data/examples/hr/app/assets/config/manifest.js +1 -0
  426. data/examples/hr/app/assets/stylesheets/application.css +10 -0
  427. data/examples/hr/app/condition_services/is_own_org_unit.rb +14 -0
  428. data/examples/hr/app/condition_services/is_own_record.rb +20 -0
  429. data/examples/hr/app/controllers/application_controller.rb +15 -0
  430. data/examples/hr/app/event_handlers/asset_assignment/on_create.rb +24 -0
  431. data/examples/hr/app/event_handlers/candidate/on_status_change.rb +18 -0
  432. data/examples/hr/app/event_handlers/leave_request/on_status_change.rb +45 -0
  433. data/examples/hr/app/helpers/application_helper.rb +2 -0
  434. data/examples/hr/app/javascript/application.js +1 -0
  435. data/examples/hr/app/jobs/application_job.rb +7 -0
  436. data/examples/hr/app/lcp_services/computed/employee_tenure.rb +28 -0
  437. data/examples/hr/app/lcp_services/computed/leave_remaining.rb +13 -0
  438. data/examples/hr/app/lcp_services/data_providers/headcount_text.rb +14 -0
  439. data/examples/hr/app/lcp_services/data_providers/open_positions_count.rb +14 -0
  440. data/examples/hr/app/lcp_services/data_providers/pending_expenses_count.rb +14 -0
  441. data/examples/hr/app/lcp_services/data_providers/pending_leaves_count.rb +14 -0
  442. data/examples/hr/app/lcp_services/defaults/current_year.rb +11 -0
  443. data/examples/hr/app/lcp_services/transforms/titlecase.rb +11 -0
  444. data/examples/hr/app/lcp_services/validators/expense_receipt_required.rb +17 -0
  445. data/examples/hr/app/lcp_services/validators/leave_balance_check.rb +37 -0
  446. data/examples/hr/app/models/application_record.rb +3 -0
  447. data/examples/hr/app/views/layouts/application.html.erb +29 -0
  448. data/examples/hr/app/views/pwa/manifest.json.erb +22 -0
  449. data/examples/hr/app/views/pwa/service-worker.js +26 -0
  450. data/examples/hr/bin/brakeman +7 -0
  451. data/examples/hr/bin/bundler-audit +6 -0
  452. data/examples/hr/bin/ci +6 -0
  453. data/examples/hr/bin/dev +2 -0
  454. data/examples/hr/bin/docker-entrypoint +8 -0
  455. data/examples/hr/bin/importmap +4 -0
  456. data/examples/hr/bin/jobs +6 -0
  457. data/examples/hr/bin/kamal +27 -0
  458. data/examples/hr/bin/rails +4 -0
  459. data/examples/hr/bin/rake +4 -0
  460. data/examples/hr/bin/rubocop +8 -0
  461. data/examples/hr/bin/setup +35 -0
  462. data/examples/hr/bin/thrust +5 -0
  463. data/examples/hr/config/application.rb +31 -0
  464. data/examples/hr/config/boot.rb +2 -0
  465. data/examples/hr/config/bundler-audit.yml +5 -0
  466. data/examples/hr/config/cache.yml +16 -0
  467. data/examples/hr/config/ci.rb +20 -0
  468. data/examples/hr/config/credentials.yml.enc +1 -0
  469. data/examples/hr/config/database.yml +36 -0
  470. data/examples/hr/config/deploy.yml +119 -0
  471. data/examples/hr/config/environment.rb +5 -0
  472. data/examples/hr/config/environments/development.rb +66 -0
  473. data/examples/hr/config/environments/production.rb +74 -0
  474. data/examples/hr/config/environments/test.rb +45 -0
  475. data/examples/hr/config/importmap.rb +3 -0
  476. data/examples/hr/config/initializers/assets.rb +7 -0
  477. data/examples/hr/config/initializers/content_security_policy.rb +29 -0
  478. data/examples/hr/config/initializers/filter_parameter_logging.rb +8 -0
  479. data/examples/hr/config/initializers/inflections.rb +16 -0
  480. data/examples/hr/config/initializers/lcp_ruby.rb +18 -0
  481. data/examples/hr/config/lcp_ruby/menu.yml +86 -0
  482. data/examples/hr/config/lcp_ruby/models/announcement.rb +33 -0
  483. data/examples/hr/config/lcp_ruby/models/asset.rb +73 -0
  484. data/examples/hr/config/lcp_ruby/models/asset_assignment.rb +39 -0
  485. data/examples/hr/config/lcp_ruby/models/audit_log.yml +57 -0
  486. data/examples/hr/config/lcp_ruby/models/candidate.rb +68 -0
  487. data/examples/hr/config/lcp_ruby/models/custom_field_definition.rb +60 -0
  488. data/examples/hr/config/lcp_ruby/models/dashboard.rb +17 -0
  489. data/examples/hr/config/lcp_ruby/models/document.rb +38 -0
  490. data/examples/hr/config/lcp_ruby/models/employee.rb +128 -0
  491. data/examples/hr/config/lcp_ruby/models/employee_skill.rb +29 -0
  492. data/examples/hr/config/lcp_ruby/models/expense_claim.rb +70 -0
  493. data/examples/hr/config/lcp_ruby/models/goal.rb +48 -0
  494. data/examples/hr/config/lcp_ruby/models/group.rb +37 -0
  495. data/examples/hr/config/lcp_ruby/models/group_membership.rb +26 -0
  496. data/examples/hr/config/lcp_ruby/models/interview.rb +54 -0
  497. data/examples/hr/config/lcp_ruby/models/job_posting.rb +67 -0
  498. data/examples/hr/config/lcp_ruby/models/leave_balance.rb +31 -0
  499. data/examples/hr/config/lcp_ruby/models/leave_request.rb +52 -0
  500. data/examples/hr/config/lcp_ruby/models/leave_type.rb +35 -0
  501. data/examples/hr/config/lcp_ruby/models/organization_unit.rb +42 -0
  502. data/examples/hr/config/lcp_ruby/models/performance_review.rb +59 -0
  503. data/examples/hr/config/lcp_ruby/models/position.rb +43 -0
  504. data/examples/hr/config/lcp_ruby/models/skill.rb +27 -0
  505. data/examples/hr/config/lcp_ruby/models/training_course.rb +53 -0
  506. data/examples/hr/config/lcp_ruby/models/training_enrollment.rb +35 -0
  507. data/examples/hr/config/lcp_ruby/pages/dashboard.yml +101 -0
  508. data/examples/hr/config/lcp_ruby/permissions/announcement.yml +25 -0
  509. data/examples/hr/config/lcp_ruby/permissions/asset.yml +35 -0
  510. data/examples/hr/config/lcp_ruby/permissions/audit_log.yml +28 -0
  511. data/examples/hr/config/lcp_ruby/permissions/candidate.yml +25 -0
  512. data/examples/hr/config/lcp_ruby/permissions/custom_field_definition.yml +28 -0
  513. data/examples/hr/config/lcp_ruby/permissions/dashboard.yml +25 -0
  514. data/examples/hr/config/lcp_ruby/permissions/default.yml +25 -0
  515. data/examples/hr/config/lcp_ruby/permissions/document.yml +37 -0
  516. data/examples/hr/config/lcp_ruby/permissions/employee.yml +55 -0
  517. data/examples/hr/config/lcp_ruby/permissions/expense_claim.yml +45 -0
  518. data/examples/hr/config/lcp_ruby/permissions/group.yml +27 -0
  519. data/examples/hr/config/lcp_ruby/permissions/job_posting.yml +34 -0
  520. data/examples/hr/config/lcp_ruby/permissions/leave_request.yml +45 -0
  521. data/examples/hr/config/lcp_ruby/permissions/performance_review.yml +42 -0
  522. data/examples/hr/config/lcp_ruby/presenters/announcement.rb +47 -0
  523. data/examples/hr/config/lcp_ruby/presenters/asset.rb +69 -0
  524. data/examples/hr/config/lcp_ruby/presenters/asset_assignment.rb +47 -0
  525. data/examples/hr/config/lcp_ruby/presenters/audit_logs.yml +43 -0
  526. data/examples/hr/config/lcp_ruby/presenters/candidate.rb +71 -0
  527. data/examples/hr/config/lcp_ruby/presenters/custom_fields.rb +124 -0
  528. data/examples/hr/config/lcp_ruby/presenters/dashboard.rb +37 -0
  529. data/examples/hr/config/lcp_ruby/presenters/document.rb +46 -0
  530. data/examples/hr/config/lcp_ruby/presenters/employee.rb +167 -0
  531. data/examples/hr/config/lcp_ruby/presenters/employee_archive.rb +38 -0
  532. data/examples/hr/config/lcp_ruby/presenters/employee_directory.rb +27 -0
  533. data/examples/hr/config/lcp_ruby/presenters/employee_skill.rb +57 -0
  534. data/examples/hr/config/lcp_ruby/presenters/expense_claim.rb +76 -0
  535. data/examples/hr/config/lcp_ruby/presenters/goal.rb +60 -0
  536. data/examples/hr/config/lcp_ruby/presenters/group.rb +48 -0
  537. data/examples/hr/config/lcp_ruby/presenters/interview.rb +59 -0
  538. data/examples/hr/config/lcp_ruby/presenters/job_posting.rb +73 -0
  539. data/examples/hr/config/lcp_ruby/presenters/leave_balance.rb +50 -0
  540. data/examples/hr/config/lcp_ruby/presenters/leave_request.rb +89 -0
  541. data/examples/hr/config/lcp_ruby/presenters/leave_type.rb +52 -0
  542. data/examples/hr/config/lcp_ruby/presenters/organization_unit.rb +56 -0
  543. data/examples/hr/config/lcp_ruby/presenters/performance_review.rb +87 -0
  544. data/examples/hr/config/lcp_ruby/presenters/position.rb +54 -0
  545. data/examples/hr/config/lcp_ruby/presenters/skill.rb +45 -0
  546. data/examples/hr/config/lcp_ruby/presenters/training_course.rb +61 -0
  547. data/examples/hr/config/lcp_ruby/presenters/training_enrollment.rb +49 -0
  548. data/examples/hr/config/lcp_ruby/views/announcements.yml +8 -0
  549. data/examples/hr/config/lcp_ruby/views/asset_assignments.yml +10 -0
  550. data/examples/hr/config/lcp_ruby/views/assets.yml +8 -0
  551. data/examples/hr/config/lcp_ruby/views/audit_logs.yml +8 -0
  552. data/examples/hr/config/lcp_ruby/views/candidates.yml +8 -0
  553. data/examples/hr/config/lcp_ruby/views/custom_fields.yml +8 -0
  554. data/examples/hr/config/lcp_ruby/views/dashboard.yml +6 -0
  555. data/examples/hr/config/lcp_ruby/views/documents.yml +10 -0
  556. data/examples/hr/config/lcp_ruby/views/employee_skills.yml +10 -0
  557. data/examples/hr/config/lcp_ruby/views/employees.yml +14 -0
  558. data/examples/hr/config/lcp_ruby/views/expense_claims.yml +10 -0
  559. data/examples/hr/config/lcp_ruby/views/goals.yml +10 -0
  560. data/examples/hr/config/lcp_ruby/views/groups.yml +8 -0
  561. data/examples/hr/config/lcp_ruby/views/interviews.yml +8 -0
  562. data/examples/hr/config/lcp_ruby/views/job_postings.yml +6 -0
  563. data/examples/hr/config/lcp_ruby/views/leave_balances.yml +10 -0
  564. data/examples/hr/config/lcp_ruby/views/leave_requests.yml +10 -0
  565. data/examples/hr/config/lcp_ruby/views/leave_types.yml +8 -0
  566. data/examples/hr/config/lcp_ruby/views/organization_units.yml +8 -0
  567. data/examples/hr/config/lcp_ruby/views/performance_reviews.yml +10 -0
  568. data/examples/hr/config/lcp_ruby/views/positions.yml +8 -0
  569. data/examples/hr/config/lcp_ruby/views/skills.yml +8 -0
  570. data/examples/hr/config/lcp_ruby/views/training_courses.yml +8 -0
  571. data/examples/hr/config/lcp_ruby/views/training_enrollments.yml +10 -0
  572. data/examples/hr/config/locales/cs.yml +496 -0
  573. data/examples/hr/config/locales/en.yml +740 -0
  574. data/examples/hr/config/locales/sk.yml +496 -0
  575. data/examples/hr/config/puma.rb +42 -0
  576. data/examples/hr/config/queue.yml +18 -0
  577. data/examples/hr/config/recurring.yml +15 -0
  578. data/examples/hr/config/routes.rb +4 -0
  579. data/examples/hr/config/storage.yml +27 -0
  580. data/examples/hr/config.ru +6 -0
  581. data/examples/hr/db/cache_schema.rb +12 -0
  582. data/examples/hr/db/migrate/20260303202825_create_active_storage_tables.active_storage.rb +57 -0
  583. data/examples/hr/db/queue_schema.rb +129 -0
  584. data/examples/hr/db/schema.rb +588 -0
  585. data/examples/hr/db/seeds.rb +932 -0
  586. data/examples/hr/erd.md +396 -0
  587. data/examples/hr/public/400.html +135 -0
  588. data/examples/hr/public/404.html +135 -0
  589. data/examples/hr/public/406-unsupported-browser.html +135 -0
  590. data/examples/hr/public/422.html +135 -0
  591. data/examples/hr/public/500.html +135 -0
  592. data/examples/hr/public/icon.svg +3 -0
  593. data/examples/hr/public/robots.txt +1 -0
  594. data/examples/showcase/Gemfile +15 -0
  595. data/examples/showcase/Gemfile.lock +425 -0
  596. data/examples/showcase/Rakefile +2 -0
  597. data/examples/showcase/app/actions/showcase_batch_task/assign_batch.rb +20 -0
  598. data/examples/showcase/app/actions/showcase_batch_task/close_task.rb +21 -0
  599. data/examples/showcase/app/actions/showcase_condition/approve.rb +20 -0
  600. data/examples/showcase/app/actions/showcase_permission/lock.rb +20 -0
  601. data/examples/showcase/app/assets/config/manifest.js +3 -0
  602. data/examples/showcase/app/assets/stylesheets/application.css +27 -0
  603. data/examples/showcase/app/condition_services/budget_threshold.rb +16 -0
  604. data/examples/showcase/app/condition_services/overdue_check.rb +11 -0
  605. data/examples/showcase/app/controllers/application_controller.rb +2 -0
  606. data/examples/showcase/app/controllers/docs_controller.rb +48 -0
  607. data/examples/showcase/app/controllers/host_inventory_items_managed_controller.rb +90 -0
  608. data/examples/showcase/app/controllers/host_inventory_items_report_controller.rb +39 -0
  609. data/examples/showcase/app/controllers/host_inventory_items_wizard_controller.rb +50 -0
  610. data/examples/showcase/app/data_providers/showcase_amount_provider.rb +52 -0
  611. data/examples/showcase/app/data_providers/weather_station_provider.rb +140 -0
  612. data/examples/showcase/app/event_handlers/showcase_model/on_status_change.rb +17 -0
  613. data/examples/showcase/app/event_handlers/showcase_permission/on_status_change.rb +17 -0
  614. data/examples/showcase/app/event_handlers/showcase_workflow/log_review_exit.rb +16 -0
  615. data/examples/showcase/app/event_handlers/showcase_workflow/notify_reviewers.rb +15 -0
  616. data/examples/showcase/app/event_handlers/showcase_workflow/request_submitted.rb +15 -0
  617. data/examples/showcase/app/lcp_metrics/showcase_metrics.rb +31 -0
  618. data/examples/showcase/app/lcp_services/computed/showcase_score.rb +18 -0
  619. data/examples/showcase/app/lcp_services/computed/showcase_total.rb +13 -0
  620. data/examples/showcase/app/lcp_services/defaults/one_week_from_now.rb +11 -0
  621. data/examples/showcase/app/lcp_services/menu_items/recent_announcements.rb +36 -0
  622. data/examples/showcase/app/lcp_services/virtual_columns/project_health.rb +26 -0
  623. data/examples/showcase/app/model_extensions/lcp_error_log_extension.rb +12 -0
  624. data/examples/showcase/app/models/host_inventory_item.rb +20 -0
  625. data/examples/showcase/app/models/platform_profile.rb +27 -0
  626. data/examples/showcase/app/models/platform_user.rb +5 -0
  627. data/examples/showcase/app/views/docs/show.html.erb +19 -0
  628. data/examples/showcase/app/views/host_inventory_items_report/index.html.erb +61 -0
  629. data/examples/showcase/app/views/host_inventory_items_report/show.html.erb +39 -0
  630. data/examples/showcase/app/views/layouts/docs.html.erb +46 -0
  631. data/examples/showcase/app/views/showcase/menu_renderers/_status_pill.html.erb +17 -0
  632. data/examples/showcase/app/views/showcase_custom/_activity_timeline.html.erb +37 -0
  633. data/examples/showcase/app/views/showcase_custom/_card_index.html.erb +74 -0
  634. data/examples/showcase/app/views/showcase_custom/_detail_show.html.erb +115 -0
  635. data/examples/showcase/app/views/showcase_custom/_location_map.html.erb +29 -0
  636. data/examples/showcase/app/views/showcase_custom/_location_view.html.erb +34 -0
  637. data/examples/showcase/app/views/showcase_custom/_quick_stats.html.erb +50 -0
  638. data/examples/showcase/app/views/showcase_custom/_stats_sidebar.html.erb +42 -0
  639. data/examples/showcase/app/views/showcase_custom/_tags_editor.html.erb +48 -0
  640. data/examples/showcase/bin/rails +4 -0
  641. data/examples/showcase/bin/rake +4 -0
  642. data/examples/showcase/config/application.rb +37 -0
  643. data/examples/showcase/config/boot.rb +2 -0
  644. data/examples/showcase/config/database.yml +12 -0
  645. data/examples/showcase/config/environment.rb +2 -0
  646. data/examples/showcase/config/initializers/devise.rb +5 -0
  647. data/examples/showcase/config/initializers/lcp_ruby.rb +175 -0
  648. data/examples/showcase/config/lcp_ruby/auth.yml +60 -0
  649. data/examples/showcase/config/lcp_ruby/jobs/data_import.yml +7 -0
  650. data/examples/showcase/config/lcp_ruby/jobs/showcase_cleanup.yml +11 -0
  651. data/examples/showcase/config/lcp_ruby/jobs/showcase_event_triggered.yml +11 -0
  652. data/examples/showcase/config/lcp_ruby/jobs/showcase_multi_step.yml +9 -0
  653. data/examples/showcase/config/lcp_ruby/jobs/showcase_webhook.yml +13 -0
  654. data/examples/showcase/config/lcp_ruby/menu.yml +249 -0
  655. data/examples/showcase/config/lcp_ruby/models/_base_document.rb +26 -0
  656. data/examples/showcase/config/lcp_ruby/models/_categorized_document.rb +19 -0
  657. data/examples/showcase/config/lcp_ruby/models/_contactable.rb +20 -0
  658. data/examples/showcase/config/lcp_ruby/models/api_token.rb +23 -0
  659. data/examples/showcase/config/lcp_ruby/models/article.rb +49 -0
  660. data/examples/showcase/config/lcp_ruby/models/article_tag.rb +10 -0
  661. data/examples/showcase/config/lcp_ruby/models/author.rb +16 -0
  662. data/examples/showcase/config/lcp_ruby/models/batch_operation.yml +79 -0
  663. data/examples/showcase/config/lcp_ruby/models/batch_operation_item.yml +45 -0
  664. data/examples/showcase/config/lcp_ruby/models/category.rb +23 -0
  665. data/examples/showcase/config/lcp_ruby/models/comment.rb +21 -0
  666. data/examples/showcase/config/lcp_ruby/models/custom_field_definition.rb +59 -0
  667. data/examples/showcase/config/lcp_ruby/models/department.rb +32 -0
  668. data/examples/showcase/config/lcp_ruby/models/employee.rb +52 -0
  669. data/examples/showcase/config/lcp_ruby/models/employee_emergency_contact.rb +24 -0
  670. data/examples/showcase/config/lcp_ruby/models/employee_profile.rb +18 -0
  671. data/examples/showcase/config/lcp_ruby/models/employee_skill.rb +10 -0
  672. data/examples/showcase/config/lcp_ruby/models/export_log.yml +27 -0
  673. data/examples/showcase/config/lcp_ruby/models/export_profile.yml +37 -0
  674. data/examples/showcase/config/lcp_ruby/models/feature.rb +76 -0
  675. data/examples/showcase/config/lcp_ruby/models/gapfree_sequence.rb +17 -0
  676. data/examples/showcase/config/lcp_ruby/models/group.rb +26 -0
  677. data/examples/showcase/config/lcp_ruby/models/group_membership.rb +21 -0
  678. data/examples/showcase/config/lcp_ruby/models/group_role_mapping.rb +14 -0
  679. data/examples/showcase/config/lcp_ruby/models/host_inventory_item.yml +111 -0
  680. data/examples/showcase/config/lcp_ruby/models/import_profile.rb +30 -0
  681. data/examples/showcase/config/lcp_ruby/models/import_row.rb +32 -0
  682. data/examples/showcase/config/lcp_ruby/models/ingredient_def.rb +11 -0
  683. data/examples/showcase/config/lcp_ruby/models/lcp_error_log.yml +61 -0
  684. data/examples/showcase/config/lcp_ruby/models/page_config.rb +19 -0
  685. data/examples/showcase/config/lcp_ruby/models/permission_config.rb +21 -0
  686. data/examples/showcase/config/lcp_ruby/models/pipeline.rb +15 -0
  687. data/examples/showcase/config/lcp_ruby/models/pipeline_stage.rb +17 -0
  688. data/examples/showcase/config/lcp_ruby/models/platform_profile.rb +104 -0
  689. data/examples/showcase/config/lcp_ruby/models/profile_setting.rb +17 -0
  690. data/examples/showcase/config/lcp_ruby/models/profile_tag.rb +19 -0
  691. data/examples/showcase/config/lcp_ruby/models/project.rb +23 -0
  692. data/examples/showcase/config/lcp_ruby/models/role.rb +24 -0
  693. data/examples/showcase/config/lcp_ruby/models/saved_filter.rb +50 -0
  694. data/examples/showcase/config/lcp_ruby/models/showcase_aggregate.rb +93 -0
  695. data/examples/showcase/config/lcp_ruby/models/showcase_aggregate_company.rb +14 -0
  696. data/examples/showcase/config/lcp_ruby/models/showcase_aggregate_item.rb +25 -0
  697. data/examples/showcase/config/lcp_ruby/models/showcase_announcement.rb +20 -0
  698. data/examples/showcase/config/lcp_ruby/models/showcase_array.rb +45 -0
  699. data/examples/showcase/config/lcp_ruby/models/showcase_attachment.rb +49 -0
  700. data/examples/showcase/config/lcp_ruby/models/showcase_audit_log.yml +44 -0
  701. data/examples/showcase/config/lcp_ruby/models/showcase_audited_record.rb +20 -0
  702. data/examples/showcase/config/lcp_ruby/models/showcase_batch_task.rb +27 -0
  703. data/examples/showcase/config/lcp_ruby/models/showcase_business_unit.rb +26 -0
  704. data/examples/showcase/config/lcp_ruby/models/showcase_condition.rb +24 -0
  705. data/examples/showcase/config/lcp_ruby/models/showcase_condition_category.rb +16 -0
  706. data/examples/showcase/config/lcp_ruby/models/showcase_condition_task.rb +16 -0
  707. data/examples/showcase/config/lcp_ruby/models/showcase_condition_threshold.rb +13 -0
  708. data/examples/showcase/config/lcp_ruby/models/showcase_contact.rb +22 -0
  709. data/examples/showcase/config/lcp_ruby/models/showcase_custom_render.rb +38 -0
  710. data/examples/showcase/config/lcp_ruby/models/showcase_delete_reason.rb +11 -0
  711. data/examples/showcase/config/lcp_ruby/models/showcase_division.rb +31 -0
  712. data/examples/showcase/config/lcp_ruby/models/showcase_extensibility.rb +25 -0
  713. data/examples/showcase/config/lcp_ruby/models/showcase_field.rb +53 -0
  714. data/examples/showcase/config/lcp_ruby/models/showcase_form.rb +23 -0
  715. data/examples/showcase/config/lcp_ruby/models/showcase_form_action.rb +38 -0
  716. data/examples/showcase/config/lcp_ruby/models/showcase_grade.rb +18 -0
  717. data/examples/showcase/config/lcp_ruby/models/showcase_hr_employee.rb +90 -0
  718. data/examples/showcase/config/lcp_ruby/models/showcase_item_class.rb +31 -0
  719. data/examples/showcase/config/lcp_ruby/models/showcase_job_execution.rb +49 -0
  720. data/examples/showcase/config/lcp_ruby/models/showcase_memo.rb +23 -0
  721. data/examples/showcase/config/lcp_ruby/models/showcase_model.rb +74 -0
  722. data/examples/showcase/config/lcp_ruby/models/showcase_organization.rb +31 -0
  723. data/examples/showcase/config/lcp_ruby/models/showcase_permission.rb +30 -0
  724. data/examples/showcase/config/lcp_ruby/models/showcase_person.rb +23 -0
  725. data/examples/showcase/config/lcp_ruby/models/showcase_positioning.rb +19 -0
  726. data/examples/showcase/config/lcp_ruby/models/showcase_quick_note.rb +13 -0
  727. data/examples/showcase/config/lcp_ruby/models/showcase_recipe.rb +17 -0
  728. data/examples/showcase/config/lcp_ruby/models/showcase_report.rb +32 -0
  729. data/examples/showcase/config/lcp_ruby/models/showcase_school_class.rb +17 -0
  730. data/examples/showcase/config/lcp_ruby/models/showcase_search.rb +84 -0
  731. data/examples/showcase/config/lcp_ruby/models/showcase_sequence.rb +46 -0
  732. data/examples/showcase/config/lcp_ruby/models/showcase_soft_delete.rb +30 -0
  733. data/examples/showcase/config/lcp_ruby/models/showcase_soft_delete_item.rb +21 -0
  734. data/examples/showcase/config/lcp_ruby/models/showcase_student.rb +17 -0
  735. data/examples/showcase/config/lcp_ruby/models/showcase_type_default.rb +49 -0
  736. data/examples/showcase/config/lcp_ruby/models/showcase_userstamps.rb +26 -0
  737. data/examples/showcase/config/lcp_ruby/models/showcase_virtual_field.rb +68 -0
  738. data/examples/showcase/config/lcp_ruby/models/showcase_workflow.rb +54 -0
  739. data/examples/showcase/config/lcp_ruby/models/skill.rb +22 -0
  740. data/examples/showcase/config/lcp_ruby/models/tag.rb +16 -0
  741. data/examples/showcase/config/lcp_ruby/models/user.rb +54 -0
  742. data/examples/showcase/config/lcp_ruby/models/weather_station.rb +20 -0
  743. data/examples/showcase/config/lcp_ruby/models/workflow_approval_request.yml +77 -0
  744. data/examples/showcase/config/lcp_ruby/models/workflow_approval_step.yml +56 -0
  745. data/examples/showcase/config/lcp_ruby/models/workflow_approval_task.yml +51 -0
  746. data/examples/showcase/config/lcp_ruby/models/workflow_audit_log.yml +74 -0
  747. data/examples/showcase/config/lcp_ruby/pages/article_detail.yml +32 -0
  748. data/examples/showcase/config/lcp_ruby/pages/author_detail.yml +27 -0
  749. data/examples/showcase/config/lcp_ruby/pages/category_detail.yml +24 -0
  750. data/examples/showcase/config/lcp_ruby/pages/chart_showcase.yml +151 -0
  751. data/examples/showcase/config/lcp_ruby/pages/department_detail.yml +55 -0
  752. data/examples/showcase/config/lcp_ruby/pages/department_explorer.yml +60 -0
  753. data/examples/showcase/config/lcp_ruby/pages/employee_overview.yml +106 -0
  754. data/examples/showcase/config/lcp_ruby/pages/employee_transfer_dialog.yml +24 -0
  755. data/examples/showcase/config/lcp_ruby/pages/hr_turnover_dashboard.yml +288 -0
  756. data/examples/showcase/config/lcp_ruby/pages/main_dashboard.yml +142 -0
  757. data/examples/showcase/config/lcp_ruby/pages/monitoring_dashboard.yml +58 -0
  758. data/examples/showcase/config/lcp_ruby/pages/pipeline_detail.yml +16 -0
  759. data/examples/showcase/config/lcp_ruby/pages/showcase_custom_zones.yml +39 -0
  760. data/examples/showcase/config/lcp_ruby/pages/showcase_form_action_dialog.yml +11 -0
  761. data/examples/showcase/config/lcp_ruby/pages/workflow_request_detail.yml +34 -0
  762. data/examples/showcase/config/lcp_ruby/permissions/api_token.yml +26 -0
  763. data/examples/showcase/config/lcp_ruby/permissions/batch_operation.yml +36 -0
  764. data/examples/showcase/config/lcp_ruby/permissions/custom_field_definition.yml +15 -0
  765. data/examples/showcase/config/lcp_ruby/permissions/default.yml +28 -0
  766. data/examples/showcase/config/lcp_ruby/permissions/export_log.yml +23 -0
  767. data/examples/showcase/config/lcp_ruby/permissions/export_profile.yml +25 -0
  768. data/examples/showcase/config/lcp_ruby/permissions/gapfree_sequence.yml +6 -0
  769. data/examples/showcase/config/lcp_ruby/permissions/group.yml +20 -0
  770. data/examples/showcase/config/lcp_ruby/permissions/group_membership.yml +20 -0
  771. data/examples/showcase/config/lcp_ruby/permissions/group_role_mapping.yml +20 -0
  772. data/examples/showcase/config/lcp_ruby/permissions/host_inventory_item.yml +34 -0
  773. data/examples/showcase/config/lcp_ruby/permissions/import_profile.yml +29 -0
  774. data/examples/showcase/config/lcp_ruby/permissions/import_row.yml +17 -0
  775. data/examples/showcase/config/lcp_ruby/permissions/lcp_error_log.yml +8 -0
  776. data/examples/showcase/config/lcp_ruby/permissions/page_config.yml +15 -0
  777. data/examples/showcase/config/lcp_ruby/permissions/permission_config.yml +15 -0
  778. data/examples/showcase/config/lcp_ruby/permissions/platform_profile.yml +23 -0
  779. data/examples/showcase/config/lcp_ruby/permissions/profile_setting.yml +23 -0
  780. data/examples/showcase/config/lcp_ruby/permissions/profile_tag.yml +23 -0
  781. data/examples/showcase/config/lcp_ruby/permissions/role.yml +20 -0
  782. data/examples/showcase/config/lcp_ruby/permissions/saved_filter.yml +39 -0
  783. data/examples/showcase/config/lcp_ruby/permissions/showcase_announcement.yml +22 -0
  784. data/examples/showcase/config/lcp_ruby/permissions/showcase_audit_log.yml +15 -0
  785. data/examples/showcase/config/lcp_ruby/permissions/showcase_audited_record.yml +15 -0
  786. data/examples/showcase/config/lcp_ruby/permissions/showcase_batch_task.yml +31 -0
  787. data/examples/showcase/config/lcp_ruby/permissions/showcase_condition.yml +54 -0
  788. data/examples/showcase/config/lcp_ruby/permissions/showcase_contact.yml +21 -0
  789. data/examples/showcase/config/lcp_ruby/permissions/showcase_custom_render.yml +21 -0
  790. data/examples/showcase/config/lcp_ruby/permissions/showcase_delete_reason.yml +12 -0
  791. data/examples/showcase/config/lcp_ruby/permissions/showcase_form_action.yml +26 -0
  792. data/examples/showcase/config/lcp_ruby/permissions/showcase_grade.yml +33 -0
  793. data/examples/showcase/config/lcp_ruby/permissions/showcase_job_execution.yml +23 -0
  794. data/examples/showcase/config/lcp_ruby/permissions/showcase_permission.yml +47 -0
  795. data/examples/showcase/config/lcp_ruby/permissions/showcase_quick_note.yml +12 -0
  796. data/examples/showcase/config/lcp_ruby/permissions/showcase_school_class.yml +37 -0
  797. data/examples/showcase/config/lcp_ruby/permissions/showcase_student.yml +43 -0
  798. data/examples/showcase/config/lcp_ruby/permissions/showcase_workflow.yml +34 -0
  799. data/examples/showcase/config/lcp_ruby/permissions/workflow_approval_request.yml +18 -0
  800. data/examples/showcase/config/lcp_ruby/permissions/workflow_approval_step.yml +18 -0
  801. data/examples/showcase/config/lcp_ruby/permissions/workflow_approval_task.yml +18 -0
  802. data/examples/showcase/config/lcp_ruby/permissions/workflow_audit_log.yml +19 -0
  803. data/examples/showcase/config/lcp_ruby/presenters/announcements.rb +59 -0
  804. data/examples/showcase/config/lcp_ruby/presenters/approval_requests.rb +69 -0
  805. data/examples/showcase/config/lcp_ruby/presenters/approval_steps.rb +59 -0
  806. data/examples/showcase/config/lcp_ruby/presenters/approval_tasks.rb +65 -0
  807. data/examples/showcase/config/lcp_ruby/presenters/article_comments_zone.rb +12 -0
  808. data/examples/showcase/config/lcp_ruby/presenters/article_related_zone.rb +15 -0
  809. data/examples/showcase/config/lcp_ruby/presenters/articles.rb +149 -0
  810. data/examples/showcase/config/lcp_ruby/presenters/articles_tiles.rb +30 -0
  811. data/examples/showcase/config/lcp_ruby/presenters/author_articles_zone.rb +12 -0
  812. data/examples/showcase/config/lcp_ruby/presenters/author_show_zone.rb +12 -0
  813. data/examples/showcase/config/lcp_ruby/presenters/authors.rb +39 -0
  814. data/examples/showcase/config/lcp_ruby/presenters/batch_operation_items.yml +45 -0
  815. data/examples/showcase/config/lcp_ruby/presenters/batch_operations.yml +66 -0
  816. data/examples/showcase/config/lcp_ruby/presenters/categories.rb +50 -0
  817. data/examples/showcase/config/lcp_ruby/presenters/category_articles_zone.rb +20 -0
  818. data/examples/showcase/config/lcp_ruby/presenters/category_children_zone.rb +10 -0
  819. data/examples/showcase/config/lcp_ruby/presenters/comment_quick_add_dialog.rb +15 -0
  820. data/examples/showcase/config/lcp_ruby/presenters/custom_fields.rb +124 -0
  821. data/examples/showcase/config/lcp_ruby/presenters/dashboard_employees.rb +16 -0
  822. data/examples/showcase/config/lcp_ruby/presenters/delete_reason_dialog.rb +13 -0
  823. data/examples/showcase/config/lcp_ruby/presenters/departments.rb +55 -0
  824. data/examples/showcase/config/lcp_ruby/presenters/dept_add_employee_dialog.rb +19 -0
  825. data/examples/showcase/config/lcp_ruby/presenters/dept_children_zone.rb +10 -0
  826. data/examples/showcase/config/lcp_ruby/presenters/dept_detail_zone.rb +14 -0
  827. data/examples/showcase/config/lcp_ruby/presenters/dept_employees_zone.rb +16 -0
  828. data/examples/showcase/config/lcp_ruby/presenters/dept_list_selection_zone.rb +19 -0
  829. data/examples/showcase/config/lcp_ruby/presenters/employee_overview_index_zone.rb +36 -0
  830. data/examples/showcase/config/lcp_ruby/presenters/employee_quick_add_dialog.rb +14 -0
  831. data/examples/showcase/config/lcp_ruby/presenters/employee_show_zone.rb +20 -0
  832. data/examples/showcase/config/lcp_ruby/presenters/employee_transfer_form_zone.rb +19 -0
  833. data/examples/showcase/config/lcp_ruby/presenters/employees.rb +165 -0
  834. data/examples/showcase/config/lcp_ruby/presenters/employees_tiles.rb +33 -0
  835. data/examples/showcase/config/lcp_ruby/presenters/export_logs.yml +58 -0
  836. data/examples/showcase/config/lcp_ruby/presenters/export_profiles.yml +70 -0
  837. data/examples/showcase/config/lcp_ruby/presenters/extensibility_quick_edit_dialog.rb +13 -0
  838. data/examples/showcase/config/lcp_ruby/presenters/feature_kanban.rb +30 -0
  839. data/examples/showcase/config/lcp_ruby/presenters/features_card.rb +179 -0
  840. data/examples/showcase/config/lcp_ruby/presenters/features_hub.rb +44 -0
  841. data/examples/showcase/config/lcp_ruby/presenters/features_table.rb +37 -0
  842. data/examples/showcase/config/lcp_ruby/presenters/features_tiles.rb +48 -0
  843. data/examples/showcase/config/lcp_ruby/presenters/group_memberships.rb +54 -0
  844. data/examples/showcase/config/lcp_ruby/presenters/group_role_mappings.rb +48 -0
  845. data/examples/showcase/config/lcp_ruby/presenters/groups.rb +74 -0
  846. data/examples/showcase/config/lcp_ruby/presenters/host_inventory_items.rb +99 -0
  847. data/examples/showcase/config/lcp_ruby/presenters/host_inventory_items_managed.rb +84 -0
  848. data/examples/showcase/config/lcp_ruby/presenters/host_inventory_items_report.rb +40 -0
  849. data/examples/showcase/config/lcp_ruby/presenters/host_inventory_items_wizard.rb +76 -0
  850. data/examples/showcase/config/lcp_ruby/presenters/import_profiles.rb +61 -0
  851. data/examples/showcase/config/lcp_ruby/presenters/import_rows.rb +39 -0
  852. data/examples/showcase/config/lcp_ruby/presenters/lcp_error_logs.yml +46 -0
  853. data/examples/showcase/config/lcp_ruby/presenters/my_api_tokens.rb +35 -0
  854. data/examples/showcase/config/lcp_ruby/presenters/my_api_tokens_create_dialog.rb +27 -0
  855. data/examples/showcase/config/lcp_ruby/presenters/my_employee_profile.rb +24 -0
  856. data/examples/showcase/config/lcp_ruby/presenters/my_settings.rb +50 -0
  857. data/examples/showcase/config/lcp_ruby/presenters/page_configs.rb +56 -0
  858. data/examples/showcase/config/lcp_ruby/presenters/permission_configs.rb +58 -0
  859. data/examples/showcase/config/lcp_ruby/presenters/pipeline_edit_zone.rb +11 -0
  860. data/examples/showcase/config/lcp_ruby/presenters/pipeline_stages.rb +51 -0
  861. data/examples/showcase/config/lcp_ruby/presenters/pipeline_stages_zone.rb +11 -0
  862. data/examples/showcase/config/lcp_ruby/presenters/pipelines.rb +40 -0
  863. data/examples/showcase/config/lcp_ruby/presenters/platform_profiles.rb +90 -0
  864. data/examples/showcase/config/lcp_ruby/presenters/projects.rb +58 -0
  865. data/examples/showcase/config/lcp_ruby/presenters/quick_note_dialog.rb +14 -0
  866. data/examples/showcase/config/lcp_ruby/presenters/roles.rb +60 -0
  867. data/examples/showcase/config/lcp_ruby/presenters/save_filter_dialog.rb +17 -0
  868. data/examples/showcase/config/lcp_ruby/presenters/saved_filters.rb +94 -0
  869. data/examples/showcase/config/lcp_ruby/presenters/showcase_aggregate_items.rb +61 -0
  870. data/examples/showcase/config/lcp_ruby/presenters/showcase_aggregates.rb +115 -0
  871. data/examples/showcase/config/lcp_ruby/presenters/showcase_aggregates_tiles.rb +44 -0
  872. data/examples/showcase/config/lcp_ruby/presenters/showcase_amount_kanban.rb +31 -0
  873. data/examples/showcase/config/lcp_ruby/presenters/showcase_arrays.rb +140 -0
  874. data/examples/showcase/config/lcp_ruby/presenters/showcase_attachments.rb +66 -0
  875. data/examples/showcase/config/lcp_ruby/presenters/showcase_audit_logs.rb +46 -0
  876. data/examples/showcase/config/lcp_ruby/presenters/showcase_audited_records.rb +58 -0
  877. data/examples/showcase/config/lcp_ruby/presenters/showcase_batch_tasks.rb +97 -0
  878. data/examples/showcase/config/lcp_ruby/presenters/showcase_batch_tasks_archive.rb +52 -0
  879. data/examples/showcase/config/lcp_ruby/presenters/showcase_business_units.rb +43 -0
  880. data/examples/showcase/config/lcp_ruby/presenters/showcase_conditions.rb +195 -0
  881. data/examples/showcase/config/lcp_ruby/presenters/showcase_contacts.rb +82 -0
  882. data/examples/showcase/config/lcp_ruby/presenters/showcase_custom_render_with.rb +50 -0
  883. data/examples/showcase/config/lcp_ruby/presenters/showcase_custom_sections.rb +88 -0
  884. data/examples/showcase/config/lcp_ruby/presenters/showcase_custom_zones_main.rb +75 -0
  885. data/examples/showcase/config/lcp_ruby/presenters/showcase_divisions.rb +53 -0
  886. data/examples/showcase/config/lcp_ruby/presenters/showcase_extensibility.rb +48 -0
  887. data/examples/showcase/config/lcp_ruby/presenters/showcase_fields_card.rb +34 -0
  888. data/examples/showcase/config/lcp_ruby/presenters/showcase_fields_table.rb +128 -0
  889. data/examples/showcase/config/lcp_ruby/presenters/showcase_fields_tiles.rb +42 -0
  890. data/examples/showcase/config/lcp_ruby/presenters/showcase_form_action_dialog.rb +27 -0
  891. data/examples/showcase/config/lcp_ruby/presenters/showcase_form_actions.rb +132 -0
  892. data/examples/showcase/config/lcp_ruby/presenters/showcase_form_actions_overflow.rb +76 -0
  893. data/examples/showcase/config/lcp_ruby/presenters/showcase_forms.rb +110 -0
  894. data/examples/showcase/config/lcp_ruby/presenters/showcase_grades.rb +38 -0
  895. data/examples/showcase/config/lcp_ruby/presenters/showcase_hr_employees.rb +84 -0
  896. data/examples/showcase/config/lcp_ruby/presenters/showcase_item_classes.rb +120 -0
  897. data/examples/showcase/config/lcp_ruby/presenters/showcase_job_executions.rb +150 -0
  898. data/examples/showcase/config/lcp_ruby/presenters/showcase_memos.rb +96 -0
  899. data/examples/showcase/config/lcp_ruby/presenters/showcase_models.rb +112 -0
  900. data/examples/showcase/config/lcp_ruby/presenters/showcase_organizations.rb +106 -0
  901. data/examples/showcase/config/lcp_ruby/presenters/showcase_people.rb +97 -0
  902. data/examples/showcase/config/lcp_ruby/presenters/showcase_permissions.rb +84 -0
  903. data/examples/showcase/config/lcp_ruby/presenters/showcase_positioning.rb +61 -0
  904. data/examples/showcase/config/lcp_ruby/presenters/showcase_recipes.rb +105 -0
  905. data/examples/showcase/config/lcp_ruby/presenters/showcase_recipes_raw.rb +32 -0
  906. data/examples/showcase/config/lcp_ruby/presenters/showcase_reports.rb +109 -0
  907. data/examples/showcase/config/lcp_ruby/presenters/showcase_school_classes.rb +34 -0
  908. data/examples/showcase/config/lcp_ruby/presenters/showcase_searches.rb +226 -0
  909. data/examples/showcase/config/lcp_ruby/presenters/showcase_sequences.rb +67 -0
  910. data/examples/showcase/config/lcp_ruby/presenters/showcase_soft_delete.rb +88 -0
  911. data/examples/showcase/config/lcp_ruby/presenters/showcase_soft_delete_archive.rb +52 -0
  912. data/examples/showcase/config/lcp_ruby/presenters/showcase_soft_delete_items.rb +47 -0
  913. data/examples/showcase/config/lcp_ruby/presenters/showcase_students.rb +33 -0
  914. data/examples/showcase/config/lcp_ruby/presenters/showcase_type_defaults.rb +92 -0
  915. data/examples/showcase/config/lcp_ruby/presenters/showcase_userstamps.rb +76 -0
  916. data/examples/showcase/config/lcp_ruby/presenters/showcase_virtual_fields.rb +104 -0
  917. data/examples/showcase/config/lcp_ruby/presenters/showcase_workflow_admin.rb +24 -0
  918. data/examples/showcase/config/lcp_ruby/presenters/showcase_workflow_kanban.rb +36 -0
  919. data/examples/showcase/config/lcp_ruby/presenters/showcase_workflows.rb +103 -0
  920. data/examples/showcase/config/lcp_ruby/presenters/tags.rb +35 -0
  921. data/examples/showcase/config/lcp_ruby/presenters/users.rb +61 -0
  922. data/examples/showcase/config/lcp_ruby/presenters/weather_stations.rb +60 -0
  923. data/examples/showcase/config/lcp_ruby/presenters/workflow_audit_logs.rb +76 -0
  924. data/examples/showcase/config/lcp_ruby/presenters/workflow_request_audit_zone.rb +28 -0
  925. data/examples/showcase/config/lcp_ruby/theme.yml +2 -0
  926. data/examples/showcase/config/lcp_ruby/types/currency.yml +15 -0
  927. data/examples/showcase/config/lcp_ruby/types/percentage.yml +16 -0
  928. data/examples/showcase/config/lcp_ruby/types/rating.yml +20 -0
  929. data/examples/showcase/config/lcp_ruby/views/announcements.yml +7 -0
  930. data/examples/showcase/config/lcp_ruby/views/approval_requests.yml +7 -0
  931. data/examples/showcase/config/lcp_ruby/views/approval_steps.yml +7 -0
  932. data/examples/showcase/config/lcp_ruby/views/approval_tasks.yml +7 -0
  933. data/examples/showcase/config/lcp_ruby/views/articles.yml +12 -0
  934. data/examples/showcase/config/lcp_ruby/views/authors.yml +7 -0
  935. data/examples/showcase/config/lcp_ruby/views/batch_operation_items.yml +6 -0
  936. data/examples/showcase/config/lcp_ruby/views/batch_operations.yml +10 -0
  937. data/examples/showcase/config/lcp_ruby/views/categories.yml +9 -0
  938. data/examples/showcase/config/lcp_ruby/views/custom_fields.rb +8 -0
  939. data/examples/showcase/config/lcp_ruby/views/dashboard.yml +10 -0
  940. data/examples/showcase/config/lcp_ruby/views/department_explorer.yml +7 -0
  941. data/examples/showcase/config/lcp_ruby/views/departments.yml +9 -0
  942. data/examples/showcase/config/lcp_ruby/views/employee_overview.yml +7 -0
  943. data/examples/showcase/config/lcp_ruby/views/employees.yml +12 -0
  944. data/examples/showcase/config/lcp_ruby/views/export_logs.yml +7 -0
  945. data/examples/showcase/config/lcp_ruby/views/export_profiles.yml +7 -0
  946. data/examples/showcase/config/lcp_ruby/views/features.yml +19 -0
  947. data/examples/showcase/config/lcp_ruby/views/group_memberships.yml +7 -0
  948. data/examples/showcase/config/lcp_ruby/views/group_role_mappings.yml +7 -0
  949. data/examples/showcase/config/lcp_ruby/views/groups.yml +7 -0
  950. data/examples/showcase/config/lcp_ruby/views/host_inventory_items.yml +16 -0
  951. data/examples/showcase/config/lcp_ruby/views/hr_turnover_dashboard.yml +7 -0
  952. data/examples/showcase/config/lcp_ruby/views/import_profiles.yml +7 -0
  953. data/examples/showcase/config/lcp_ruby/views/import_rows.yml +7 -0
  954. data/examples/showcase/config/lcp_ruby/views/lcp_error_logs.yml +10 -0
  955. data/examples/showcase/config/lcp_ruby/views/monitoring.yml +7 -0
  956. data/examples/showcase/config/lcp_ruby/views/my_api_tokens.rb +9 -0
  957. data/examples/showcase/config/lcp_ruby/views/page_configs.yml +11 -0
  958. data/examples/showcase/config/lcp_ruby/views/permission_configs.yml +11 -0
  959. data/examples/showcase/config/lcp_ruby/views/pipeline_stages.yml +7 -0
  960. data/examples/showcase/config/lcp_ruby/views/pipelines.yml +7 -0
  961. data/examples/showcase/config/lcp_ruby/views/platform_profiles.yml +7 -0
  962. data/examples/showcase/config/lcp_ruby/views/projects.yml +9 -0
  963. data/examples/showcase/config/lcp_ruby/views/roles.yml +7 -0
  964. data/examples/showcase/config/lcp_ruby/views/saved_filters.yml +7 -0
  965. data/examples/showcase/config/lcp_ruby/views/showcase_aggregate_items.yml +7 -0
  966. data/examples/showcase/config/lcp_ruby/views/showcase_aggregates.yml +10 -0
  967. data/examples/showcase/config/lcp_ruby/views/showcase_arrays.yml +7 -0
  968. data/examples/showcase/config/lcp_ruby/views/showcase_attachments.yml +7 -0
  969. data/examples/showcase/config/lcp_ruby/views/showcase_audit_logs.yml +11 -0
  970. data/examples/showcase/config/lcp_ruby/views/showcase_audited_records.yml +10 -0
  971. data/examples/showcase/config/lcp_ruby/views/showcase_batch_tasks.yml +10 -0
  972. data/examples/showcase/config/lcp_ruby/views/showcase_business_units.yml +7 -0
  973. data/examples/showcase/config/lcp_ruby/views/showcase_conditions.yml +7 -0
  974. data/examples/showcase/config/lcp_ruby/views/showcase_contacts.yml +7 -0
  975. data/examples/showcase/config/lcp_ruby/views/showcase_custom_render_with.yml +7 -0
  976. data/examples/showcase/config/lcp_ruby/views/showcase_custom_sections.yml +7 -0
  977. data/examples/showcase/config/lcp_ruby/views/showcase_custom_zones.yml +7 -0
  978. data/examples/showcase/config/lcp_ruby/views/showcase_divisions.yml +9 -0
  979. data/examples/showcase/config/lcp_ruby/views/showcase_extensibility.yml +7 -0
  980. data/examples/showcase/config/lcp_ruby/views/showcase_fields.yml +13 -0
  981. data/examples/showcase/config/lcp_ruby/views/showcase_form_actions.yml +10 -0
  982. data/examples/showcase/config/lcp_ruby/views/showcase_forms.yml +7 -0
  983. data/examples/showcase/config/lcp_ruby/views/showcase_grades.yml +9 -0
  984. data/examples/showcase/config/lcp_ruby/views/showcase_hr_employees.yml +9 -0
  985. data/examples/showcase/config/lcp_ruby/views/showcase_item_classes.yml +7 -0
  986. data/examples/showcase/config/lcp_ruby/views/showcase_job_executions.yml +9 -0
  987. data/examples/showcase/config/lcp_ruby/views/showcase_memos.yml +7 -0
  988. data/examples/showcase/config/lcp_ruby/views/showcase_models.yml +7 -0
  989. data/examples/showcase/config/lcp_ruby/views/showcase_organizations.yml +7 -0
  990. data/examples/showcase/config/lcp_ruby/views/showcase_people.yml +7 -0
  991. data/examples/showcase/config/lcp_ruby/views/showcase_permissions.yml +7 -0
  992. data/examples/showcase/config/lcp_ruby/views/showcase_positioning.yml +7 -0
  993. data/examples/showcase/config/lcp_ruby/views/showcase_recipes.yml +10 -0
  994. data/examples/showcase/config/lcp_ruby/views/showcase_reports.yml +7 -0
  995. data/examples/showcase/config/lcp_ruby/views/showcase_school_classes.yml +7 -0
  996. data/examples/showcase/config/lcp_ruby/views/showcase_searches.yml +7 -0
  997. data/examples/showcase/config/lcp_ruby/views/showcase_sequences.yml +7 -0
  998. data/examples/showcase/config/lcp_ruby/views/showcase_soft_delete.yml +10 -0
  999. data/examples/showcase/config/lcp_ruby/views/showcase_soft_delete_items.yml +8 -0
  1000. data/examples/showcase/config/lcp_ruby/views/showcase_students.yml +9 -0
  1001. data/examples/showcase/config/lcp_ruby/views/showcase_userstamps.yml +7 -0
  1002. data/examples/showcase/config/lcp_ruby/views/showcase_virtual_fields.yml +7 -0
  1003. data/examples/showcase/config/lcp_ruby/views/showcase_workflows.yml +16 -0
  1004. data/examples/showcase/config/lcp_ruby/views/tags.yml +7 -0
  1005. data/examples/showcase/config/lcp_ruby/views/users.yml +7 -0
  1006. data/examples/showcase/config/lcp_ruby/views/weather_stations.yml +7 -0
  1007. data/examples/showcase/config/lcp_ruby/views/workflow_audit_logs.yml +9 -0
  1008. data/examples/showcase/config/lcp_ruby/workflows/showcase_form_action.rb +48 -0
  1009. data/examples/showcase/config/lcp_ruby/workflows/showcase_workflow.rb +199 -0
  1010. data/examples/showcase/config/locales/cs.yml +2818 -0
  1011. data/examples/showcase/config/locales/en.yml +67 -0
  1012. data/examples/showcase/config/locales/lcp_ruby/api_tokens.cs.yml +46 -0
  1013. data/examples/showcase/config/locales/lcp_ruby/api_tokens.en.yml +49 -0
  1014. data/examples/showcase/config/routes.rb +21 -0
  1015. data/examples/showcase/config/storage.yml +3 -0
  1016. data/examples/showcase/config.ru +2 -0
  1017. data/examples/showcase/db/migrate/20260219124321_create_active_storage_tables.active_storage.rb +57 -0
  1018. data/examples/showcase/db/migrate/20260220072723_create_lcp_ruby_users.rb +42 -0
  1019. data/examples/showcase/db/migrate/20260406120000_create_host_inventory_items.rb +32 -0
  1020. data/examples/showcase/db/migrate/20260428134542_add_oidc_columns_to_lcp_ruby_users.rb +13 -0
  1021. data/examples/showcase/db/migrate/20260502120000_create_platform_profiles.rb +19 -0
  1022. data/examples/showcase/db/schema.rb +1222 -0
  1023. data/examples/showcase/db/seeds.rb +3085 -0
  1024. data/examples/showcase/erd.md +567 -0
  1025. data/examples/showcase/test/fixtures/import_articles.csv +4 -0
  1026. data/examples/showcase/test/fixtures/import_employees.csv +4 -0
  1027. data/examples/todo/Gemfile +8 -0
  1028. data/examples/todo/Gemfile.lock +415 -0
  1029. data/examples/todo/Rakefile +2 -0
  1030. data/examples/todo/app/assets/config/manifest.js +1 -0
  1031. data/examples/todo/app/controllers/application_controller.rb +6 -0
  1032. data/examples/todo/app/lcp_services/defaults/one_week_from_now.rb +11 -0
  1033. data/examples/todo/bin/rails +4 -0
  1034. data/examples/todo/bin/rake +4 -0
  1035. data/examples/todo/config/application.rb +31 -0
  1036. data/examples/todo/config/boot.rb +2 -0
  1037. data/examples/todo/config/database.yml +12 -0
  1038. data/examples/todo/config/environment.rb +2 -0
  1039. data/examples/todo/config/lcp_ruby/models/todo_item.yml +67 -0
  1040. data/examples/todo/config/lcp_ruby/models/todo_list.yml +49 -0
  1041. data/examples/todo/config/lcp_ruby/permissions/default.yml +10 -0
  1042. data/examples/todo/config/lcp_ruby/permissions/todo_item.yml +21 -0
  1043. data/examples/todo/config/lcp_ruby/presenters/todo_item.yml +75 -0
  1044. data/examples/todo/config/lcp_ruby/presenters/todo_list.yml +68 -0
  1045. data/examples/todo/config/lcp_ruby/views/todo_items.yml +11 -0
  1046. data/examples/todo/config/lcp_ruby/views/todo_lists.yml +9 -0
  1047. data/examples/todo/config/routes.rb +4 -0
  1048. data/examples/todo/config/storage.yml +3 -0
  1049. data/examples/todo/config.ru +2 -0
  1050. data/examples/todo/db/migrate/20260219103416_create_active_storage_tables.active_storage.rb +57 -0
  1051. data/examples/todo/db/schema.rb +63 -0
  1052. data/examples/todo/db/seeds.rb +24 -0
  1053. data/examples/todo/erd.md +21 -0
  1054. data/exe/lcp +33 -0
  1055. data/lib/generators/lcp_ruby/agent_setup_generator.rb +102 -0
  1056. data/lib/generators/lcp_ruby/api_tokens_generator.rb +102 -0
  1057. data/lib/generators/lcp_ruby/auditing_generator.rb +54 -0
  1058. data/lib/generators/lcp_ruby/background_jobs_generator.rb +61 -0
  1059. data/lib/generators/lcp_ruby/batch_operations_generator.rb +62 -0
  1060. data/lib/generators/lcp_ruby/claude_skills_generator.rb +47 -0
  1061. data/lib/generators/lcp_ruby/custom_fields_generator.rb +54 -0
  1062. data/lib/generators/lcp_ruby/dsl_to_yaml.rb +72 -0
  1063. data/lib/generators/lcp_ruby/entity/color_palette.rb +22 -0
  1064. data/lib/generators/lcp_ruby/entity/field_descriptor.rb +24 -0
  1065. data/lib/generators/lcp_ruby/entity/field_token_parser.rb +101 -0
  1066. data/lib/generators/lcp_ruby/entity/role_discovery.rb +45 -0
  1067. data/lib/generators/lcp_ruby/entity_generator.rb +1104 -0
  1068. data/lib/generators/lcp_ruby/export_generator.rb +71 -0
  1069. data/lib/generators/lcp_ruby/format_support.rb +56 -0
  1070. data/lib/generators/lcp_ruby/gapfree_sequences_generator.rb +64 -0
  1071. data/lib/generators/lcp_ruby/groups_generator.rb +94 -0
  1072. data/lib/generators/lcp_ruby/host_controller_generator.rb +202 -0
  1073. data/lib/generators/lcp_ruby/import_generator.rb +96 -0
  1074. data/lib/generators/lcp_ruby/install_auth_generator.rb +432 -0
  1075. data/lib/generators/lcp_ruby/install_generator.rb +319 -0
  1076. data/lib/generators/lcp_ruby/monitoring_generator.rb +58 -0
  1077. data/lib/generators/lcp_ruby/oidc_role_mappings_generator.rb +60 -0
  1078. data/lib/generators/lcp_ruby/pages_generator.rb +66 -0
  1079. data/lib/generators/lcp_ruby/permission_source_generator.rb +66 -0
  1080. data/lib/generators/lcp_ruby/role_model_generator.rb +73 -0
  1081. data/lib/generators/lcp_ruby/saved_filters_generator.rb +62 -0
  1082. data/lib/generators/lcp_ruby/templates/add_oidc_columns_to_lcp_ruby_users.rb.erb +13 -0
  1083. data/lib/generators/lcp_ruby/templates/agent_setup/agents_md.md +3 -0
  1084. data/lib/generators/lcp_ruby/templates/agent_setup/claude_md.md +12 -0
  1085. data/lib/generators/lcp_ruby/templates/api_tokens/create_dialog.rb +31 -0
  1086. data/lib/generators/lcp_ruby/templates/api_tokens/locales.en.yml +43 -0
  1087. data/lib/generators/lcp_ruby/templates/api_tokens/model.rb +23 -0
  1088. data/lib/generators/lcp_ruby/templates/api_tokens/permissions.yml +30 -0
  1089. data/lib/generators/lcp_ruby/templates/api_tokens/presenter.rb +38 -0
  1090. data/lib/generators/lcp_ruby/templates/api_tokens/view_group.rb +13 -0
  1091. data/lib/generators/lcp_ruby/templates/auditing/model.rb +34 -0
  1092. data/lib/generators/lcp_ruby/templates/auditing/permissions.yml +19 -0
  1093. data/lib/generators/lcp_ruby/templates/auditing/presenter.rb +41 -0
  1094. data/lib/generators/lcp_ruby/templates/auditing/view_group.rb +10 -0
  1095. data/lib/generators/lcp_ruby/templates/background_jobs/model.rb +59 -0
  1096. data/lib/generators/lcp_ruby/templates/background_jobs/permissions.yml +22 -0
  1097. data/lib/generators/lcp_ruby/templates/background_jobs/presenter.rb +82 -0
  1098. data/lib/generators/lcp_ruby/templates/background_jobs/view_group.rb +9 -0
  1099. data/lib/generators/lcp_ruby/templates/batch_operations/item_model.rb +28 -0
  1100. data/lib/generators/lcp_ruby/templates/batch_operations/item_presenter.rb +43 -0
  1101. data/lib/generators/lcp_ruby/templates/batch_operations/model.rb +42 -0
  1102. data/lib/generators/lcp_ruby/templates/batch_operations/permissions.yml +44 -0
  1103. data/lib/generators/lcp_ruby/templates/batch_operations/presenter.rb +61 -0
  1104. data/lib/generators/lcp_ruby/templates/batch_operations/view_group.rb +9 -0
  1105. data/lib/generators/lcp_ruby/templates/create_lcp_ruby_users.rb.erb +50 -0
  1106. data/lib/generators/lcp_ruby/templates/custom_fields/model.rb +60 -0
  1107. data/lib/generators/lcp_ruby/templates/custom_fields/permissions.yml +19 -0
  1108. data/lib/generators/lcp_ruby/templates/custom_fields/presenter.rb +128 -0
  1109. data/lib/generators/lcp_ruby/templates/custom_fields/view_group.rb +9 -0
  1110. data/lib/generators/lcp_ruby/templates/entity/model.rb +42 -0
  1111. data/lib/generators/lcp_ruby/templates/entity/permissions.yml +20 -0
  1112. data/lib/generators/lcp_ruby/templates/entity/presenter.rb +55 -0
  1113. data/lib/generators/lcp_ruby/templates/entity/view_group.rb +8 -0
  1114. data/lib/generators/lcp_ruby/templates/export/export_log_model.rb +30 -0
  1115. data/lib/generators/lcp_ruby/templates/export/export_log_permissions.yml +24 -0
  1116. data/lib/generators/lcp_ruby/templates/export/export_logs_presenter.rb +51 -0
  1117. data/lib/generators/lcp_ruby/templates/export/export_profile_model.rb +28 -0
  1118. data/lib/generators/lcp_ruby/templates/export/export_profile_permissions.yml +26 -0
  1119. data/lib/generators/lcp_ruby/templates/export/export_profiles_presenter.rb +59 -0
  1120. data/lib/generators/lcp_ruby/templates/gapfree_sequences/model.rb +18 -0
  1121. data/lib/generators/lcp_ruby/templates/gapfree_sequences/permissions.yml +19 -0
  1122. data/lib/generators/lcp_ruby/templates/gapfree_sequences/presenter.rb +51 -0
  1123. data/lib/generators/lcp_ruby/templates/gapfree_sequences/view_group.rb +9 -0
  1124. data/lib/generators/lcp_ruby/templates/groups/group_membership_model.rb +17 -0
  1125. data/lib/generators/lcp_ruby/templates/groups/group_model.rb +28 -0
  1126. data/lib/generators/lcp_ruby/templates/groups/group_permissions.yml +19 -0
  1127. data/lib/generators/lcp_ruby/templates/groups/group_presenter.rb +53 -0
  1128. data/lib/generators/lcp_ruby/templates/groups/group_role_mapping_model.rb +15 -0
  1129. data/lib/generators/lcp_ruby/templates/groups/group_view_group.rb +9 -0
  1130. data/lib/generators/lcp_ruby/templates/host_controller/controller.rb.erb +131 -0
  1131. data/lib/generators/lcp_ruby/templates/import/data_import_job.yml +7 -0
  1132. data/lib/generators/lcp_ruby/templates/import/import_profile_model.rb +30 -0
  1133. data/lib/generators/lcp_ruby/templates/import/import_profile_permissions.yml +29 -0
  1134. data/lib/generators/lcp_ruby/templates/import/import_profiles_presenter.rb +57 -0
  1135. data/lib/generators/lcp_ruby/templates/import/import_row_model.rb +32 -0
  1136. data/lib/generators/lcp_ruby/templates/import/import_row_permissions.yml +11 -0
  1137. data/lib/generators/lcp_ruby/templates/import/import_rows_presenter.rb +35 -0
  1138. data/lib/generators/lcp_ruby/templates/install/default_permissions.yml +26 -0
  1139. data/lib/generators/lcp_ruby/templates/install/menu.yml.tt +71 -0
  1140. data/lib/generators/lcp_ruby/templates/install/model.rb +20 -0
  1141. data/lib/generators/lcp_ruby/templates/install/permissions.yml +25 -0
  1142. data/lib/generators/lcp_ruby/templates/install/presenter.rb +44 -0
  1143. data/lib/generators/lcp_ruby/templates/install/view_group.rb +10 -0
  1144. data/lib/generators/lcp_ruby/templates/install_auth/oidc/entra.yml.erb +46 -0
  1145. data/lib/generators/lcp_ruby/templates/install_auth/oidc/generic.yml.erb +55 -0
  1146. data/lib/generators/lcp_ruby/templates/install_auth/oidc/google.yml.erb +42 -0
  1147. data/lib/generators/lcp_ruby/templates/install_auth/oidc/keycloak.yml.erb +45 -0
  1148. data/lib/generators/lcp_ruby/templates/install_auth/oidc/okta.yml.erb +45 -0
  1149. data/lib/generators/lcp_ruby/templates/install_auth/user.rb +36 -0
  1150. data/lib/generators/lcp_ruby/templates/monitoring/model.rb +34 -0
  1151. data/lib/generators/lcp_ruby/templates/monitoring/model_extension.rb +12 -0
  1152. data/lib/generators/lcp_ruby/templates/monitoring/page.yml +49 -0
  1153. data/lib/generators/lcp_ruby/templates/monitoring/permissions.yml +8 -0
  1154. data/lib/generators/lcp_ruby/templates/monitoring/presenter.rb +44 -0
  1155. data/lib/generators/lcp_ruby/templates/oidc_role_mappings/locales.en.yml +15 -0
  1156. data/lib/generators/lcp_ruby/templates/oidc_role_mappings/model.rb +32 -0
  1157. data/lib/generators/lcp_ruby/templates/oidc_role_mappings/permissions.yml +21 -0
  1158. data/lib/generators/lcp_ruby/templates/oidc_role_mappings/presenter.rb +41 -0
  1159. data/lib/generators/lcp_ruby/templates/pages/model.rb +19 -0
  1160. data/lib/generators/lcp_ruby/templates/pages/permissions.yml +19 -0
  1161. data/lib/generators/lcp_ruby/templates/pages/presenter.rb +51 -0
  1162. data/lib/generators/lcp_ruby/templates/pages/view_group.rb +10 -0
  1163. data/lib/generators/lcp_ruby/templates/permission_source/model.rb +21 -0
  1164. data/lib/generators/lcp_ruby/templates/permission_source/permissions.yml +19 -0
  1165. data/lib/generators/lcp_ruby/templates/permission_source/presenter.rb +51 -0
  1166. data/lib/generators/lcp_ruby/templates/permission_source/view_group.rb +10 -0
  1167. data/lib/generators/lcp_ruby/templates/role_model/model.rb +24 -0
  1168. data/lib/generators/lcp_ruby/templates/role_model/permissions.yml +19 -0
  1169. data/lib/generators/lcp_ruby/templates/role_model/presenter.rb +62 -0
  1170. data/lib/generators/lcp_ruby/templates/role_model/view_group.rb +10 -0
  1171. data/lib/generators/lcp_ruby/templates/saved_filters/model.rb +51 -0
  1172. data/lib/generators/lcp_ruby/templates/saved_filters/permissions.yml +44 -0
  1173. data/lib/generators/lcp_ruby/templates/saved_filters/presenter.rb +96 -0
  1174. data/lib/generators/lcp_ruby/templates/saved_filters/save_dialog_presenter.rb +19 -0
  1175. data/lib/generators/lcp_ruby/templates/workflow_approvals/request_model.rb +50 -0
  1176. data/lib/generators/lcp_ruby/templates/workflow_approvals/request_permissions.yml +10 -0
  1177. data/lib/generators/lcp_ruby/templates/workflow_approvals/request_presenter.rb +44 -0
  1178. data/lib/generators/lcp_ruby/templates/workflow_approvals/request_view_group.rb +10 -0
  1179. data/lib/generators/lcp_ruby/templates/workflow_approvals/step_model.rb +36 -0
  1180. data/lib/generators/lcp_ruby/templates/workflow_approvals/step_permissions.yml +10 -0
  1181. data/lib/generators/lcp_ruby/templates/workflow_approvals/task_model.rb +34 -0
  1182. data/lib/generators/lcp_ruby/templates/workflow_approvals/task_permissions.yml +10 -0
  1183. data/lib/generators/lcp_ruby/templates/workflow_approvals/task_presenter.rb +39 -0
  1184. data/lib/generators/lcp_ruby/templates/workflow_approvals/task_view_group.rb +10 -0
  1185. data/lib/generators/lcp_ruby/templates/workflow_audit_log/model.rb +48 -0
  1186. data/lib/generators/lcp_ruby/templates/workflow_audit_log/permissions.yml +10 -0
  1187. data/lib/generators/lcp_ruby/templates/workflow_audit_log/presenter.rb +44 -0
  1188. data/lib/generators/lcp_ruby/templates/workflow_audit_log/view_group.rb +10 -0
  1189. data/lib/generators/lcp_ruby/templates/workflow_definition/model.rb +43 -0
  1190. data/lib/generators/lcp_ruby/templates/workflow_definition/permissions.yml +19 -0
  1191. data/lib/generators/lcp_ruby/templates/workflow_definition/presenter.rb +70 -0
  1192. data/lib/generators/lcp_ruby/templates/workflow_definition/view_group.rb +10 -0
  1193. data/lib/generators/lcp_ruby/workflow_approvals_generator.rb +93 -0
  1194. data/lib/generators/lcp_ruby/workflow_audit_log_generator.rb +54 -0
  1195. data/lib/generators/lcp_ruby/workflow_definition_generator.rb +77 -0
  1196. data/lib/lcp.rb +6 -0
  1197. data/lib/lcp_ruby/actions/action_executor.rb +66 -0
  1198. data/lib/lcp_ruby/actions/action_registry.rb +48 -0
  1199. data/lib/lcp_ruby/actions/api_tokens/revoke.rb +13 -0
  1200. data/lib/lcp_ruby/actions/base_action.rb +79 -0
  1201. data/lib/lcp_ruby/actions/form_action_pipeline.rb +138 -0
  1202. data/lib/lcp_ruby/aggregates/query_builder.rb +6 -0
  1203. data/lib/lcp_ruby/api_tokens/model_extension.rb +41 -0
  1204. data/lib/lcp_ruby/api_tokens/resolver_registry.rb +53 -0
  1205. data/lib/lcp_ruby/api_tokens/token_generator.rb +27 -0
  1206. data/lib/lcp_ruby/api_tokens/verifier.rb +38 -0
  1207. data/lib/lcp_ruby/app_template.rb +181 -0
  1208. data/lib/lcp_ruby/array_query.rb +120 -0
  1209. data/lib/lcp_ruby/asset_copier.rb +62 -0
  1210. data/lib/lcp_ruby/association_fk_type.rb +191 -0
  1211. data/lib/lcp_ruby/association_join_column.rb +28 -0
  1212. data/lib/lcp_ruby/association_options_builder.rb +231 -0
  1213. data/lib/lcp_ruby/auditing/audit_writer.rb +258 -0
  1214. data/lib/lcp_ruby/auditing/contract_validator.rb +95 -0
  1215. data/lib/lcp_ruby/auditing/registry.rb +29 -0
  1216. data/lib/lcp_ruby/auditing/setup.rb +49 -0
  1217. data/lib/lcp_ruby/authentication/audit_subscriber.rb +51 -0
  1218. data/lib/lcp_ruby/authentication/bearer_jwt_verifier.rb +139 -0
  1219. data/lib/lcp_ruby/authentication/devise_setup.rb +47 -0
  1220. data/lib/lcp_ruby/authentication/errors.rb +27 -0
  1221. data/lib/lcp_ruby/authentication/http_fetcher.rb +36 -0
  1222. data/lib/lcp_ruby/authentication/jwks_cache.rb +91 -0
  1223. data/lib/lcp_ruby/authentication/oidc_bearer_resolver.rb +84 -0
  1224. data/lib/lcp_ruby/authentication/omniauth_builder.rb +147 -0
  1225. data/lib/lcp_ruby/authentication/provider.rb +108 -0
  1226. data/lib/lcp_ruby/authentication/provider_registry.rb +227 -0
  1227. data/lib/lcp_ruby/authentication/role_mapper.rb +94 -0
  1228. data/lib/lcp_ruby/authentication/test_support.rb +257 -0
  1229. data/lib/lcp_ruby/authentication/user_resolver.rb +169 -0
  1230. data/lib/lcp_ruby/authentication.rb +40 -0
  1231. data/lib/lcp_ruby/authorization/association_lookup.rb +56 -0
  1232. data/lib/lcp_ruby/authorization/authorization_error.rb +12 -0
  1233. data/lib/lcp_ruby/authorization/cache.rb +89 -0
  1234. data/lib/lcp_ruby/authorization/codes.rb +17 -0
  1235. data/lib/lcp_ruby/authorization/impersonated_user.rb +29 -0
  1236. data/lib/lcp_ruby/authorization/includes_hint.rb +110 -0
  1237. data/lib/lcp_ruby/authorization/inherited_parent_validator.rb +142 -0
  1238. data/lib/lcp_ruby/authorization/invariant_check/configuration.rb +132 -0
  1239. data/lib/lcp_ruby/authorization/invariant_error.rb +15 -0
  1240. data/lib/lcp_ruby/authorization/misconfigured_page_error.rb +30 -0
  1241. data/lib/lcp_ruby/authorization/page_gate.rb +57 -0
  1242. data/lib/lcp_ruby/authorization/permission_evaluator.rb +343 -0
  1243. data/lib/lcp_ruby/authorization/policy_factory.rb +91 -0
  1244. data/lib/lcp_ruby/authorization/runtime_invariant_validator.rb +421 -0
  1245. data/lib/lcp_ruby/authorization/scope_builder.rb +227 -0
  1246. data/lib/lcp_ruby/authorization/scope_resolver.rb +28 -0
  1247. data/lib/lcp_ruby/authorized_controller.rb +44 -0
  1248. data/lib/lcp_ruby/background_jobs/base_handler.rb +113 -0
  1249. data/lib/lcp_ruby/background_jobs/change_handler.rb +17 -0
  1250. data/lib/lcp_ruby/background_jobs/contract.rb +16 -0
  1251. data/lib/lcp_ruby/background_jobs/contract_validator.rb +112 -0
  1252. data/lib/lcp_ruby/background_jobs/declarative/base_action.rb +11 -0
  1253. data/lib/lcp_ruby/background_jobs/declarative/call_webhook_action.rb +174 -0
  1254. data/lib/lcp_ruby/background_jobs/declarative/fire_event_action.rb +24 -0
  1255. data/lib/lcp_ruby/background_jobs/declarative/registry.rb +34 -0
  1256. data/lib/lcp_ruby/background_jobs/declarative/run_scope_action.rb +98 -0
  1257. data/lib/lcp_ruby/background_jobs/declarative/send_notification_action.rb +13 -0
  1258. data/lib/lcp_ruby/background_jobs/definition.rb +134 -0
  1259. data/lib/lcp_ruby/background_jobs/enqueue.rb +83 -0
  1260. data/lib/lcp_ruby/background_jobs/errors.rb +9 -0
  1261. data/lib/lcp_ruby/background_jobs/executor_job.rb +111 -0
  1262. data/lib/lcp_ruby/background_jobs/handler_factory.rb +46 -0
  1263. data/lib/lcp_ruby/background_jobs/host_source.rb +33 -0
  1264. data/lib/lcp_ruby/background_jobs/model_source.rb +93 -0
  1265. data/lib/lcp_ruby/background_jobs/registry.rb +81 -0
  1266. data/lib/lcp_ruby/background_jobs/resolver.rb +29 -0
  1267. data/lib/lcp_ruby/background_jobs/schedule_adapter.rb +14 -0
  1268. data/lib/lcp_ruby/background_jobs/setup.rb +145 -0
  1269. data/lib/lcp_ruby/background_jobs/static_source.rb +19 -0
  1270. data/lib/lcp_ruby/background_jobs/steps_executor.rb +52 -0
  1271. data/lib/lcp_ruby/background_jobs/triggers/event_trigger.rb +97 -0
  1272. data/lib/lcp_ruby/background_jobs/triggers/trigger_installer.rb +20 -0
  1273. data/lib/lcp_ruby/background_jobs/unique_key_builder.rb +31 -0
  1274. data/lib/lcp_ruby/batch_actions/base_service.rb +70 -0
  1275. data/lib/lcp_ruby/batch_actions/batch_action_handler.rb +200 -0
  1276. data/lib/lcp_ruby/batch_actions/custom_action_dispatcher.rb +133 -0
  1277. data/lib/lcp_ruby/batch_actions/destroy_service.rb +37 -0
  1278. data/lib/lcp_ruby/batch_actions/permanently_destroy_service.rb +33 -0
  1279. data/lib/lcp_ruby/batch_actions/restore_service.rb +33 -0
  1280. data/lib/lcp_ruby/batch_actions.rb +5 -0
  1281. data/lib/lcp_ruby/bulk_updater.rb +25 -0
  1282. data/lib/lcp_ruby/cli/docs_command.rb +29 -0
  1283. data/lib/lcp_ruby/cli/examples_command.rb +29 -0
  1284. data/lib/lcp_ruby/cli/new_command.rb +509 -0
  1285. data/lib/lcp_ruby/cli/run_command.rb +155 -0
  1286. data/lib/lcp_ruby/cli/skills_command.rb +54 -0
  1287. data/lib/lcp_ruby/cli.rb +74 -0
  1288. data/lib/lcp_ruby/condition_evaluator.rb +366 -0
  1289. data/lib/lcp_ruby/condition_service_registry.rb +58 -0
  1290. data/lib/lcp_ruby/condition_services/current_user_role.rb +28 -0
  1291. data/lib/lcp_ruby/condition_services/feature_flag.rb +63 -0
  1292. data/lib/lcp_ruby/condition_services/impersonating.rb +24 -0
  1293. data/lib/lcp_ruby/conditions/validator.rb +35 -0
  1294. data/lib/lcp_ruby/configuration.rb +431 -0
  1295. data/lib/lcp_ruby/controller/authentication.rb +118 -0
  1296. data/lib/lcp_ruby/controller/authorization.rb +198 -0
  1297. data/lib/lcp_ruby/controller/bearer_authentication.rb +76 -0
  1298. data/lib/lcp_ruby/controller/crud_helpers.rb +233 -0
  1299. data/lib/lcp_ruby/controller/error_handling.rb +94 -0
  1300. data/lib/lcp_ruby/controller/impersonation.rb +70 -0
  1301. data/lib/lcp_ruby/controller/locale_binding.rb +62 -0
  1302. data/lib/lcp_ruby/controller/path_helpers.rb +125 -0
  1303. data/lib/lcp_ruby/controller/presenter_setup.rb +89 -0
  1304. data/lib/lcp_ruby/controller/search.rb +321 -0
  1305. data/lib/lcp_ruby/controller/view_helpers.rb +105 -0
  1306. data/lib/lcp_ruby/current.rb +13 -0
  1307. data/lib/lcp_ruby/custom_fields/applicator.rb +194 -0
  1308. data/lib/lcp_ruby/custom_fields/contract_validator.rb +77 -0
  1309. data/lib/lcp_ruby/custom_fields/definition_change_handler.rb +21 -0
  1310. data/lib/lcp_ruby/custom_fields/query.rb +112 -0
  1311. data/lib/lcp_ruby/custom_fields/registry.rb +70 -0
  1312. data/lib/lcp_ruby/custom_fields/setup.rb +58 -0
  1313. data/lib/lcp_ruby/custom_fields/utils.rb +40 -0
  1314. data/lib/lcp_ruby/custom_fields.rb +9 -0
  1315. data/lib/lcp_ruby/data_source/api_error_placeholder.rb +47 -0
  1316. data/lib/lcp_ruby/data_source/api_filter_translator.rb +72 -0
  1317. data/lib/lcp_ruby/data_source/api_model_concern.rb +131 -0
  1318. data/lib/lcp_ruby/data_source/api_preloader.rb +44 -0
  1319. data/lib/lcp_ruby/data_source/base.rb +85 -0
  1320. data/lib/lcp_ruby/data_source/cached_wrapper.rb +107 -0
  1321. data/lib/lcp_ruby/data_source/host.rb +71 -0
  1322. data/lib/lcp_ruby/data_source/registry.rb +39 -0
  1323. data/lib/lcp_ruby/data_source/resilient_wrapper.rb +67 -0
  1324. data/lib/lcp_ruby/data_source/rest_json.rb +247 -0
  1325. data/lib/lcp_ruby/data_source/setup.rb +57 -0
  1326. data/lib/lcp_ruby/dev_toolbar.rb +8 -0
  1327. data/lib/lcp_ruby/display/base_renderer.rb +21 -0
  1328. data/lib/lcp_ruby/display/count_badge.rb +11 -0
  1329. data/lib/lcp_ruby/display/icon_badge.rb +17 -0
  1330. data/lib/lcp_ruby/display/renderer_registry.rb +138 -0
  1331. data/lib/lcp_ruby/display/renderers/attachment_link.rb +26 -0
  1332. data/lib/lcp_ruby/display/renderers/attachment_list.rb +32 -0
  1333. data/lib/lcp_ruby/display/renderers/attachment_preview.rb +26 -0
  1334. data/lib/lcp_ruby/display/renderers/avatar.rb +14 -0
  1335. data/lib/lcp_ruby/display/renderers/badge.rb +43 -0
  1336. data/lib/lcp_ruby/display/renderers/boolean_icon.rb +54 -0
  1337. data/lib/lcp_ruby/display/renderers/code.rb +39 -0
  1338. data/lib/lcp_ruby/display/renderers/collection.rb +34 -0
  1339. data/lib/lcp_ruby/display/renderers/color_swatch.rb +25 -0
  1340. data/lib/lcp_ruby/display/renderers/concerns/attachment_helpers.rb +73 -0
  1341. data/lib/lcp_ruby/display/renderers/concerns/workflow_helpers.rb +35 -0
  1342. data/lib/lcp_ruby/display/renderers/copy_code.rb +33 -0
  1343. data/lib/lcp_ruby/display/renderers/currency.rb +14 -0
  1344. data/lib/lcp_ruby/display/renderers/date.rb +17 -0
  1345. data/lib/lcp_ruby/display/renderers/datetime.rb +17 -0
  1346. data/lib/lcp_ruby/display/renderers/email_link.rb +15 -0
  1347. data/lib/lcp_ruby/display/renderers/file_size.rb +11 -0
  1348. data/lib/lcp_ruby/display/renderers/heading.rb +11 -0
  1349. data/lib/lcp_ruby/display/renderers/image.rb +17 -0
  1350. data/lib/lcp_ruby/display/renderers/internal_link.rb +23 -0
  1351. data/lib/lcp_ruby/display/renderers/link.rb +15 -0
  1352. data/lib/lcp_ruby/display/renderers/link_list.rb +90 -0
  1353. data/lib/lcp_ruby/display/renderers/markdown.rb +33 -0
  1354. data/lib/lcp_ruby/display/renderers/number.rb +14 -0
  1355. data/lib/lcp_ruby/display/renderers/percentage.rb +12 -0
  1356. data/lib/lcp_ruby/display/renderers/phone_link.rb +15 -0
  1357. data/lib/lcp_ruby/display/renderers/progress_bar.rb +15 -0
  1358. data/lib/lcp_ruby/display/renderers/rating.rb +15 -0
  1359. data/lib/lcp_ruby/display/renderers/record_link.rb +101 -0
  1360. data/lib/lcp_ruby/display/renderers/relative_date.rb +15 -0
  1361. data/lib/lcp_ruby/display/renderers/rich_text.rb +15 -0
  1362. data/lib/lcp_ruby/display/renderers/text.rb +12 -0
  1363. data/lib/lcp_ruby/display/renderers/truncate.rb +17 -0
  1364. data/lib/lcp_ruby/display/renderers/url_link.rb +22 -0
  1365. data/lib/lcp_ruby/display/renderers/workflow_badge.rb +37 -0
  1366. data/lib/lcp_ruby/display/renderers/workflow_timeline.rb +173 -0
  1367. data/lib/lcp_ruby/display/renderers.rb +3 -0
  1368. data/lib/lcp_ruby/display/text_badge.rb +15 -0
  1369. data/lib/lcp_ruby/dsl/condition_builder.rb +190 -0
  1370. data/lib/lcp_ruby/dsl/dsl_loader.rb +365 -0
  1371. data/lib/lcp_ruby/dsl/field_builder.rb +35 -0
  1372. data/lib/lcp_ruby/dsl/job_builder.rb +92 -0
  1373. data/lib/lcp_ruby/dsl/model_builder.rb +544 -0
  1374. data/lib/lcp_ruby/dsl/presenter_builder.rb +1272 -0
  1375. data/lib/lcp_ruby/dsl/source_location_capture.rb +52 -0
  1376. data/lib/lcp_ruby/dsl/type_builder.rb +88 -0
  1377. data/lib/lcp_ruby/dsl/view_group_builder.rb +92 -0
  1378. data/lib/lcp_ruby/dsl/workflow_builder.rb +319 -0
  1379. data/lib/lcp_ruby/dynamic.rb +7 -0
  1380. data/lib/lcp_ruby/dynamic_references/resolver.rb +154 -0
  1381. data/lib/lcp_ruby/dynamic_references/validator.rb +92 -0
  1382. data/lib/lcp_ruby/embed_providers/base.rb +18 -0
  1383. data/lib/lcp_ruby/embed_providers/grafana.rb +38 -0
  1384. data/lib/lcp_ruby/embed_providers/metabase.rb +37 -0
  1385. data/lib/lcp_ruby/engine.rb +680 -0
  1386. data/lib/lcp_ruby/events/async_handler_job.rb +21 -0
  1387. data/lib/lcp_ruby/events/dispatcher.rb +52 -0
  1388. data/lib/lcp_ruby/events/handler_base.rb +51 -0
  1389. data/lib/lcp_ruby/events/handler_registry.rb +49 -0
  1390. data/lib/lcp_ruby/export/data_generator.rb +158 -0
  1391. data/lib/lcp_ruby/export/export_handler.rb +315 -0
  1392. data/lib/lcp_ruby/export/field_tree_builder.rb +219 -0
  1393. data/lib/lcp_ruby/export/setup.rb +94 -0
  1394. data/lib/lcp_ruby/export/value_formatter.rb +223 -0
  1395. data/lib/lcp_ruby/export.rb +9 -0
  1396. data/lib/lcp_ruby/gem_paths.rb +51 -0
  1397. data/lib/lcp_ruby/generators/entity_menu_writer.rb +258 -0
  1398. data/lib/lcp_ruby/generators/feature_registry.rb +208 -0
  1399. data/lib/lcp_ruby/generators/prerequisites.rb +90 -0
  1400. data/lib/lcp_ruby/grouped_query/builder.rb +206 -0
  1401. data/lib/lcp_ruby/grouped_query/result_wrapper.rb +72 -0
  1402. data/lib/lcp_ruby/grouped_query/row.rb +31 -0
  1403. data/lib/lcp_ruby/groups/change_handler.rb +18 -0
  1404. data/lib/lcp_ruby/groups/contract.rb +42 -0
  1405. data/lib/lcp_ruby/groups/contract_validator.rb +110 -0
  1406. data/lib/lcp_ruby/groups/host_loader.rb +54 -0
  1407. data/lib/lcp_ruby/groups/model_loader.rb +186 -0
  1408. data/lib/lcp_ruby/groups/registry.rb +113 -0
  1409. data/lib/lcp_ruby/groups/setup.rb +129 -0
  1410. data/lib/lcp_ruby/groups/yaml_loader.rb +97 -0
  1411. data/lib/lcp_ruby/hash_utils.rb +42 -0
  1412. data/lib/lcp_ruby/i18n_check/configuration.rb +104 -0
  1413. data/lib/lcp_ruby/i18n_check/heuristics.rb +26 -0
  1414. data/lib/lcp_ruby/i18n_check/key_deriver.rb +136 -0
  1415. data/lib/lcp_ruby/i18n_check/offense.rb +29 -0
  1416. data/lib/lcp_ruby/i18n_check/registry_walker.rb +621 -0
  1417. data/lib/lcp_ruby/i18n_check/reporter.rb +96 -0
  1418. data/lib/lcp_ruby/i18n_check/runner.rb +46 -0
  1419. data/lib/lcp_ruby/i18n_check.rb +15 -0
  1420. data/lib/lcp_ruby/i18n_lint.rb +145 -0
  1421. data/lib/lcp_ruby/import/auto_mapper.rb +98 -0
  1422. data/lib/lcp_ruby/import/field_tree_builder.rb +178 -0
  1423. data/lib/lcp_ruby/import/file_parser.rb +223 -0
  1424. data/lib/lcp_ruby/import/import_dialog_handler.rb +410 -0
  1425. data/lib/lcp_ruby/import/import_job_handler.rb +224 -0
  1426. data/lib/lcp_ruby/import/row_processor.rb +281 -0
  1427. data/lib/lcp_ruby/import/setup.rb +277 -0
  1428. data/lib/lcp_ruby/import/value_coercer.rb +143 -0
  1429. data/lib/lcp_ruby/import.rb +14 -0
  1430. data/lib/lcp_ruby/json_item_wrapper.rb +152 -0
  1431. data/lib/lcp_ruby/kanban/board.rb +28 -0
  1432. data/lib/lcp_ruby/kanban/column.rb +28 -0
  1433. data/lib/lcp_ruby/kanban/default_provider.rb +376 -0
  1434. data/lib/lcp_ruby/kanban/host_provider.rb +94 -0
  1435. data/lib/lcp_ruby/kanban/move_result.rb +52 -0
  1436. data/lib/lcp_ruby/kanban/provider_test_harness.rb +54 -0
  1437. data/lib/lcp_ruby/kanban/swimlane.rb +21 -0
  1438. data/lib/lcp_ruby/menu.rb +46 -0
  1439. data/lib/lcp_ruby/metadata/aggregate_definition.rb +6 -0
  1440. data/lib/lcp_ruby/metadata/association_definition.rb +196 -0
  1441. data/lib/lcp_ruby/metadata/auth_validator.rb +222 -0
  1442. data/lib/lcp_ruby/metadata/configuration_validator.rb +7958 -0
  1443. data/lib/lcp_ruby/metadata/contract_result.rb +9 -0
  1444. data/lib/lcp_ruby/metadata/display_template_definition.rb +77 -0
  1445. data/lib/lcp_ruby/metadata/enum_label_resolver.rb +27 -0
  1446. data/lib/lcp_ruby/metadata/erd_generator.rb +274 -0
  1447. data/lib/lcp_ruby/metadata/event_definition.rb +55 -0
  1448. data/lib/lcp_ruby/metadata/field_definition.rb +267 -0
  1449. data/lib/lcp_ruby/metadata/group_definition.rb +31 -0
  1450. data/lib/lcp_ruby/metadata/i18n_label.rb +29 -0
  1451. data/lib/lcp_ruby/metadata/loader.rb +916 -0
  1452. data/lib/lcp_ruby/metadata/menu_definition.rb +116 -0
  1453. data/lib/lcp_ruby/metadata/menu_item.rb +792 -0
  1454. data/lib/lcp_ruby/metadata/menu_item_resolver.rb +105 -0
  1455. data/lib/lcp_ruby/metadata/model_definition.rb +612 -0
  1456. data/lib/lcp_ruby/metadata/model_hash_merger.rb +88 -0
  1457. data/lib/lcp_ruby/metadata/model_inheritance_resolver.rb +165 -0
  1458. data/lib/lcp_ruby/metadata/page_definition.rb +245 -0
  1459. data/lib/lcp_ruby/metadata/path_template.rb +231 -0
  1460. data/lib/lcp_ruby/metadata/permission_definition.rb +237 -0
  1461. data/lib/lcp_ruby/metadata/permission_merger.rb +81 -0
  1462. data/lib/lcp_ruby/metadata/presenter_definition.rb +689 -0
  1463. data/lib/lcp_ruby/metadata/reserved_names.rb +79 -0
  1464. data/lib/lcp_ruby/metadata/responsive_policy.rb +95 -0
  1465. data/lib/lcp_ruby/metadata/schema_validator.rb +172 -0
  1466. data/lib/lcp_ruby/metadata/validation_definition.rb +69 -0
  1467. data/lib/lcp_ruby/metadata/view_group_definition.rb +208 -0
  1468. data/lib/lcp_ruby/metadata/virtual_column_definition.rb +154 -0
  1469. data/lib/lcp_ruby/metadata/zone_definition.rb +423 -0
  1470. data/lib/lcp_ruby/metrics/collector.rb +123 -0
  1471. data/lib/lcp_ruby/metrics/collector_registry.rb +57 -0
  1472. data/lib/lcp_ruby/metrics/error_recorder.rb +70 -0
  1473. data/lib/lcp_ruby/metrics/fingerprint.rb +33 -0
  1474. data/lib/lcp_ruby/metrics/json_query.rb +47 -0
  1475. data/lib/lcp_ruby/metrics/metric_definitions.rb +105 -0
  1476. data/lib/lcp_ruby/metrics/prometheus_check.rb +9 -0
  1477. data/lib/lcp_ruby/metrics/rate_limiter.rb +99 -0
  1478. data/lib/lcp_ruby/metrics/setup.rb +71 -0
  1479. data/lib/lcp_ruby/metrics/subscriber.rb +126 -0
  1480. data/lib/lcp_ruby/model_factory/aggregate_applicator.rb +14 -0
  1481. data/lib/lcp_ruby/model_factory/api_association_applicator.rb +119 -0
  1482. data/lib/lcp_ruby/model_factory/api_builder.rb +85 -0
  1483. data/lib/lcp_ruby/model_factory/array_type.rb +78 -0
  1484. data/lib/lcp_ruby/model_factory/array_type_applicator.rb +33 -0
  1485. data/lib/lcp_ruby/model_factory/association_applicator.rb +201 -0
  1486. data/lib/lcp_ruby/model_factory/attachment_applicator.rb +160 -0
  1487. data/lib/lcp_ruby/model_factory/auditing_applicator.rb +72 -0
  1488. data/lib/lcp_ruby/model_factory/builder.rb +235 -0
  1489. data/lib/lcp_ruby/model_factory/callback_applicator.rb +63 -0
  1490. data/lib/lcp_ruby/model_factory/computed_applicator.rb +55 -0
  1491. data/lib/lcp_ruby/model_factory/default_applicator.rb +85 -0
  1492. data/lib/lcp_ruby/model_factory/enum_applicator.rb +24 -0
  1493. data/lib/lcp_ruby/model_factory/inherited_parent_validator_applicator.rb +39 -0
  1494. data/lib/lcp_ruby/model_factory/label_method_builder.rb +82 -0
  1495. data/lib/lcp_ruby/model_factory/managed_tracking.rb +71 -0
  1496. data/lib/lcp_ruby/model_factory/positioning_applicator.rb +23 -0
  1497. data/lib/lcp_ruby/model_factory/ransack_applicator.rb +66 -0
  1498. data/lib/lcp_ruby/model_factory/registry.rb +33 -0
  1499. data/lib/lcp_ruby/model_factory/schema_manager.rb +655 -0
  1500. data/lib/lcp_ruby/model_factory/scope_applicator.rb +87 -0
  1501. data/lib/lcp_ruby/model_factory/sequence_applicator.rb +173 -0
  1502. data/lib/lcp_ruby/model_factory/service_accessor_applicator.rb +40 -0
  1503. data/lib/lcp_ruby/model_factory/soft_delete_applicator.rb +141 -0
  1504. data/lib/lcp_ruby/model_factory/transform_applicator.rb +51 -0
  1505. data/lib/lcp_ruby/model_factory/tree_applicator.rb +239 -0
  1506. data/lib/lcp_ruby/model_factory/userstamps_applicator.rb +73 -0
  1507. data/lib/lcp_ruby/model_factory/validation_applicator.rb +319 -0
  1508. data/lib/lcp_ruby/model_factory/virtual_column_applicator.rb +79 -0
  1509. data/lib/lcp_ruby/model_factory/workflow_applicator.rb +141 -0
  1510. data/lib/lcp_ruby/pages/change_handler.rb +15 -0
  1511. data/lib/lcp_ruby/pages/contract_validator.rb +74 -0
  1512. data/lib/lcp_ruby/pages/definition_validator.rb +42 -0
  1513. data/lib/lcp_ruby/pages/filter_form.rb +200 -0
  1514. data/lib/lcp_ruby/pages/filter_form_validator.rb +636 -0
  1515. data/lib/lcp_ruby/pages/registry.rb +133 -0
  1516. data/lib/lcp_ruby/pages/resolver.rb +32 -0
  1517. data/lib/lcp_ruby/pages/scope_context_resolver.rb +37 -0
  1518. data/lib/lcp_ruby/pages/scope_filter_set.rb +57 -0
  1519. data/lib/lcp_ruby/pages/setup.rb +46 -0
  1520. data/lib/lcp_ruby/path_utils.rb +12 -0
  1521. data/lib/lcp_ruby/permissions/change_handler.rb +22 -0
  1522. data/lib/lcp_ruby/permissions/contract_validator.rb +74 -0
  1523. data/lib/lcp_ruby/permissions/definition_validator.rb +119 -0
  1524. data/lib/lcp_ruby/permissions/registry.rb +135 -0
  1525. data/lib/lcp_ruby/permissions/setup.rb +51 -0
  1526. data/lib/lcp_ruby/permissions/source_resolver.rb +56 -0
  1527. data/lib/lcp_ruby/presenter/action_set.rb +236 -0
  1528. data/lib/lcp_ruby/presenter/breadcrumb_builder.rb +183 -0
  1529. data/lib/lcp_ruby/presenter/breadcrumb_path_helper.rb +17 -0
  1530. data/lib/lcp_ruby/presenter/column_set.rb +268 -0
  1531. data/lib/lcp_ruby/presenter/enrichment.rb +136 -0
  1532. data/lib/lcp_ruby/presenter/field_value_resolver.rb +237 -0
  1533. data/lib/lcp_ruby/presenter/includes_resolver/association_dependency.rb +59 -0
  1534. data/lib/lcp_ruby/presenter/includes_resolver/dependency_collector.rb +394 -0
  1535. data/lib/lcp_ruby/presenter/includes_resolver/loading_strategy.rb +70 -0
  1536. data/lib/lcp_ruby/presenter/includes_resolver/strategy_resolver.rb +123 -0
  1537. data/lib/lcp_ruby/presenter/includes_resolver.rb +42 -0
  1538. data/lib/lcp_ruby/presenter/layout_builder.rb +467 -0
  1539. data/lib/lcp_ruby/presenter/link_resolver.rb +65 -0
  1540. data/lib/lcp_ruby/presenter/metadata_lookup.rb +28 -0
  1541. data/lib/lcp_ruby/presenter/resolver.rb +25 -0
  1542. data/lib/lcp_ruby/record_aliases/metadata_checker.rb +213 -0
  1543. data/lib/lcp_ruby/record_aliases/setup.rb +212 -0
  1544. data/lib/lcp_ruby/reserved_route_segments.rb +37 -0
  1545. data/lib/lcp_ruby/roles/change_handler.rb +11 -0
  1546. data/lib/lcp_ruby/roles/contract_validator.rb +67 -0
  1547. data/lib/lcp_ruby/roles/registry.rb +89 -0
  1548. data/lib/lcp_ruby/roles/setup.rb +50 -0
  1549. data/lib/lcp_ruby/routing/presenter_routes.rb +104 -0
  1550. data/lib/lcp_ruby/saved_filters/change_handler.rb +13 -0
  1551. data/lib/lcp_ruby/saved_filters/contract_validator.rb +85 -0
  1552. data/lib/lcp_ruby/saved_filters/registry.rb +36 -0
  1553. data/lib/lcp_ruby/saved_filters/resolver.rb +108 -0
  1554. data/lib/lcp_ruby/saved_filters/setup.rb +42 -0
  1555. data/lib/lcp_ruby/saved_filters/stale_field_validator.rb +84 -0
  1556. data/lib/lcp_ruby/schemas/auth.json +208 -0
  1557. data/lib/lcp_ruby/schemas/menu.json +338 -0
  1558. data/lib/lcp_ruby/schemas/model.json +1161 -0
  1559. data/lib/lcp_ruby/schemas/page.json +877 -0
  1560. data/lib/lcp_ruby/schemas/permission.json +454 -0
  1561. data/lib/lcp_ruby/schemas/presenter.json +2274 -0
  1562. data/lib/lcp_ruby/schemas/theme.json +62 -0
  1563. data/lib/lcp_ruby/schemas/type.json +146 -0
  1564. data/lib/lcp_ruby/schemas/view_group.json +163 -0
  1565. data/lib/lcp_ruby/search/custom_field_filter.rb +171 -0
  1566. data/lib/lcp_ruby/search/custom_filter_interceptor.rb +40 -0
  1567. data/lib/lcp_ruby/search/filter_metadata_builder.rb +409 -0
  1568. data/lib/lcp_ruby/search/filter_param_builder.rb +177 -0
  1569. data/lib/lcp_ruby/search/operator_registry.rb +79 -0
  1570. data/lib/lcp_ruby/search/param_sanitizer.rb +25 -0
  1571. data/lib/lcp_ruby/search/parameter_definition.rb +187 -0
  1572. data/lib/lcp_ruby/search/parameterized_scope_applicator.rb +129 -0
  1573. data/lib/lcp_ruby/search/query_builder.rb +143 -0
  1574. data/lib/lcp_ruby/search/query_language_parser.rb +549 -0
  1575. data/lib/lcp_ruby/search/query_language_serializer.rb +193 -0
  1576. data/lib/lcp_ruby/search/quick_search.rb +162 -0
  1577. data/lib/lcp_ruby/search/relative_date_expander.rb +57 -0
  1578. data/lib/lcp_ruby/search_result.rb +70 -0
  1579. data/lib/lcp_ruby/sequences/sequence_manager.rb +51 -0
  1580. data/lib/lcp_ruby/services/accessors/json_field.rb +23 -0
  1581. data/lib/lcp_ruby/services/built_in_accessors.rb +17 -0
  1582. data/lib/lcp_ruby/services/built_in_defaults.rb +22 -0
  1583. data/lib/lcp_ruby/services/built_in_transforms.rb +20 -0
  1584. data/lib/lcp_ruby/services/checker.rb +133 -0
  1585. data/lib/lcp_ruby/services/registry.rb +83 -0
  1586. data/lib/lcp_ruby/skills_installer.rb +73 -0
  1587. data/lib/lcp_ruby/sort/enum_sort_order.rb +38 -0
  1588. data/lib/lcp_ruby/tasks/destroy_order_resolver.rb +57 -0
  1589. data/lib/lcp_ruby/tasks/doctor.rb +294 -0
  1590. data/lib/lcp_ruby/tasks/permission_resolve_formatter.rb +245 -0
  1591. data/lib/lcp_ruby/types/built_in_types.rb +157 -0
  1592. data/lib/lcp_ruby/types/transforms/base_transform.rb +11 -0
  1593. data/lib/lcp_ruby/types/transforms/downcase.rb +11 -0
  1594. data/lib/lcp_ruby/types/transforms/normalize_phone.rb +19 -0
  1595. data/lib/lcp_ruby/types/transforms/normalize_url.rb +16 -0
  1596. data/lib/lcp_ruby/types/transforms/strip.rb +11 -0
  1597. data/lib/lcp_ruby/types/type_definition.rb +112 -0
  1598. data/lib/lcp_ruby/types/type_registry.rb +75 -0
  1599. data/lib/lcp_ruby/url_safety.rb +97 -0
  1600. data/lib/lcp_ruby/user_snapshot.rb +15 -0
  1601. data/lib/lcp_ruby/version.rb +3 -0
  1602. data/lib/lcp_ruby/view_slots/registry.rb +71 -0
  1603. data/lib/lcp_ruby/view_slots/slot_component.rb +22 -0
  1604. data/lib/lcp_ruby/view_slots/slot_context.rb +20 -0
  1605. data/lib/lcp_ruby/virtual_columns/builder.rb +234 -0
  1606. data/lib/lcp_ruby/virtual_columns/collector.rb +186 -0
  1607. data/lib/lcp_ruby/virtual_columns.rb +4 -0
  1608. data/lib/lcp_ruby/virtual_fields/synthetic_marker.rb +17 -0
  1609. data/lib/lcp_ruby/virtual_fields/types/array_of.rb +49 -0
  1610. data/lib/lcp_ruby/virtual_fields/virtual_field.rb +107 -0
  1611. data/lib/lcp_ruby/virtual_fields/virtual_form.rb +144 -0
  1612. data/lib/lcp_ruby/widgets/chart_palette.rb +25 -0
  1613. data/lib/lcp_ruby/widgets/chartkick_check.rb +9 -0
  1614. data/lib/lcp_ruby/widgets/data_resolver.rb +676 -0
  1615. data/lib/lcp_ruby/widgets/date_grouper.rb +54 -0
  1616. data/lib/lcp_ruby/widgets/presenter_zone_resolver.rb +170 -0
  1617. data/lib/lcp_ruby/widgets/record_source_resolver.rb +56 -0
  1618. data/lib/lcp_ruby/widgets/scope_applicator.rb +187 -0
  1619. data/lib/lcp_ruby/workflow/approval/activation_handler.rb +39 -0
  1620. data/lib/lcp_ruby/workflow/approval/approval_definition.rb +117 -0
  1621. data/lib/lcp_ruby/workflow/approval/approver_resolver.rb +98 -0
  1622. data/lib/lcp_ruby/workflow/approval/cleanup_handler.rb +37 -0
  1623. data/lib/lcp_ruby/workflow/approval/contract_validator.rb +96 -0
  1624. data/lib/lcp_ruby/workflow/approval/data_builder.rb +53 -0
  1625. data/lib/lcp_ruby/workflow/approval/discard_handler.rb +51 -0
  1626. data/lib/lcp_ruby/workflow/approval/engine.rb +314 -0
  1627. data/lib/lcp_ruby/workflow/approval/registry.rb +40 -0
  1628. data/lib/lcp_ruby/workflow/approval/resolution_handler.rb +103 -0
  1629. data/lib/lcp_ruby/workflow/approval/setup.rb +138 -0
  1630. data/lib/lcp_ruby/workflow/approval/step_definition.rb +52 -0
  1631. data/lib/lcp_ruby/workflow/approval/step_evaluator.rb +163 -0
  1632. data/lib/lcp_ruby/workflow/approval/system_evaluator.rb +29 -0
  1633. data/lib/lcp_ruby/workflow/approval/task_manager.rb +202 -0
  1634. data/lib/lcp_ruby/workflow/audit_contract_validator.rb +64 -0
  1635. data/lib/lcp_ruby/workflow/audit_registry.rb +24 -0
  1636. data/lib/lcp_ruby/workflow/audit_writer.rb +51 -0
  1637. data/lib/lcp_ruby/workflow/change_handler.rb +14 -0
  1638. data/lib/lcp_ruby/workflow/contract.rb +21 -0
  1639. data/lib/lcp_ruby/workflow/contract_validator.rb +44 -0
  1640. data/lib/lcp_ruby/workflow/errors.rb +12 -0
  1641. data/lib/lcp_ruby/workflow/host_source.rb +19 -0
  1642. data/lib/lcp_ruby/workflow/mermaid_builder.rb +217 -0
  1643. data/lib/lcp_ruby/workflow/model_source.rb +79 -0
  1644. data/lib/lcp_ruby/workflow/registry.rb +113 -0
  1645. data/lib/lcp_ruby/workflow/resolver.rb +32 -0
  1646. data/lib/lcp_ruby/workflow/setup.rb +135 -0
  1647. data/lib/lcp_ruby/workflow/state_definition.rb +59 -0
  1648. data/lib/lcp_ruby/workflow/state_machine.rb +78 -0
  1649. data/lib/lcp_ruby/workflow/static_source.rb +20 -0
  1650. data/lib/lcp_ruby/workflow/transition_action_builder.rb +46 -0
  1651. data/lib/lcp_ruby/workflow/transition_definition.rb +70 -0
  1652. data/lib/lcp_ruby/workflow/transition_executor.rb +140 -0
  1653. data/lib/lcp_ruby/workflow/transition_label_resolver.rb +21 -0
  1654. data/lib/lcp_ruby/workflow/transition_result.rb +20 -0
  1655. data/lib/lcp_ruby/workflow/value_resolver.rb +58 -0
  1656. data/lib/lcp_ruby/workflow/workflow_definition.rb +195 -0
  1657. data/lib/lcp_ruby.rb +764 -0
  1658. data/lib/rubocop/cop/lcp_ruby/no_hardcoded_i18n_string.rb +249 -0
  1659. data/lib/tasks/lcp_ruby.rake +432 -0
  1660. data/lib/tasks/lcp_ruby_assets.rake +37 -0
  1661. data/lib/tasks/lcp_ruby_auth.rake +49 -0
  1662. data/lib/tasks/lcp_ruby_db.rake +76 -0
  1663. data/lib/tasks/lcp_ruby_doctor.rake +20 -0
  1664. data/lib/tasks/lcp_ruby_feature_catalog.rake +61 -0
  1665. data/lib/tasks/lcp_ruby_gapfree_sequences.rake +39 -0
  1666. data/lib/tasks/lcp_ruby_i18n_check.rake +23 -0
  1667. data/lib/tasks/lcp_ruby_i18n_lint.rake +20 -0
  1668. data/lib/tasks/lcp_ruby_invariant_check.rake +72 -0
  1669. data/vendor/assets/javascripts/lcp_ruby/activestorage.min.js +866 -0
  1670. data/vendor/assets/javascripts/lcp_ruby/highlight.min.js +1244 -0
  1671. data/vendor/assets/javascripts/lcp_ruby/lucide.min.js +12 -0
  1672. data/vendor/assets/javascripts/lcp_ruby/stimulus.umd.js +2588 -0
  1673. data/vendor/assets/javascripts/lcp_ruby/tom-select.complete.min.js +444 -0
  1674. data/vendor/assets/stylesheets/lcp_ruby/highlight-github.min.css +12 -0
  1675. data/vendor/assets/stylesheets/lcp_ruby/tom-select.css +412 -0
  1676. metadata +1950 -0
@@ -0,0 +1,2218 @@
1
+ require "digest"
2
+
3
+ module LcpRuby
4
+ class ResourcesController < ApplicationController
5
+ include LcpRuby::AssociationOptionsBuilder
6
+ include LcpRuby::DialogRendering
7
+ include LcpRuby::FormActionExecution
8
+ include LcpRuby::ZoneResolution
9
+ include LcpRuby::AuthorizedController
10
+ include LcpRuby::PageAuthorization
11
+
12
+ before_action :set_record, only: [ :show, :edit, :update, :destroy, :evaluate_conditions, :reorder, :reparent, :kanban_move, :zone, :execute_transition ]
13
+ before_action :set_record_with_discarded, only: [ :restore, :permanently_destroy ]
14
+ # Declared AFTER :set_record so PageGate.evaluate sees the loaded
15
+ # @record on show/edit/update/destroy paths (Rails before_actions
16
+ # run in declaration order; registering inside the concern's
17
+ # `included do` would put the gate before set_record). The
18
+ # `routable?` guard scopes to slug-resolved page paths —
19
+ # dialog-only pages render through a separate pipeline.
20
+ # See docs/design/authorization_hardening.md § "Concern include
21
+ # order in ResourcesController is load-bearing".
22
+ before_action :authorize_page_if_present,
23
+ if: -> { current_page.present? && current_page.routable? }
24
+
25
+ def index
26
+ if raw_format?
27
+ return head :not_acceptable if raw_index_unsupported?
28
+ return render_raw_index
29
+ end
30
+
31
+ # Standalone grid pages (dashboards) bypass normal CRUD authorization.
32
+ # Page-level access is gated by LcpRuby::PageAuthorization (before_action
33
+ # above), which calls skip_authorization on :allow so verify_authorized
34
+ # sees authorization-having-happened.
35
+ if current_page&.standalone? && current_page&.grid?
36
+ load_dashboard
37
+ return
38
+ end
39
+
40
+ # Composite pages may specify a separate index_presenter for index actions
41
+ if current_page&.index_presenter
42
+ @presenter_definition = LcpRuby.loader.presenter_definition(current_page.index_presenter)
43
+ else
44
+ # Check for index composite page — either the current page itself,
45
+ # or an explicit composite page defined for this model
46
+ @index_composite_page = if current_page&.index_composite?
47
+ current_page
48
+ else
49
+ find_index_composite_for_model
50
+ end
51
+
52
+ if @index_composite_page
53
+ @presenter_definition = LcpRuby.loader.presenter_definitions[@index_composite_page.main_presenter_name]
54
+ authorize @model_class
55
+ load_index_composite
56
+ return
57
+ end
58
+ end
59
+
60
+ authorize @model_class
61
+
62
+ if api_model?
63
+ load_api_index
64
+ return
65
+ end
66
+
67
+ scope = policy_scope(@model_class)
68
+ scope = apply_soft_delete_scope(scope)
69
+
70
+ apply_default_saved_filter!
71
+
72
+ if current_presenter.index_render_with
73
+ load_flat_index(scope)
74
+ else
75
+ case current_presenter.index_layout
76
+ when :tree
77
+ load_tree_index(scope) if current_model_definition.tree?
78
+ when :grouped
79
+ load_grouped_index(scope)
80
+ when :tiles
81
+ load_flat_index(scope)
82
+ when :kanban
83
+ load_kanban_index(scope)
84
+ else
85
+ load_flat_index(scope)
86
+ end
87
+ end
88
+ end
89
+
90
+ def show
91
+ authorize @record
92
+
93
+ if raw_format?
94
+ return head :not_acceptable if raw_show_unsupported?
95
+ return render_raw_show
96
+ end
97
+
98
+ warn_missing_presenter_config(:show)
99
+
100
+ if dialog_context?
101
+ setup_show_view_objects(current_presenter)
102
+ return render partial: "lcp_ruby/dialogs/dialog_show_frame",
103
+ locals: {
104
+ record: @record,
105
+ layout_builder: @layout_builder,
106
+ column_set: @column_set,
107
+ field_resolver: @field_resolver
108
+ },
109
+ layout: false
110
+ end
111
+
112
+ if current_page&.composite?
113
+ load_composite_page
114
+ return
115
+ end
116
+
117
+ if api_model?
118
+ setup_show_view_objects(current_presenter)
119
+ return
120
+ end
121
+
122
+ setup_show_view_objects(current_presenter)
123
+ load_show_virtual_columns
124
+ preload_associations(@record, :show)
125
+ @record.strict_loading! if LcpRuby.configuration.strict_loading_enabled?
126
+ end
127
+
128
+ def zone
129
+ authorize @record, :show?
130
+
131
+ zone_name = params[:zone_name]
132
+ zone = find_composite_zone(zone_name)
133
+ return head(:not_found) unless zone
134
+ return head(:not_found) unless zone_accessible?(zone)
135
+
136
+ data = resolve_zone_data(zone, zone_params: zone_params(zone))
137
+
138
+ frame_id = zone.area == "tabs" ? "lcp-zone-tab-content" : "lcp-zone-#{zone.name}"
139
+
140
+ render partial: "lcp_ruby/zones/zone_frame",
141
+ locals: { zone: zone, data: data, frame_id: frame_id, record: @record },
142
+ layout: false
143
+ end
144
+
145
+ def new
146
+ return head(:not_found) if api_model?
147
+ @record = @model_class.new
148
+ authorize @record
149
+ warn_missing_presenter_config(:form)
150
+ apply_presenter_defaults(@record)
151
+ @form_actions = current_form_actions(@record)
152
+
153
+ if dialog_context?
154
+ return render_dialog_form
155
+ end
156
+
157
+ build_nested_records(@record)
158
+ @layout_builder = Presenter::LayoutBuilder.new(current_presenter, current_model_definition)
159
+ end
160
+
161
+ def create
162
+ return head(:not_found) if api_model?
163
+ @record = @model_class.new(permitted_params)
164
+ authorize @record
165
+ fa_config = resolve_form_action(@record)
166
+
167
+ process_json_field_params(@record)
168
+ validate_association_values!(@record)
169
+ apply_set_fields!(@record, fa_config)
170
+
171
+ if @record.errors.any?
172
+ return render_create_failure
173
+ end
174
+
175
+ result = execute_form_action_pipeline(@record, fa_config)
176
+
177
+ if result.success?
178
+ dispatch_deferred_events(result)
179
+ broadcast_model_change(current_model_definition.name)
180
+
181
+ if dialog_context?
182
+ behavior = form_action_dialog_behavior(fa_config)
183
+ flash_msg = behavior == "reset" ? pipeline_flash_message("create", result) : nil
184
+ return render_dialog_success(behavior, flash_message: flash_msg, result_config: fa_config["result"])
185
+ end
186
+
187
+ redirect_to pipeline_redirect_path("create", fa_config, @record, result),
188
+ status: :see_other,
189
+ notice: pipeline_flash_message("create", result)
190
+ else
191
+ render_create_failure
192
+ end
193
+ end
194
+
195
+ def edit
196
+ return head(:not_found) if api_model?
197
+ authorize @record
198
+ warn_missing_presenter_config(:form)
199
+ @form_actions = current_form_actions(@record)
200
+
201
+ if dialog_context?
202
+ return render_dialog_form
203
+ end
204
+
205
+ @layout_builder = Presenter::LayoutBuilder.new(current_presenter, current_model_definition)
206
+ load_edit_virtual_columns
207
+ preload_associations(@record, :form)
208
+ @record.strict_loading! if LcpRuby.configuration.strict_loading_enabled?
209
+ end
210
+
211
+ def update
212
+ return head(:not_found) if api_model?
213
+ authorize @record
214
+ @record.assign_attributes(permitted_params)
215
+ fa_config = resolve_form_action(@record)
216
+ purge_removed_attachments!(@record)
217
+
218
+ process_json_field_params(@record)
219
+ validate_association_values!(@record)
220
+ apply_set_fields!(@record, fa_config)
221
+
222
+ if @record.errors.any?
223
+ return render_update_failure
224
+ end
225
+
226
+ result = execute_form_action_pipeline(@record, fa_config)
227
+
228
+ if result.success?
229
+ dispatch_deferred_events(result)
230
+ broadcast_model_change(current_model_definition.name)
231
+
232
+ if dialog_context?
233
+ behavior = form_action_dialog_behavior(fa_config)
234
+ flash_msg = behavior == "reset" ? pipeline_flash_message("update", result) : nil
235
+ return render_dialog_success(behavior, flash_message: flash_msg, result_config: fa_config["result"])
236
+ end
237
+
238
+ path = if current_page&.form_zone&.submit_redirect_self?
239
+ resource_path(@record)
240
+ else
241
+ pipeline_redirect_path("update", fa_config, @record, result)
242
+ end
243
+ redirect_to path, status: :see_other,
244
+ notice: pipeline_flash_message("update", result)
245
+ else
246
+ render_update_failure
247
+ end
248
+ end
249
+
250
+ def destroy
251
+ return head(:not_found) if api_model?
252
+ authorize @record
253
+ if current_model_definition.soft_delete?
254
+ @record.discard!(by: current_user)
255
+ else
256
+ @record.destroy!
257
+ end
258
+
259
+ broadcast_model_change(current_model_definition.name)
260
+
261
+ notice_key = current_model_definition.soft_delete? ? "lcp_ruby.flash.archived" : "lcp_ruby.flash.deleted"
262
+ default_msg = current_model_definition.soft_delete? ? "%{model} was successfully archived." : "%{model} was successfully deleted."
263
+ redirect_to resources_path,
264
+ notice: I18n.t(notice_key, model: current_model_definition.resolved_label, default: default_msg)
265
+ end
266
+
267
+ def execute_transition
268
+ authorize @record, :update?
269
+
270
+ transition_name = params[:transition_name]
271
+ comment = params[:transition_comment]
272
+
273
+ Workflow::TransitionExecutor.execute(
274
+ @record, transition_name,
275
+ user: current_user, evaluator: current_evaluator,
276
+ comment: comment, triggered_by: "user"
277
+ )
278
+
279
+ redirect_to resource_path(@record), status: :see_other,
280
+ notice: I18n.t("lcp_ruby.flash.transition_executed",
281
+ transition: resolve_transition_label(transition_name),
282
+ default: "Transition '%{transition}' executed successfully.")
283
+ rescue Workflow::CommentRequiredError
284
+ redirect_to resource_path(@record), status: :see_other,
285
+ alert: I18n.t("lcp_ruby.flash.transition_comment_required",
286
+ default: "A comment is required for this transition.")
287
+ rescue Workflow::TransitionDeniedError => e
288
+ redirect_to resource_path(@record), status: :see_other, alert: e.localized_message
289
+ end
290
+
291
+ def restore
292
+ authorize @record
293
+ @record.undiscard!
294
+ redirect_to resources_path, status: :see_other,
295
+ notice: I18n.t("lcp_ruby.flash.restored", model: current_model_definition.resolved_label,
296
+ default: "%{model} was successfully restored.")
297
+ end
298
+
299
+ def permanently_destroy
300
+ authorize @record
301
+ @record.destroy!
302
+ redirect_to resources_path, status: :see_other,
303
+ notice: I18n.t("lcp_ruby.flash.permanently_deleted", model: current_model_definition.resolved_label,
304
+ default: "%{model} was permanently deleted.")
305
+ end
306
+
307
+ def select_options
308
+ field_name = params[:field]
309
+ field_config = find_form_field_config(field_name)
310
+ unless field_config
311
+ skip_authorization
312
+ return render(json: [])
313
+ end
314
+
315
+ # Resolve association from enriched field config (covers association_select + multi_select)
316
+ assoc = field_config["association"] || field_config["multi_select_association"]
317
+ # Fallback to FK lookup for non-enriched cases
318
+ assoc ||= resolve_association_for_field(field_name)
319
+ unless assoc&.lcp_model?
320
+ skip_authorization
321
+ return render(json: [])
322
+ end
323
+
324
+ # Authorize against the TARGET model's policy, not the page-level
325
+ # presenter model. The user is requesting a list of records FROM
326
+ # `assoc.target_model`; access to the presenter alone does not
327
+ # imply read access to the association target. `before_action
328
+ # :authorize_presenter_access` (Controller::Authorization) already
329
+ # gates presenter visibility, so we only need the additional
330
+ # target-model `:index?` check here.
331
+ authorize_select_options_target!(assoc.target_model)
332
+
333
+ input_options = field_config["input_options"] || {}
334
+
335
+ # API-backed target model: delegate to data source
336
+ if api_target_model?(assoc)
337
+ return render json: build_api_select_options(assoc, input_options)
338
+ end
339
+
340
+ # Reverse cascade: resolve ancestor chain for a given value
341
+ if params[:ancestors_for].present?
342
+ ancestors = resolve_field_ancestors(field_name, params[:ancestors_for])
343
+ return render json: { ancestors: ancestors }
344
+ end
345
+
346
+ # Tree select mode
347
+ if params[:tree] == "true"
348
+ tree = build_tree_select_options(assoc, input_options)
349
+ return render json: tree
350
+ end
351
+
352
+ # Paginated search mode when q or page param present
353
+ if params[:q].present? || params[:page].present?
354
+ result = build_select_options_search(assoc, input_options)
355
+ render json: result
356
+ else
357
+ options = build_select_options_json(assoc, input_options)
358
+ render json: options
359
+ end
360
+ end
361
+
362
+ def evaluate_conditions
363
+ authorize @record, :edit?
364
+ @record.assign_attributes(permitted_params)
365
+ render json: evaluate_service_conditions(@record)
366
+ end
367
+
368
+ def evaluate_conditions_new
369
+ @record = @model_class.new(permitted_params)
370
+ authorize @record, :create?
371
+ render json: evaluate_service_conditions(@record)
372
+ end
373
+
374
+ def parse_ql
375
+ authorize @model_class, :index?
376
+
377
+ ql_text = params[:ql].to_s.first(Search::QueryLanguageParser::MAX_INPUT_LENGTH + 1)
378
+ max_depth = current_presenter.advanced_filter_config["max_nesting_depth"] || 2
379
+ parser = Search::QueryLanguageParser.new(ql_text, max_nesting_depth: max_depth)
380
+ tree = parser.parse
381
+
382
+ render json: { success: true, tree: tree }
383
+ rescue Search::QueryLanguageParser::ParseError => e
384
+ render json: { success: false, error: e.message, position: e.position }
385
+ end
386
+
387
+ def filter_fields
388
+ authorize @model_class, :index?
389
+
390
+ builder = Search::FilterMetadataBuilder.new(current_presenter, current_model_definition, current_evaluator)
391
+ metadata = builder.build
392
+
393
+ result = { fields: metadata[:fields] }
394
+ result[:scopes] = metadata[:scopes] if metadata[:scopes]&.any?
395
+ render json: result
396
+ end
397
+
398
+ def batch_count
399
+ authorize @model_class, "index?"
400
+ scope = policy_scope(@model_class)
401
+ scope = apply_soft_delete_scope(scope)
402
+ scope = apply_advanced_search(scope)
403
+ render json: { count: scope.count }
404
+ end
405
+
406
+ def export_fields
407
+ authorize @model_class, "index?"
408
+ head(:forbidden) and return unless current_evaluator.can_execute_action?(:export)
409
+
410
+ tree = Export::FieldTreeBuilder.build(
411
+ model_definition: current_model_definition,
412
+ evaluator: current_evaluator,
413
+ presenter: current_presenter,
414
+ current_user: current_user
415
+ )
416
+ render json: tree
417
+ end
418
+
419
+ def export_profiles
420
+ authorize @model_class, "index?"
421
+ head(:forbidden) and return unless current_evaluator.can_execute_action?(:export)
422
+
423
+ profile_model = LcpRuby.registry.model_for("export_profile")
424
+ profiles = profile_model
425
+ .for_presenter(current_presenter.name)
426
+ .where(
427
+ profile_model.arel_table[:visibility].eq("global")
428
+ .or(profile_model.arel_table[:visibility].eq("role")
429
+ .and(profile_model.arel_table[:target_role].in(current_evaluator.roles)))
430
+ .or(profile_model.arel_table[:visibility].eq("personal")
431
+ .and(profile_model.arel_table[:owner_id].eq(current_user&.id)))
432
+ )
433
+ .order(:profile_name)
434
+
435
+ # Build field tree to detect stale fields
436
+ field_tree = Export::FieldTreeBuilder.build(
437
+ model_definition: current_model_definition,
438
+ evaluator: current_evaluator,
439
+ presenter: current_presenter,
440
+ current_user: current_user
441
+ )
442
+ available_paths = collect_field_paths(field_tree)
443
+
444
+ render json: profiles.map { |p|
445
+ selected = Array(p.selected_fields)
446
+ stale = selected - available_paths
447
+ valid = selected - stale
448
+
449
+ {
450
+ id: p.id,
451
+ profile_name: p.profile_name,
452
+ format: p.format,
453
+ selected_fields: valid,
454
+ formatting_options: p.formatting_options,
455
+ stale_fields: stale
456
+ }
457
+ }
458
+ rescue LcpRuby::Error
459
+ render json: []
460
+ end
461
+
462
+ def import_fields
463
+ authorize @model_class, "index?"
464
+ head(:forbidden) and return unless current_evaluator.can_execute_action?(:import)
465
+
466
+ tree = Import::FieldTreeBuilder.build(
467
+ model_definition: current_model_definition,
468
+ evaluator: current_evaluator,
469
+ presenter: current_presenter,
470
+ current_user: current_user
471
+ )
472
+ render json: tree
473
+ end
474
+
475
+ def import_profiles
476
+ authorize @model_class, "index?"
477
+ head(:forbidden) and return unless current_evaluator.can_execute_action?(:import)
478
+
479
+ profile_model = LcpRuby.registry.model_for("import_profile")
480
+ profiles = profile_model
481
+ .for_model(current_model_definition.name)
482
+ .where(
483
+ profile_model.arel_table[:visibility].eq("global")
484
+ .or(profile_model.arel_table[:visibility].eq("role")
485
+ .and(profile_model.arel_table[:target_role].in(current_evaluator.roles)))
486
+ .or(profile_model.arel_table[:visibility].eq("personal")
487
+ .and(profile_model.arel_table[:owner_id].eq(current_user&.id)))
488
+ )
489
+ .order(:profile_name)
490
+
491
+ render json: profiles.map { |p|
492
+ {
493
+ id: p.id,
494
+ profile_name: p.profile_name,
495
+ strategy: p.strategy,
496
+ match_attribute: p.match_attribute,
497
+ column_mapping: p.column_mapping,
498
+ lookup_config: p.lookup_config,
499
+ parsing_options: p.parsing_options
500
+ }
501
+ }
502
+ rescue LcpRuby::Error
503
+ render json: []
504
+ end
505
+
506
+ def batch_selection
507
+ authorize @model_class, "index?"
508
+ ids = params[:ids] || []
509
+ token = SecureRandom.uuid
510
+ session[:batch_selections] ||= {}
511
+ session[:batch_selections][token] = ids.map(&:to_s)
512
+ render json: { token: token }
513
+ end
514
+
515
+ # PATCH /:id/kanban_move — dispatches the move through the configured
516
+ # provider and maps Kanban::MoveResult → HTTP per docs/design/kanban_view.md.
517
+ def kanban_move
518
+ unless current_presenter.kanban?
519
+ head :not_found
520
+ return
521
+ end
522
+
523
+ authorize @record, :update?
524
+
525
+ provider_class = kanban_provider_class
526
+ provider = build_kanban_provider(provider_class)
527
+
528
+ target_column = params[:target_column]
529
+ target_swimlane = params[:target_swimlane]
530
+ target_position = params[:target_position].presence&.to_i
531
+ comment = params[:transition_comment]
532
+
533
+ group_by_field = current_presenter.kanban_config["group_by"].to_s
534
+ source_column = group_by_field.present? ? @record.public_send(group_by_field).to_s : params[:source_column].to_s
535
+
536
+ result =
537
+ begin
538
+ provider.move(
539
+ record: @record,
540
+ target_column: target_column,
541
+ target_swimlane: target_swimlane,
542
+ target_position: target_position,
543
+ user: current_user,
544
+ comment: comment
545
+ )
546
+ rescue StandardError => e
547
+ raise unless Rails.env.production?
548
+ LcpRuby.record_error(e, subsystem: "kanban", record_id: @record.id, presenter: current_presenter.name) if LcpRuby.respond_to?(:record_error)
549
+ LcpRuby::Kanban::MoveResult.error(code: :unknown, message: e.message)
550
+ end
551
+
552
+ handle_kanban_move_result(result, source_column: source_column, target_column: target_column.to_s, group_by_field: group_by_field, provider_class: provider_class)
553
+ end
554
+
555
+ def reorder
556
+ unless current_model_definition.positioned?
557
+ head :not_found
558
+ return
559
+ end
560
+
561
+ authorize @record, :update?
562
+ authorize_position_field!
563
+
564
+ stale = verify_list_version!
565
+ return if stale
566
+
567
+ position_value = parse_position_param
568
+ pos_field = current_model_definition.positioning_field
569
+ @record.update!(pos_field => position_value)
570
+
571
+ render json: {
572
+ position: @record.reload.send(pos_field),
573
+ list_version: compute_list_version(@record)
574
+ }
575
+ end
576
+
577
+ def reparent
578
+ unless current_model_definition.tree?
579
+ head :not_found
580
+ return
581
+ end
582
+
583
+ authorize @record, :update?
584
+ authorize_parent_field_writable!
585
+
586
+ stale = verify_tree_version!
587
+ return if stale
588
+
589
+ parent_field = current_model_definition.tree_parent_field
590
+ new_parent_id = parse_parent_id_param
591
+
592
+ @record.send("#{parent_field}=", new_parent_id)
593
+
594
+ # Optional position when tree is ordered
595
+ if params[:position].present? && current_model_definition.tree_ordered?
596
+ pos_field = current_model_definition.tree_position_field
597
+ @record.send("#{pos_field}=", parse_position_param)
598
+ end
599
+
600
+ if @record.save
601
+ render json: {
602
+ id: @record.id,
603
+ parent_id: @record[parent_field],
604
+ tree_version: compute_tree_version
605
+ }
606
+ else
607
+ render json: { errors: @record.errors.full_messages }, status: :unprocessable_content
608
+ end
609
+ end
610
+
611
+ def inline_create_form
612
+ target_model_name = params[:target_model]
613
+ return head(:bad_request) unless target_model_name.present?
614
+
615
+ target_presenter = find_presenter_for_inline_create(target_model_name)
616
+ return head(:not_found) unless target_presenter
617
+
618
+ target_model_def = LcpRuby.loader.model_definition(target_model_name)
619
+ target_class = LcpRuby.registry.model_for(target_model_name)
620
+ target_record = target_class.new
621
+
622
+ authorize_inline_target!(target_model_name, target_record, :create?)
623
+
624
+ layout_builder = Presenter::LayoutBuilder.new(target_presenter, target_model_def)
625
+ @inline_fields = inline_form_fields(layout_builder, target_model_def)
626
+
627
+ render partial: "lcp_ruby/resources/inline_create_form",
628
+ locals: { fields: @inline_fields, model_name: target_model_name },
629
+ layout: false
630
+ end
631
+
632
+ def inline_create
633
+ target_model_name = params[:target_model]
634
+ return head(:bad_request) unless target_model_name.present?
635
+
636
+ target_model_def = LcpRuby.loader.model_definition(target_model_name)
637
+ target_class = LcpRuby.registry.model_for(target_model_name)
638
+ target_record = target_class.new
639
+
640
+ authorize_inline_target!(target_model_name, target_record, :create?)
641
+
642
+ permitted = inline_create_params(target_model_def)
643
+ record = target_class.new(permitted)
644
+
645
+ if record.save
646
+ label_method = resolve_inline_label_method(target_model_def, params[:label_method])
647
+ render json: { id: record.id, label: resolve_label(record, label_method) }, status: :created
648
+ else
649
+ render json: { errors: record.errors.full_messages }, status: :unprocessable_content
650
+ end
651
+ end
652
+
653
+ private
654
+
655
+ # Authorizes a Pundit policy query against an arbitrary target model
656
+ # that is *different* from the current presenter's model. Goes around
657
+ # `Controller::Authorization#authorize` (which hardcodes
658
+ # `current_presenter.model` for policy lookup) by going through
659
+ # `PolicyFactory.policy_for(target_model_name)` directly. Logs denial
660
+ # via the standard channel and raises `Pundit::NotAuthorizedError` so
661
+ # the existing rescue_from chain renders 403/redirect uniformly.
662
+ # Calls `mark_authorized!` on success so `verify_authorized` is
663
+ # satisfied.
664
+ #
665
+ # @param target_model_name [String] LCP model name
666
+ # @param target_record [Object] record (or class) passed to
667
+ # `policy.new(current_user, target_record)` — use the class for
668
+ # class-level queries like `:index?`, an instance for
669
+ # record-level queries like `:show?` / `:create?`
670
+ # @param query [Symbol] policy query method (e.g. :index?, :create?)
671
+ # @param action_label [String] human-readable action name used in
672
+ # the denial log + raised error message
673
+ def authorize_target!(target_model_name, target_record, query, action_label:)
674
+ target_policy = Authorization::PolicyFactory
675
+ .policy_for(target_model_name)
676
+ .new(current_user, target_record)
677
+ if target_policy.public_send(query)
678
+ mark_authorized!
679
+ return
680
+ end
681
+
682
+ log_authorization_denied(
683
+ action: action_label,
684
+ resource: target_model_name,
685
+ detail: "#{action_label} #{query} on target denied"
686
+ )
687
+ raise Pundit::NotAuthorizedError, "#{action_label} #{query} on #{target_model_name} denied"
688
+ end
689
+
690
+ # Authorizes a CRUD action against an inline-create target (e.g. the
691
+ # inline-create dropdown creating an associated record).
692
+ def authorize_inline_target!(target_model_name, target_record, query)
693
+ authorize_target!(target_model_name, target_record, query, action_label: "inline")
694
+ end
695
+
696
+ # Authorizes a read against the target model behind a `/select_options`
697
+ # request. The user needs `:index?` on the association's target, not
698
+ # on the presenter model (the presenter access check is handled by
699
+ # `before_action :authorize_presenter_access`).
700
+ def authorize_select_options_target!(target_model_name)
701
+ target_class = LcpRuby.registry.model_for(target_model_name)
702
+ authorize_target!(target_model_name, target_class, :index?, action_label: "select_options")
703
+ end
704
+
705
+ # Returns an ActiveRecord::Relation scoped through the target model's
706
+ # Pundit Scope class for `current_user`. Used by `/select_options` and
707
+ # reverse-cascade `ancestors_for` lookups so that records the user
708
+ # cannot see never appear in dropdown options or ancestor chains.
709
+ def scoped_for_user(target_model_name, target_class)
710
+ Authorization::ScopeResolver.resolve(current_user, target_class, target_model_name)
711
+ end
712
+
713
+ # Raw `.json` / `.csv` index/show endpoints. See docs/design/raw_data_endpoints.md.
714
+ def raw_index_unsupported?
715
+ return true if current_page&.standalone? && current_page&.grid?
716
+ return true if current_page&.composite?
717
+ return true if api_model?
718
+ return true if dialog_context?
719
+
720
+ false
721
+ end
722
+
723
+ def raw_show_unsupported?
724
+ return true if current_page&.composite?
725
+ return true if api_model?
726
+ return true if dialog_context?
727
+
728
+ false
729
+ end
730
+
731
+ def render_raw_index
732
+ authorize @model_class
733
+ scope = policy_scope(@model_class)
734
+ scope = apply_soft_delete_scope(scope)
735
+ apply_default_saved_filter!
736
+ load_flat_index(scope)
737
+
738
+ allowed = @column_set.visible_table_columns.map { |c| c["field"] }.compact
739
+ requested = parse_fields_param(params[:fields])
740
+ return unless validate_fields_subset(requested, allowed)
741
+
742
+ selected = requested || allowed
743
+ generator = Export::DataGenerator.new(
744
+ records: @records,
745
+ selected_fields: selected,
746
+ model_definition: current_model_definition,
747
+ evaluator: current_evaluator,
748
+ raw: true
749
+ )
750
+
751
+ envelope = ActiveModel::Type::Boolean.new.cast(params[:envelope])
752
+ send_raw_payload(generator, base_filename: current_presenter.name, envelope: envelope)
753
+ end
754
+
755
+ def render_raw_show
756
+ # Mirror the HTML show pipeline: VC values must be resolved before the
757
+ # generator runs, and association field paths (`?fields=author.name`)
758
+ # need eager-loading to avoid N+1.
759
+ load_show_virtual_columns
760
+ preload_associations(@record, :show)
761
+
762
+ allowed = readable_show_field_paths
763
+ requested = parse_fields_param(params[:fields])
764
+ return unless validate_fields_subset(requested, allowed)
765
+
766
+ selected = requested || allowed
767
+ generator = Export::DataGenerator.new(
768
+ records: [ @record ],
769
+ selected_fields: selected,
770
+ model_definition: current_model_definition,
771
+ evaluator: current_evaluator,
772
+ raw: true
773
+ )
774
+
775
+ send_raw_payload(generator, base_filename: "#{current_presenter.name}-#{@record.id}", single: true)
776
+ end
777
+
778
+ def readable_show_field_paths
779
+ # Virtual columns aren't in `all_field_names`, so `field_readable?`
780
+ # always rejects them. Include them unconditionally — they are computed
781
+ # model expressions, not user-managed data.
782
+ concrete = current_evaluator.readable_fields.select { |name| current_evaluator.field_readable?(name) }
783
+ (concrete + current_model_definition.virtual_column_names).uniq
784
+ end
785
+
786
+ def parse_fields_param(raw)
787
+ return nil if raw.blank?
788
+
789
+ tokens = raw.to_s.split(",").map(&:strip).reject(&:empty?).uniq
790
+ tokens.empty? ? nil : tokens
791
+ end
792
+
793
+ def validate_fields_subset(requested, allowed)
794
+ return true if requested.nil?
795
+
796
+ unknown = requested - allowed
797
+ return true if unknown.empty?
798
+
799
+ message = I18n.t("lcp_ruby.errors.unknown_fields",
800
+ fields: unknown.join(", "),
801
+ default: "Unknown field(s): %{fields}")
802
+ render json: { error: message }, status: :bad_request
803
+ false
804
+ end
805
+
806
+ def send_raw_payload(generator, base_filename:, single: false, envelope: false)
807
+ respond_to do |format|
808
+ format.json do
809
+ payload = generator.to_json_array
810
+ if single
811
+ render json: payload.first
812
+ elsif envelope
813
+ render json: {
814
+ data: payload,
815
+ meta: {
816
+ total: @records.total_count,
817
+ page: @records.current_page,
818
+ per_page: @records.limit_value,
819
+ total_pages: @records.total_pages
820
+ }
821
+ }
822
+ else
823
+ render json: payload
824
+ end
825
+ end
826
+ format.csv do
827
+ send_data generator.to_csv,
828
+ type: :csv,
829
+ filename: "#{base_filename}.csv",
830
+ disposition: "attachment"
831
+ end
832
+ end
833
+ end
834
+
835
+ def resolve_transition_label(transition_name)
836
+ workflow_def = Workflow::Registry.workflow_for_model(current_model_definition.name)
837
+ return transition_name.to_s.humanize unless workflow_def
838
+
839
+ transition = workflow_def.transition(transition_name) || transition_name
840
+ Workflow::TransitionLabelResolver.resolve(workflow_def.name, transition)
841
+ end
842
+
843
+ def render_create_failure
844
+ @form_actions = current_form_actions(@record)
845
+
846
+ if dialog_context?
847
+ return render_dialog_form_with_errors
848
+ end
849
+
850
+ @layout_builder = Presenter::LayoutBuilder.new(current_presenter, current_model_definition)
851
+ render :new, status: :unprocessable_content
852
+ end
853
+
854
+ def render_update_failure
855
+ @form_actions = current_form_actions(@record)
856
+
857
+ if dialog_context?
858
+ return render_dialog_form_with_errors
859
+ end
860
+
861
+ if current_page&.form_zone
862
+ load_composite_page_for_form_error
863
+ render :show, status: :unprocessable_content
864
+ else
865
+ @layout_builder = Presenter::LayoutBuilder.new(current_presenter, current_model_definition)
866
+ load_dirty_record_virtual_columns
867
+ render :edit, status: :unprocessable_content
868
+ end
869
+ end
870
+
871
+ def collect_field_paths(nodes, prefix = "")
872
+ nodes.flat_map do |node|
873
+ name = (node[:name] || node["name"]).to_s
874
+ type = (node[:type] || node["type"]).to_s
875
+ if type == "group"
876
+ children = node[:children] || node["children"] || []
877
+ # Custom fields group is a UI grouping only — field paths stay flat
878
+ is_custom_fields_group = children.any? { |c| c[:custom_field] || c["custom_field"] }
879
+ child_prefix = if is_custom_fields_group
880
+ prefix
881
+ else
882
+ prefix.empty? ? name : "#{prefix}.#{name}"
883
+ end
884
+ collect_field_paths(children, child_prefix)
885
+ else
886
+ prefix.empty? ? name : "#{prefix}.#{name}"
887
+ end
888
+ end
889
+ end
890
+
891
+ def warn_missing_presenter_config(context)
892
+ presenter = current_presenter
893
+ return unless presenter
894
+
895
+ case context
896
+ when :show
897
+ unless presenter.has_show_sections?
898
+ Rails.logger.warn("[LcpRuby] Presenter '#{presenter.name}' is rendering show page " \
899
+ "but has no show layout configured. The page will be empty or show only custom fields.")
900
+ end
901
+ when :form
902
+ form_sections = presenter.form_config["sections"]
903
+ unless form_sections.is_a?(Array) && form_sections.any?
904
+ Rails.logger.warn("[LcpRuby] Presenter '#{presenter.name}' is rendering a form " \
905
+ "but has no form sections configured. The form will be empty or show only custom fields.")
906
+ end
907
+ end
908
+ end
909
+
910
+ def load_api_index
911
+ # Translate filter params for API data source
912
+ field_names = current_model_definition.fields.map(&:name)
913
+ # SECURITY: to_unsafe_h needed for dynamic filter keys. Validated by ApiFilterTranslator against field_names.
914
+ raw_filters = params[:f]&.to_unsafe_h || {}
915
+ supported_ops = @model_class.lcp_data_source&.supported_operators
916
+
917
+ filters = DataSource::ApiFilterTranslator.translate(
918
+ raw_filters,
919
+ field_names: field_names,
920
+ supported_operators: supported_ops
921
+ )
922
+
923
+ # Sort
924
+ sort = if current_sort_field.present?
925
+ { field: current_sort_field, direction: current_sort_direction }
926
+ end
927
+
928
+ page = (params[:page] || 1).to_i
929
+ per = effective_per_page
930
+
931
+ @api_result = @model_class.lcp_search(filters: filters, sort: sort, page: page, per: per)
932
+
933
+ # Wrap in Kaminari-compatible array for view pagination
934
+ @records = Kaminari.paginate_array(
935
+ @api_result.to_a,
936
+ total_count: @api_result.total_count
937
+ ).page(page).per(per)
938
+
939
+ setup_index_view_objects
940
+ end
941
+
942
+ def load_composite_page
943
+ @active_tab = params[:tab]
944
+
945
+ # Set up main zone view objects
946
+ main_zone = current_page.main_zone
947
+ if main_zone&.form_zone?
948
+ # Form zone: authorize edit, set up form context
949
+ authorize @record, :edit?
950
+ main_presenter = LcpRuby.loader.presenter_definition(main_zone.presenter)
951
+ @layout_builder = Presenter::LayoutBuilder.new(main_presenter, current_model_definition)
952
+ @action_set = Presenter::ActionSet.new(main_presenter, current_evaluator, context: condition_context)
953
+ @form_actions = current_form_actions(@record)
954
+ load_edit_virtual_columns
955
+ preload_associations(@record, :form)
956
+ @record.strict_loading! if LcpRuby.configuration.strict_loading_enabled?
957
+ elsif main_zone&.presenter_zone?
958
+ main_presenter = LcpRuby.loader.presenter_definition(main_zone.presenter)
959
+ setup_show_view_objects(main_presenter)
960
+ load_show_virtual_columns
961
+ preload_associations(@record, :show)
962
+ @record.strict_loading! if LcpRuby.configuration.strict_loading_enabled?
963
+ end
964
+
965
+ load_secondary_zones(main_zone)
966
+ end
967
+
968
+ # Re-renders the composite page after a form zone validation error.
969
+ # Uses dirty VCs (singleton methods) to preserve @record's errors and changed attributes.
970
+ def load_composite_page_for_form_error
971
+ @active_tab = params[:tab]
972
+
973
+ main_zone = current_page.main_zone
974
+ main_presenter = LcpRuby.loader.presenter_definition(main_zone.presenter)
975
+ @layout_builder = Presenter::LayoutBuilder.new(main_presenter, current_model_definition)
976
+ @action_set = Presenter::ActionSet.new(main_presenter, current_evaluator, context: condition_context)
977
+ @form_actions = current_form_actions(@record)
978
+ load_dirty_record_virtual_columns
979
+
980
+ load_secondary_zones(main_zone)
981
+ end
982
+
983
+ def load_index_composite
984
+ build_filter_form(@index_composite_page)
985
+
986
+ # Master-detail: extract selection and active tab from URL params
987
+ if @index_composite_page.master_detail?
988
+ @selected_id = params[:selected].presence
989
+ @active_tab = params[:tab].presence
990
+ end
991
+
992
+ scope = policy_scope(@model_class)
993
+ scope = apply_soft_delete_scope(scope)
994
+
995
+ # Apply filter_form: + scope_filters: to the main index scope.
996
+ # No-op when the page has no filter_form / scope_filters declarations.
997
+ scope = Widgets::ScopeApplicator.apply_filter_form(scope, @model_class, @filter_form)
998
+ scope = Widgets::ScopeApplicator.apply_scope_filters(scope, @model_class, @scope_filter_set)
999
+
1000
+ apply_default_saved_filter!
1001
+
1002
+ case current_presenter.index_layout
1003
+ when :tree
1004
+ load_tree_index(scope) if current_model_definition.tree?
1005
+ when :tiles
1006
+ load_flat_index(scope)
1007
+ else
1008
+ load_flat_index(scope)
1009
+ end
1010
+
1011
+ # For master-detail, the selection zone is the "primary" (rendered by the index pipeline),
1012
+ # so exclude it from secondary zones. For standard index composites, exclude the main zone.
1013
+ primary_zone = if @index_composite_page.master_detail?
1014
+ @index_composite_page.selection_zone
1015
+ else
1016
+ @index_composite_page.main_zone
1017
+ end
1018
+ load_secondary_zones(primary_zone, page: @index_composite_page)
1019
+ end
1020
+
1021
+ # Find an explicit index composite page for the current model
1022
+ def find_index_composite_for_model
1023
+ return nil unless current_page&.model
1024
+
1025
+ # Only search for index composites when on the primary page of a view group
1026
+ # (or no view group). Non-primary views (e.g., tiles alternative) must not be hijacked.
1027
+ vg = current_view_group
1028
+ return nil if vg && !vg.primary_page?(current_page.name)
1029
+
1030
+ LcpRuby.loader.page_definitions.each_value do |page|
1031
+ next if page == current_page
1032
+ next unless page.model == current_page.model
1033
+
1034
+ return page if page.index_composite?
1035
+ end
1036
+ nil
1037
+ end
1038
+
1039
+ # Builds the per-request FilterForm + ScopeFilterSet for the
1040
+ # current page. Both are nil if the page declares no filters,
1041
+ # letting downstream code short-circuit with `@filter_form&.any?`.
1042
+ def build_filter_form(page)
1043
+ return unless page
1044
+
1045
+ @filter_form = Pages::FilterForm.new(
1046
+ page: page, params: params, current_user: current_user
1047
+ ) if page.respond_to?(:filter_form_definition) && page.filter_form_definition.any?
1048
+
1049
+ @scope_filter_set = Pages::ScopeFilterSet.new(
1050
+ page: page, params: params
1051
+ ) if page.respond_to?(:scope_filter_definitions) && page.scope_filter_definitions.any?
1052
+ end
1053
+
1054
+ def broadcast_model_change(model_name)
1055
+ return unless defined?(Turbo::StreamsChannel) && defined?(ActionCable)
1056
+
1057
+ Turbo::StreamsChannel.broadcast_refresh_to("lcp_model_#{model_name}")
1058
+ rescue StandardError => e
1059
+ raise unless Rails.env.production?
1060
+ LcpRuby.record_error(e, subsystem: "broadcast", model: model_name)
1061
+ end
1062
+
1063
+ def load_dashboard
1064
+ build_filter_form(current_page)
1065
+
1066
+ @zone_data = {}
1067
+ current_page.zones.each do |zone|
1068
+ next unless zone.widget? || zone.presenter_zone?
1069
+ next unless zone_accessible?(zone)
1070
+
1071
+ @zone_data[zone] = resolve_zone_data(zone)
1072
+ end
1073
+ end
1074
+
1075
+ def load_flat_index(scope)
1076
+ scope = apply_advanced_search(scope)
1077
+ scope = apply_sort(scope)
1078
+ scope = apply_virtual_columns(scope)
1079
+
1080
+ # Load saved filters for the UI
1081
+ if SavedFilters::Registry.available? && current_presenter.saved_filters_enabled?
1082
+ @saved_filters = SavedFilters::Resolver.visible_filters(
1083
+ presenter_name: current_presenter.name,
1084
+ user: current_user,
1085
+ evaluator: current_evaluator
1086
+ )
1087
+ @slot_locals = (@slot_locals || {}).merge(
1088
+ saved_filters: @saved_filters,
1089
+ active_saved_filter: @active_saved_filter,
1090
+ saved_filter_warnings: @saved_filter_warnings
1091
+ )
1092
+ end
1093
+
1094
+ if @filter_form&.any?
1095
+ @slot_locals = (@slot_locals || {}).merge(
1096
+ page_parameter_fields: @filter_form.fields.map(&:name)
1097
+ )
1098
+ end
1099
+
1100
+ @summaries = compute_summaries(scope) if summary_columns_present?
1101
+ compute_summary_bar(scope) if current_presenter.summary_enabled?
1102
+
1103
+ strategy = resolve_loading_strategy(:index)
1104
+ scope = strategy.apply(scope)
1105
+ scope = scope.strict_loading if LcpRuby.configuration.strict_loading_enabled?
1106
+
1107
+ @records = scope.page(params[:page]).per(effective_per_page)
1108
+
1109
+ # Batch-preload API associations after AR scope materializes
1110
+ strategy.apply_api_preloads(@records.to_a) if strategy.api_preloads.any?
1111
+
1112
+ # GROUP BY scopes break Kaminari's .count (returns Hash instead of Integer).
1113
+ # Pre-compute total via a clean scope and inject it.
1114
+ if @has_grouped_virtual_columns
1115
+ total = scope.except(:select, :group, :joins).distinct.count(:id)
1116
+ @records.define_singleton_method(:total_count) { total }
1117
+ end
1118
+
1119
+ setup_index_view_objects
1120
+ end
1121
+
1122
+ # Maps a Kanban::MoveResult to the HTTP response shape declared in
1123
+ # docs/design/kanban_view.md → "Drag endpoint". On success: recompute
1124
+ # counts for source + target columns only, broadcast model change, and
1125
+ # emit `kanban.card_moved` instrumentation.
1126
+ def handle_kanban_move_result(result, source_column:, target_column:, group_by_field:, provider_class:)
1127
+ if result.ok?
1128
+ counts = recompute_kanban_counts(source: source_column, target: target_column, group_by_field: group_by_field)
1129
+ broadcast_model_change(current_model_definition.name)
1130
+ ActiveSupport::Notifications.instrument(
1131
+ "kanban.card_moved",
1132
+ model: current_model_definition.name,
1133
+ from_col: source_column,
1134
+ to_col: target_column,
1135
+ user_id: current_user&.id,
1136
+ provider: provider_class.name
1137
+ )
1138
+ render json: {
1139
+ ok: true,
1140
+ record_id: @record.id,
1141
+ column: target_column,
1142
+ counts: counts
1143
+ }
1144
+ return
1145
+ end
1146
+
1147
+ case result.code
1148
+ when :comment_required
1149
+ render json: { error: "comment_required", transition_name: result.details[:transition_name], message: result.message }, status: :unprocessable_content
1150
+ when :no_transition, :guard_failed, :wip_exceeded, :field_readonly, :validation, :not_allowed
1151
+ render json: { error: result.code.to_s, message: result.message, details: result.details }, status: :unprocessable_content
1152
+ when :forbidden
1153
+ render json: { error: "forbidden", message: result.message }, status: :forbidden
1154
+ when :conflict
1155
+ render json: { error: "conflict", message: result.message }, status: :conflict
1156
+ else
1157
+ render json: { error: "unknown", message: result.message }, status: :internal_server_error
1158
+ end
1159
+ end
1160
+
1161
+ # Recomputes column counts after a successful move. Per spec: only the
1162
+ # affected columns (source + target) — not the whole board.
1163
+ def recompute_kanban_counts(source:, target:, group_by_field:)
1164
+ return {} if group_by_field.blank?
1165
+
1166
+ affected = [ source, target ].compact.uniq.reject(&:blank?)
1167
+ return {} if affected.empty?
1168
+
1169
+ scope = policy_scope(@model_class)
1170
+ scope = apply_soft_delete_scope(scope)
1171
+ grouped = scope.where(group_by_field => affected).group(group_by_field).count
1172
+ affected.each_with_object({}) { |val, h| h[val.to_s] = grouped[val].to_i }
1173
+ rescue StandardError => e
1174
+ raise unless Rails.env.production?
1175
+ LcpRuby.record_error(e, subsystem: "kanban_counts") if LcpRuby.respond_to?(:record_error)
1176
+ {}
1177
+ end
1178
+
1179
+ # Resolves the configured provider, applies the filter pipeline + the
1180
+ # configured eager-loading strategy, then asks the provider for `columns`
1181
+ # and `fetch_records`. Builds `@kanban_board` for `_kanban_index.html.erb`.
1182
+ # When `params[:kanban_column]` is present, renders only that column's
1183
+ # Turbo Frame ("Load more" pagination contract).
1184
+ def load_kanban_index(scope)
1185
+ scope = apply_advanced_search(scope)
1186
+ scope = apply_sort(scope)
1187
+ scope = apply_virtual_columns(scope)
1188
+
1189
+ strategy = resolve_loading_strategy(:index)
1190
+ scope = strategy.apply(scope)
1191
+ scope = scope.strict_loading if LcpRuby.configuration.strict_loading_enabled?
1192
+
1193
+ provider_class = kanban_provider_class
1194
+ @kanban_provider = build_kanban_provider(provider_class)
1195
+
1196
+ context = {
1197
+ params: params.to_unsafe_h,
1198
+ locale: I18n.locale,
1199
+ saved_filter: @active_saved_filter,
1200
+ view_group: @active_view_group,
1201
+ presenter_slug: current_presenter.slug,
1202
+ kanban_column: params[:kanban_column].presence,
1203
+ kanban_page: params[:kanban_page].presence&.to_i
1204
+ }
1205
+
1206
+ all_columns = @kanban_provider.columns(scope: scope, user: current_user, context: context)
1207
+
1208
+ cols_to_fetch =
1209
+ if context[:kanban_column]
1210
+ col = all_columns.find { |c| c.value.to_s == context[:kanban_column].to_s }
1211
+ [ col ].compact
1212
+ else
1213
+ all_columns
1214
+ end
1215
+
1216
+ records_by_value = if cols_to_fetch.empty?
1217
+ {}
1218
+ else
1219
+ @kanban_provider.fetch_records(
1220
+ scope: scope,
1221
+ columns: cols_to_fetch,
1222
+ user: current_user,
1223
+ context: context
1224
+ )
1225
+ end
1226
+
1227
+ if strategy.api_preloads.any?
1228
+ all_records = records_by_value.values.flatten
1229
+ strategy.apply_api_preloads(all_records) if all_records.any?
1230
+ end
1231
+
1232
+ cells = records_by_value.each_with_object({}) do |(col_value, records), h|
1233
+ h[[ col_value, :_default ]] = records
1234
+ end
1235
+
1236
+ @kanban_board = LcpRuby::Kanban::Board.new(
1237
+ columns: all_columns,
1238
+ cells: cells,
1239
+ provider: @kanban_provider
1240
+ )
1241
+ @kanban_columns_to_render = cols_to_fetch
1242
+ @kanban_partial_request = context[:kanban_column].present?
1243
+
1244
+ setup_index_view_objects
1245
+
1246
+ # Lazy-load contract: when ?kanban_column=<value> is set, return ONLY
1247
+ # that column's Turbo Frame, without page chrome.
1248
+ if @kanban_partial_request
1249
+ target = cols_to_fetch.first
1250
+ if target
1251
+ render partial: "lcp_ruby/resources/kanban_column",
1252
+ locals: { column: target, board: @kanban_board },
1253
+ layout: false
1254
+ else
1255
+ head :not_found
1256
+ end
1257
+ end
1258
+ end
1259
+
1260
+ def kanban_provider_class
1261
+ current_presenter.kanban_config["provider"]&.safe_constantize \
1262
+ || LcpRuby::Kanban::DefaultProvider
1263
+ end
1264
+
1265
+ def build_kanban_provider(provider_class)
1266
+ provider_class.new(
1267
+ config: current_presenter.kanban_config,
1268
+ model_definition: current_model_definition
1269
+ )
1270
+ end
1271
+
1272
+ def load_grouped_index(scope)
1273
+ scope = apply_advanced_search(scope)
1274
+ scope = GroupedQuery::Builder.apply(scope, current_presenter, current_model_definition)
1275
+ scope = apply_grouped_sort(scope)
1276
+
1277
+ if current_presenter.group_limit
1278
+ @records = scope.limit(current_presenter.group_limit).to_a
1279
+ @records = GroupedQuery::ResultWrapper.wrap(@records, current_presenter, current_model_definition)
1280
+ @grouped_limited = true
1281
+ else
1282
+ # Kaminari GROUP BY fix: count groups via subquery instead of .count.length
1283
+ paginated = scope.page(params[:page]).per(effective_per_page)
1284
+ count_scope = scope.except(:select, :order)
1285
+ total_groups = scope.connection.select_value(
1286
+ "SELECT COUNT(*) FROM (#{count_scope.select('1').to_sql}) AS grouped_count"
1287
+ ).to_i
1288
+ paginated.define_singleton_method(:total_count) { total_groups }
1289
+
1290
+ @records = GroupedQuery::ResultWrapper.wrap(paginated, current_presenter, current_model_definition)
1291
+
1292
+ # Restore Kaminari pagination methods lost during Array wrapping
1293
+ cp = paginated.current_page
1294
+ lv = paginated.limit_value
1295
+ tp = paginated.total_pages
1296
+ tg = total_groups
1297
+ @records.define_singleton_method(:current_page) { cp }
1298
+ @records.define_singleton_method(:limit_value) { lv }
1299
+ @records.define_singleton_method(:total_pages) { tp }
1300
+ @records.define_singleton_method(:total_count) { tg }
1301
+ end
1302
+
1303
+ setup_index_view_objects
1304
+ @fk_map = {}
1305
+ end
1306
+
1307
+ def apply_grouped_sort(scope)
1308
+ sort_field = params[:sort] || current_presenter.index_config.dig("default_sort", "field")
1309
+ return scope unless sort_field
1310
+
1311
+ direction = params[:direction] || current_presenter.index_config.dig("default_sort", "direction") || "asc"
1312
+ direction = "asc" unless %w[asc desc].include?(direction.to_s.downcase)
1313
+
1314
+ # Validate sort field is a defined column
1315
+ valid_columns = current_presenter.table_columns.map { |c| c["field"] }
1316
+ return scope unless valid_columns.include?(sort_field.to_s)
1317
+
1318
+ conn = scope.connection
1319
+ strategy = Sort::EnumSortOrder.resolve_strategy(sort_field, current_presenter, current_model_definition)
1320
+ if strategy == "definition_order"
1321
+ col_config = current_presenter.table_columns.find { |c| c["field"] == sort_field.to_s }
1322
+ source_field = col_config&.dig("group_field") || sort_field.to_s
1323
+ enum_field = Sort::EnumSortOrder.enum_field_for(source_field, current_model_definition)
1324
+ case_sql = Sort::EnumSortOrder.case_when_sql(enum_field, current_model_definition.table_name, conn)
1325
+ scope.order(Arel.sql("#{case_sql} #{direction}"))
1326
+ else
1327
+ scope.order(Arel.sql("#{conn.quote_column_name(sort_field)} #{direction}"))
1328
+ end
1329
+ end
1330
+
1331
+ def load_tree_index(scope)
1332
+ parent_field = current_model_definition.tree_parent_field
1333
+ scope = scope.all unless scope.is_a?(ActiveRecord::Relation)
1334
+ scope = apply_virtual_columns(scope)
1335
+
1336
+ # Detect if search is active
1337
+ @search_active = search_active?
1338
+
1339
+ if @search_active
1340
+ # Run search pipeline to get matching IDs
1341
+ filtered_scope = apply_advanced_search(scope)
1342
+ filtered_scope = apply_sort(filtered_scope)
1343
+ @match_ids = Set.new(filtered_scope.pluck(:id))
1344
+
1345
+ # Load all records to build complete tree with ancestor context
1346
+ all_records = scope.to_a
1347
+ @children_map, @roots = build_filtered_tree(all_records, @match_ids, parent_field)
1348
+ else
1349
+ # Load all records (no pagination for tree view)
1350
+ sorted = apply_sort(scope)
1351
+ sorted = sorted.all unless sorted.is_a?(ActiveRecord::Relation)
1352
+ all_records = sorted.to_a
1353
+ @children_map = all_records.group_by { |r| r[parent_field] }
1354
+ @roots = @children_map[nil] || []
1355
+ @match_ids = nil
1356
+ end
1357
+
1358
+ @records = all_records
1359
+
1360
+ @tree_version = compute_tree_version if current_presenter.reparentable?
1361
+
1362
+ # Precompute subtree IDs from in-memory children_map (avoids N+1 CTE queries per row)
1363
+ if current_presenter.reparentable?
1364
+ @subtree_ids_map = precompute_subtree_ids(@children_map)
1365
+ end
1366
+
1367
+ setup_index_view_objects
1368
+ end
1369
+
1370
+ # Inject virtual column subqueries/expressions into the scope for index/tree views.
1371
+ def apply_virtual_columns(scope)
1372
+ vc_names = collect_virtual_column_names(:index)
1373
+ return scope if vc_names.empty?
1374
+
1375
+ scope, _service_only, @has_grouped_virtual_columns = VirtualColumns::Builder.apply(
1376
+ scope, current_model_definition, vc_names.to_a, current_user: current_user
1377
+ )
1378
+
1379
+ scope
1380
+ end
1381
+
1382
+ # Collect virtual column names needed for a given context.
1383
+ def collect_virtual_column_names(context)
1384
+ VirtualColumns::Collector.collect(
1385
+ presenter_def: current_presenter,
1386
+ model_def: current_model_definition,
1387
+ context: context,
1388
+ sort_field: context == :index ? params[:sort] : nil
1389
+ )
1390
+ end
1391
+
1392
+ def effective_per_page
1393
+ options = current_presenter.per_page_options
1394
+ if options.is_a?(Array) && params[:per_page].present?
1395
+ requested = params[:per_page].to_i
1396
+ return requested if options.include?(requested)
1397
+ end
1398
+ current_presenter.per_page
1399
+ end
1400
+
1401
+ def compute_summary_bar(scope)
1402
+ fields = current_presenter.summary_config["fields"]
1403
+ return unless fields.is_a?(Array) && fields.any?
1404
+
1405
+ col_names = @model_class.column_names
1406
+ summary_bar = fields.filter_map do |field_config|
1407
+ field_name = field_config["field"]
1408
+ function = field_config["function"]
1409
+ next unless field_name && function
1410
+ next unless col_names.include?(field_name)
1411
+
1412
+ # Strip aggregate subquery columns that would produce invalid SQL
1413
+ # inside COUNT/SUM/etc.
1414
+ plain = scope.unscope(:select)
1415
+ value = case function
1416
+ when "sum" then plain.sum(field_name)
1417
+ when "avg" then plain.average(field_name)
1418
+ when "count" then plain.count
1419
+ when "min" then plain.minimum(field_name)
1420
+ when "max" then plain.maximum(field_name)
1421
+ end
1422
+
1423
+ { value: value, function: function, config: field_config }
1424
+ end
1425
+
1426
+ @slot_locals = (@slot_locals || {}).merge(summary_bar: summary_bar)
1427
+ end
1428
+
1429
+ # Load virtual column values for a show page record.
1430
+ def load_show_virtual_columns
1431
+ load_record_virtual_columns(:show)
1432
+ end
1433
+
1434
+ # Load virtual column values for an edit page record.
1435
+ def load_edit_virtual_columns
1436
+ load_record_virtual_columns(:edit)
1437
+ end
1438
+
1439
+ # Load virtual columns onto a dirty record (update validation failure).
1440
+ # Loads a clean record with VC expressions and copies values as singleton methods.
1441
+ def load_dirty_record_virtual_columns
1442
+ load_record_virtual_columns(:edit, dirty: true)
1443
+ end
1444
+
1445
+ # Shared loader: re-queries @record with VC subqueries and resolves service VCs.
1446
+ # When dirty: true, copies values as singleton methods instead of replacing @record.
1447
+ def load_record_virtual_columns(context, dirty: false)
1448
+ vc_names = collect_virtual_column_names(context)
1449
+ return if vc_names.empty?
1450
+
1451
+ scope = @model_class.where(id: @record.id)
1452
+ scope, service_only, _grouped = VirtualColumns::Builder.apply(
1453
+ scope, current_model_definition, vc_names.to_a, current_user: current_user
1454
+ )
1455
+
1456
+ clean_record = scope.first
1457
+
1458
+ if dirty
1459
+ (vc_names - service_only.to_set).each do |name|
1460
+ value = clean_record&.public_send(name)
1461
+ @record.define_singleton_method(name) { value }
1462
+ end
1463
+ elsif clean_record
1464
+ @record = clean_record
1465
+ end
1466
+
1467
+ resolve_service_virtual_columns(@record, service_only)
1468
+ end
1469
+
1470
+ # Compute service virtual column values and define singleton methods on the record.
1471
+ def resolve_service_virtual_columns(record, service_names)
1472
+ return if service_names.blank?
1473
+
1474
+ service_names.each do |vc_name|
1475
+ vc_def = current_model_definition.virtual_column(vc_name)
1476
+ next unless vc_def
1477
+
1478
+ service = Services::Registry.lookup_vc_service(vc_def.service)
1479
+ next unless service
1480
+
1481
+ value = service.call(record, options: vc_def.options)
1482
+ record.define_singleton_method(vc_name) { value }
1483
+ end
1484
+ end
1485
+
1486
+ def search_active?
1487
+ params[:qs].present? || params[:f].present? || params[:filter].present? || params[:cf].present?
1488
+ end
1489
+
1490
+ def build_filtered_tree(all_records, match_ids, parent_field)
1491
+ # Build lookup
1492
+ by_id = all_records.index_by(&:id)
1493
+
1494
+ # Collect ancestor IDs for all matching records
1495
+ ancestor_ids = Set.new
1496
+ match_ids.each do |mid|
1497
+ record = by_id[mid]
1498
+ next unless record
1499
+
1500
+ pid = record[parent_field]
1501
+ while pid.present? && !ancestor_ids.include?(pid)
1502
+ ancestor_ids << pid
1503
+ parent_record = by_id[pid]
1504
+ break unless parent_record
1505
+ pid = parent_record[parent_field]
1506
+ end
1507
+ end
1508
+
1509
+ display_ids = match_ids | ancestor_ids
1510
+ display_records = all_records.select { |r| display_ids.include?(r.id) }
1511
+
1512
+ children_map = display_records.group_by { |r| r[parent_field] }
1513
+ roots = children_map[nil] || []
1514
+
1515
+ [ children_map, roots ]
1516
+ end
1517
+
1518
+ # Build a Hash { record_id => "id1,id2,..." } of subtree IDs from the in-memory children_map.
1519
+ # This replaces per-row `record.subtree_ids` calls that each fire a recursive CTE query.
1520
+ def precompute_subtree_ids(children_map)
1521
+ result = {}
1522
+ collect_subtree = ->(record_id) do
1523
+ ids = [ record_id ]
1524
+ (children_map[record_id] || []).each { |child| ids.concat(collect_subtree.call(child.id)) }
1525
+ ids
1526
+ end
1527
+ children_map.each_value do |records|
1528
+ records.each do |record|
1529
+ result[record.id] = collect_subtree.call(record.id).join(",") unless result.key?(record.id)
1530
+ end
1531
+ end
1532
+ result
1533
+ end
1534
+
1535
+ def compute_tree_version
1536
+ parent_field = current_model_definition.tree_parent_field
1537
+ pairs = @model_class.order(:id).pluck(:id, parent_field)
1538
+ Digest::SHA256.hexdigest(pairs.map { |id, pid| "#{id}:#{pid}" }.join(","))
1539
+ end
1540
+
1541
+ def no_explicit_filter_params?
1542
+ params[:f].blank? && params[:filter].blank? && params[:qs].blank? &&
1543
+ params[:saved_filter].blank? && params[:scope].blank?
1544
+ end
1545
+
1546
+ # Resolve the user's default saved filter (if any) and set params[:saved_filter]
1547
+ # so the downstream pipeline applies it. Shared by HTML and raw `.json` / `.csv` paths.
1548
+ def apply_default_saved_filter!
1549
+ return unless no_explicit_filter_params?
1550
+ return unless SavedFilters::Registry.available? && current_presenter.saved_filters_enabled?
1551
+
1552
+ default = SavedFilters::Resolver.default_filter_for(
1553
+ presenter_name: current_presenter.name,
1554
+ user: current_user,
1555
+ evaluator: current_evaluator
1556
+ )
1557
+ params[:saved_filter] = default.id.to_s if default
1558
+ end
1559
+
1560
+ VALID_REDIRECT_TARGETS = %w[index show edit new].freeze
1561
+
1562
+ def redirect_path_for(action, record)
1563
+ target = current_presenter.options&.dig("redirect_after", action)
1564
+
1565
+ if target && !VALID_REDIRECT_TARGETS.include?(target)
1566
+ Rails.logger.warn("[LcpRuby] Invalid redirect_after target '#{target}' for action '#{action}' " \
1567
+ "in presenter '#{current_presenter.name}', falling back to 'show'")
1568
+ target = nil
1569
+ end
1570
+
1571
+ case target
1572
+ when "index" then resources_path
1573
+ when "show" then resource_path(record)
1574
+ when "edit" then edit_resource_path(record)
1575
+ when "new" then new_resource_path
1576
+ else resource_path(record)
1577
+ end
1578
+ end
1579
+
1580
+ def purge_removed_attachments!(record)
1581
+ current_model_definition.fields.select(&:attachment?).each do |field|
1582
+ remove_key = "remove_#{field.name}"
1583
+ next unless params[:record]&.dig(remove_key) == "1"
1584
+
1585
+ attachment = record.send(field.name)
1586
+ attachment.purge if attachment.attached?
1587
+ end
1588
+ end
1589
+
1590
+ def find_form_field_config(field_name)
1591
+ return nil unless field_name.present?
1592
+
1593
+ if current_presenter
1594
+ layout_builder = Presenter::LayoutBuilder.new(current_presenter, current_model_definition)
1595
+ config = layout_builder.form_sections
1596
+ .flat_map { |s| s["fields"] || [] }
1597
+ .find { |f| f["field"] == field_name }
1598
+ return config if config
1599
+
1600
+ sp_config = find_search_parameter_field_config(field_name)
1601
+ return sp_config if sp_config
1602
+ end
1603
+
1604
+ # Filter-form fallback (PR 5): dashboards have no current_presenter,
1605
+ # so the lookup walks current_page.filter_form_definition via the
1606
+ # FilterForm enrichment helper. Spec § 5 step 12 (additive
1607
+ # fallback chain entry — does not displace the presenter or
1608
+ # search-parameter sources).
1609
+ find_filter_form_field_config(field_name)
1610
+ end
1611
+
1612
+ def find_filter_form_field_config(field_name)
1613
+ return nil unless current_page&.respond_to?(:filter_form_definition)
1614
+ return nil if current_page.filter_form_definition.blank?
1615
+
1616
+ ff = @filter_form ||= Pages::FilterForm.new(
1617
+ page: current_page, params: params, current_user: current_user
1618
+ )
1619
+ config = ff.field_config_for(field_name)
1620
+ config.empty? ? nil : config
1621
+ end
1622
+
1623
+ def find_search_parameter_field_config(field_name)
1624
+ return nil unless current_presenter.has_search_parameters?
1625
+
1626
+ param = current_presenter.search_parameters.find { |p|
1627
+ p.association_select? && p.field == field_name
1628
+ }
1629
+ return nil unless param
1630
+
1631
+ assoc = current_model_definition.belongs_to_fk_map[field_name]
1632
+ return nil unless assoc
1633
+
1634
+ { "field" => field_name, "association" => assoc, "input_options" => param.to_input_options.merge("search" => true) }
1635
+ end
1636
+
1637
+ def resolve_association_for_field(field_name)
1638
+ current_model_definition.associations.find { |a| a.foreign_key == field_name.to_s }
1639
+ end
1640
+
1641
+ # Walk up the depends_on chain from a field and resolve parent values.
1642
+ # Returns an array of {field, value, label} hashes from the immediate parent
1643
+ # up to the root of the dependency chain.
1644
+ def resolve_field_ancestors(field_name, value_id)
1645
+ ancestors = []
1646
+ seen = Set.new
1647
+ current_field = field_name.to_s
1648
+ # Keep value as a string — the join column may be a business key
1649
+ # like "EUR" or "0042" which must not be coerced to integer.
1650
+ current_value = value_id.to_s
1651
+
1652
+ loop do
1653
+ break if seen.include?(current_field)
1654
+ seen << current_field
1655
+
1656
+ # Find the field config for the current field
1657
+ field_config = find_form_field_config(current_field)
1658
+ break unless field_config
1659
+
1660
+ input_options = field_config["input_options"] || {}
1661
+ depends_on = input_options["depends_on"]
1662
+ break unless depends_on
1663
+
1664
+ parent_field = depends_on["field"]
1665
+ parent_fk = depends_on["foreign_key"]
1666
+ break unless parent_field && parent_fk
1667
+
1668
+ # Look up the current record to get the parent FK value
1669
+ assoc = field_config["association"] || resolve_association_for_field(current_field)
1670
+ break unless assoc&.lcp_model?
1671
+
1672
+ target_class = LcpRuby.registry.model_for(assoc.target_model)
1673
+ join_col = LcpRuby::AssociationJoinColumn.for(assoc, target_class).to_sym
1674
+ record = scoped_for_user(assoc.target_model, target_class).find_by(join_col => current_value)
1675
+ break unless record
1676
+
1677
+ parent_value = record.respond_to?(parent_fk) ? record.send(parent_fk) : nil
1678
+ break unless parent_value.present?
1679
+
1680
+ # Resolve the parent's label. When the parent is an lcp_model
1681
+ # association and the user cannot see the parent record (because
1682
+ # the target Pundit::Scope filters it out), break the chain
1683
+ # rather than leaking the parent_value. Without this guard,
1684
+ # ancestors_for would expose the IDs of records the user is
1685
+ # forbidden to read, just by walking depends_on:.
1686
+ parent_field_config = find_form_field_config(parent_field)
1687
+ parent_assoc = parent_field_config && (parent_field_config["association"] || resolve_association_for_field(parent_field))
1688
+ parent_label = parent_value.to_s
1689
+ if parent_assoc&.lcp_model?
1690
+ parent_input_options = (parent_field_config["input_options"] || {}) if parent_field_config
1691
+ label_method = ((parent_input_options && parent_input_options["label_method"]) || resolve_default_label_method(parent_assoc)).to_sym
1692
+ parent_class = LcpRuby.registry.model_for(parent_assoc.target_model)
1693
+ parent_join_col = LcpRuby::AssociationJoinColumn.for(parent_assoc, parent_class).to_sym
1694
+ parent_record = scoped_for_user(parent_assoc.target_model, parent_class).find_by(parent_join_col => parent_value)
1695
+ break unless parent_record
1696
+ parent_label = resolve_label(parent_record, label_method)
1697
+ end
1698
+
1699
+ ancestors << { field: parent_field, value: parent_value, label: parent_label }
1700
+
1701
+ # Move up the chain
1702
+ current_field = parent_field
1703
+ current_value = parent_value.to_s
1704
+ end
1705
+
1706
+ ancestors
1707
+ end
1708
+
1709
+ def build_select_options_json(assoc, input_options)
1710
+ depends_on_values = extract_depends_on_from_params(input_options)
1711
+ oq = build_options_query(assoc, input_options, role: current_user_role_name,
1712
+ depends_on_values: depends_on_values, user: current_user)
1713
+ format_options_for_json(oq, input_options)
1714
+ end
1715
+
1716
+ def extract_depends_on_from_params(input_options)
1717
+ depends_on = input_options["depends_on"]
1718
+ return {} unless depends_on && params[:depends_on].present?
1719
+
1720
+ field = depends_on["field"]
1721
+ { field => params[:depends_on][field] }
1722
+ end
1723
+
1724
+ def build_select_options_search(assoc, input_options)
1725
+ target_class = LcpRuby.registry.model_for(assoc.target_model)
1726
+ depends_on_values = extract_depends_on_from_params(input_options)
1727
+ query = apply_option_scope(target_class, input_options, role: current_user_role_name,
1728
+ user: current_user, target_model_name: assoc.target_model)
1729
+ query = query.where(input_options["filter"]) if input_options["filter"]
1730
+
1731
+ depends_on = input_options["depends_on"]
1732
+ if depends_on && depends_on_values.present?
1733
+ fk = depends_on["foreign_key"]
1734
+ parent_value = depends_on_values[depends_on["field"]]
1735
+ query = query.where(fk => parent_value) if fk && parent_value.present?
1736
+ end
1737
+
1738
+ # Fulltext LIKE search on configured search_fields
1739
+ search_fields = Array(input_options["search_fields"])
1740
+ if params[:q].present? && search_fields.any?
1741
+ conn = target_class.connection
1742
+ conditions = search_fields
1743
+ .select { |f| target_class.column_names.include?(f.to_s) }
1744
+ .map { |f| "#{conn.quote_column_name(f)} LIKE :q" }
1745
+ .join(" OR ")
1746
+ sanitized_q = ActiveRecord::Base.sanitize_sql_like(params[:q])
1747
+ query = query.where(conditions, q: "%#{sanitized_q}%") if conditions.present?
1748
+ end
1749
+
1750
+ query = query.order(input_options["sort"]) if input_options["sort"]
1751
+
1752
+ label_method = (input_options["label_method"] || resolve_default_label_method(assoc)).to_sym
1753
+ join_col = LcpRuby::AssociationJoinColumn.for(assoc, target_class).to_sym
1754
+ disabled_ids = resolve_disabled_values(assoc, input_options)
1755
+
1756
+ # Pagination
1757
+ page = [ (params[:page] || 1).to_i, 1 ].max
1758
+ per_page = [ [ (params[:per_page] || input_options["per_page"] || 25).to_i, 1 ].max, 100 ].min
1759
+ offset = (page - 1) * per_page
1760
+
1761
+ total = query.count
1762
+ records = query.offset(offset).limit(per_page)
1763
+
1764
+ options = records.map do |r|
1765
+ value = r.public_send(join_col)
1766
+ opt = { value: value, label: resolve_label(r, label_method) }
1767
+ opt[:disabled] = true if disabled_ids.include?(value)
1768
+ opt
1769
+ end
1770
+
1771
+ {
1772
+ options: options,
1773
+ has_more: (offset + per_page) < total,
1774
+ total: total
1775
+ }
1776
+ end
1777
+
1778
+ def build_tree_select_options(assoc, input_options)
1779
+ target_class = LcpRuby.registry.model_for(assoc.target_model)
1780
+ query = apply_option_scope(target_class, input_options, role: current_user_role_name,
1781
+ user: current_user, target_model_name: assoc.target_model)
1782
+ query = query.order(input_options["sort"]) if input_options["sort"]
1783
+
1784
+ max = (input_options["max_options"] || MAX_SELECT_OPTIONS).to_i
1785
+ query = query.limit(max)
1786
+
1787
+ label_method = (input_options["label_method"] || resolve_default_label_method(assoc)).to_sym
1788
+ parent_field = input_options["parent_field"] || "parent_id"
1789
+ max_depth = (input_options["max_depth"] || 10).to_i
1790
+ join_col = LcpRuby::AssociationJoinColumn.for(assoc, target_class).to_sym
1791
+
1792
+ records = query.to_a
1793
+ build_tree_json(records, parent_field, label_method, max_depth, join_col: join_col)
1794
+ end
1795
+
1796
+ # Check if the association's target model is API-backed.
1797
+ def api_target_model?(assoc)
1798
+ target_class = LcpRuby.registry.model_for(assoc.target_model)
1799
+ target_class.respond_to?(:lcp_api_model?) && target_class.lcp_api_model?
1800
+ rescue LcpRuby::RegistryError
1801
+ false
1802
+ end
1803
+
1804
+ # Build select options for an API-backed target model via its data source.
1805
+ def build_api_select_options(assoc, input_options)
1806
+ target_class = LcpRuby.registry.model_for(assoc.target_model)
1807
+ label_method = input_options["label_method"] || resolve_default_label_method(assoc)
1808
+ limit = (input_options["max_options"] || MAX_SELECT_OPTIONS).to_i
1809
+
1810
+ options = target_class.lcp_select_options(
1811
+ search: params[:q],
1812
+ sort: input_options["sort"],
1813
+ label_method: label_method,
1814
+ limit: limit
1815
+ )
1816
+
1817
+ if params[:q].present? || params[:page].present?
1818
+ { options: options, has_more: false, total: options.size }
1819
+ else
1820
+ options
1821
+ end
1822
+ end
1823
+
1824
+ # Internal recursion stays on r.id because the target's parent_field
1825
+ # references the actual id column. The emitted payload carries
1826
+ # `value: r.send(join_col)` which is what the JS tree_select
1827
+ # controller reads — see form_helper.rb#build_tree_data for the
1828
+ # matching schema.
1829
+ def build_tree_json(records, parent_field, label_method, max_depth, parent_id = nil, depth = 0, join_col: :id)
1830
+ return [] if depth >= max_depth
1831
+
1832
+ records.select { |r| r.send(parent_field) == parent_id }.map do |r|
1833
+ {
1834
+ value: r.public_send(join_col),
1835
+ label: resolve_label(r, label_method),
1836
+ children: build_tree_json(records, parent_field, label_method, max_depth, r.id, depth + 1, join_col: join_col)
1837
+ }
1838
+ end
1839
+ end
1840
+
1841
+ def current_user_role_name
1842
+ current_user&.send(LcpRuby.configuration.role_method).to_s
1843
+ rescue NoMethodError
1844
+ ""
1845
+ end
1846
+
1847
+ def summary_columns_present?
1848
+ current_presenter.table_columns.any? { |c| c["summary"].present? }
1849
+ end
1850
+
1851
+ def compute_summaries(scope)
1852
+ columns = current_presenter.table_columns.select { |c| c["summary"].present? }
1853
+ columns.each_with_object({}) do |col, h|
1854
+ field = col["field"]
1855
+ next unless @model_class.column_names.include?(field)
1856
+
1857
+ case col["summary"]
1858
+ when "sum" then h[field] = scope.sum(field)
1859
+ when "avg" then h[field] = scope.average(field)
1860
+ when "count" then h[field] = scope.where.not(field => nil).count
1861
+ end
1862
+ end
1863
+ end
1864
+
1865
+ def validate_association_values!(record)
1866
+ layout_builder = Presenter::LayoutBuilder.new(current_presenter, current_model_definition)
1867
+ layout_builder.form_sections
1868
+ .flat_map { |s| s["fields"] || [] }
1869
+ .each do |field_config|
1870
+ field_name = field_config["field"]
1871
+ next unless field_name
1872
+
1873
+ assoc = field_config["association"] || field_config["multi_select_association"]
1874
+ next unless assoc&.lcp_model?
1875
+
1876
+ submitted_value = record.public_send(field_name)
1877
+ next if submitted_value.blank?
1878
+
1879
+ input_options = field_config["input_options"] || {}
1880
+
1881
+ # Build the allowed options using the same logic as the select builder
1882
+ options = build_select_options_json(assoc, input_options)
1883
+ allowed_ids = extract_allowed_ids(options)
1884
+ disabled_ids = resolve_disabled_values(assoc, input_options)
1885
+
1886
+ # Do not coerce — the join column may be a string business
1887
+ # key. Comparison happens as strings on both sides below.
1888
+ submitted_ids = Array(submitted_value)
1889
+ allowed_ids_s = allowed_ids.map(&:to_s)
1890
+ disabled_ids_s = disabled_ids.map(&:to_s)
1891
+
1892
+ submitted_ids.each do |sid|
1893
+ sid_s = sid.to_s
1894
+ unless allowed_ids_s.include?(sid_s) && !disabled_ids_s.include?(sid_s)
1895
+ # Allow legacy scope values on existing records (edit)
1896
+ if input_options["legacy_scope"] && !record.new_record?
1897
+ legacy = resolve_legacy_record(assoc, input_options, sid)
1898
+ next if legacy
1899
+ end
1900
+
1901
+ record.errors.add(field_name, I18n.t("lcp_ruby.form.errors.contains_not_allowed"))
1902
+ break
1903
+ end
1904
+ end
1905
+ end
1906
+ end
1907
+
1908
+ def extract_allowed_ids(options)
1909
+ ids = Set.new
1910
+ if options.is_a?(Array) && options.first.is_a?(Hash) && options.first.key?(:group)
1911
+ # Grouped format: [{group: "...", options: [{value:, label:}]}]
1912
+ # Do NOT coerce o[:value] — string business keys must
1913
+ # round-trip unchanged.
1914
+ options.each { |g| g[:options].each { |o| ids << o[:value] } }
1915
+ elsif options.is_a?(Array)
1916
+ # Flat format: [{value:, label:}]
1917
+ options.each { |o| ids << o[:value] }
1918
+ end
1919
+ ids
1920
+ end
1921
+
1922
+ def build_nested_records(record)
1923
+ sections = current_presenter.form_config["sections"] || []
1924
+ sections.each do |section|
1925
+ next unless section["type"] == "nested_fields"
1926
+
1927
+ if section["json_field"]
1928
+ # Pre-populate empty JSON items for min requirement
1929
+ json_field_name = section["json_field"]
1930
+ min = (section["min"] || 0).to_i
1931
+ next unless min > 0
1932
+
1933
+ current_items = record.respond_to?(json_field_name) ? (record.send(json_field_name) || []) : []
1934
+ if current_items.size < min
1935
+ empty_items = (min - current_items.size).times.map { {} }
1936
+ record.send("#{json_field_name}=", current_items + empty_items)
1937
+ end
1938
+ next
1939
+ end
1940
+
1941
+ assoc_name = section["association"]
1942
+ min = (section["min"] || 0).to_i
1943
+ next unless min > 0
1944
+
1945
+ if record.respond_to?(assoc_name) && record.public_send(assoc_name).size < min
1946
+ existing_count = record.public_send(assoc_name).size
1947
+ pos = sortable_position_field(section)
1948
+ (min - existing_count).times do |i|
1949
+ new_record = record.public_send(assoc_name).build
1950
+ if pos && new_record.respond_to?("#{pos}=")
1951
+ new_record[pos] = existing_count + i
1952
+ end
1953
+ end
1954
+ end
1955
+ end
1956
+ end
1957
+
1958
+ # Process json_field sections: parse hash-of-hashes params into array of hashes.
1959
+ # For model-backed sections (target_model), wraps items in JsonItemWrapper
1960
+ # for type coercion and validation.
1961
+ def process_json_field_params(record)
1962
+ layout_builder = Presenter::LayoutBuilder.new(current_presenter, current_model_definition)
1963
+ sections = layout_builder.form_sections
1964
+
1965
+ sections.each do |section|
1966
+ next unless section["type"] == "nested_fields" && section["json_field"]
1967
+
1968
+ json_field_name = section["json_field"]
1969
+ raw = params.dig(:record, json_field_name)
1970
+ next unless raw.is_a?(ActionController::Parameters) || raw.is_a?(Hash)
1971
+
1972
+ # Collect allowed field names from the section (including sub_sections)
1973
+ allowed_keys = (section["fields"] || []).map { |f| f["field"] }.compact
1974
+ (section["sub_sections"] || []).each do |ss|
1975
+ allowed_keys += (ss["fields"] || []).map { |f| f["field"] }.compact
1976
+ end
1977
+ allowed_keys.uniq!
1978
+ target_model_def = section["target_model_definition"]
1979
+
1980
+ items = []
1981
+ raw.each_value.with_index do |item_params, idx|
1982
+ item_params = ActionController::Parameters.new(item_params) unless item_params.is_a?(ActionController::Parameters)
1983
+ item_params = item_params.permit(*allowed_keys, :_destroy)
1984
+ next if item_params[:_destroy].to_s.in?(%w[1 true])
1985
+
1986
+ item = {}
1987
+ allowed_keys.each do |key|
1988
+ item[key] = item_params[key] if item_params.key?(key)
1989
+ end
1990
+ # Skip rows where every permitted value is blank (empty template rows).
1991
+ # Note: boolean "0" is not blank in Ruby, so unchecked checkboxes survive.
1992
+ next if item.values.all?(&:blank?)
1993
+
1994
+ if target_model_def
1995
+ # Wrap in JsonItemWrapper for type coercion and validation
1996
+ wrapper = JsonItemWrapper.new(item, target_model_def)
1997
+ wrapper.validate_with_model_rules!
1998
+
1999
+ if wrapper.errors.any?
2000
+ wrapper.errors.each do |error|
2001
+ record.errors.add(
2002
+ :base,
2003
+ "#{json_field_name} item #{idx + 1}: #{error.attribute} #{error.message}"
2004
+ )
2005
+ end
2006
+ end
2007
+
2008
+ items << wrapper.to_hash
2009
+ else
2010
+ items << item
2011
+ end
2012
+ end
2013
+
2014
+ # Enforce min/max item limits
2015
+ min = section["min"]&.to_i
2016
+ if min && min > 0 && items.size < min
2017
+ record.errors.add(:base, "#{json_field_name}: too few items (minimum is #{min})")
2018
+ end
2019
+
2020
+ max = section["max"]&.to_i
2021
+ if max && max > 0 && items.size > max
2022
+ record.errors.add(:base, "#{json_field_name}: too many items (maximum is #{max})")
2023
+ items = items.first(max)
2024
+ end
2025
+
2026
+ record.send("#{json_field_name}=", items)
2027
+ end
2028
+ end
2029
+
2030
+ def sortable_position_field(section)
2031
+ return unless section["sortable"]
2032
+ section["sortable"].is_a?(String) ? section["sortable"] : "position"
2033
+ end
2034
+
2035
+ def evaluate_service_conditions(record)
2036
+ ctx = condition_context
2037
+ results = {}
2038
+ sections = current_presenter.form_config["sections"] || []
2039
+ sections.each_with_index do |section, idx|
2040
+ %w[visible_when disable_when].each do |cond_key|
2041
+ cond = section[cond_key]
2042
+ next unless cond.is_a?(Hash) && !ConditionEvaluator.client_evaluable?(cond)
2043
+
2044
+ result_key = "section_#{idx}_#{cond_key == 'visible_when' ? 'visible' : 'disable'}"
2045
+ results[result_key] = ConditionEvaluator.evaluate_any(record, cond, context: ctx)
2046
+ end
2047
+
2048
+ (section["fields"] || []).each do |field_config|
2049
+ field_name = field_config["field"]
2050
+ next unless field_name
2051
+
2052
+ %w[visible_when disable_when].each do |cond_key|
2053
+ cond = field_config[cond_key]
2054
+ next unless cond.is_a?(Hash) && !ConditionEvaluator.client_evaluable?(cond)
2055
+
2056
+ result_key = "#{field_name}_#{cond_key == 'visible_when' ? 'visible' : 'disable'}"
2057
+ results[result_key] = ConditionEvaluator.evaluate_any(record, cond, context: ctx)
2058
+ end
2059
+ end
2060
+ end
2061
+ results
2062
+ end
2063
+
2064
+ def resolve_loading_strategy(context)
2065
+ sort_field = params[:sort] if context == :index
2066
+ search_fields = current_presenter.search_config["searchable_fields"] if context == :index && params[:qs].present?
2067
+ Presenter::IncludesResolver.resolve(
2068
+ presenter_def: current_presenter,
2069
+ model_def: current_model_definition,
2070
+ context: context,
2071
+ sort_field: sort_field,
2072
+ search_fields: search_fields
2073
+ )
2074
+ rescue StandardError => e
2075
+ LcpRuby.record_error(e, subsystem: "eager_loading", context: context)
2076
+ Presenter::IncludesResolver::LoadingStrategy.new
2077
+ end
2078
+
2079
+ def preload_associations(record, context)
2080
+ strategy = resolve_loading_strategy(context)
2081
+ return if strategy.empty?
2082
+
2083
+ assocs = strategy.includes + strategy.eager_load
2084
+ ActiveRecord::Associations::Preloader.new(records: [ record ], associations: assocs).call if assocs.any?
2085
+ rescue StandardError => e
2086
+ LcpRuby.record_error(e, subsystem: "eager_loading", context: context)
2087
+ end
2088
+
2089
+ # -- Inline create helpers --
2090
+
2091
+ def find_presenter_for_inline_create(model_name)
2092
+ LcpRuby.loader.presenter_definitions.values.find do |p|
2093
+ p.model == model_name && p.form_config["sections"]&.any?
2094
+ end
2095
+ end
2096
+
2097
+ def inline_create_params(model_def)
2098
+ all_fields = model_def.fields.map { |f| f.name.to_sym }
2099
+
2100
+ # Filter through permission evaluator to respect field-level write restrictions
2101
+ begin
2102
+ perm_def = LcpRuby.loader.permission_definition(model_def.name)
2103
+ evaluator = Authorization::PermissionEvaluator.new(perm_def, current_user, model_def.name)
2104
+ writable = evaluator.writable_fields.map(&:to_sym)
2105
+ allowed = all_fields & writable
2106
+ rescue LcpRuby::MetadataError
2107
+ # No permission definition found; allow all model fields
2108
+ allowed = all_fields
2109
+ end
2110
+
2111
+ params.require(:inline_record).permit(*allowed)
2112
+ end
2113
+
2114
+ def resolve_inline_label_method(model_def, explicit_method)
2115
+ return explicit_method.to_sym if explicit_method.present?
2116
+ method = model_def.label_method
2117
+ method && method != "to_s" ? method.to_sym : :to_label
2118
+ end
2119
+
2120
+ def authorize_parent_field_writable!
2121
+ authorize_field_writable!(current_model_definition.tree_parent_field, "tree parent")
2122
+ end
2123
+
2124
+ def parse_parent_id_param
2125
+ raw = params[:parent_id]
2126
+ return nil if raw.blank? || raw == "null"
2127
+ raw.to_i
2128
+ end
2129
+
2130
+ def verify_tree_version!
2131
+ return false unless params[:tree_version].present?
2132
+
2133
+ current_version = compute_tree_version
2134
+ return false if current_version == params[:tree_version]
2135
+
2136
+ render json: { error: "tree_version_mismatch", tree_version: current_version },
2137
+ status: :conflict
2138
+ true
2139
+ end
2140
+
2141
+ def authorize_position_field!
2142
+ authorize_field_writable!(current_model_definition.positioning_field, "positioning")
2143
+ end
2144
+
2145
+ def authorize_field_writable!(field, label = field)
2146
+ unless current_evaluator.field_writable?(field)
2147
+ raise Pundit::NotAuthorizedError,
2148
+ "Not allowed to write #{label} field '#{field}'"
2149
+ end
2150
+ end
2151
+
2152
+ def verify_list_version!
2153
+ return false unless params[:list_version].present?
2154
+
2155
+ current_version = compute_list_version(@record)
2156
+ return false if current_version == params[:list_version]
2157
+
2158
+ render json: { error: "list_version_mismatch", list_version: current_version },
2159
+ status: :conflict
2160
+ true
2161
+ end
2162
+
2163
+ def compute_list_version(record)
2164
+ scope = @model_class.all
2165
+ current_model_definition.positioning_scope.each do |col|
2166
+ scope = scope.where(col => record.send(col))
2167
+ end
2168
+ pos_field = current_model_definition.positioning_field
2169
+ ids_in_order = scope.order(pos_field => :asc).pluck(:id)
2170
+ Digest::SHA256.hexdigest(ids_in_order.join(","))
2171
+ end
2172
+
2173
+ VALID_POSITION_KEYS = %i[after before].freeze
2174
+
2175
+ def parse_position_param
2176
+ raw = params[:position]
2177
+ case raw
2178
+ when ActionController::Parameters, Hash
2179
+ # SECURITY: to_unsafe_h needed for position hash. Validated by VALID_POSITION_KEYS whitelist below.
2180
+ h = raw.to_unsafe_h.transform_values { |v| v.to_i }.symbolize_keys
2181
+ invalid = h.keys - VALID_POSITION_KEYS
2182
+ if invalid.any?
2183
+ raise ActionController::BadRequest, "Invalid position keys: #{invalid.join(', ')}. Expected: after, before"
2184
+ end
2185
+ h
2186
+ when "first"
2187
+ :first
2188
+ when "last"
2189
+ :last
2190
+ else
2191
+ raw.to_i
2192
+ end
2193
+ end
2194
+
2195
+ def inline_form_fields(layout_builder, model_def)
2196
+ simple_types = %w[string text integer float decimal boolean date datetime enum email phone url color]
2197
+ layout_builder.form_sections
2198
+ .flat_map { |s| s["fields"] || [] }
2199
+ .select do |f|
2200
+ field_def = model_def.fields.find { |fd| fd.name == f["field"] }
2201
+ next false unless field_def
2202
+ # Only include simple field types (no associations, no nested)
2203
+ simple_types.include?(field_def.type.to_s) || field_def.enum?
2204
+ end
2205
+ .map do |f|
2206
+ field_def = model_def.fields.find { |fd| fd.name == f["field"] }
2207
+ {
2208
+ name: f["field"],
2209
+ label: f["label"] || f["field"].humanize,
2210
+ type: field_def.type.to_s,
2211
+ required: field_def.validations&.any? { |v| v.type == "presence" },
2212
+ enum_values: field_def.enum? ? field_def.enum_value_names : nil,
2213
+ placeholder: f["placeholder"]
2214
+ }
2215
+ end
2216
+ end
2217
+ end
2218
+ end