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,3627 @@
1
+ # Presenters Reference
2
+
3
+ File: `config/lcp_ruby/presenters/<name>.yml`
4
+
5
+ Presenter YAML defines the UI layer: how records are listed, displayed, edited, searched, and what actions are available. Multiple presenters can reference the same model to provide different views (e.g., an admin view and a read-only pipeline view).
6
+
7
+ ## Top-Level Attributes
8
+
9
+ ```yaml
10
+ presenter:
11
+ name: <presenter_name>
12
+ model: <model_name>
13
+ label: "Display Label"
14
+ slug: <url_slug>
15
+ icon: <icon_name>
16
+ read_only: false
17
+ embeddable: false
18
+ redirect_after: { create: show, update: show }
19
+ scope: <soft_delete_scope>
20
+ record_aliases: { me: { resolve: current_user, field: user } }
21
+ dialog: { size: medium, closable: true, title_key: <i18n_key> }
22
+ index: {}
23
+ show: {}
24
+ form: {}
25
+ search: {}
26
+ actions: {}
27
+ ```
28
+
29
+ ### `name`
30
+
31
+ | | |
32
+ |---|---|
33
+ | **Required** | yes |
34
+ | **Type** | string |
35
+
36
+ Unique identifier for the presenter. Referenced from [permissions](permissions.md) (`presenters` attribute) and used internally for resolution.
37
+
38
+ ### `model`
39
+
40
+ | | |
41
+ |---|---|
42
+ | **Required** | yes |
43
+ | **Type** | string |
44
+
45
+ Name of the [model](models.md) this presenter displays. Must match a model's `name` attribute.
46
+
47
+ ### `label`
48
+
49
+ | | |
50
+ |---|---|
51
+ | **Required** | no |
52
+ | **Default** | `name.humanize` |
53
+ | **Type** | string |
54
+
55
+ Display label for the presenter, shown in navigation menus and page titles.
56
+
57
+ ### `slug`
58
+
59
+ | | |
60
+ |---|---|
61
+ | **Required** | no |
62
+ | **Type** | string |
63
+
64
+ URL path segment. When set, the presenter is routable at `/<slug>`. If omitted, the presenter is not directly accessible via URL (useful for embedded or programmatic-only presenters).
65
+
66
+ ### `icon`
67
+
68
+ | | |
69
+ |---|---|
70
+ | **Required** | no |
71
+ | **Type** | string |
72
+
73
+ Icon name displayed in navigation menus. The engine uses these as CSS class hints (e.g., `dollar-sign`, `check-square`, `users`).
74
+
75
+ ### `menu_position`
76
+
77
+ > **Deprecated.** `menu_position` is a side-channel workaround that lives on the wrong primitive (the presenter rather than navigation) and creates a third ordering surface alongside view-group `navigation: { position: N }` and explicit `menu.yml` order. **Write `config/lcp_ruby/menu.yml`** for explicit menu placement — the entity generator's `--menu=[<section>:]ROOT|<DropdownLabel>[:bottom|:right]` flag makes this a one-line addition at scaffold time. The field still works in v1 (boot emits a one-time info-level log when a presenter actually uses it) and will be removed in a future release.
78
+
79
+ | | |
80
+ |---|---|
81
+ | **Required** | no |
82
+ | **Type** | integer |
83
+
84
+ Position hint for the auto-created view group when this is the only presenter on the model.
85
+
86
+ When a model has exactly one presenter and no explicit view group YAML, the engine [auto-creates a view group](view-groups.md#auto-creation-behavior) with position `900 + alphabetical_index`. The result is alphabetical menu order, which scales poorly past ~5 entries.
87
+
88
+ `menu_position` lifts the auto-VG out of that tier into a configurator-controlled position without forcing a full `config/lcp_ruby/views/<name>.yml`:
89
+
90
+ ```yaml
91
+ # config/lcp_ruby/presenters/school_class.yml
92
+ presenter:
93
+ name: school_class
94
+ model: school_class
95
+ slug: school-classes
96
+ menu_position: 10 # → auto-VG appears at position 10
97
+
98
+ # config/lcp_ruby/presenters/student.yml
99
+ presenter:
100
+ name: student
101
+ model: student
102
+ slug: students
103
+ menu_position: 20 # → auto-VG appears at position 20
104
+
105
+ # Other presenters without menu_position keep the 900+alphabetical default.
106
+ ```
107
+
108
+ **Conventions:**
109
+
110
+ - User-defined positions: `0–899`. Mix `menu_position` on presenters with explicit view-group `navigation: { position: N }` freely; the engine merges both into the same tier.
111
+ - Auto-generated default: `900–999`.
112
+
113
+ **Ignored when:**
114
+
115
+ - An explicit view group already covers the model — the explicit `navigation: { position: N }` wins, this hint is silently dropped.
116
+ - The presenter is one of multiple presenters on the same model — auto-VG creation is skipped entirely (you must define an explicit view group anyway).
117
+ - The presenter isn't routable (`slug:` blank) — no auto-VG is ever created.
118
+
119
+ ### `default`
120
+
121
+ | | |
122
+ |---|---|
123
+ | **Required** | no (required when the model has multiple routable presenters and any column/field uses `link: true` / `link_to: :show` without a per-link `link_presenter:` override) |
124
+ | **Default** | `false` |
125
+ | **Type** | boolean |
126
+
127
+ Marks this presenter as the **canonical link target** for its model. When a column or field elsewhere uses `link: true` / `link_to: :show` to a record of this model, the link resolves to the presenter marked `default: true`.
128
+
129
+ ```yaml
130
+ presenter:
131
+ name: contact_full
132
+ model: contact
133
+ slug: contacts
134
+ default: true # canonical detail view for `contact` records
135
+ ```
136
+
137
+ ```ruby
138
+ define_presenter :contact_full do
139
+ model :contact
140
+ slug "contacts"
141
+ default true
142
+ end
143
+ ```
144
+
145
+ **Validator rules** (boot-time):
146
+
147
+ - Exactly one presenter per model may be marked `default: true`. Two defaults on the same model raise an error with both presenter names.
148
+ - When the model has multiple routable presenters and a column/field uses `link: true`, either a `default: true` presenter must exist or every link entry must override with `link_presenter:`. The validator surfaces the missing-resolution case with both options spelled out.
149
+
150
+ **See also:** [Multiple presenters per model](#multiple-presenters-per-model) for the full pattern, and `link_presenter` on column/field entries (in `index` / `show` field reference) for per-link overrides.
151
+
152
+ ### `read_only`
153
+
154
+ | | |
155
+ |---|---|
156
+ | **Required** | no |
157
+ | **Default** | `false` |
158
+ | **Type** | boolean |
159
+
160
+ When `true`, disables create, edit, and destroy operations for this presenter. The model data is still writable through other presenters or direct code. Use this for dashboard or reporting views.
161
+
162
+ ### `embeddable`
163
+
164
+ | | |
165
+ |---|---|
166
+ | **Required** | no |
167
+ | **Default** | `false` |
168
+ | **Type** | boolean |
169
+
170
+ Marks this presenter as embeddable within other views (e.g., as an inline table within a parent record's show page). This is a metadata flag for the UI layer to decide rendering behavior.
171
+
172
+ ### `redirect_after`
173
+
174
+ | | |
175
+ |---|---|
176
+ | **Required** | no |
177
+ | **Type** | hash |
178
+
179
+ Controls where the user is redirected after successful create or update operations. Each key maps a CRUD action to a target page. Destroy always redirects to index.
180
+
181
+ | Key | Allowed values | Default |
182
+ |-----|---------------|---------|
183
+ | `create` | `index`, `show`, `edit`, `new` | `show` |
184
+ | `update` | `index`, `show`, `edit`, `new` | `show` |
185
+
186
+ ```yaml
187
+ presenter:
188
+ name: quick_entry
189
+ model: ticket
190
+ slug: tickets
191
+
192
+ # After creating a ticket, go back to the list.
193
+ # After updating, stay on the edit page.
194
+ redirect_after:
195
+ create: index
196
+ update: edit
197
+ ```
198
+
199
+ ### `scope`
200
+
201
+ | | |
202
+ |---|---|
203
+ | **Required** | no |
204
+ | **Default** | none (uses `kept` scope when model has `soft_delete`) |
205
+ | **Type** | string |
206
+ | **Allowed values** | `discarded`, `with_discarded` |
207
+
208
+ Controls the soft delete scope applied to the index query for this presenter. Only meaningful when the referenced model has `soft_delete` enabled.
209
+
210
+ - **Not set (default)** — the index shows only kept (non-discarded) records
211
+ - `"discarded"` — the index shows only discarded (archived) records
212
+ - `"with_discarded"` — the index shows all records regardless of discard status
213
+
214
+ Use this to create archive presenters that show discarded records with `restore` and `permanently_destroy` actions:
215
+
216
+ ```yaml
217
+ presenter:
218
+ name: project_archive
219
+ model: project
220
+ label: "Archived Projects"
221
+ slug: projects-archive
222
+ scope: discarded
223
+
224
+ actions:
225
+ collection: []
226
+ single:
227
+ - { name: show, type: built_in }
228
+ - { name: restore, type: built_in }
229
+ - { name: permanently_destroy, type: built_in, confirm: true }
230
+ ```
231
+
232
+ See [Soft Delete Guide](../guides/soft-delete.md) for a complete example.
233
+
234
+ ### `record_aliases`
235
+
236
+ | | |
237
+ |---|---|
238
+ | **Required** | no |
239
+ | **Default** | `{}` (none) |
240
+ | **Type** | object (alias name → config) |
241
+
242
+ Named URL segments that resolve to a specific record dynamically. Keys are alias names (URL path segments like `me`, `last`, `my`), values are config objects specifying the resolution strategy.
243
+
244
+ Aliases are presenter-level because the same model may have different aliases in different contexts (e.g., a user-facing presenter has `me`, an admin presenter doesn't).
245
+
246
+ #### Resolve strategies
247
+
248
+ | Strategy | Config keys | Behavior |
249
+ |----------|------------|----------|
250
+ | `current_user` | `field` (optional) | Guard `current_user` nil → 404. With `field:` → resolve belongs_to FK: `scope.find_by!(assoc.foreign_key => current_user.id)`. Without `field:` → self-referential: `scope.find(current_user.id)` (requires the presenter's model to share a table with the configured user class; verified at boot). |
251
+ | `field_on_user` | `user_field` | Guard `current_user` nil → 404; `scope.find(current_user.public_send(user_field))` |
252
+ | `scope` | `scope` | `scope.send(scope_name).first!` — must be a zero-arity scope |
253
+
254
+ All strategies apply within the existing `policy_scope` and soft-delete scope — authorization is never bypassed.
255
+
256
+ #### Resolve chain
257
+
258
+ When `params[:id]` arrives, the controller resolves in this order:
259
+
260
+ 1. **Alias match** — if presenter defines `record_aliases` and `params[:id]` matches a key, use the alias strategy.
261
+ 2. **Slug field** — if model defines [`slug_field`](models.md#slug_field) and `params[:id]` is not purely numeric, try `find_by!(slug_field => param)`.
262
+ 3. **Numeric ID** — fallback `scope.find(params[:id])`.
263
+
264
+ This means an alias named `me` always takes priority over a record with slug `me`. Numeric-looking params always use ID lookup.
265
+
266
+ #### Alias name constraints
267
+
268
+ - Must not be purely numeric (`42` would shadow that record's ID)
269
+ - Must not collide with reserved route segments (`new`, `edit`, `saved-filters`, `actions`, etc.)
270
+
271
+ #### URL preservation
272
+
273
+ When a record is entered via alias (e.g., `GET /preferences/me`), all ResourcesController member URLs generated during that request preserve the alias — the user never sees a jarring switch from `/preferences/me` to `/preferences/42`. Cross-presenter links use `to_param` instead (slug or ID, not alias).
274
+
275
+ #### Example: user preferences with `/preferences/me`
276
+
277
+ ```yaml
278
+ presenter:
279
+ name: my_preferences
280
+ model: user_preference
281
+ slug: preferences
282
+ record_aliases:
283
+ me:
284
+ resolve: current_user
285
+ field: user # belongs_to association name
286
+ ```
287
+
288
+ #### Example: self-referential alias on the User model (no `field:`)
289
+
290
+ When the presenter's model *is* the user model (same table), `field:` can be omitted — the alias resolves directly to the current user's own row.
291
+
292
+ ```yaml
293
+ presenter:
294
+ name: my_settings
295
+ model: user # same table as the configured user_class
296
+ slug: my-settings
297
+ record_aliases:
298
+ me:
299
+ resolve: current_user
300
+ # no field: — resolves to scope.find(current_user.id)
301
+ ```
302
+
303
+ #### Example: last issue shortcut
304
+
305
+ ```yaml
306
+ presenter:
307
+ name: issues
308
+ model: issue
309
+ slug: issues
310
+ record_aliases:
311
+ last:
312
+ resolve: scope
313
+ scope: latest # model must define scope :latest
314
+ ```
315
+
316
+ #### Example: my team
317
+
318
+ ```yaml
319
+ presenter:
320
+ name: teams
321
+ model: team
322
+ slug: teams
323
+ record_aliases:
324
+ my:
325
+ resolve: field_on_user
326
+ user_field: team_id # current_user.team_id → Team.find(value)
327
+ ```
328
+
329
+ **DSL:**
330
+
331
+ ```ruby
332
+ define_presenter :my_preferences do
333
+ model :user_preference
334
+ slug "preferences"
335
+
336
+ record_alias :me, resolve: :current_user, field: :user
337
+ end
338
+ ```
339
+
340
+ See [Record Aliases Guide](../guides/record-aliases.md) for full examples.
341
+
342
+ ### `dialog`
343
+
344
+ | | |
345
+ |---|---|
346
+ | **Required** | no |
347
+ | **Type** | object |
348
+
349
+ Dialog rendering configuration for when this presenter's auto-generated page is opened as a modal. Properties: `size` (`small`/`medium`/`large`/`fullscreen`, default `medium`), `closable` (boolean, default `true`), `title_key` (i18n key). When present on a presenter, the auto-generated page inherits this dialog config.
350
+
351
+ ```yaml
352
+ presenter:
353
+ name: task_quick_form
354
+ model: task
355
+ dialog:
356
+ size: large
357
+ closable: false
358
+ title_key: lcp_ruby.dialogs.quick_task
359
+ ```
360
+
361
+ See [Dialogs Reference](dialogs.md) for details.
362
+
363
+ ## Multiple presenters per model
364
+
365
+ A model may have any number of presenters that provide different views — full detail, compact card, kanban, archive, dialog-only, etc. When another presenter links to a record of this model with `link: true` or `link_to: :show`, LCP must pick which presenter the link resolves to.
366
+
367
+ > **Migrating from earlier LCP**: existing host apps with multi-presenter models will boot-error after upgrade until exactly one presenter per model is marked `default: true` *or* every link entry uses `link_presenter:`. Before this rule existed, `link: true` resolved to whichever presenter happened to be alphabetically first — which was rarely what the configurator intended. Single-presenter models need no annotation; the change is opt-in by problem (multi-presenter + linking).
368
+
369
+ **Resolution order** (`Presenter::Enrichment.resolve_presenter_slug`):
370
+
371
+ 1. **Per-link override** — if the column/field entry has `link_presenter: <slug>`, that wins.
372
+ 2. **Single presenter** — if the model has exactly one presenter, its slug is used (no decision needed; this is the most common case).
373
+ 3. **Per-model default** — among multiple presenters, the one marked `default: true` is canonical.
374
+
375
+ If none of these resolves a slug, the link silently breaks. The validator catches this case at boot — see the rules under [`default`](#default).
376
+
377
+ ### Worked example: Contact with three views
378
+
379
+ ```yaml
380
+ # contact_full.yml — full detail page (canonical)
381
+ presenter:
382
+ name: contact_full
383
+ model: contact
384
+ slug: contacts
385
+ default: true
386
+
387
+ # contact_short.yml — compact card embedded in dialogs / sidebars
388
+ presenter:
389
+ name: contact_short
390
+ model: contact
391
+ slug: contacts-short
392
+
393
+ # contact_tiles.yml — image-heavy tile grid for the directory
394
+ presenter:
395
+ name: contact_tiles
396
+ model: contact
397
+ slug: contacts-tiles
398
+ ```
399
+
400
+ ```yaml
401
+ # orders.yml — has columns that link to contact records
402
+ presenter:
403
+ name: orders
404
+ model: order
405
+ slug: orders
406
+ index:
407
+ table_columns:
408
+ # Default link target — uses contact_full (canonical) at /contacts/:id
409
+ - { field: customer.name, link: true }
410
+ # Per-link override — opens compact card on this index column only
411
+ - { field: secondary_contact.name, link: true, link_presenter: contacts-short }
412
+ ```
413
+
414
+ ### Validator rules
415
+
416
+ - **Exactly one default per model** — if two presenters mark `default: true`, the validator raises an error naming both.
417
+ - **Multi-presenter resolution required** — if a column/field uses `link: true` to a model with two or more routable presenters and there's no `default: true` and no per-link `link_presenter:`, the validator emits an error with both resolution options spelled out:
418
+
419
+ ```
420
+ Presenter 'orders', column 'customer.name': link target model 'contact' has
421
+ multiple presenters (contacts, contacts-short, contacts-tiles) and no resolution
422
+ strategy. Either:
423
+ (a) Mark one presenter as canonical: `default true` (DSL) / `default: true` (YAML), or
424
+ (b) Override per-link: `link_presenter: <slug>` on this column/field
425
+ ```
426
+
427
+ - **`link_presenter:` must reference an existing slug** for the target model. Typos surface a `Did you mean '<closest>'?` hint with the available list.
428
+
429
+ ### Single-presenter models
430
+
431
+ A model with exactly one presenter never needs `default: true` — single presenter is unambiguous and used directly. Adding the flag is harmless but redundant; the validator only enforces it when the resolution is otherwise ambiguous.
432
+
433
+ ## Index Configuration
434
+
435
+ Controls the record list view. When `render_with` is set, the entire content area is replaced by a custom host-app partial while preserving LCP's toolbar, search, nav, and layout. See also [Rendering Extension Points](../guides/rendering-extension-points.md).
436
+
437
+ ```yaml
438
+ index:
439
+ render_with: "analytics/dashboard" # optional: replace the entire index content area
440
+ layout: tiles # table (default), tiles, or tree
441
+ description: "Browse and manage all records."
442
+ default_sort: { field: created_at, direction: desc }
443
+ per_page: 25
444
+ per_page_options: [10, 25, 50, 100]
445
+ show_record_count: true
446
+ pagination_ends: false
447
+ row_click: show
448
+ empty_message: "No records found."
449
+ actions_position: dropdown
450
+ reorderable: false
451
+ table_columns: []
452
+ tile:
453
+ columns: 3
454
+ card:
455
+ title_field: name
456
+ subtitle_field: status
457
+ description_field: description
458
+ image_field: cover_image
459
+ actions: dropdown
460
+ fields:
461
+ - field: price
462
+ label: Price
463
+ sort_fields:
464
+ - field: name
465
+ label: Name
466
+ - field: price
467
+ label: Price
468
+ summary:
469
+ enabled: true
470
+ fields:
471
+ - field: price
472
+ function: sum
473
+ label: Total Value
474
+ ```
475
+
476
+ ### `render_with`
477
+
478
+ | | |
479
+ |---|---|
480
+ | **Required** | no |
481
+ | **Type** | string (partial path) |
482
+
483
+ Replaces the entire index content area with a custom host-app partial. The search/sort/scope pipeline still runs — the partial receives the processed `records`. When set, `layout` is ignored. See also [Rendering Extension Points](../guides/rendering-extension-points.md).
484
+
485
+ ```yaml
486
+ index:
487
+ render_with: "analytics/dashboard"
488
+ ```
489
+
490
+ The partial receives: `records`, `presenter`, `evaluator`, `action_set`, `ransack_search`, `total_count`, `current_user`, `page_definition`, `params`.
491
+
492
+ ### `layout`
493
+
494
+ | | |
495
+ |---|---|
496
+ | **Default** | `"table"` |
497
+ | **Type** | string (`table`, `tiles`, `tree`) |
498
+
499
+ Index page layout mode. `table` renders a standard data table, `tiles` renders a responsive card grid, `tree` renders a tree hierarchy. The `tree_view: true` flag is still supported for backward compatibility but `layout: tree` is preferred. Ignored when `render_with` is set.
500
+
501
+ ### `description`
502
+
503
+ | | |
504
+ |---|---|
505
+ | **Required** | no |
506
+ | **Type** | string |
507
+
508
+ Descriptive text displayed below the page heading. Available on `index`, `show`, and `form` views.
509
+
510
+ ```yaml
511
+ index:
512
+ description: "Browse all deals in your pipeline."
513
+ ```
514
+
515
+ ### `default_view`
516
+
517
+ | | |
518
+ |---|---|
519
+ | **Default** | `"table"` |
520
+ | **Type** | string |
521
+
522
+ The default display mode for the index page.
523
+
524
+ ### `views_available`
525
+
526
+ | | |
527
+ |---|---|
528
+ | **Required** | no |
529
+ | **Default** | not set |
530
+ | **Type** | array of strings |
531
+
532
+ List of available view modes the user can switch between (e.g., `[table, tiles]`). This is a metadata attribute for future UI support of multiple view modes.
533
+
534
+ ### `default_sort`
535
+
536
+ | | |
537
+ |---|---|
538
+ | **Required** | no |
539
+ | **Type** | hash |
540
+
541
+ Default sorting for the index page.
542
+
543
+ ```yaml
544
+ default_sort: { field: created_at, direction: desc }
545
+ ```
546
+
547
+ - `field` — column name to sort by
548
+ - `direction` — `asc` or `desc`
549
+
550
+ ### `per_page`
551
+
552
+ | | |
553
+ |---|---|
554
+ | **Default** | `25` |
555
+ | **Type** | integer |
556
+
557
+ Number of records per page. Used by Kaminari pagination.
558
+
559
+ ### `row_click`
560
+
561
+ | | |
562
+ |---|---|
563
+ | **Required** | no |
564
+ | **Default** | not set |
565
+ | **Type** | string |
566
+
567
+ When set to `"show"`, clicking any table row navigates to the record's show page. This makes the entire row clickable, not just link columns. When omitted, rows are not clickable (users navigate via link columns or action buttons).
568
+
569
+ ```yaml
570
+ index:
571
+ row_click: show
572
+ ```
573
+
574
+ ### `empty_message`
575
+
576
+ | | |
577
+ |---|---|
578
+ | **Required** | no |
579
+ | **Default** | not set |
580
+ | **Type** | string |
581
+
582
+ Custom message displayed when no records match the current search or filter. If omitted, a generic empty state is shown.
583
+
584
+ ```yaml
585
+ index:
586
+ empty_message: "No deals match your criteria. Try adjusting your filters."
587
+ ```
588
+
589
+ ### `actions_position`
590
+
591
+ | | |
592
+ |---|---|
593
+ | **Required** | no |
594
+ | **Default** | inline |
595
+ | **Type** | string |
596
+
597
+ Controls how single-record actions are rendered in each table row. When set to `"dropdown"`, all single actions are grouped into a dropdown menu (useful when there are many actions). The default behavior renders each action as an inline button.
598
+
599
+ ```yaml
600
+ index:
601
+ actions_position: dropdown
602
+ ```
603
+
604
+ ### `includes`
605
+
606
+ | | |
607
+ |---|---|
608
+ | **Required** | no |
609
+ | **Default** | `[]` |
610
+ | **Type** | array of strings or nested hashes |
611
+
612
+ Manually specify associations to preload for display purposes. Auto-detection handles most cases, but this allows explicit overrides. See [Eager Loading](eager-loading.md).
613
+
614
+ ```yaml
615
+ index:
616
+ includes: [company, contact]
617
+ ```
618
+
619
+ ### `eager_load`
620
+
621
+ | | |
622
+ |---|---|
623
+ | **Required** | no |
624
+ | **Default** | `[]` |
625
+ | **Type** | array of strings or nested hashes |
626
+
627
+ Manually specify associations to eager load via LEFT JOIN. Use when associations are needed for sorting or filtering. See [Eager Loading](eager-loading.md).
628
+
629
+ ```yaml
630
+ index:
631
+ eager_load: [company]
632
+ ```
633
+
634
+ ### `reorderable`
635
+
636
+ | | |
637
+ |---|---|
638
+ | **Required** | no |
639
+ | **Default** | `false` |
640
+ | **Type** | boolean |
641
+
642
+ Enables drag-and-drop reordering of records in the index table. Requires the model to have a [`positioning`](models.md#positioning) configuration.
643
+
644
+ When `reorderable: true`:
645
+
646
+ - Drag handles are rendered as the first column in each table row
647
+ - `default_sort` is automatically set to `{ field: <position_field>, direction: asc }` unless explicitly overridden
648
+ - A `PATCH /:slug/:id/reorder` endpoint is available for position updates
649
+ - The table includes `data-reorder-url` and `data-list-version` attributes for the frontend
650
+ - Drag handles are disabled when a search query is active or when sorting by a non-position column
651
+
652
+ Reordering requires the user to have `update` CRUD permission and write access to the position field. See [Permissions — Positioning](#positioning-and-permissions) for details.
653
+
654
+ ```yaml
655
+ index:
656
+ reorderable: true
657
+ table_columns:
658
+ - { field: name, link_to: show }
659
+ - { field: position, sortable: true }
660
+ ```
661
+
662
+ See [Record Positioning](../design/record_positioning.md) for the full design.
663
+
664
+ ### `tree_view`
665
+
666
+ | | |
667
+ |---|---|
668
+ | **Default** | `false` |
669
+ | **Type** | boolean |
670
+
671
+ Enables tree index rendering for models with `tree` enabled. Instead of a flat paginated table, records are displayed in a hierarchical tree with expand/collapse controls, connecting guide lines, and indentation. All records are loaded (no pagination).
672
+
673
+ The model must have `options.tree` enabled, otherwise `ConfigurationValidator` raises an error.
674
+
675
+ ```yaml
676
+ index:
677
+ tree_view: true
678
+ table_columns:
679
+ - { field: name, width: "40%", link_to: show }
680
+ - { field: "parent.name", label: "Parent" }
681
+ ```
682
+
683
+ ### `default_expanded`
684
+
685
+ | | |
686
+ |---|---|
687
+ | **Default** | `0` |
688
+ | **Type** | integer or `"all"` |
689
+
690
+ Controls how many tree levels are expanded by default when the tree index loads. `0` collapses everything (only roots visible), `1` shows roots and their direct children, `"all"` expands the entire tree. Only meaningful when `tree_view: true`.
691
+
692
+ During search/filter, all matched nodes and their ancestors are always expanded regardless of this setting.
693
+
694
+ ```yaml
695
+ index:
696
+ tree_view: true
697
+ default_expanded: 1 # expand one level
698
+ # default_expanded: "all" # expand everything
699
+ ```
700
+
701
+ ### `reparentable`
702
+
703
+ | | |
704
+ |---|---|
705
+ | **Default** | `false` |
706
+ | **Type** | boolean |
707
+
708
+ Enables drag-and-drop reparenting on the tree index. Rows get a drag handle, and users can drag nodes to change their parent. Dropping on the root drop zone makes a node a root. Cycle detection prevents invalid drops (dropping a parent onto its descendant).
709
+
710
+ Requires `tree_view: true` and model `tree` to be enabled. The user must have `update` CRUD permission and write access to the parent field. Uses optimistic locking via tree version hash to prevent conflicts.
711
+
712
+ ```yaml
713
+ index:
714
+ tree_view: true
715
+ default_expanded: 1
716
+ reparentable: true
717
+ ```
718
+
719
+ See [Tree Structures Reference](tree-structures.md) for full reparenting endpoint details.
720
+
721
+ ### `tile`
722
+
723
+ | | |
724
+ |---|---|
725
+ | **Required** | no (only meaningful with `layout: tiles`) |
726
+ | **Type** | hash |
727
+
728
+ Tile-layout-specific grid configuration. Card zone rendering lives in the sibling [`card`](#card) block.
729
+
730
+ | Key | Type | Default | Description |
731
+ |-----|------|---------|-------------|
732
+ | `columns` | integer | 3 | Grid columns (overridden by responsive breakpoints) |
733
+
734
+ ### `card`
735
+
736
+ | | |
737
+ |---|---|
738
+ | **Required** | when using a card-based layout (`tiles`, `kanban`, future) |
739
+ | **Type** | hash |
740
+
741
+ Per-record card interior shared across card-based layouts. Zone resolution lives in `LcpRuby::Display::CardHelper`; per-layout body partials compose these zones in their own order. See the [Tiles View Guide](../guides/tiles.md) for layout-specific examples.
742
+
743
+ | Key | Type | Default | Description |
744
+ |-----|------|---------|-------------|
745
+ | `title_field` | string | *required* | Field displayed as the card title. Title becomes a link when index-level `row_click` is set. |
746
+ | `subtitle_field` | string | — | Field displayed below the title |
747
+ | `subtitle_renderer` | string | — | Renderer for the subtitle |
748
+ | `subtitle_options` | hash | — | Options passed to the subtitle renderer |
749
+ | `description_field` | string | — | Field for card description (line-clamped) |
750
+ | `description_max_lines` | integer | 3 | Max description lines |
751
+ | `image_field` | string | — | Field containing image URL or Active Storage attachment for the banner zone |
752
+ | `image_alt_field` | string | — | Field providing alt text for the banner image (falls back to `title_field` value) |
753
+ | `avatar_field` | string | — | Field containing image URL or attachment for the small circular avatar |
754
+ | `avatar_alt_field` | string | — | Field providing alt text for the avatar (defaults to "" — decorative) |
755
+ | `color_field` | string | — | Field whose value drives the color accent. Mapped through `color_map` (or workflow state color) to a whitelisted palette token. Raw values never reach CSS. |
756
+ | `color_map` | hash | — | Maps field values to whitelisted palette tokens (`success`, `danger`, `warning`, `info`, `muted`, `primary`) |
757
+ | `actions` | string | `dropdown` | `dropdown`, `inline`, or `none` |
758
+ | `render_with` | string | — | Host-app partial path (e.g., `my_app/cards/deal`) that overrides the entire card body |
759
+ | `fields` | array | — | Additional label-value fields in the card body. Each entry: `{ field, label?, renderer?, options?, partial? }` (`partial` wins over `renderer` if both set) |
760
+
761
+ > **Note:** the legacy `card_link:` key (placed inside `tile:` in earlier versions) is removed. Use index-level [`row_click`](#row_click) instead.
762
+
763
+ ### `sort_fields`
764
+
765
+ | | |
766
+ |---|---|
767
+ | **Required** | no |
768
+ | **Type** | array of `{ field, label }` objects |
769
+
770
+ Fields available in the sort dropdown. Shows a `<select>` in the filter bar allowing users to pick a sort field and toggle direction.
771
+
772
+ ```yaml
773
+ sort_fields:
774
+ - field: name
775
+ label: Name
776
+ - field: price
777
+ label: Price
778
+ ```
779
+
780
+ ### `per_page_options`
781
+
782
+ | | |
783
+ |---|---|
784
+ | **Required** | no |
785
+ | **Type** | array of positive integers |
786
+
787
+ Selectable page sizes shown in a dropdown near pagination. When set, users can switch between page sizes.
788
+
789
+ ```yaml
790
+ per_page: 25
791
+ per_page_options: [10, 25, 50, 100]
792
+ ```
793
+
794
+ ### `show_record_count`
795
+
796
+ | | |
797
+ |---|---|
798
+ | **Required** | no |
799
+ | **Default** | `true` |
800
+ | **Type** | boolean |
801
+
802
+ Shows a record count text in the pagination footer bar (e.g., "Showing 1-25 of 312 records"). When `false`, the count text is hidden but pagination links and per-page selector still render.
803
+
804
+ On grouped layouts the text reads "groups" instead of "records" because the total reflects the number of groups, not individual records.
805
+
806
+ ```yaml
807
+ index:
808
+ show_record_count: false # hide record count
809
+ ```
810
+
811
+ ### `pagination_ends`
812
+
813
+ | | |
814
+ |---|---|
815
+ | **Required** | no |
816
+ | **Default** | `false` (from `LcpRuby.configuration.pagination_ends`) |
817
+ | **Type** | boolean |
818
+
819
+ Shows "First" and "Last" page links alongside the Prev/Next navigation in the pagination footer. Per-presenter override of the global [engine configuration](engine-configuration.md#pagination_ends) setting.
820
+
821
+ ```yaml
822
+ index:
823
+ pagination_ends: true # show First/Last links for this presenter
824
+ ```
825
+
826
+ ### `summary`
827
+
828
+ | | |
829
+ |---|---|
830
+ | **Required** | no |
831
+ | **Type** | hash with `enabled` and `fields` |
832
+
833
+ Horizontal bar below the index content displaying aggregate values computed on the filtered scope (before pagination).
834
+
835
+ ```yaml
836
+ summary:
837
+ enabled: true
838
+ fields:
839
+ - field: price
840
+ function: sum
841
+ label: Total Revenue
842
+ - field: price
843
+ function: avg
844
+ label: Average Price
845
+ ```
846
+
847
+ Each field requires `field` and `function` (`sum`, `avg`, `count`, `min`, `max`). Optional `label`, `renderer`, and `options` keys.
848
+
849
+ ### `item_classes`
850
+
851
+ | | |
852
+ |---|---|
853
+ | **Default** | `[]` |
854
+ | **Type** | array of rule objects |
855
+
856
+ Conditional CSS classes applied to each row (`<tr>`) or card element based on record field values. All matching rules accumulate — a record matching multiple rules gets all their CSS classes.
857
+
858
+ ```yaml
859
+ index:
860
+ item_classes:
861
+ - class: "lcp-row-muted lcp-row-strikethrough"
862
+ when: { field: status, operator: eq, value: "done" }
863
+ - class: "lcp-row-danger"
864
+ when: { field: status, operator: eq, value: "overdue" }
865
+ - class: "lcp-row-bold"
866
+ when: { field: priority, operator: eq, value: "critical" }
867
+ ```
868
+
869
+ #### Rule Attributes
870
+
871
+ | Key | Type | Required | Description |
872
+ |-----|------|----------|-------------|
873
+ | `class` | string | yes | One or more CSS class names (space-separated) to apply when the condition matches |
874
+ | `when` | hash | yes | A [condition](condition-operators.md) (`field`/`operator`/`value` or `service`) evaluated against each record |
875
+
876
+ #### Cross-Layout Support
877
+
878
+ `item_classes` applies across all index layouts:
879
+
880
+ | Layout | Applied to |
881
+ |--------|-----------|
882
+ | `table` | `<tr>` element in the table body |
883
+ | `tree` | `<tr>` element for each tree node |
884
+ | `tiles` | `.lcp-tile-card` container element |
885
+
886
+ #### Built-in Utility Classes
887
+
888
+ | Class | Visual Effect |
889
+ |-------|---------------|
890
+ | `lcp-row-danger` | Light red background (`--lcp-row-danger-bg`) |
891
+ | `lcp-row-warning` | Light amber background (`--lcp-row-warning-bg`) |
892
+ | `lcp-row-success` | Light green background (`--lcp-row-success-bg`) |
893
+ | `lcp-row-info` | Light blue background (`--lcp-row-info-bg`) |
894
+ | `lcp-row-muted` | Reduced opacity (`--lcp-row-muted-opacity`) |
895
+ | `lcp-row-bold` | Bold text |
896
+ | `lcp-row-strikethrough` | Line-through text decoration |
897
+
898
+ All background classes use CSS custom properties for host app theming. Custom CSS classes are also supported.
899
+
900
+ #### Permissions
901
+
902
+ Conditions are evaluated regardless of field read permissions. The styling is server-side and does not expose field values — it only adds CSS classes to the HTML element.
903
+
904
+ #### Validation
905
+
906
+ `ConfigurationValidator` validates `item_classes` at boot using the same rules as `visible_when`/`disable_when`: field existence, operator validity, and operator-type compatibility.
907
+
908
+ ### `table_columns`
909
+
910
+ | | |
911
+ |---|---|
912
+ | **Default** | `[]` |
913
+ | **Type** | array of column objects |
914
+
915
+ Defines which columns appear in the index table and how they render.
916
+
917
+ ```yaml
918
+ table_columns:
919
+ - field: title
920
+ width: "30%"
921
+ link_to: show
922
+ sortable: true
923
+ renderer: null
924
+ ```
925
+
926
+ #### Column Attributes
927
+
928
+ | Attribute | Type | Description |
929
+ |-----------|------|-------------|
930
+ | `field` | string | Model field name to display. Supports dot-notation (e.g., `"company.name"`) and template syntax (e.g., `"{first_name} {last_name}"`) |
931
+ | `label` | string | Custom column header label. Defaults to humanized last segment of the field name. Useful for dot-path fields (e.g., `"company.name"` → label `"Company"`) |
932
+ | `width` | string | CSS width (e.g., `"30%"`, `"200px"`) |
933
+ | `link_to` | string | **Legacy.** Makes the cell a link to the **current row's** show page. Value `show`. Preserved for backward compatibility — prefer `link: true` below for new configurations |
934
+ | `link` | boolean | Wrap the cell value in a link. Auto-detects the target: dot-path (`"manager.display_name"`) links to the last `belongs_to` record in the chain (including multi-level chains like `"manager.organizational_unit.name"`); a plain `belongs_to` field name links to that record; a scalar field links to the current row (same as legacy `link_to: :show`). Use `link_through:` to override |
935
+ | `link_through` | string | Name of a `belongs_to` association on this model to follow for the link target. Implies `link: true`. Use when the displayed field's name doesn't correspond to the linked entity (e.g. a precomputed virtual column label). Incompatible with `link_to: :show` |
936
+ | `link_presenter` | string | Override the target presenter slug for this link. Use when the linked model has multiple presenters and you want a non-default one (e.g. compact card on this column, full detail elsewhere). Falls back to the model's `default: true` presenter when absent. See [Multiple presenters per model](#multiple-presenters-per-model) |
937
+ | `sortable` | boolean | Enables column header sorting |
938
+ | `renderer` | string | Renderer for the field value (see [Renderers](#renderers)). Alternatively, use `partial: "path/to/partial"` to render with a custom view partial |
939
+ | `options` | hash | Options passed to the renderer (see [Renderers](#renderers) for per-renderer options) |
940
+ | `hidden_on` | array/string | Hide column at specific breakpoints. Values: `"mobile"`, `"tablet"`. Accepts a single string or an array |
941
+ | `pinned` | string | Pin column to one side on horizontal scroll. Value: `"left"` |
942
+ | `summary` | string | Adds a summary row at the bottom of the table for this column. Values: `"sum"`, `"avg"`, `"count"` |
943
+ | `display_in_index` | boolean | Per-column opt-in for fields whose type carries `default_index_visible: false` (`:text`, `:rich_text`, `:attachment`, `:json`, `:array`, `:file`). Without this kwarg the configuration validator emits a row-unfriendly-column warning suggesting either an explicit `renderer:` or this opt-in. No runtime effect — purely a validator-silencer. See [Types Reference — `default_index_visible`](types.md#default_index_visible) |
944
+
945
+ **Example with new column attributes:**
946
+
947
+ ```yaml
948
+ table_columns:
949
+ - field: name
950
+ width: "25%"
951
+ link_to: show
952
+ sortable: true
953
+ pinned: left
954
+ - field: email
955
+ hidden_on: [mobile, tablet]
956
+ - field: status
957
+ renderer: badge
958
+ options:
959
+ color_map:
960
+ active: green
961
+ inactive: gray
962
+ pending: yellow
963
+ - field: revenue
964
+ renderer: currency
965
+ options:
966
+ currency: "$"
967
+ precision: 2
968
+ sortable: true
969
+ summary: sum
970
+ - field: deals_count
971
+ summary: count
972
+ hidden_on: mobile
973
+ ```
974
+
975
+ ## Grouped Query Mode
976
+
977
+ Setting `query_mode: grouped` on the `index` block switches the presenter from a flat record list to a GROUP BY query. The index table shows one row per group (e.g., one row per category, one row per month), with aggregate columns computing COUNT, SUM, AVG, MIN, or MAX across the records in each group.
978
+
979
+ Grouped presenters are read-only by design: show, edit, form, and CRUD actions are not available. Use them for reporting views, summary dashboards, and drill-through navigation.
980
+
981
+ ### Index-Level Attributes
982
+
983
+ | Attribute | Type | Default | Description |
984
+ |-----------|------|---------|-------------|
985
+ | `query_mode` | string | — | Set to `grouped` to enable grouped query mode |
986
+ | `group_by` | array | (required when grouped) | Fields to GROUP BY. Each element is a field name (string) or a hash `{ field: name, period: year\|quarter\|month\|week\|day }` for date truncation |
987
+ | `having` | array | `[]` | Post-aggregation filters applied after grouping. Each entry: `{ field: aggregate_column, operator: op, value: val }`. Operators: `eq`, `neq`, `gt`, `gte`, `lt`, `lte` |
988
+ | `limit` | integer | — | Maximum number of group rows returned. When set, pagination is disabled |
989
+ | `row_click` | hash | — | `{ mode: filter, target: presenter-slug }` — clicking a row navigates to the target presenter with Ransack filter params pre-filled from the group's values |
990
+
991
+ ### Column-Level Attributes
992
+
993
+ When `query_mode: grouped`, `table_columns` entries support two additional attributes:
994
+
995
+ | Attribute | Type | Description |
996
+ |-----------|------|-------------|
997
+ | `aggregate` | string or hash | Aggregate function for this column. Short form: `count`. Full form: `{ function: sum, field: amount }` |
998
+ | `aggregate.function` | string | `count`, `sum`, `avg`, `min`, `max` |
999
+ | `aggregate.field` | string | Source field for sum/avg/min/max aggregates |
1000
+ | `aggregate.distinct` | boolean | When `true`, generates `AGG(DISTINCT field)` |
1001
+ | `group_field` | string | References the original `group_by` field when the column name differs from the grouped field (e.g., a period-truncated date) |
1002
+ | `sort_strategy` | string | Enum columns only. `definition_order` (default for enums) sorts by position in the model's enum values list using CASE WHEN SQL. `value` sorts alphabetically by raw value. Applies to both grouped and non-grouped index views |
1003
+
1004
+ Columns without an `aggregate` key display the group-by value for that column. Columns with `aggregate` display the computed aggregate.
1005
+
1006
+ ### Basic Example
1007
+
1008
+ ```yaml
1009
+ presenter:
1010
+ name: products_by_category
1011
+ model: product
1012
+ label: "Products by Category"
1013
+ slug: products-by-category
1014
+
1015
+ index:
1016
+ query_mode: grouped
1017
+ group_by: [category]
1018
+ row_click:
1019
+ mode: filter
1020
+ target: products
1021
+ table_columns:
1022
+ - { field: category, sortable: true }
1023
+ - { field: product_count, aggregate: count, sortable: true }
1024
+ - { field: total_price, aggregate: { function: sum, field: price }, sortable: true }
1025
+ ```
1026
+
1027
+ ### HAVING Filters
1028
+
1029
+ Use `having` to filter groups after aggregation — for example, to show only categories with at least 5 products:
1030
+
1031
+ ```yaml
1032
+ index:
1033
+ query_mode: grouped
1034
+ group_by: [category]
1035
+ having:
1036
+ - { field: product_count, operator: gte, value: 5 }
1037
+ table_columns:
1038
+ - { field: category, sortable: true }
1039
+ - { field: product_count, aggregate: count, sortable: true }
1040
+ ```
1041
+
1042
+ ### Date Period Grouping
1043
+
1044
+ Group by a date field truncated to a period (year, quarter, month, week, or day):
1045
+
1046
+ ```yaml
1047
+ index:
1048
+ query_mode: grouped
1049
+ group_by:
1050
+ - { field: created_at, period: month }
1051
+ table_columns:
1052
+ - { field: month, group_field: created_at, sortable: true }
1053
+ - { field: order_count, aggregate: count, sortable: true }
1054
+ - { field: revenue, aggregate: { function: sum, field: total }, sortable: true }
1055
+ ```
1056
+
1057
+ The `group_field` attribute on the column tells the presenter which `group_by` entry this column corresponds to when the column name (`month`) differs from the source field (`created_at`).
1058
+
1059
+ ### Constraints
1060
+
1061
+ - `show`, `edit`, `form` sections are ignored in grouped mode
1062
+ - CRUD actions (`create`, `edit`, `destroy`) are not available
1063
+ - `row_click: show` has no effect (no individual record to show); use `{ mode: filter, target: ... }` for drill-through instead
1064
+ - Quick search and advanced filters apply to the base scope before grouping, not to the group rows themselves
1065
+
1066
+ ---
1067
+
1068
+ ## Field Path Syntax
1069
+
1070
+ The `field` attribute in `table_columns` and show `fields` supports three syntaxes beyond simple field names:
1071
+
1072
+ ### Dot-Notation (Association Traversal)
1073
+
1074
+ Use dot-notation to display fields from associated records:
1075
+
1076
+ ```yaml
1077
+ table_columns:
1078
+ - { field: "company.name", sortable: true } # belongs_to traversal
1079
+ - { field: "company.industry", renderer: badge } # with renderer
1080
+ - { field: "contacts.full_name", renderer: collection } # has_many traversal
1081
+ ```
1082
+
1083
+ For `belongs_to`/`has_one`, the resolved value is a scalar. For `has_many`, the resolved value is an array (use the `collection` renderer).
1084
+
1085
+ Dot-paths can be nested: `company.industry.name` traverses `company` → `industry` → `name`.
1086
+
1087
+ **Permissions:** Each segment in the dot-path is checked against `readable_fields` on the target model. If any segment is not readable, the column is hidden.
1088
+
1089
+ **Eager loading:** Dot-path fields are automatically detected by the `IncludesResolver` and the required associations are preloaded to prevent N+1 queries.
1090
+
1091
+ ### Template Syntax (Multi-Field Interpolation)
1092
+
1093
+ Use `{field}` syntax to combine multiple fields into a single display value:
1094
+
1095
+ ```yaml
1096
+ table_columns:
1097
+ - { field: "{first_name} {last_name}" }
1098
+ - { field: "{company.name}: {title}" } # dot-paths inside templates
1099
+ ```
1100
+
1101
+ Template fields extract all `{ref}` references and resolve each one individually. Dot-paths inside templates work the same as standalone dot-paths.
1102
+
1103
+ **Permissions:** All referenced fields must be readable for the template column to be visible.
1104
+
1105
+ ### Collection Renderer
1106
+
1107
+ The `collection` renderer renders arrays (typically from `has_many` dot-paths) as formatted lists:
1108
+
1109
+ ```yaml
1110
+ table_columns:
1111
+ - field: "contacts.full_name"
1112
+ renderer: collection
1113
+ options:
1114
+ separator: ", " # default: ", "
1115
+ limit: 3 # max items to show
1116
+ overflow: "..." # text appended when truncated (default: "...")
1117
+ item_renderer: badge # apply a renderer to each item
1118
+ item_options: # options for the per-item renderer
1119
+ color_map: { ... }
1120
+ ```
1121
+
1122
+ | Option | Type | Default | Description |
1123
+ |--------|------|---------|-------------|
1124
+ | `separator` | string | `", "` | Separator between items |
1125
+ | `limit` | integer | all | Maximum number of items to display |
1126
+ | `overflow` | string | `"..."` | Text appended when items are truncated |
1127
+ | `item_renderer` | string | none | Renderer to apply to each item before joining |
1128
+ | `item_options` | hash | `{}` | Options for the per-item renderer |
1129
+
1130
+ ### Custom Renderers
1131
+
1132
+ Custom renderers defined in `app/renderers/` can be referenced by name in the `renderer` attribute:
1133
+
1134
+ ```yaml
1135
+ table_columns:
1136
+ - field: stage
1137
+ renderer: conditional_badge
1138
+ options:
1139
+ rules:
1140
+ - match: { in: [closed_won] }
1141
+ renderer: badge
1142
+ options: { color_map: { closed_won: green } }
1143
+ - default:
1144
+ renderer: badge
1145
+ ```
1146
+
1147
+ You can also use `partial:` to render a field with a custom view partial instead of a renderer class:
1148
+
1149
+ ```yaml
1150
+ table_columns:
1151
+ - field: stage
1152
+ partial: "shared/stage_indicator"
1153
+ ```
1154
+
1155
+ See [Custom Renderers Guide](../guides/custom-renderers.md) for creating custom renderers.
1156
+
1157
+ ## Renderers
1158
+
1159
+ Renderers control how field values are rendered in index tables and show pages. Each renderer can accept `options` to customize its behavior. You can also use `partial: "path/to/partial"` instead of a renderer to render a field with a custom view partial.
1160
+
1161
+ The renderer name is validated at boot — if you pass an unknown key (e.g. `renderer: extrnal_link`), `bundle exec rake lcp_ruby:validate` raises an error with a "Did you mean…" suggestion and the full list of available renderers. **Type names are accepted as renderer synonyms** when the type's `TypeDefinition#renderer` is set: `renderer: enum` resolves to `:badge` (the `:enum` type's default renderer), `renderer: text` keeps its existing alias to passthrough (the alias table wins over the type-registry fallback). See [Aliases](#aliases) below for the full precedence rules.
1162
+
1163
+ ### Row-unfriendly column warning
1164
+
1165
+ `bundle exec rake lcp_ruby:validate` warns when an index `table_columns` entry references a field whose type is row-unfriendly (`:text`, `:rich_text`, `:attachment`, `:json`, `:array`, `:file` — types whose values don't render well in a row). Three escape hatches silence the warning:
1166
+
1167
+ - **`renderer:`** — pick a row-friendly renderer (e.g. `:truncate` for `:text`, `:code` for `:json`).
1168
+ - **`partial:`** — supply a custom view partial that handles the cell content.
1169
+ - **`display_in_index: true`** — explicit opt-in; tells the validator the configurator knows the value is non-trivial in a row but wants it anyway. Has no runtime effect (purely a validator-silencer).
1170
+
1171
+ Example:
1172
+
1173
+ ```yaml
1174
+ table_columns:
1175
+ - field: title
1176
+ - field: body # WARN: :text not row-friendly
1177
+ - field: body # silenced — explicit renderer
1178
+ renderer: truncate
1179
+ - field: body # silenced — opt-in
1180
+ display_in_index: true
1181
+ ```
1182
+
1183
+ The warning is suppressed for `bind_to:` models (host AR class owns its presenter authoring decisions).
1184
+
1185
+ ### Aliases
1186
+
1187
+ Several renderer keys are aliases for canonical renderers. Use whichever name reads better in your presenter:
1188
+
1189
+ | Alias | Canonical | Notes |
1190
+ |-------|-----------|-------|
1191
+ | `boolean` | `boolean_icon` | Renders ✓/✗ glyph (with Lucide SVG when available) |
1192
+ | `email` | `email_link` | `<a href="mailto:…">` |
1193
+ | `phone` | `phone_link` | `<a href="tel:…">` |
1194
+ | `url` / `external_link` | `url_link` | `<a target="_blank" rel="noopener">` for safe http(s) URLs |
1195
+ | `json` | `code` | Uses `code`'s JSON pretty-print path |
1196
+ | `text` | (passthrough) | Renders `value.to_s` — equivalent to omitting `renderer:` |
1197
+ | `enum` | `badge` | Implicit alias resolved via the type registry — `:enum` type's `TypeDefinition#renderer` is `"badge"`. See note below |
1198
+
1199
+ **Type-name fallback (Phase 4 of type-system defaults).** Any registered type whose `TypeDefinition#renderer` is set is accepted as a renderer synonym. Resolution order:
1200
+
1201
+ 1. **Registered renderer table** (the explicit aliases above) — wins. E.g. `renderer: text` returns the passthrough Text renderer regardless of what `:text` type's default is.
1202
+ 2. **Type registry fallback** — if the key isn't a registered renderer, runtime consults `Types::TypeRegistry.renderer_for(key)` and uses the resolved renderer if any. E.g. `renderer: enum` is NOT in the alias table but the `:enum` type's renderer is `"badge"`, so it resolves to the Badge renderer.
1203
+ 3. **Returns nil** if neither layer resolves — view template falls through to the value-shape heuristic (Array → collection, AR::Base → display_association_value, etc.).
1204
+
1205
+ `bundle exec rake lcp_ruby:validate` accepts type-name synonyms as valid renderer keys (Phase 4 alignment); the validator does not mutate the metadata, so `renderer: enum` stays as `"enum"` in the parsed metadata and runtime resolves it via the type registry on every render. See `docs/design/type_system_defaults.md` Decision 4 for the full rationale.
1206
+
1207
+ ### Type-driven renderer defaults (`runtime_type_renderers`)
1208
+
1209
+ When `LcpRuby.configuration.runtime_type_renderers = true`, presenter columns whose `renderer:` is **absent** get a type-driven default at column-set build time. So `column :flag` on a `:boolean` field auto-renders as `boolean_icon` (✓/✗ glyph) and `column :status` on an `:enum` field auto-renders as a `badge` — without writing `renderer:` explicitly. The defaults come from each type's `TypeDefinition#renderer` (see [Types Reference — `renderer`](types.md#renderer)).
1210
+
1211
+ The flag is `false` for one release after Phase 4 lands so existing apps keep their current rendering; `true` from the start in new apps via `lcp new`. To opt in:
1212
+
1213
+ ```ruby
1214
+ # config/initializers/lcp_ruby.rb
1215
+ LcpRuby.configure do |config|
1216
+ config.runtime_type_renderers = true
1217
+ end
1218
+ ```
1219
+
1220
+ Behavior changes when the flag is on:
1221
+
1222
+ - `column :flag` (where `flag` is `:boolean`) — was raw `"true"`/`"false"` text, becomes `boolean_icon`.
1223
+ - `column :status` (where `status` is `:enum`) — was the humanized enum label via `format_enum_display`, becomes a `badge` (same label, wrapped in a badge box).
1224
+
1225
+ Explicit `renderer:` always wins (idempotent skip), so columns that already specify a renderer are unaffected. See [Engine Configuration — `runtime_type_renderers`](engine-configuration.md#runtime_type_renderers).
1226
+
1227
+ ### `heading`
1228
+
1229
+ Renders the value as `<strong>` text. Useful for primary identifiers.
1230
+
1231
+ ```yaml
1232
+ { field: title, renderer: heading }
1233
+ ```
1234
+
1235
+ ### `badge`
1236
+
1237
+ Renders the value as a colored badge. Useful for enum and status fields.
1238
+
1239
+ | Option | Type | Description |
1240
+ |--------|------|-------------|
1241
+ | `color_map` | hash | Maps field values to badge colors |
1242
+
1243
+ Available colors: `green`, `red`, `blue`, `yellow`, `orange`, `purple`, `gray`, `teal`, `cyan`, `pink`.
1244
+
1245
+ ```yaml
1246
+ - field: status
1247
+ renderer: badge
1248
+ options:
1249
+ color_map:
1250
+ active: green
1251
+ inactive: gray
1252
+ pending: yellow
1253
+ suspended: red
1254
+ ```
1255
+
1256
+ ### `truncate`
1257
+
1258
+ Truncates long text with an ellipsis.
1259
+
1260
+ | Option | Type | Default | Description |
1261
+ |--------|------|---------|-------------|
1262
+ | `max` | integer | `50` | Maximum number of characters before truncation |
1263
+
1264
+ ```yaml
1265
+ - field: description
1266
+ renderer: truncate
1267
+ options:
1268
+ max: 100
1269
+ ```
1270
+
1271
+ ### `boolean_icon`
1272
+
1273
+ Shows a colored glyph (✓ / ✗) for true / false values. The glyph is wrapped in `<i data-lucide="check|x">`; the engine layout ships Lucide and replaces it with an inline SVG on first paint and on every Turbo navigation. The Unicode glyph in the placeholder text stays visible as a graceful fallback if Lucide ever fails to load. The visible label is exposed via an `.sr-only` span for screen readers.
1274
+
1275
+ Pass `true_icon` / `false_icon` to opt out of the Lucide / glyph default and render plain text content instead (useful when you want a custom icon character or a localized short text).
1276
+
1277
+ | Option | Type | Description |
1278
+ |--------|------|-------------|
1279
+ | `true_icon` | string | Plain-text content for true (disables Lucide + glyph) |
1280
+ | `false_icon` | string | Plain-text content for false (disables Lucide + glyph) |
1281
+
1282
+ ```yaml
1283
+ - field: verified
1284
+ renderer: boolean_icon
1285
+ # legacy plain-text mode:
1286
+ - field: verified
1287
+ renderer: boolean_icon
1288
+ options:
1289
+ true_icon: "✅"
1290
+ false_icon: "❌"
1291
+ ```
1292
+
1293
+ ### `progress_bar`
1294
+
1295
+ Renders a horizontal progress bar.
1296
+
1297
+ | Option | Type | Default | Description |
1298
+ |--------|------|---------|-------------|
1299
+ | `max` | integer | `100` | Maximum value (100% mark) |
1300
+
1301
+ ```yaml
1302
+ - field: completion
1303
+ renderer: progress_bar
1304
+ options:
1305
+ max: 100
1306
+ ```
1307
+
1308
+ ### `image`
1309
+
1310
+ Renders the field value as an image URL.
1311
+
1312
+ | Option | Type | Description |
1313
+ |--------|------|-------------|
1314
+ | `size` | string | Image size: `"small"`, `"medium"`, `"large"` |
1315
+
1316
+ ```yaml
1317
+ - field: photo_url
1318
+ renderer: image
1319
+ options:
1320
+ size: medium
1321
+ ```
1322
+
1323
+ ### `avatar`
1324
+
1325
+ Renders a circular avatar image.
1326
+
1327
+ | Option | Type | Default | Description |
1328
+ |--------|------|---------|-------------|
1329
+ | `size` | integer | `32` | Avatar diameter in pixels |
1330
+
1331
+ ```yaml
1332
+ - field: profile_image
1333
+ renderer: avatar
1334
+ options:
1335
+ size: 48
1336
+ ```
1337
+
1338
+ ### `currency`
1339
+
1340
+ Formats a numeric value as currency. Uses `I18n.locale` for thousands separator, decimal mark, and unit positioning — your locale's `number.currency.format` (e.g., `cs.yml` ships `"%n %u"` with `,` decimal and ` ` thousands → `1 490,00 Kč`). Pass `currency:` to override the unit symbol; the format itself stays locale-driven.
1341
+
1342
+ | Option | Type | Description |
1343
+ |--------|------|-------------|
1344
+ | `currency` | string | Currency unit override (e.g., `"USD"`, `"€"`). When omitted, the locale's default unit applies. |
1345
+ | `precision` | integer | Number of decimal places |
1346
+
1347
+ ```yaml
1348
+ - field: amount
1349
+ renderer: currency
1350
+ options:
1351
+ currency: "USD"
1352
+ precision: 2
1353
+ ```
1354
+
1355
+ ### `percentage`
1356
+
1357
+ Formats a numeric value as a percentage.
1358
+
1359
+ | Option | Type | Description |
1360
+ |--------|------|-------------|
1361
+ | `precision` | integer | Number of decimal places |
1362
+
1363
+ ```yaml
1364
+ - field: margin
1365
+ renderer: percentage
1366
+ options:
1367
+ precision: 1
1368
+ ```
1369
+
1370
+ ### `number`
1371
+
1372
+ Formats a numeric value with delimiters and precision.
1373
+
1374
+ | Option | Type | Description |
1375
+ |--------|------|-------------|
1376
+ | `delimiter` | string | Thousands separator (e.g., `","`) |
1377
+ | `precision` | integer | Number of decimal places |
1378
+
1379
+ ```yaml
1380
+ - field: population
1381
+ renderer: number
1382
+ options:
1383
+ delimiter: ","
1384
+ precision: 0
1385
+ ```
1386
+
1387
+ ### `date`
1388
+
1389
+ Formats a date value.
1390
+
1391
+ | Option | Type | Default | Description |
1392
+ |--------|------|---------|-------------|
1393
+ | `format` | string | `"%Y-%m-%d"` | strftime format string |
1394
+
1395
+ ```yaml
1396
+ - field: birth_date
1397
+ renderer: date
1398
+ options:
1399
+ format: "%B %d, %Y"
1400
+ ```
1401
+
1402
+ ### `datetime`
1403
+
1404
+ Formats a datetime value. When `format` is omitted, the renderer calls `I18n.l(value, format: :default)` so the output respects your locale's `time.formats.default`. Pass an explicit `format` strftime string to bypass the locale lookup.
1405
+
1406
+ | Option | Type | Description |
1407
+ |--------|------|-------------|
1408
+ | `format` | string | Optional strftime format. Omit to use locale-aware `I18n.l(:default)`. |
1409
+
1410
+ ```yaml
1411
+ # Locale-aware (recommended) — cs locale renders e.g. "Sat 25. April 2026, 14:30:00 +0200"
1412
+ - field: created_at
1413
+ renderer: datetime
1414
+
1415
+ # Explicit ISO format
1416
+ - field: created_at
1417
+ renderer: datetime
1418
+ options:
1419
+ format: "%Y-%m-%d %H:%M"
1420
+ ```
1421
+
1422
+ ### `relative_date`
1423
+
1424
+ Shows a human-readable relative time (e.g., "3 days ago", "in 2 hours").
1425
+
1426
+ ```yaml
1427
+ { field: updated_at, renderer: relative_date }
1428
+ ```
1429
+
1430
+ ### `email_link`
1431
+
1432
+ Renders the value as a `mailto:` link.
1433
+
1434
+ ```yaml
1435
+ { field: email, renderer: email_link }
1436
+ ```
1437
+
1438
+ ### `phone_link`
1439
+
1440
+ Renders the value as a `tel:` link.
1441
+
1442
+ ```yaml
1443
+ { field: phone, renderer: phone_link }
1444
+ ```
1445
+
1446
+ ### `url_link`
1447
+
1448
+ Renders the value as an external link that opens in a new tab.
1449
+
1450
+ ```yaml
1451
+ { field: website, renderer: url_link }
1452
+ ```
1453
+
1454
+ ### `color_swatch`
1455
+
1456
+ Renders a color preview swatch alongside the color value.
1457
+
1458
+ ```yaml
1459
+ { field: brand_color, renderer: color_swatch }
1460
+ ```
1461
+
1462
+ ### `rating`
1463
+
1464
+ Displays a numeric value as filled stars.
1465
+
1466
+ | Option | Type | Default | Description |
1467
+ |--------|------|---------|-------------|
1468
+ | `max` | integer | `5` | Maximum number of stars |
1469
+
1470
+ ```yaml
1471
+ - field: score
1472
+ renderer: rating
1473
+ options:
1474
+ max: 5
1475
+ ```
1476
+
1477
+ ### `code`
1478
+
1479
+ Renders the value in monospace code formatting.
1480
+
1481
+ ```yaml
1482
+ { field: api_key, renderer: code }
1483
+ ```
1484
+
1485
+ ### `file_size`
1486
+
1487
+ Renders a numeric byte value as human-readable file size (e.g., "2.4 MB").
1488
+
1489
+ ```yaml
1490
+ { field: attachment_size, renderer: file_size }
1491
+ ```
1492
+
1493
+ ### `rich_text`
1494
+
1495
+ Renders HTML content safely.
1496
+
1497
+ ```yaml
1498
+ { field: body, renderer: rich_text }
1499
+ ```
1500
+
1501
+ ### `link`
1502
+
1503
+ Renders the value as a clickable link. Uses `to_label` (if defined on the model) or `to_s` for the display text.
1504
+
1505
+ ```yaml
1506
+ { field: reference, renderer: link }
1507
+ ```
1508
+
1509
+ ### `attachment_preview`
1510
+
1511
+ Renders an image preview for attachment fields. For image files, displays the image (optionally using a named variant). For non-image files, falls back to a download link.
1512
+
1513
+ | Option | Type | Description |
1514
+ |--------|------|-------------|
1515
+ | `variant` | string | Named variant to use for the image (e.g., `"thumbnail"`, `"medium"`) |
1516
+
1517
+ ```yaml
1518
+ - field: photo
1519
+ renderer: attachment_preview
1520
+ options:
1521
+ variant: medium
1522
+ ```
1523
+
1524
+ ### `attachment_list`
1525
+
1526
+ Renders a list of download links with filenames and file sizes. Designed for multiple attachment fields.
1527
+
1528
+ ```yaml
1529
+ - field: files
1530
+ renderer: attachment_list
1531
+ ```
1532
+
1533
+ ### `attachment_link`
1534
+
1535
+ Renders a single download link with the filename. Designed for single non-image attachment fields.
1536
+
1537
+ ```yaml
1538
+ - field: contract
1539
+ renderer: attachment_link
1540
+ ```
1541
+
1542
+ ### `empty_value`
1543
+
1544
+ | | |
1545
+ |---|---|
1546
+ | **Required** | no |
1547
+ | **Type** | string |
1548
+
1549
+ Overrides the placeholder text displayed when a field value is `nil` or blank. Takes precedence over the global `LcpRuby.configure { |c| c.empty_value = "..." }` setting and the `lcp_ruby.empty_value` i18n key. The placeholder is rendered with CSS class `lcp-empty-value`.
1550
+
1551
+ ```yaml
1552
+ presenter:
1553
+ name: showcase_fields
1554
+ model: showcase_field
1555
+ empty_value: "N/A"
1556
+ ```
1557
+
1558
+ ## Show Configuration
1559
+
1560
+ Controls the record detail view.
1561
+
1562
+ ```yaml
1563
+ show:
1564
+ description: "View record details and related items."
1565
+ copy_url: true
1566
+ includes: [contacts, deals]
1567
+ layout:
1568
+ - section: "Section Title"
1569
+ description: "Key information about this record."
1570
+ columns: 2
1571
+ fields:
1572
+ - { field: title, renderer: heading }
1573
+ - { field: stage, renderer: badge }
1574
+ - { type: info, text: "This explains the fields above." }
1575
+ - section: "Related Items"
1576
+ type: association_list
1577
+ association: contacts
1578
+ ```
1579
+
1580
+ ### `copy_url`
1581
+
1582
+ | | |
1583
+ |---|---|
1584
+ | **Required** | no |
1585
+ | **Default** | `true` |
1586
+ | **Type** | boolean |
1587
+
1588
+ Controls whether the "Copy link" toolbar button is shown on the show page. Set to `false` to hide it (e.g., for read-only summary views where the URL is not useful).
1589
+
1590
+ ```yaml
1591
+ show:
1592
+ copy_url: false
1593
+ ```
1594
+
1595
+ ### `includes` / `eager_load`
1596
+
1597
+ Same as index configuration. Manually specify associations to preload for the show page. Auto-detection handles `association_list` sections automatically. See [Eager Loading](eager-loading.md).
1598
+
1599
+ ### `render_with`
1600
+
1601
+ | | |
1602
+ |---|---|
1603
+ | **Required** | no |
1604
+ | **Type** | string (partial path) |
1605
+
1606
+ Replaces the entire show content area with a custom host-app partial while preserving LCP's layout, nav, toolbar, breadcrumbs, and flash messages. Ignored in dialog (`?_dialog=1`) and composite page contexts. See also [Rendering Extension Points](../guides/rendering-extension-points.md).
1607
+
1608
+ ```yaml
1609
+ show:
1610
+ render_with: "products/custom_show"
1611
+ ```
1612
+
1613
+ The partial receives these locals: `record`, `presenter`, `evaluator`, `action_set`, `layout_builder`, `column_set`, `field_value_resolver`, `current_user`, `page_definition`, `params`.
1614
+
1615
+ ### `layout`
1616
+
1617
+ Array of section objects. Each section is rendered as a card or panel.
1618
+
1619
+ #### Section Attributes
1620
+
1621
+ | Attribute | Type | Description |
1622
+ |-----------|------|-------------|
1623
+ | `section` | string | Section heading text. Used as the i18n fallback when no entry exists at `lcp_ruby.presenters.<presenter>.sections.<parameterized_section>`. |
1624
+ | `section_key` | string | Explicit i18n lookup key. Decouples the i18n key from the human-readable label so non-English literals don't leak into the en locale. Lookup: `lcp_ruby.presenters.<presenter>.sections.<section_key>`. Falls back to `section` (or humanized key) when missing. |
1625
+ | `description` | string | Explanatory text displayed below the section heading |
1626
+ | `columns` | integer | Number of columns in the field grid (default: 1) |
1627
+ | `fields` | array | Fields to display (see below) |
1628
+ | `type` | string | `association_list`, `json_items_list`, `audit_history`, or `custom` |
1629
+ | `association` | string | Association name (required when `type: association_list`) |
1630
+ | `partial` | string | Host-app partial path (required when `type: custom`) |
1631
+ | `responsive` | hash | Responsive overrides per breakpoint (see below) |
1632
+ | `visible_when` | hash | Condition object. When false, the section is not rendered. Same syntax as [field conditions](#conditional-visibility) |
1633
+ | `disable_when` | hash | Condition object. When true, the section has a disabled appearance. Same syntax as [field conditions](#conditional-disabling) |
1634
+
1635
+ Show page conditions are evaluated **server-side only** — hidden sections are not rendered in the DOM (no client-side JavaScript toggling).
1636
+
1637
+ #### Custom Sections (Show)
1638
+
1639
+ Set `type: custom` to delegate rendering of a section to a host-app partial. The `fields:` key is ignored — the partial owns all content inside the section wrapper. `columns:` and `disable_when:` are also ignored.
1640
+
1641
+ ```yaml
1642
+ show:
1643
+ layout:
1644
+ - section: "Location"
1645
+ type: custom
1646
+ partial: "employees/location_map"
1647
+ ```
1648
+
1649
+ The partial receives: `record`, `presenter`, `evaluator`, `section_config`, `current_user`, `page_definition`, `params`.
1650
+
1651
+ ```yaml
1652
+ - section: "Metrics"
1653
+ visible_when: { field: stage, operator: not_eq, value: lead }
1654
+ fields:
1655
+ - { field: priority }
1656
+ - { field: progress }
1657
+ ```
1658
+
1659
+ #### Association List Sections
1660
+
1661
+ Use `type: association_list` to render a list of associated records within the show page:
1662
+
1663
+ ```yaml
1664
+ - section: "Contacts"
1665
+ type: association_list
1666
+ association: contacts
1667
+ display_template: default
1668
+ link: true
1669
+ sort: { last_name: asc }
1670
+ limit: 5
1671
+ empty_message: "No contacts yet."
1672
+ scope: active
1673
+ ```
1674
+
1675
+ | Attribute | Type | Default | Description |
1676
+ |-----------|------|---------|-------------|
1677
+ | `association` | string | — | **Required.** Association name from the model |
1678
+ | `display_template` | string | `"default"` | Name of the display template defined on the target model |
1679
+ | `link` | boolean | `false` | Wrap each record in a link to its show page |
1680
+ | `link_through` | string | — | Follow this `belongs_to` association on the target model to resolve the link target (implies `link: true`) |
1681
+ | `link_presenter` | string | — | Override the target presenter slug. Use when the linked model has multiple presenters. Falls back to the model's `default: true` presenter. See [Multiple presenters per model](#multiple-presenters-per-model) |
1682
+ | `sort` | hash | — | Sort field and direction (e.g., `{ last_name: asc }`) |
1683
+ | `limit` | integer | — | Maximum number of records to display |
1684
+ | `empty_message` | string | `"No records."` | Message when no associated records exist |
1685
+ | `scope` | string | — | Named scope to apply on the association |
1686
+ | `visible_when` | hash | — | Condition object. When false, the section is not rendered |
1687
+ | `disable_when` | hash | — | Condition object. When true, the section has a disabled appearance |
1688
+
1689
+ When `display_template` references a display template defined on the target model (see [Models Reference — Display Templates](models.md#display-templates)), records render with rich HTML including title, subtitle, icon, and badge. Without a display template, records fall back to `to_label`.
1690
+
1691
+ When `link: true`, each record is wrapped in a link to the target model's show page (the first presenter for that model is used for routing).
1692
+
1693
+ When `link_through` is set, each record links to the related record reached by following the named `belongs_to` association on the target model. For example, `link_through: employee` on a `group_memberships` association list links each membership to the employee's show page instead of the membership's own page. The `link_through` association must be a non-polymorphic `belongs_to`. If the related record is `nil` (orphaned FK) or soft-deleted, the item renders without a link. The user must have `show` permission on the link target model for links to appear.
1694
+
1695
+ ```yaml
1696
+ # Link through a join model to the real entity
1697
+ - section: "Members"
1698
+ type: association_list
1699
+ association: group_memberships
1700
+ link_through: employee # Links to employee show page
1701
+ display_template: default
1702
+ ```
1703
+
1704
+ Sort and limit operate in-memory on preloaded records (unless `scope` is specified, which triggers a SQL query).
1705
+
1706
+ #### Responsive Sections
1707
+
1708
+ Use `responsive` to override the number of columns at different breakpoints:
1709
+
1710
+ ```yaml
1711
+ - section: "Deal Information"
1712
+ columns: 3
1713
+ responsive:
1714
+ tablet:
1715
+ columns: 2
1716
+ mobile:
1717
+ columns: 1
1718
+ fields:
1719
+ - { field: title, renderer: heading }
1720
+ - { field: stage, renderer: badge }
1721
+ - { field: value, renderer: currency }
1722
+ ```
1723
+
1724
+ #### Show Field Attributes
1725
+
1726
+ | Attribute | Type | Description |
1727
+ |-----------|------|-------------|
1728
+ | `field` | string | Model field name. Supports dot-notation and template syntax |
1729
+ | `label` | string | Custom field label. Defaults to humanized last segment of the field name. Useful for dot-path fields (e.g., `"company.name"` → label `"Company"`) |
1730
+ | `renderer` | string | Renderer for the field value (see [Renderers](#renderers)). Alternatively, use `partial: "path/to/partial"` to render with a custom view partial |
1731
+ | `options` | hash | Options passed to the renderer (see [Renderers](#renderers) for per-renderer options) |
1732
+ | `copyable` | boolean | Adds a copy-to-clipboard icon next to the field value. When clicked, copies the displayed value to the clipboard with a "Copied!" tooltip. Coexists with `link:` — the link wraps the value and the copy button is rendered outside it |
1733
+ | `link` | boolean | Wrap the field value in a link to the associated record. Auto-detects the target from a dot-path (`"manager.display_name"` → manager; `"manager.organizational_unit.name"` → org unit) or from a plain `belongs_to` field name (`field: manager`). Nil / soft-deleted targets, blank values, and users without `:show` on the target render without a link. Incompatible with link-emitting renderers (`email_link`, `url_link`, `markdown`, `rich_text`, …) |
1734
+ | `link_through` | string | Name of a `belongs_to` association on this model to follow for the link target. Implies `link: true`. Use when the displayed field's name doesn't correspond to the linked entity (e.g. a precomputed virtual column) |
1735
+ | `link_presenter` | string | Override the target presenter slug for this link. Use when the linked model has multiple presenters. Falls back to the model's `default: true` presenter when absent. See [Multiple presenters per model](#multiple-presenters-per-model) |
1736
+ | `col_span` | integer | Number of grid columns this field spans (defaults to 1) |
1737
+ | `hidden_on` | array/string | Hide field at specific breakpoints. Values: `"mobile"`, `"tablet"` |
1738
+
1739
+ **Example with new show field attributes:**
1740
+
1741
+ ```yaml
1742
+ show:
1743
+ layout:
1744
+ - section: "Overview"
1745
+ columns: 3
1746
+ responsive:
1747
+ mobile:
1748
+ columns: 1
1749
+ fields:
1750
+ - { field: title, renderer: heading, col_span: 3 }
1751
+ - field: status
1752
+ renderer: badge
1753
+ options:
1754
+ color_map:
1755
+ active: green
1756
+ inactive: red
1757
+ - { field: email, renderer: email_link }
1758
+ - { field: phone, renderer: phone_link, hidden_on: mobile }
1759
+ - field: revenue
1760
+ renderer: currency
1761
+ options:
1762
+ currency: "$"
1763
+ precision: 2
1764
+ ```
1765
+
1766
+ **Example — link-through on show fields:**
1767
+
1768
+ ```yaml
1769
+ show:
1770
+ layout:
1771
+ - section: "Work Details"
1772
+ columns: 2
1773
+ fields:
1774
+ - { field: "manager.display_name", label: "Manager", link: true }
1775
+ - { field: "organizational_unit.name", label: "Org Unit", link: true }
1776
+ - { field: position, link: true } # plain belongs_to
1777
+ - field: "manager.organizational_unit.name"
1778
+ label: "Manager's Dept"
1779
+ link: true # multi-level → org unit
1780
+ - { field: manager_label, link_through: manager } # virtual/precomputed
1781
+ ```
1782
+
1783
+ Same auto-detection rules apply to index columns via `link:` / `link_through:` — see [Column Attributes](#column-attributes).
1784
+
1785
+ ## Form Configuration
1786
+
1787
+ Controls the create and edit forms.
1788
+
1789
+ ```yaml
1790
+ form:
1791
+ description: "Fill in the record details below."
1792
+ layout: flat
1793
+ includes: [todo_items]
1794
+ sections:
1795
+ - title: "Section Title"
1796
+ description: "Basic information about the record."
1797
+ columns: 2
1798
+ fields:
1799
+ - { type: info, text: "Prices are in USD." }
1800
+ - { field: title, placeholder: "Enter title...", autofocus: true }
1801
+ - { field: stage, input_type: select }
1802
+ - { field: value, input_type: number, prefix: "$" }
1803
+ - { field: company_id, input_type: association_select }
1804
+ ```
1805
+
1806
+ ### `includes` / `eager_load`
1807
+
1808
+ Same as index configuration. Manually specify associations to preload for the form. Auto-detection handles `nested_fields` sections automatically. See [Eager Loading](eager-loading.md).
1809
+
1810
+ ### `layout`
1811
+
1812
+ | | |
1813
+ |---|---|
1814
+ | **Required** | no |
1815
+ | **Default** | `"flat"` |
1816
+ | **Type** | string |
1817
+
1818
+ Controls how form sections are rendered. When set to `"tabs"`, each section becomes a tab in a tabbed interface. The default `"flat"` layout renders sections as stacked cards.
1819
+
1820
+ ```yaml
1821
+ form:
1822
+ layout: tabs
1823
+ sections:
1824
+ - title: "General"
1825
+ fields:
1826
+ - { field: title }
1827
+ - { field: description, input_type: textarea }
1828
+ - title: "Pricing"
1829
+ fields:
1830
+ - { field: price, input_type: number }
1831
+ - { field: currency, input_type: select }
1832
+ - title: "Advanced"
1833
+ fields:
1834
+ - { field: notes, input_type: rich_text_editor }
1835
+ ```
1836
+
1837
+ ### `sections`
1838
+
1839
+ Array of form section objects.
1840
+
1841
+ #### Form Section Attributes
1842
+
1843
+ | Attribute | Type | Description |
1844
+ |-----------|------|-------------|
1845
+ | `title` | string | Section heading text. Used as the i18n fallback when no entry exists at `lcp_ruby.presenters.<presenter>.sections.<parameterized_title>`. |
1846
+ | `section_key` | string | Explicit i18n lookup key. Decouples the i18n key from the human-readable label so non-English literals don't leak into the en locale. Lookup: `lcp_ruby.presenters.<presenter>.sections.<section_key>`. Falls back to `title` (or humanized key) when missing. |
1847
+ | `description` | string | Explanatory text displayed below the section heading |
1848
+ | `columns` | integer | Number of columns in the field grid (default: 1) |
1849
+ | `fields` | array | Form fields (see below) |
1850
+ | `type` | string | `nested_fields` or `custom`. Default: standard field section |
1851
+ | `partial` | string | Host-app partial path (required when `type: custom`) |
1852
+ | `additional_permitted_params` | array | Extra param names permitted from the custom section's inputs. Supports scalar strings and nested hash specs. Only for `type: custom` |
1853
+ | `collapsible` | boolean | When `true`, the section can be collapsed/expanded by clicking its header |
1854
+ | `collapsed` | boolean | When `true` (and `collapsible` is `true`), the section starts in the collapsed state |
1855
+ | `visible_when` | hash | Condition object. When the condition evaluates to false, the entire section (fieldset) is hidden. Same syntax as field conditions. See [Conditional Visibility](#conditional-visibility) |
1856
+ | `disable_when` | hash | Condition object. When the condition evaluates to true, the entire section is visually disabled. Same syntax as field conditions. See [Conditional Disabling](#conditional-disabling) |
1857
+ | `responsive` | hash | Responsive overrides per breakpoint (see below) |
1858
+
1859
+ #### Custom Sections (Form)
1860
+
1861
+ Set `type: custom` to delegate rendering of a form section to a host-app partial. The `fields:`, `columns:`, and `disable_when:` keys are ignored — the partial owns all content inside the fieldset wrapper.
1862
+
1863
+ ```yaml
1864
+ form:
1865
+ sections:
1866
+ - title: "Variants"
1867
+ type: custom
1868
+ partial: "products/variants_editor"
1869
+ additional_permitted_params:
1870
+ - variants_json
1871
+ - address:
1872
+ - street
1873
+ - city
1874
+ - zip
1875
+ ```
1876
+
1877
+ The partial receives: `record`, `presenter`, `evaluator`, `section_config`, `form` (ActionView form builder), `current_user`, `page_definition`, `params`. The `form` local is the same `FormBuilder` instance from the enclosing `form_with` block.
1878
+
1879
+ **Example with conditional sections:**
1880
+
1881
+ ```yaml
1882
+ form:
1883
+ sections:
1884
+ - title: "Basic Information"
1885
+ columns: 2
1886
+ fields:
1887
+ - { field: name }
1888
+ - { field: stage, input_type: select }
1889
+ - title: "Revenue Details"
1890
+ columns: 2
1891
+ visible_when: { field: stage, operator: not_eq, value: lead }
1892
+ fields:
1893
+ - { field: expected_revenue, input_type: number, prefix: "$" }
1894
+ - { field: probability, input_type: slider }
1895
+ - title: "Closed Deal Info"
1896
+ disable_when: { field: stage, operator: not_in, value: [closed_won, closed_lost] }
1897
+ fields:
1898
+ - { field: close_date, input_type: date }
1899
+ - { field: close_reason, input_type: textarea }
1900
+ ```
1901
+
1902
+ **Example with collapsible sections:**
1903
+
1904
+ ```yaml
1905
+ form:
1906
+ sections:
1907
+ - title: "Basic Information"
1908
+ columns: 2
1909
+ fields:
1910
+ - { field: name }
1911
+ - { field: email }
1912
+ - title: "Advanced Options"
1913
+ columns: 2
1914
+ collapsible: true
1915
+ collapsed: true
1916
+ fields:
1917
+ - { field: api_key }
1918
+ - { field: webhook_url }
1919
+ ```
1920
+
1921
+ **Example with responsive sections:**
1922
+
1923
+ ```yaml
1924
+ form:
1925
+ sections:
1926
+ - title: "Contact Details"
1927
+ columns: 3
1928
+ responsive:
1929
+ tablet:
1930
+ columns: 2
1931
+ mobile:
1932
+ columns: 1
1933
+ fields:
1934
+ - { field: first_name }
1935
+ - { field: last_name }
1936
+ - { field: email }
1937
+ ```
1938
+
1939
+ #### Form Field Attributes
1940
+
1941
+ > **Note — `required:` is not a form-field attribute.** Presence is enforced on the **model**, not on the form. Use one of:
1942
+ >
1943
+ > - `belongs_to :customer, required: true` (for FK associations) — adds `validates: { presence: true }` automatically
1944
+ > - `validates: { presence: true }` on a `field` declaration in the model YAML/DSL (see [Validations](models.md#validations))
1945
+ >
1946
+ > Adding `required: true` directly to a form field will be rejected by the validator. The HTML5 `required` attribute on form-only basis (without model validation) is tracked as a future feature — see [aggregated.md C25](../improve-tasks/aggregated.md).
1947
+
1948
+ | Attribute | Type | Description |
1949
+ |-----------|------|-------------|
1950
+ | `field` | string | Model field name or FK column name |
1951
+ | `label` | string | Custom label for the field. Defaults to the model field's label. Also used as the text label on `type: divider` pseudo-fields |
1952
+ | `input_type` | string | Override the default input type (see below) |
1953
+ | `placeholder` | string | Placeholder text for the input |
1954
+ | `autofocus` | boolean | Auto-focus this field when the form loads |
1955
+ | `prefix` | string | Text prefix displayed before the input (e.g., `"$"` for currency) |
1956
+ | `suffix` | string | Text suffix displayed after the input (e.g., `"kg"`, `"%"`) |
1957
+ | `col_span` | integer | Number of grid columns this field spans (defaults to 1) |
1958
+ | `hint` | string | Help text displayed below the input |
1959
+ | `readonly` | boolean | Renders the field as read-only (visible but not editable) |
1960
+ | `visible_when` | hash | Condition object. When the condition evaluates to false, the field is hidden (`display:none`). Supports field-value conditions and service conditions. See [Conditional Visibility](#conditional-visibility) |
1961
+ | `disable_when` | hash | Condition object. When the condition evaluates to true, the field is visually disabled (opacity reduced, pointer-events disabled) but values are still submitted. Same syntax as `visible_when`. See [Conditional Disabling](#conditional-disabling) |
1962
+ | `default` | string | Default value for new records. Supports dynamic defaults (see below) |
1963
+ | `input_options` | hash | Type-specific input options (see below) |
1964
+ | `options` | hash | Options passed to the renderer when field is shown in read-only mode |
1965
+ | `hidden_on` | array/string | Hide field at specific breakpoints. Values: `"mobile"`, `"tablet"` |
1966
+
1967
+ **Example with new field attributes:**
1968
+
1969
+ ```yaml
1970
+ fields:
1971
+ - field: title
1972
+ placeholder: "Enter title..."
1973
+ autofocus: true
1974
+ col_span: 2
1975
+ hint: "A descriptive title for the deal"
1976
+ - field: value
1977
+ input_type: number
1978
+ prefix: "$"
1979
+ suffix: "USD"
1980
+ input_options:
1981
+ min: 0
1982
+ step: 0.01
1983
+ disable_when: { field: stage, operator: in, value: [closed_won, closed_lost] }
1984
+ - field: internal_code
1985
+ readonly: true
1986
+ hint: "Auto-generated, cannot be changed"
1987
+ - field: renewal_date
1988
+ visible_when: { field: stage, operator: not_in, value: [lead] }
1989
+ - field: created_at
1990
+ default: current_date
1991
+ hidden_on: mobile
1992
+ ```
1993
+
1994
+ #### Dynamic Defaults
1995
+
1996
+ The `default` attribute supports dynamic values that are resolved at form render time:
1997
+
1998
+ | Value | Description |
1999
+ |-------|-------------|
2000
+ | `"current_date"` | Sets the default to today's date |
2001
+ | `"current_user.id"` | Sets the default to the current user's ID |
2002
+
2003
+ ```yaml
2004
+ - { field: start_date, default: current_date }
2005
+ - { field: assigned_to_id, default: current_user.id }
2006
+ ```
2007
+
2008
+ #### Conditional Visibility
2009
+
2010
+ The `visible_when` attribute on **form fields** and **form sections** accepts a condition object that controls whether the element is shown. When the condition evaluates to false, the element is hidden with `display:none` — values are preserved in the DOM and still submitted with the form.
2011
+
2012
+ Two types of conditions are supported:
2013
+
2014
+ **Field-value conditions** reference another field on the same record and are evaluated client-side with JavaScript for instant reactivity:
2015
+
2016
+ ```yaml
2017
+ # Show only when stage is not "lead"
2018
+ - field: expected_revenue
2019
+ visible_when: { field: stage, operator: not_in, value: [lead] }
2020
+
2021
+ # Show only when a boolean flag is true
2022
+ - field: discount_reason
2023
+ visible_when: { field: discounted, operator: eq, value: true }
2024
+ ```
2025
+
2026
+ **Service conditions** are evaluated server-side (on initial render and via AJAX when field values change):
2027
+
2028
+ ```yaml
2029
+ # Show only when the record is persisted (has been saved)
2030
+ - field: internal_code
2031
+ visible_when: { service: persisted_check }
2032
+ ```
2033
+
2034
+ > **Note — `role:` shortcut is page-zone only.** On presenter sections and fields, use a service condition with the `current_user_role` registered service:
2035
+ >
2036
+ > ```yaml
2037
+ > visible_when:
2038
+ > service:
2039
+ > name: current_user_role
2040
+ > in: ["admin", "manager"]
2041
+ > ```
2042
+ >
2043
+ > The `role: [...]` top-level shortcut is recognized only by [page zone visible_when](pages.md#zone-visibility), not by section/field visible_when. The validator emits an explicit hint when this is used in the wrong place.
2044
+
2045
+ See [Condition Operators](condition-operators.md) for the full list of supported operators.
2046
+
2047
+ #### Conditional Disabling
2048
+
2049
+ The `disable_when` attribute on **form fields** and **form sections** accepts a condition object with the same syntax as `visible_when`. When the condition evaluates to true, the element is visually disabled — rendered with reduced opacity (`opacity: 0.6`) and `pointer-events: none`. Unlike the HTML `disabled` attribute, this CSS-based approach means **values are still submitted** with the form.
2050
+
2051
+ ```yaml
2052
+ # Disable the value field when the deal is closed
2053
+ - field: value
2054
+ input_type: number
2055
+ prefix: "$"
2056
+ disable_when: { field: stage, operator: in, value: [closed_won, closed_lost] }
2057
+
2058
+ # Disable notes when stage is blank
2059
+ - field: notes
2060
+ input_type: textarea
2061
+ disable_when: { field: stage, operator: blank }
2062
+ ```
2063
+
2064
+ Field-value conditions use client-side JavaScript for instant reactivity. Service conditions are evaluated server-side.
2065
+
2066
+ #### Input Types
2067
+
2068
+ | Input Type | Description | Default For |
2069
+ |------------|-------------|-------------|
2070
+ | `text` | Single-line text input | `string` fields |
2071
+ | `textarea` | Multi-line text area | `text` fields |
2072
+ | `select` | Dropdown (populated from `enum_values`) | `enum` fields |
2073
+ | `number` | Numeric input | `integer`, `float`, `decimal` fields |
2074
+ | `date` / `date_picker` | Date picker | `date` fields |
2075
+ | `datetime` | Datetime picker | `datetime` fields |
2076
+ | `boolean` / `checkbox` | Checkbox (`<input type="checkbox">`) | `boolean` fields |
2077
+ | `association_select` | Dropdown populated from associated model's records | FK fields (e.g., `company_id`) |
2078
+ | `rich_text_editor` | Rich text editor | `rich_text` fields |
2079
+ | `slider` | Range slider input | - |
2080
+ | `toggle` | Toggle switch (on/off) | - |
2081
+ | `rating` | Star rating input | - |
2082
+ | `file_upload` | File upload input with optional preview, drag-and-drop, and direct upload | `attachment` fields |
2083
+ | `array_input` / `tags` | Tag-style chip input for multi-valued fields. Add items by typing and pressing Enter | `array` fields |
2084
+
2085
+ #### Input Options
2086
+
2087
+ Input options provide type-specific configuration for form inputs.
2088
+
2089
+ **Text / Textarea:**
2090
+
2091
+ | Option | Type | Description |
2092
+ |--------|------|-------------|
2093
+ | `rows` | integer | Number of visible rows (textarea only) |
2094
+ | `max_length` | integer | Maximum character count |
2095
+ | `show_counter` | boolean | Show a character counter below the input |
2096
+
2097
+ ```yaml
2098
+ - field: description
2099
+ input_type: textarea
2100
+ input_options:
2101
+ rows: 6
2102
+ max_length: 500
2103
+ show_counter: true
2104
+ ```
2105
+
2106
+ **Number:**
2107
+
2108
+ | Option | Type | Description |
2109
+ |--------|------|-------------|
2110
+ | `min` | number | Minimum allowed value |
2111
+ | `max` | number | Maximum allowed value |
2112
+ | `step` | number | Step increment for the input |
2113
+
2114
+ ```yaml
2115
+ - field: quantity
2116
+ input_type: number
2117
+ input_options:
2118
+ min: 1
2119
+ max: 1000
2120
+ step: 1
2121
+ ```
2122
+
2123
+ **Slider:**
2124
+
2125
+ | Option | Type | Description |
2126
+ |--------|------|-------------|
2127
+ | `min` | number | Minimum slider value |
2128
+ | `max` | number | Maximum slider value |
2129
+ | `step` | number | Step increment |
2130
+ | `show_value` | boolean | Display the current value alongside the slider |
2131
+
2132
+ ```yaml
2133
+ - field: priority
2134
+ input_type: slider
2135
+ input_options:
2136
+ min: 1
2137
+ max: 10
2138
+ step: 1
2139
+ show_value: true
2140
+ ```
2141
+
2142
+ **Rating:**
2143
+
2144
+ | Option | Type | Description |
2145
+ |--------|------|-------------|
2146
+ | `max` | integer | Maximum number of stars |
2147
+
2148
+ ```yaml
2149
+ - field: satisfaction
2150
+ input_type: rating
2151
+ input_options:
2152
+ max: 5
2153
+ ```
2154
+
2155
+ **File Upload:**
2156
+
2157
+ | Option | Type | Default | Description |
2158
+ |--------|------|---------|-------------|
2159
+ | `preview` | boolean | `false` | Show a preview of the current file (image thumbnail or filename) |
2160
+ | `drag_drop` | boolean | `false` | Enable drag-and-drop upload zone |
2161
+ | `direct_upload` | boolean | `false` | Use Active Storage direct upload (uploads before form submission) |
2162
+
2163
+ ```yaml
2164
+ - field: photo
2165
+ input_type: file_upload
2166
+ input_options:
2167
+ preview: true
2168
+ drag_drop: true
2169
+ direct_upload: true
2170
+ ```
2171
+
2172
+ When `drag_drop` is enabled, the drop zone text adapts automatically: "Drop file here or click to browse" for single attachments, "Drop files here or click to browse" for multiple.
2173
+
2174
+ Attachment fields with existing files display a "Remove" checkbox on edit forms. When checked, the attachment is purged on save. For multiple attachments, each file has its own remove checkbox.
2175
+
2176
+ Attachment fields auto-resolve to `input_type: file_upload` — you only need to set `input_type` explicitly if you want to override the default.
2177
+
2178
+ **Select (enum):**
2179
+
2180
+ | Option | Type | Description |
2181
+ |--------|------|-------------|
2182
+ | `include_blank` | boolean/string | Show a blank option. `true` (default) adds an empty option, `false` removes it, a string uses custom text |
2183
+ | `include_values` | hash | Role-based whitelist. Keys are role names, values are arrays of allowed enum values |
2184
+ | `exclude_values` | hash | Role-based blacklist. Keys are role names, values are arrays of excluded enum values |
2185
+ | `sort` | string | Sort options. See ordering table below. Applies to both `select` and `radio` input types |
2186
+
2187
+ **Enum option ordering** (`input_options: { sort: ... }`):
2188
+
2189
+ | `sort` value | Behavior |
2190
+ |--------------|----------|
2191
+ | (omitted) | **Default** — declaration order from `enum_values` in the model YAML/DSL |
2192
+ | `"alphabetical"` | Sort by label (case-insensitive) |
2193
+ | `"reverse"` | Reverse declaration order |
2194
+
2195
+ When both `include_values` and `exclude_values` are specified for the same role, `include_values` is applied first (whitelist), then `exclude_values` removes from the remaining set.
2196
+
2197
+ ```yaml
2198
+ # Viewers can only see active and archived statuses
2199
+ - field: status
2200
+ input_type: select
2201
+ input_options:
2202
+ include_blank: false
2203
+ include_values:
2204
+ viewer: [active, archived]
2205
+ exclude_values:
2206
+ editor: [deleted]
2207
+ ```
2208
+
2209
+ ```yaml
2210
+ # Sort enum options alphabetically
2211
+ - field: category
2212
+ input_type: select
2213
+ input_options:
2214
+ sort: alphabetical
2215
+ ```
2216
+
2217
+ **Association Select:**
2218
+
2219
+ | Option | Type | Description |
2220
+ |--------|------|-------------|
2221
+ | `include_blank` | boolean/string | Blank option text. Default: `"-- Select --"`. Set to `false` to remove |
2222
+ | `scope` | string | Named scope to apply on the target model (e.g., `"active"`) |
2223
+ | `filter` | hash | Hash of field-value pairs passed to `.where()` |
2224
+ | `sort` | hash | Hash of field-direction pairs passed to `.order()` (e.g., `{ name: asc }`) |
2225
+ | `label_method` | string | Method to call on each record for display text. Default: `to_label` |
2226
+ | `group_by` | string | Group options by this field (renders `<optgroup>` tags) |
2227
+ | `depends_on` | hash | Cascading select configuration (see below) |
2228
+ | `scope_by_role` | hash | Role-based scope selection. Keys are role names, values are scope names or `"all"` |
2229
+
2230
+ ```yaml
2231
+ # Sorted, with custom label and blank text
2232
+ - field: company_id
2233
+ input_type: association_select
2234
+ input_options:
2235
+ sort: { name: asc }
2236
+ label_method: full_name
2237
+ include_blank: "-- Choose company --"
2238
+
2239
+ # Grouped by industry
2240
+ - field: company_id
2241
+ input_type: association_select
2242
+ input_options:
2243
+ sort: { name: asc }
2244
+ group_by: industry
2245
+
2246
+ # Scoped to active records
2247
+ - field: contact_id
2248
+ input_type: association_select
2249
+ input_options:
2250
+ scope: active_contacts
2251
+ sort: { last_name: asc }
2252
+ label_method: full_name
2253
+ ```
2254
+
2255
+ **Dependent (cascading) selects:**
2256
+
2257
+ Use `depends_on` to create cascading select relationships. When the parent field changes, the dependent select options are refreshed via AJAX.
2258
+
2259
+ | Key | Type | Description |
2260
+ |-----|------|-------------|
2261
+ | `field` | string | Parent field name that this select depends on |
2262
+ | `foreign_key` | string | FK column on the target model to filter by |
2263
+ | `reset_strategy` | string | What happens to the current value when parent changes: `"clear"` (default) or `"keep_if_valid"` |
2264
+
2265
+ ```yaml
2266
+ - field: company_id
2267
+ input_type: association_select
2268
+
2269
+ - field: contact_id
2270
+ input_type: association_select
2271
+ input_options:
2272
+ depends_on:
2273
+ field: company_id
2274
+ foreign_key: company_id
2275
+ sort: { last_name: asc }
2276
+ label_method: full_name
2277
+ ```
2278
+
2279
+ **Role-based scope:**
2280
+
2281
+ Use `scope_by_role` to apply different scopes depending on the current user's role. The special value `"all"` means no scope is applied (returns all records).
2282
+
2283
+ ```yaml
2284
+ - field: company_id
2285
+ input_type: association_select
2286
+ input_options:
2287
+ scope_by_role:
2288
+ admin: all
2289
+ editor: active_companies
2290
+ viewer: my_companies
2291
+ ```
2292
+
2293
+ When `scope_by_role` is present, the `scope` option is ignored.
2294
+
2295
+ **Multi Select:**
2296
+
2297
+ For `has_many :through` associations, use `input_type: multi_select` to render a `<select multiple>`.
2298
+
2299
+ | Option | Type | Description |
2300
+ |--------|------|-------------|
2301
+ | `association` | string | Name of the `has_many :through` association |
2302
+ | `scope` | string | Named scope on the target model |
2303
+ | `sort` | hash | Ordering for the options |
2304
+ | `label_method` | string | Method for display text |
2305
+ | `min` | integer | Minimum required selections |
2306
+ | `max` | integer | Maximum allowed selections |
2307
+
2308
+ ```yaml
2309
+ - field: tag_ids
2310
+ input_type: multi_select
2311
+ input_options:
2312
+ association: tags
2313
+ scope: active
2314
+ sort: { name: asc }
2315
+ min: 1
2316
+ max: 5
2317
+ ```
2318
+
2319
+ **Array Input:**
2320
+
2321
+ Tag-style chip input for `type: array` fields. Values appear as removable chips. Type text and press Enter to add, click x to remove.
2322
+
2323
+ | Option | Type | Default | Description |
2324
+ |--------|------|---------|-------------|
2325
+ | `placeholder` | string | `"Add item..."` | Placeholder text for the text input |
2326
+ | `max` | integer | - | Maximum number of items allowed |
2327
+ | `suggestions` | array | `[]` | Static suggestion list shown as a dropdown. Already-added values are filtered out |
2328
+
2329
+ ```ruby
2330
+ field :tags, input_type: :array_input, input_options: {
2331
+ placeholder: "Add a tag...",
2332
+ max: 10,
2333
+ suggestions: %w[ruby rails javascript python]
2334
+ }
2335
+ ```
2336
+
2337
+ Array fields automatically default to `input_type: array_input` and `renderer: collection` when no explicit override is set.
2338
+
2339
+ #### Divider Pseudo-Field
2340
+
2341
+ Use a divider to visually separate groups of fields within a section:
2342
+
2343
+ ```yaml
2344
+ fields:
2345
+ - { field: first_name }
2346
+ - { field: last_name }
2347
+ - { type: divider, label: "Contact Information" }
2348
+ - { field: email }
2349
+ - { field: phone }
2350
+ - { type: divider }
2351
+ - { field: notes, input_type: textarea }
2352
+ ```
2353
+
2354
+ A divider with a `label` renders a labeled horizontal rule. A divider without a `label` renders a plain separator line.
2355
+
2356
+ #### Info Pseudo-Field
2357
+
2358
+ Use an info pseudo-field to add contextual help text within a section. The info text spans the full width of the grid and renders as a styled callout.
2359
+
2360
+ ```yaml
2361
+ fields:
2362
+ - { type: info, text: "Prices are in USD. Tax is calculated automatically." }
2363
+ - { field: price, input_type: number }
2364
+ - { field: tax_rate, input_type: number }
2365
+ ```
2366
+
2367
+ Info pseudo-fields work in both `form` and `show` sections.
2368
+
2369
+ #### Nested Fields
2370
+
2371
+ Use `type: nested_fields` to manage a list of structured items inline within the parent form. Items can come from three different data sources, all rendered through the same pipeline:
2372
+
2373
+ | Source | Config Key | Data Origin | Persistence |
2374
+ |--------|-----------|-------------|-------------|
2375
+ | Association | `association:` | has_many child records | `accepts_nested_attributes_for` |
2376
+ | JSON field (inline) | `json_field:` | JSON column value | JSON array in single column |
2377
+ | JSON field (model-backed) | `json_field:` + `target_model:` | JSON column value | JSON array, virtual model field defs |
2378
+
2379
+ ##### Association Source
2380
+
2381
+ The most common mode — edits `has_many` child records inline:
2382
+
2383
+ ```yaml
2384
+ form:
2385
+ sections:
2386
+ - title: "Line Items"
2387
+ type: nested_fields
2388
+ association: line_items
2389
+ allow_add: true
2390
+ allow_remove: true
2391
+ add_label: "Add Line Item"
2392
+ min: 1
2393
+ max: 20
2394
+ columns: 3
2395
+ fields:
2396
+ - { field: product_id, input_type: association_select }
2397
+ - { field: quantity, input_type: number }
2398
+ - { field: unit_price, input_type: number, prefix: "$" }
2399
+ ```
2400
+
2401
+ ##### JSON Field Source (Inline)
2402
+
2403
+ Stores items as a JSON array of hashes in a single column. Field types and labels are declared directly in the presenter — no separate model needed:
2404
+
2405
+ ```yaml
2406
+ form:
2407
+ sections:
2408
+ - title: "Workflow Steps"
2409
+ type: nested_fields
2410
+ json_field: steps
2411
+ sortable: true
2412
+ allow_add: true
2413
+ allow_remove: true
2414
+ columns: 2
2415
+ fields:
2416
+ - { field: name, type: string, label: "Step Name" }
2417
+ - field: action_type
2418
+ type: string
2419
+ input_type: select
2420
+ options: [review, approve, notify]
2421
+ - field: timeout_days
2422
+ type: integer
2423
+ label: "Timeout (days)"
2424
+ visible_when: { field: action_type, operator: eq, value: review }
2425
+ ```
2426
+
2427
+ The parent model must have a `json` type field (e.g., `steps: json`). Each item is stored as a hash in the array. Inline mode is best for simple, ad-hoc structures where creating a full model definition is unnecessary.
2428
+
2429
+ ##### JSON Field Source (Model-Backed)
2430
+
2431
+ For complex, reusable structures that need validations, transforms, or custom types. The item structure is defined as a virtual model (`table_name: _virtual`):
2432
+
2433
+ ```yaml
2434
+ # config/lcp_ruby/models/address.yml
2435
+ model:
2436
+ name: address
2437
+ table_name: _virtual
2438
+ fields:
2439
+ - name: street
2440
+ type: string
2441
+ validations: [{ type: presence }]
2442
+ - name: city
2443
+ type: string
2444
+ validations: [{ type: presence }]
2445
+ - name: zip
2446
+ type: string
2447
+ - name: country
2448
+ type: string
2449
+ default: "CZ"
2450
+ ```
2451
+
2452
+ The presenter references the virtual model via `target_model:`:
2453
+
2454
+ ```yaml
2455
+ # Presenter section
2456
+ - title: "Addresses"
2457
+ type: nested_fields
2458
+ json_field: addresses
2459
+ target_model: address
2460
+ allow_add: true
2461
+ allow_remove: true
2462
+ columns: 2
2463
+ fields:
2464
+ - { field: street }
2465
+ - { field: city }
2466
+ - { field: zip }
2467
+ - { field: country }
2468
+ ```
2469
+
2470
+ Field definitions (type, label, validations) come from the target model. Each item is wrapped in a `JsonItemWrapper` for form rendering and validated against the model's rules on save. See [Virtual Models](models.md#virtual-models) for details.
2471
+
2472
+ ##### Sub-Sections in Nested Rows
2473
+
2474
+ For complex items with many fields, use `sub_sections` instead of `fields` to group fields within each row:
2475
+
2476
+ ```yaml
2477
+ - title: "Addresses"
2478
+ type: nested_fields
2479
+ json_field: addresses
2480
+ target_model: address
2481
+ allow_add: true
2482
+ allow_remove: true
2483
+ sub_sections:
2484
+ - title: "Location"
2485
+ columns: 2
2486
+ fields:
2487
+ - { field: street }
2488
+ - { field: city }
2489
+ - { field: zip }
2490
+ - { field: country }
2491
+ - title: "Additional"
2492
+ columns: 1
2493
+ collapsible: true
2494
+ collapsed: true
2495
+ fields:
2496
+ - { field: notes }
2497
+ - { field: is_primary, input_type: boolean }
2498
+ ```
2499
+
2500
+ Each sub-section renders as a `<fieldset>` inside the row. Sub-sections support `collapsible`/`collapsed`, `visible_when`, `disable_when`, and their own `columns` grid. You cannot mix `fields` and `sub_sections` in the same section — use one or the other.
2501
+
2502
+ ##### Field-Level Conditions in Nested Rows
2503
+
2504
+ Fields inside nested rows support `visible_when` and `disable_when` conditions, evaluated against the **current row's data** (not the parent record). This works for both association and JSON field sources:
2505
+
2506
+ ```yaml
2507
+ fields:
2508
+ - { field: item_type, input_type: select }
2509
+ - field: discount_percent
2510
+ input_type: number
2511
+ visible_when: { field: item_type, operator: eq, value: discount }
2512
+ hint: "Enter discount percentage"
2513
+ col_span: 2
2514
+ - field: notes
2515
+ visible_when: { field: item_type, operator: in, value: "service,discount" }
2516
+ prefix: "Note:"
2517
+ ```
2518
+
2519
+ Each row gets a `data-lcp-condition-scope` attribute that scopes the JavaScript field lookup to the current row container. This prevents cross-row interference when multiple rows have fields with the same name. See [Row-Scoped Conditions](../guides/conditional-rendering.md#row-scoped-conditions-in-nested-fields) for details.
2520
+
2521
+ ##### Nested Fields Section Attributes
2522
+
2523
+ | Attribute | Type | Default | Description |
2524
+ |-----------|------|---------|-------------|
2525
+ | `type` | string | - | Must be `"nested_fields"` |
2526
+ | `association` | string | - | Name of the `has_many` association (mutually exclusive with `json_field`) |
2527
+ | `json_field` | string | - | JSON column name to store items as array of hashes (mutually exclusive with `association`) |
2528
+ | `target_model` | string | - | Virtual model name defining item structure and validations (only with `json_field`) |
2529
+ | `allow_add` | boolean | `true` | Show a button to add new items |
2530
+ | `allow_remove` | boolean | `true` | Show a remove button on each row |
2531
+ | `add_label` | string | `"Add"` | Label for the add button |
2532
+ | `min` | integer | - | Minimum number of items required |
2533
+ | `max` | integer | - | Maximum number of items allowed |
2534
+ | `empty_message` | string | - | Message displayed when there are no items |
2535
+ | `columns` | integer | - | Number of grid columns for each row's field layout |
2536
+ | `fields` | array | - | Field definitions for each row (mutually exclusive with `sub_sections`) |
2537
+ | `sub_sections` | array | - | Sub-section definitions for grouping fields within rows (mutually exclusive with `fields`) |
2538
+ | `sortable` | boolean or string | `false` | Enable drag-and-drop reordering. `true` uses `position` field, or a string for a custom field name |
2539
+
2540
+ ##### Sub-Section Attributes
2541
+
2542
+ | Attribute | Type | Default | Description |
2543
+ |-----------|------|---------|-------------|
2544
+ | `title` | string | - | Sub-section heading (renders as `<legend>`) |
2545
+ | `columns` | integer | - | Number of grid columns for this sub-section's fields |
2546
+ | `collapsible` | boolean | `false` | Whether the sub-section can be collapsed/expanded |
2547
+ | `collapsed` | boolean | `false` | Whether the sub-section starts collapsed (requires `collapsible: true`) |
2548
+ | `visible_when` | hash | - | Condition for sub-section visibility (evaluated against the current row) |
2549
+ | `disable_when` | hash | - | Condition for sub-section disabling |
2550
+ | `fields` | array | - | Field definitions for this sub-section |
2551
+
2552
+ ##### Sortable Nested Forms
2553
+
2554
+ When `sortable` is set, nested form rows get drag handles for reordering via HTML5 Drag and Drop. The position field is automatically hidden from the visible form fields and rendered as a hidden input that updates on drag. The position field is also auto-permitted in the controller.
2555
+
2556
+ ```yaml
2557
+ form:
2558
+ sections:
2559
+ - title: "Items"
2560
+ type: nested_fields
2561
+ association: line_items
2562
+ sortable: true
2563
+ fields:
2564
+ - { field: name }
2565
+ - { field: quantity, input_type: number }
2566
+ ```
2567
+
2568
+ The child model should have an integer position field, and the parent association should specify `order: { position: asc }` to load children in the correct order. For JSON field items, array order is the position — no position key needed.
2569
+
2570
+ **Custom position field name:**
2571
+
2572
+ ```yaml
2573
+ sortable: "sort_order"
2574
+ ```
2575
+
2576
+ #### How Association Selects Work
2577
+
2578
+ When a form field has `input_type: association_select` on a foreign key column (e.g., `company_id`):
2579
+
2580
+ 1. The `LayoutBuilder` matches the FK field name against `association.foreign_key` in model metadata
2581
+ 2. Creates a synthetic `FieldDefinition` (type: integer) with the `AssociationDefinition` attached
2582
+ 3. The form renders a `<select>` populated from the target model's records
2583
+ 4. Display text uses `to_label` (if defined) or `to_s`
2584
+ 5. Falls back to a number input if the target model is not registered in LCP
2585
+ 6. FK fields bypass the `field_writable?` permission check — they are permitted separately in the controller
2586
+
2587
+ ## Search Configuration
2588
+
2589
+ Controls quick search, predefined filters, and the advanced filter builder on the index page.
2590
+
2591
+ > **Need persistent dropdown filters (selectboxes)?** Use [Search Parameters](#search-parameters) — configure `parameters` under `search` to add always-visible typed filter controls (dropdowns, toggles, ranges) directly in the toolbar. For filters that apply across **all zones** on a composite page, use [Page Parameters](../reference/pages.md#page-parameters) instead.
2592
+
2593
+ The quick search parameter is `?qs=` (query string). Previous versions used `?q=`; the rename avoids collision with Ransack's internal `q` parameter.
2594
+
2595
+ ```yaml
2596
+ search:
2597
+ enabled: true
2598
+ searchable_fields: [title, description]
2599
+ placeholder: "Search..."
2600
+ auto_search: true
2601
+ debounce_ms: 500
2602
+ min_query_length: 3
2603
+ predefined_filters:
2604
+ - { name: all, label: "All", default: true }
2605
+ - { name: open, label: "Open", scope: open_deals }
2606
+ - { name: won, label: "Won", scope: won }
2607
+
2608
+ advanced_filter:
2609
+ enabled: true
2610
+ max_conditions: 20
2611
+ max_association_depth: 3
2612
+ default_combinator: and
2613
+ allow_or_groups: true
2614
+ query_language: false
2615
+
2616
+ filterable_fields:
2617
+ - title
2618
+ - stage
2619
+ - value
2620
+ - company.name
2621
+ - contact.email
2622
+
2623
+ field_options:
2624
+ stage:
2625
+ operators: [eq, not_eq, in, not_in]
2626
+ value:
2627
+ operators: [eq, gt, gteq, lt, lteq, between]
2628
+ step: 0.01
2629
+
2630
+ presets:
2631
+ - name: high_value_open
2632
+ label: "High-value open deals"
2633
+ conditions:
2634
+ - { field: stage, operator: not_in, value: [closed_won, closed_lost] }
2635
+ - { field: value, operator: gteq, value: 10000 }
2636
+
2637
+ custom_filters:
2638
+ - name: region
2639
+ label: "Region"
2640
+ type: string
2641
+ - name: active
2642
+ label: "Active Only"
2643
+ type: boolean
2644
+
2645
+ advanced_filter:
2646
+ enabled: true
2647
+ saved_filters:
2648
+ enabled: true
2649
+ display: inline
2650
+ max_visible_pinned: 5
2651
+ ```
2652
+
2653
+ ### Search Attributes
2654
+
2655
+ | Attribute | Type | Description |
2656
+ |-----------|------|-------------|
2657
+ | `enabled` | boolean | Enable/disable the search bar |
2658
+ | `searchable_fields` | array | Field names to search with quick search (`?qs=`). Type-aware: numeric fields skip non-numeric queries, date fields match by range, enum fields match by display label |
2659
+ | `placeholder` | string | Search input placeholder text |
2660
+ | `auto_search` | boolean | When `true`, the form auto-submits as the user types (default: `false`) |
2661
+ | `debounce_ms` | integer | Debounce delay in milliseconds before auto-submit (default: `300`) |
2662
+ | `min_query_length` | integer | Minimum query length to trigger auto-submit; empty input (length 0) always triggers to clear the search (default: `2`) |
2663
+ | `columns` | integer | Number of grid columns for the search parameter layout (default: `1`) |
2664
+ | `responsive` | object | Responsive column overrides: `{ mobile: { columns: 1 }, tablet: { columns: 2 } }` |
2665
+ | `parameters` | array | Typed filter controls in the toolbar (see [Search Parameters](#search-parameters) below) |
2666
+ | `predefined_filters` | array | Filter buttons (see below) |
2667
+ | `advanced_filter` | object | Advanced filter builder configuration (see below) |
2668
+ | `saved_filters` | object | Saved filter configuration (see below) |
2669
+
2670
+ ### Quick Search Behavior
2671
+
2672
+ Quick search (`?qs=term`) is type-aware. Instead of applying a blanket `LIKE` to all `searchable_fields`, the search module checks each field's type:
2673
+
2674
+ - **String/text fields** — `ILIKE '%term%'` (contains match)
2675
+ - **Numeric fields** (integer, float, decimal) — exact match when the term is numeric, skipped otherwise
2676
+ - **Date/datetime fields** — range match when the term parses as a date, skipped otherwise
2677
+ - **Enum fields** — matches against enum display labels (e.g., searching "won" matches `closed_won` if its label contains "won")
2678
+
2679
+ Models can override quick search entirely with a `default_query` class method (escape hatch). The method receives the search term and must return a relation (which is merged with the current scope):
2680
+
2681
+ ```ruby
2682
+ # Model extension
2683
+ def self.default_query(term)
2684
+ where("title ILIKE :q OR reference_number = :exact", q: "%#{term}%", exact: term)
2685
+ end
2686
+ ```
2687
+
2688
+ ### Search Parameters
2689
+
2690
+ The `parameters` key under `search` adds typed filter controls directly in the search toolbar — dropdowns, toggles, and range inputs. Each control generates standard Ransack `?f[...]` URL params, so they flow through the existing filter pipeline with no additional configuration.
2691
+
2692
+ ```yaml
2693
+ search:
2694
+ enabled: true
2695
+ columns: 2
2696
+ searchable_fields: [name, email]
2697
+ parameters:
2698
+ - name: status
2699
+ type: enum_select
2700
+ field: status
2701
+
2702
+ - name: department
2703
+ type: association_select
2704
+ field: department_id
2705
+ label_method: full_name
2706
+ include_blank: "Any department"
2707
+
2708
+ - name: active
2709
+ type: boolean_select
2710
+ field: active
2711
+
2712
+ - name: salary_range
2713
+ type: number_range
2714
+ field: salary
2715
+ # col_span auto-defaults to 2 for range types when columns >= 2
2716
+
2717
+ - name: hired
2718
+ type: date_range
2719
+ field: hired_at
2720
+
2721
+ - name: tenant
2722
+ type: association_select
2723
+ model: tenant
2724
+ field: tenant_id
2725
+ visible_when:
2726
+ role: admin
2727
+ ```
2728
+
2729
+ #### Parameter Types
2730
+
2731
+ | Type | UI Control | Generated URL params |
2732
+ |------|-----------|---------------------|
2733
+ | `enum_select` | Tom Select dropdown | `?f[status_eq]=active` |
2734
+ | `association_select` | Tom Select (local or remote) | `?f[department_id_eq]=42` |
2735
+ | `boolean_select` | Tom Select (All / Yes / No) | `?f[active_eq]=true` |
2736
+ | `number_range` | Two number inputs (min / max) | `?f[salary_gteq]=1000&f[salary_lteq]=5000` |
2737
+ | `date_range` | Two date inputs (from / to) | `?f[hired_at_gteq]=2025-01-01&f[hired_at_lteq]=2025-12-31` |
2738
+
2739
+ #### Parameter Attributes
2740
+
2741
+ | Attribute | Type | Applies to | Description |
2742
+ |-----------|------|-----------|-------------|
2743
+ | `name` | string | all | Parameter identifier (required) |
2744
+ | `type` | string | all | One of `enum_select`, `association_select`, `boolean_select`, `number_range`, `date_range` (required) |
2745
+ | `field` | string | all | Model field name to filter by (required) |
2746
+ | `model` | string | `association_select` | Target model name. Auto-derived from the field's `belongs_to` association if omitted |
2747
+ | `options` | array | `enum_select` | Explicit option values. Auto-derived from model enum definition if omitted |
2748
+ | `label_key` | string | all | i18n key for the parameter label. Falls back to `lcp_ruby.parameters.<name>`, then humanized name |
2749
+ | `label_method` | string | `association_select` | Method called on each record to produce the display label. Defaults to `to_s` |
2750
+ | `include_blank` | string/boolean | select types | Custom label for the blank "All" option. Set to `false` to omit the blank option entirely |
2751
+ | `visible_when` | object | all | Role-based visibility: `{ role: admin }` or `{ role: [admin, manager] }`. Only the `role` key is supported (no condition-based evaluation on index pages). Hidden parameters are not rendered AND their `?f[...]` params are stripped server-side |
2752
+ | `search` | boolean | `association_select` | Enable remote server-side search (Tom Select remote mode). Default: local mode with preloaded options |
2753
+ | `min_query_length` | integer | `association_select` | Minimum characters before remote search triggers (requires `search: true`) |
2754
+ | `per_page` | integer | `association_select` | Results per page for remote search (requires `search: true`) |
2755
+ | `col_span` | integer | all | Number of grid columns this parameter spans. Range types auto-default to 2 when `columns >= 2` |
2756
+
2757
+ #### Grid Layout
2758
+
2759
+ The search parameter bar uses CSS Grid, configured via `search.columns` (default: 1). Each grid cell displays the label inline beside the control. The quick search text input is also a grid item.
2760
+
2761
+ - **`columns: 1`** (default) — single-column stack, one parameter per row.
2762
+ - **`columns: 2`** or more — multi-column grid. Range types auto-span 2 columns. Use `col_span` to override.
2763
+ - **`responsive`** — override columns per breakpoint: `responsive: { mobile: { columns: 1 } }`.
2764
+
2765
+ #### Auto-Derivation
2766
+
2767
+ - **`enum_select`** without `options`: options derived from the model's enum field definition.
2768
+ - **`association_select`** without `model`: model derived from the field's `belongs_to` association (e.g., `field: department_id` looks up `belongs_to :department`).
2769
+ - **`boolean_select`**: always renders All / Yes / No.
2770
+
2771
+ #### Interaction with Other Features
2772
+
2773
+ - **Quick search / predefined filters / advanced filter**: all combine with AND logic. Parameters and the advanced filter builder share the `?f[...]` namespace — they are two UI views over the same filter state.
2774
+ - **Saved filters**: parameter values are naturally captured when saving a filter (no special handling needed).
2775
+ - **Page filter form**: when a presenter is rendered inside a composite page that declares a `filter_form:` block, presenter search parameters whose `field` matches a `filter_form:` field are suppressed to avoid duplicate controls. (The legacy `parameters:` page block was retired in PR 5; see [Page Filters reference](page_filters.md).)
2776
+ - **Export**: works automatically — `QueryBuilder` already processes `?f[...]` params.
2777
+
2778
+ ### Predefined Filter Attributes
2779
+
2780
+ | Attribute | Type | Description |
2781
+ |-----------|------|-------------|
2782
+ | `name` | string | Filter identifier |
2783
+ | `label` | string | Button text |
2784
+ | `default` | boolean | Whether this filter is active by default |
2785
+ | `scope` | string | Named [scope](models.md#scopes) to apply. Omit for the "all" filter |
2786
+
2787
+ Predefined filters render as buttons above the table. Each filter (except the default "all") maps to a named scope defined in the model YAML.
2788
+
2789
+ ### Advanced Filter Attributes
2790
+
2791
+ The `advanced_filter` key controls the visual filter builder that lets users construct field-level filter conditions.
2792
+
2793
+ | Attribute | Type | Default | Description |
2794
+ |-----------|------|---------|-------------|
2795
+ | `enabled` | boolean | `false` | Show the filter builder. Requires both `search.enabled: true` and `advanced_filter.enabled: true` |
2796
+ | `max_conditions` | integer | `10` | Safety limit on filter rows |
2797
+ | `max_association_depth` | integer | `1` | Maximum levels of association traversal (e.g., `company.country.name` = depth 2) |
2798
+ | `default_combinator` | string | `"and"` | Top-level combinator: `"and"` or `"or"` |
2799
+ | `allow_or_groups` | boolean | `true` | Allow users to create OR groups within the filter |
2800
+ | `query_language` | boolean | `false` | Show "Edit as QL" toggle for text-based query input |
2801
+ | `max_nesting_depth` | integer | `2` | Maximum depth of AND/OR nesting. `1` = flat AND only, `2` = AND with OR groups (current default), `3+` = full recursive nesting |
2802
+ | `filterable_fields` | array | auto-detected | Fields available in the filter dropdown. Supports dot-path association syntax (e.g., `company.name`). If omitted, all readable fields are auto-detected from permissions. Mutually exclusive with `filterable_fields_except` |
2803
+ | `filterable_fields_except` | array | `[]` | Fields/associations to exclude from auto-detection. Supports direct field names, association names (excludes entire subtree), and dot-paths. Mutually exclusive with `filterable_fields` |
2804
+ | `field_options` | object | `{}` | Per-field operator overrides and input hints (see below) |
2805
+ | `presets` | array | `[]` | Predefined filter combinations that populate the builder with one click |
2806
+ | `custom_filters` | array | `[]` | Explicit declaration of `filter_*` methods for UI exposure (see [Custom Filter Methods](../guides/extensibility.md#custom-filter-methods)) |
2807
+
2808
+ #### `filterable_fields`
2809
+
2810
+ When omitted, the platform auto-detects filterable fields from the model definition, filtered by the current user's read permissions. When specified, only the listed fields appear in the filter dropdown. Association fields use dot notation:
2811
+
2812
+ ```yaml
2813
+ filterable_fields:
2814
+ - title # Direct field
2815
+ - stage # Enum — auto-detects values for dropdown
2816
+ - value # Numeric — auto-detects numeric operators
2817
+ - company.name # belongs_to association (1 level)
2818
+ - contact.company.country # Association chain (2 levels)
2819
+ ```
2820
+
2821
+ #### `filterable_fields_except`
2822
+
2823
+ When using auto-detection (no `filterable_fields`), exclude specific fields or entire association subtrees:
2824
+
2825
+ ```yaml
2826
+ filterable_fields_except:
2827
+ - internal_notes # Exclude a direct field
2828
+ - audit_log # Exclude entire association subtree
2829
+ - company.tax_id # Exclude a specific association field
2830
+ - company.audit_log # Exclude a sub-association
2831
+ ```
2832
+
2833
+ Exclusion matching rules:
2834
+ - **Direct field name** (`internal_notes`) — excludes that field from the root model.
2835
+ - **Association name without dot** (`audit_log`) — excludes the entire association subtree.
2836
+ - **Dot-path to a field** (`company.tax_id`) — excludes that specific field on the associated model.
2837
+ - **Dot-path to an association** (`company.audit_log`) — excludes a sub-association within an association.
2838
+
2839
+ Setting both `filterable_fields` and `filterable_fields_except` is a configuration error.
2840
+
2841
+ #### `field_options`
2842
+
2843
+ Override the default operators or input hints for specific fields:
2844
+
2845
+ ```yaml
2846
+ field_options:
2847
+ stage:
2848
+ operators: [eq, not_eq, in, not_in] # Only these operators
2849
+ value:
2850
+ operators: [eq, gt, gteq, lt, lteq, between, present, blank]
2851
+ step: 0.01 # Numeric input step
2852
+ ```
2853
+
2854
+ See the [Advanced Search design document](../design/advanced_search.md) for the full operator-by-type matrix.
2855
+
2856
+ #### `presets`
2857
+
2858
+ Presets are predefined filter combinations that appear as one-click pill buttons at the top of the filter panel. Each preset has a `name`, `label`, and a list of `conditions`:
2859
+
2860
+ ```yaml
2861
+ presets:
2862
+ - name: high_value_open
2863
+ label: "High-value open deals"
2864
+ conditions:
2865
+ - { field: stage, operator: not_in, value: [closed_won, closed_lost] }
2866
+ - { field: value, operator: gteq, value: 10000 }
2867
+ - name: closing_soon
2868
+ label: "Closing this month"
2869
+ conditions:
2870
+ - { field: expected_close_date, operator: this_month }
2871
+ ```
2872
+
2873
+ DSL equivalent:
2874
+
2875
+ ```ruby
2876
+ advanced_filter do
2877
+ preset :high_value_open,
2878
+ label: "High-value open deals",
2879
+ conditions: [
2880
+ { field: "stage", operator: "not_in", value: %w[closed_won closed_lost] },
2881
+ { field: "value", operator: "gteq", value: "10000" }
2882
+ ]
2883
+ end
2884
+ ```
2885
+
2886
+ **UI behavior:**
2887
+
2888
+ - The preset bar renders inside the filter panel, above the condition rows, with a "Presets:" label followed by pill-shaped buttons.
2889
+ - The preset bar only appears when at least one preset is defined.
2890
+ - **Click** applies the preset immediately — fills conditions and navigates (page reload with filter URL params).
2891
+ - **Active highlighting** — if the current URL filters exactly match a preset's conditions, that preset button is highlighted in blue.
2892
+ - Presets work with all operator types including no-value operators (`true`, `present`, `this_month`, etc.) and multi-value operators (`in`, `not_in`).
2893
+
2894
+ Each condition in a preset uses the same `field`, `operator`, and `value` format as the filter builder. The `field` supports dot-path association syntax (e.g., `company.name`). The `operator` must be one of the [supported operators](condition-operators.md). The `value` is optional for no-value operators (`present`, `blank`, `null`, `true`, `this_month`, etc.).
2895
+
2896
+ #### `custom_filters`
2897
+
2898
+ Declares `filter_*` class methods on the model so they appear in the filter dropdown UI. Each entry has a `name`, `label`, and `type` (for the value input):
2899
+
2900
+ ```yaml
2901
+ custom_filters:
2902
+ - name: region
2903
+ label: "Region"
2904
+ type: string
2905
+ - name: active
2906
+ label: "Active Only"
2907
+ type: boolean
2908
+ ```
2909
+
2910
+ See [Custom Filter Methods](../guides/extensibility.md#custom-filter-methods) for implementation details.
2911
+
2912
+ ### Saved Filter Attributes
2913
+
2914
+ The `saved_filters` block (nested inside `advanced_filter`) enables user-persistent named filters. Users can save filter conditions, pin favorites, set defaults, and share with roles/groups.
2915
+
2916
+ **Prerequisite:** The saved filter model must exist. Generate it with `rails generate lcp_ruby:saved_filters`.
2917
+
2918
+ ```yaml
2919
+ advanced_filter:
2920
+ enabled: true
2921
+ saved_filters:
2922
+ enabled: true
2923
+ display: inline
2924
+ max_visible_pinned: 5
2925
+ ```
2926
+
2927
+ | Attribute | Type | Default | Description |
2928
+ |-----------|------|---------|-------------|
2929
+ | `enabled` | boolean | `false` | Allow users to save, load, and manage personal filters |
2930
+ | `display` | string | `"inline"` | UI display mode: `"inline"` (pill buttons above filter), `"dropdown"` (select menu), `"sidebar"` (side panel) |
2931
+ | `max_visible_pinned` | integer | `3` | Maximum number of pinned filters shown directly (overflow goes to "more" menu) |
2932
+
2933
+ **Saved filter visibility levels:**
2934
+
2935
+ | Level | Description |
2936
+ |-------|-------------|
2937
+ | `personal` | Only the owner can see and use the filter |
2938
+ | `role` | Visible to all users with the `target_role` |
2939
+ | `group` | Visible to members of the `target_group` |
2940
+ | `global` | Visible to all users |
2941
+
2942
+ **Saved filter model fields:** `name`, `description`, `target_presenter`, `condition_tree` (JSON), `ql_text`, `visibility` (enum), `owner_id`, `target_role`, `target_group`, `position`, `icon`, `color`, `pinned` (boolean), `default_filter` (boolean).
2943
+
2944
+ See the [Saved Filters design spec](../design/saved_filters.md) for the complete feature description.
2945
+
2946
+ ## Actions Configuration
2947
+
2948
+ Controls CRUD buttons, custom actions, and form submit buttons.
2949
+
2950
+ ```yaml
2951
+ actions:
2952
+ collection:
2953
+ - { name: create, type: built_in, label: "New Deal", icon: plus }
2954
+ single:
2955
+ - { name: show, type: built_in, icon: eye }
2956
+ - { name: edit, type: built_in, icon: pencil }
2957
+ - name: close_won
2958
+ type: custom
2959
+ label: "Close as Won"
2960
+ icon: check-circle
2961
+ confirm: true
2962
+ confirm_message: "Mark this deal as won?"
2963
+ visible_when: { field: stage, operator: not_in, value: [closed_won, closed_lost] }
2964
+ - { name: destroy, type: built_in, icon: trash, confirm: true, style: danger, batch: true }
2965
+ batch: []
2966
+ form:
2967
+ - { name: save, type: built_in, style: primary, redirect: show }
2968
+ - { name: save_and_new, label: "Save & New", redirect: new, only_on: create }
2969
+ - { name: cancel, type: built_in, style: secondary }
2970
+ ```
2971
+
2972
+ ### Action Categories
2973
+
2974
+ | Category | Description |
2975
+ |----------|-------------|
2976
+ | `collection` | Actions on the collection (no specific record). Displayed above the table. |
2977
+ | `single` | Actions on a single record. Displayed in each table row. |
2978
+ | `batch` | Actions on multiple selected records. Renders checkboxes and a fixed toolbar at the bottom of the index page. See [Batch Actions Guide](../guides/batch-actions.md). |
2979
+ | `form` | Submit buttons on create/edit forms. Replaces the default Save/Cancel when configured. See [Form Actions](#form-actions). |
2980
+
2981
+ ### Auto-Derived Batch Actions
2982
+
2983
+ Single actions can auto-derive a batch version using the `batch` attribute:
2984
+
2985
+ ```yaml
2986
+ single:
2987
+ - { name: destroy, type: built_in, confirm: true, style: danger, batch: true }
2988
+ - { name: archive, type: custom, icon: box, batch: { select_all_filter: false } }
2989
+ ```
2990
+
2991
+ When `batch: true`, the platform derives a batch action inheriting `name`, `type`, `icon`, `confirm`, `confirm_message`, `style`, `label`, and `action_class`. When `batch:` is a Hash, the hash values are merged as batch-specific overrides. Attributes like `visible_when`, `disable_when`, and `dialog` are not inherited. If an explicit batch action with the same name exists, it takes precedence over the derived one.
2992
+
2993
+ `batch:` on `type: dialog` or `type: transition` actions is ignored. `batch:` on collection actions has no effect.
2994
+
2995
+ See [Batch Actions Guide](../guides/batch-actions.md) for full details and examples.
2996
+
2997
+ ### Batch Action Attributes
2998
+
2999
+ Batch actions (both explicit and auto-derived) support the same base attributes as other actions (`name`, `type`, `icon`, `confirm`, `style`) plus the following batch-specific attributes:
3000
+
3001
+ | Attribute | Type | Default | Description |
3002
+ |-----------|------|---------|-------------|
3003
+ | `min_selection` | integer | 1 | Minimum records that must be selected for the action button to activate |
3004
+ | `max_selection` | integer | — | Maximum records that can be selected |
3005
+ | `max_batch_records` | integer | — | Server-side limit on records affected. If exceeded, the action returns an error without executing. Applied to both ID and filter selection modes. |
3006
+ | `result_log` | boolean | `false` | Enable per-record result tracking in the batch operation log. Requires the `lcp_ruby:batch_operations` generator to have been run. |
3007
+ | `select_all_filter` | boolean | `true` | Allow "select all matching filter" mode for this action |
3008
+ | `execution` | string | `inline` | Execution mode: `inline` (synchronous), `background` (always enqueue job), or `auto` (choose based on `background_threshold`). `background` and `auto` require the `lcp_ruby:background_jobs` generator. |
3009
+ | `background_threshold` | integer | 100 | For `execution: auto` only — record count above which the action runs as a background job |
3010
+
3011
+ **Built-in batch action names:** `destroy`, `restore`, `permanently_destroy`, `update`, `export`. The `restore` and `permanently_destroy` actions require `soft_delete: true` on the model.
3012
+
3013
+ ### Action Attributes
3014
+
3015
+ | Attribute | Type | Description |
3016
+ |-----------|------|-------------|
3017
+ | `name` | string | Action identifier. For built-in: `show`, `edit`, `destroy`, `create`, `restore`, `permanently_destroy`, `export` |
3018
+ | `type` | string | `built_in`, `custom`, or `dialog` |
3019
+ | `label` | string | Display text |
3020
+ | `icon` | string | Icon name |
3021
+ | `confirm` | boolean or hash | Show a confirmation dialog before executing (see [Confirm Per Role](#confirm-per-role) and [Styled Confirm](#styled-confirm)) |
3022
+ | `confirm_message` | string | Custom text for the confirmation dialog |
3023
+ | `style` | string | CSS style hint (e.g., `danger` for destructive actions) |
3024
+ | `visible_when` | object | Condition controlling visibility (see below) |
3025
+ | `disable_when` | object | Condition controlling disabled state. When true, the action button renders as a disabled span instead of a clickable link/button (see below) |
3026
+ | `dialog` | object | Dialog configuration (for `type: dialog` actions only). Keys: `page`, `on_success`, `record`, `defaults`. See [Dialogs Reference](dialogs.md). |
3027
+ | `batch` | boolean or object | Single actions only. `true` auto-derives a batch action; a Hash merges batch-specific overrides. See [Auto-Derived Batch Actions](#auto-derived-batch-actions). |
3028
+
3029
+ ### Action Types
3030
+
3031
+ - **`built_in`** — standard CRUD actions (`show`, `edit`, `destroy`, `create`, `restore`, `permanently_destroy`) and non-CRUD built-in actions (`export`). CRUD actions are authorized via `PermissionEvaluator.can?`. Non-CRUD built-in actions (like `export`) are authorized via `can_execute_action?` against the `actions` permission config. The `restore` and `permanently_destroy` actions are used with [soft delete](models.md#soft_delete) archive presenters. The `export` action requires the [export configuration](export.md).
3032
+ - **`custom`** — user-defined actions. Authorization checked via `can_execute_action?`. Dispatched to registered action classes. See [Custom Actions](../guides/custom-actions.md).
3033
+ - **`dialog`** — opens a page in a modal dialog. Authorization checked against the dialog page's presenter. Config: `dialog: { page: <page_name>, on_success: reload|close|redirect|confirm_action, record: current, defaults: {...} }`. See [Dialogs Reference](dialogs.md).
3034
+
3035
+ ### Action Visibility
3036
+
3037
+ Action buttons are filtered through three checks (in order):
3038
+
3039
+ 1. **Record rules** — built-in `edit`/`destroy` actions are automatically hidden when [record_rules](permissions.md#record-rules) deny the corresponding CRUD operation for that record. No `visible_when` configuration needed. The `show` action is not affected.
3040
+ 2. **`visible_when`** — a [condition object](condition-operators.md) evaluated per-record via `ConditionEvaluator`. When omitted, the action is always visible.
3041
+ 3. **`disable_when`** — same syntax, controls disabled state (see below).
3042
+
3043
+ All three checks apply with AND semantics. Record rules and `visible_when` can be used independently or together.
3044
+
3045
+ ```yaml
3046
+ visible_when: { field: stage, operator: not_in, value: [closed_won, closed_lost] }
3047
+ ```
3048
+
3049
+ When `visible_when` is omitted and no record_rules apply, the action is always visible (subject to role-level permission checks).
3050
+
3051
+ ### Action Disabling
3052
+
3053
+ The `disable_when` attribute uses the same [condition object](condition-operators.md) syntax as `visible_when`. When the condition evaluates to true, the action button is rendered as a disabled `<span>` instead of a clickable link or button:
3054
+
3055
+ ```yaml
3056
+ single:
3057
+ - name: send_invoice
3058
+ type: custom
3059
+ label: "Send Invoice"
3060
+ icon: mail
3061
+ disable_when: { field: value, operator: blank }
3062
+ - name: close_won
3063
+ type: custom
3064
+ label: "Close as Won"
3065
+ icon: check-circle
3066
+ visible_when: { field: stage, operator: not_in, value: [closed_won, closed_lost] }
3067
+ disable_when: { field: value, operator: lte, value: 0 }
3068
+ ```
3069
+
3070
+ An action can use both `visible_when` and `disable_when` together. The visibility condition is evaluated first — if the action is hidden, `disable_when` has no effect.
3071
+
3072
+ ### Confirm Per Role
3073
+
3074
+ The `confirm` attribute supports role-based resolution. Instead of a simple boolean, you can use a hash with `except` or `only` keys to control which roles see the confirmation dialog:
3075
+
3076
+ ```yaml
3077
+ actions:
3078
+ single:
3079
+ # Confirm for everyone (existing behavior):
3080
+ - name: destroy
3081
+ type: built_in
3082
+ confirm: true
3083
+
3084
+ # Confirm for all EXCEPT these roles (admin skips confirm):
3085
+ - name: archive
3086
+ type: custom
3087
+ confirm:
3088
+ except: [admin]
3089
+
3090
+ # Confirm ONLY for these roles (others skip):
3091
+ - name: force_delete
3092
+ type: custom
3093
+ confirm:
3094
+ only: [viewer, sales_rep]
3095
+ ```
3096
+
3097
+ | Value | Behavior |
3098
+ |-------|----------|
3099
+ | `true` | Confirm for all roles (backward compatible) |
3100
+ | `false` or omitted | No confirm for any role (backward compatible) |
3101
+ | `{ except: [roles] }` | Confirm for all roles EXCEPT the listed ones |
3102
+ | `{ only: [roles] }` | Confirm ONLY for the listed roles |
3103
+
3104
+ The resolved `confirm` value (true/false) is set on the action before rendering, so view templates work unchanged.
3105
+
3106
+ ### Styled Confirm
3107
+
3108
+ For custom confirmation styling with title, message, and button style:
3109
+
3110
+ ```yaml
3111
+ - name: deactivate
3112
+ type: custom
3113
+ confirm:
3114
+ title_key: lcp_ruby.confirm.deactivate_title
3115
+ message_key: lcp_ruby.confirm.deactivate_message
3116
+ style: danger
3117
+ ```
3118
+
3119
+ | Key | Type | Description |
3120
+ |-----|------|-------------|
3121
+ | `title_key` | string | i18n key for the dialog title |
3122
+ | `message_key` | string | i18n key for the confirmation message |
3123
+ | `style` | string | Button style: `danger`, `warning`, `primary` |
3124
+
3125
+ ### Page-Based Confirm
3126
+
3127
+ For complex confirmations requiring user input (e.g., a reason field), use a page reference:
3128
+
3129
+ ```yaml
3130
+ - name: delete_with_reason
3131
+ type: custom
3132
+ confirm:
3133
+ page: delete_reason_form
3134
+ ```
3135
+
3136
+ The confirmation page is typically a virtual model (`table_name: _virtual`) with validation. On success, `on_success: confirm_action` submits the original action with `confirmation_data[...]` params. See [Dialogs Reference](dialogs.md) for details.
3137
+
3138
+ ## Form Actions
3139
+
3140
+ Form actions define the submit buttons rendered in the form footer on create/edit pages. When `actions.form` is not configured, the platform renders default Save and Cancel buttons (backward compatible).
3141
+
3142
+ When `actions.form` is explicitly configured, **only the listed actions appear** — the configurator has full control over which buttons render, in what order.
3143
+
3144
+ ### Form Action Attributes
3145
+
3146
+ | Attribute | Type | Default | Description |
3147
+ |-----------|------|---------|-------------|
3148
+ | `name` | string | required | Unique identifier. Sent as `_form_action` param to the controller. |
3149
+ | `type` | string | — | `built_in` for platform-provided actions (`save`, `cancel`). Omit for custom form actions. |
3150
+ | `label` | string | i18n lookup | Button label text. |
3151
+ | `icon` | string | — | Icon name (rendered before label). |
3152
+ | `style` | string | `secondary` | Button style: `primary`, `secondary`, `success`, `danger`, `warning`, `outline`. |
3153
+ | `redirect` | string or hash | presenter's `redirect_after` | Where to redirect after pipeline completes. String: `show`, `edit`, `new`, `index`. Hash: `{ association: <name>, action: <target> }` or `{ presenter: <slug>, action: <target>, defaults: {...} }` for cross-presenter navigation. Ignored in dialog context. |
3154
+ | `set_fields` | hash | — | Fields to set before save. Same value syntax as workflow `set_fields` (`{ date: now }`, `{ current_user: id }`, static values). Values are configurator-controlled and bypass field-level write permissions. |
3155
+ | `confirm` | boolean | `false` | Show confirmation dialog before submit. |
3156
+ | `confirm_message` | string | — | Custom confirmation text. |
3157
+ | `visible_when` | condition | — | Field-value condition for visibility. |
3158
+ | `disable_when` | condition | — | Condition for disabled state. |
3159
+ | `only_on` | string | both | `create`, `update`, or omit for both. |
3160
+ | `only_roles` | array | all | Roles that see this button. |
3161
+ | `except_roles` | array | none | Roles that don't see this button. |
3162
+ | `dialog_behavior` | string | `close` in dialog context | Dialog-only: `close` (save & close), `reset` (save & clear form), `reload` (save & reload parent). Ignored outside dialog context. |
3163
+ | `pipeline` | array | `[save]` | Ordered operations to execute in a single transaction. Steps: `save`, `{ transition: <name> }`, `{ action: <name> }`. See [Pipeline](#pipeline). |
3164
+ | `transition` | string | — | Sugar for `pipeline: [save, { transition: <name> }]`. Cannot combine with `pipeline` or `action`. |
3165
+ | `action` | string | — | Sugar for `pipeline: [save, { action: <name> }]`. Cannot combine with `pipeline` or `transition`. |
3166
+ | `position` | string | `inline` | `inline` (rendered in row), `dropdown` (forced into overflow menu). |
3167
+
3168
+ ### Built-In Form Actions
3169
+
3170
+ | Name | Behavior |
3171
+ |------|----------|
3172
+ | `save` | Save the record and redirect per config. |
3173
+ | `cancel` | Does **not** submit the form. Renders as a link to index (or close button in dialog context). |
3174
+
3175
+ ### Default Form Actions
3176
+
3177
+ When `actions.form` is absent, the platform injects:
3178
+
3179
+ ```yaml
3180
+ actions:
3181
+ form:
3182
+ - { name: save, type: built_in, style: primary }
3183
+ - { name: cancel, type: built_in, style: secondary }
3184
+ ```
3185
+
3186
+ ### Interaction with `redirect_after`
3187
+
3188
+ When form actions are present, `redirect_after` serves as the **fallback** for any form action that does not specify its own `redirect`:
3189
+
3190
+ ```yaml
3191
+ redirect_after:
3192
+ create: show
3193
+ update: show
3194
+
3195
+ actions:
3196
+ form:
3197
+ - name: save
3198
+ type: built_in
3199
+ style: primary
3200
+ # redirect not set → falls back to redirect_after (show)
3201
+ - name: save_and_new
3202
+ redirect: new
3203
+ only_on: create
3204
+ - name: cancel
3205
+ type: built_in
3206
+ ```
3207
+
3208
+ ### Form Action i18n
3209
+
3210
+ Button labels use the following lookup chain:
3211
+
3212
+ 1. Explicit `label` attribute on the form action (if set)
3213
+ 2. `lcp_ruby.presenters.<presenter_name>.actions.form.<action_name>` (presenter-specific)
3214
+ 3. `lcp_ruby.actions.form.<action_name>` (global)
3215
+ 4. Humanized `name` as fallback (e.g., `save_and_new` → "Save and new")
3216
+
3217
+ Built-in `save` uses `lcp_ruby.form.create` / `lcp_ruby.form.update` depending on context.
3218
+
3219
+ ### Authorization
3220
+
3221
+ 1. **Role-based filtering** — `only_roles` / `except_roles` filter buttons by the current user's roles at render time.
3222
+ 2. **Pundit policy** — built-in `save` checks `create?` / `update?` policy. Non-built-in form actions check `can_execute_action?(name)`. Built-in `cancel` requires no authorization.
3223
+ 3. **Submit-time whitelist** — the controller re-validates `_form_action` against the authorized form actions at submit time (re-checks `only_on`, roles, permissions). Unknown or unauthorized values fall back to `save` behavior.
3224
+
3225
+ ### `set_fields` Precedence
3226
+
3227
+ `set_fields` values are applied **after** user input from the form. This means configurator values intentionally override anything the user submits for the same field:
3228
+
3229
+ 1. `record.assign_attributes(permitted_params)` — user input
3230
+ 2. `apply_set_fields(record, form_action_config)` — configurator overrides
3231
+ 3. `record.save!`
3232
+
3233
+ ### Examples
3234
+
3235
+ **Quick data entry:**
3236
+
3237
+ ```yaml
3238
+ actions:
3239
+ form:
3240
+ - { name: save, type: built_in, style: primary, redirect: show }
3241
+ - { name: save_and_new, label: "Save & New", redirect: new, only_on: create }
3242
+ - { name: cancel, type: built_in }
3243
+ ```
3244
+
3245
+ **Content publishing with set_fields:**
3246
+
3247
+ ```yaml
3248
+ actions:
3249
+ form:
3250
+ - name: save_draft
3251
+ label: "Save Draft"
3252
+ redirect: edit
3253
+ - name: save_and_publish
3254
+ label: "Publish"
3255
+ style: primary
3256
+ icon: globe
3257
+ set_fields:
3258
+ status: published
3259
+ published_at: { date: now }
3260
+ confirm: true
3261
+ redirect: show
3262
+ visible_when: { field: status, operator: not_eq, value: published }
3263
+ - { name: cancel, type: built_in }
3264
+ ```
3265
+
3266
+ **Dialog with "add another":**
3267
+
3268
+ ```yaml
3269
+ actions:
3270
+ form:
3271
+ - { name: save_close, label: "Save & Close", style: primary, dialog_behavior: close }
3272
+ - { name: save_another, label: "Save & Add Another", dialog_behavior: reset, only_on: create }
3273
+ - { name: cancel, type: built_in }
3274
+ ```
3275
+
3276
+ **Save + workflow transition (pipeline):**
3277
+
3278
+ ```yaml
3279
+ actions:
3280
+ form:
3281
+ - name: save_draft
3282
+ label: "Save Draft"
3283
+ style: secondary
3284
+ redirect: edit
3285
+
3286
+ - name: save_and_submit
3287
+ label: "Save & Submit"
3288
+ style: primary
3289
+ icon: send
3290
+ transition: submit
3291
+ confirm: true
3292
+ confirm_message: "Submit for approval? You won't be able to edit after this."
3293
+ visible_when: { field: status, operator: eq, value: draft }
3294
+ redirect: show
3295
+
3296
+ - { name: cancel, type: built_in }
3297
+ ```
3298
+
3299
+ **Explicit pipeline (save + transition + custom action):**
3300
+
3301
+ ```yaml
3302
+ actions:
3303
+ form:
3304
+ - name: save_submit_and_notify
3305
+ label: "Save, Submit & Notify"
3306
+ style: primary
3307
+ pipeline:
3308
+ - save
3309
+ - { transition: submit }
3310
+ - { action: notify_approvers }
3311
+ confirm: true
3312
+ visible_when: { field: status, operator: eq, value: draft }
3313
+ redirect: show
3314
+
3315
+ - { name: cancel, type: built_in }
3316
+ ```
3317
+
3318
+ **Cross-presenter redirect:**
3319
+
3320
+ ```yaml
3321
+ actions:
3322
+ form:
3323
+ - name: save_and_add_line_item
3324
+ label: "Save & Add Line Item"
3325
+ redirect:
3326
+ presenter: line_items
3327
+ action: new
3328
+ defaults:
3329
+ order_id: { field_ref: id }
3330
+ only_on: update
3331
+ ```
3332
+
3333
+ ### Pipeline
3334
+
3335
+ Each form action executes a **pipeline** — an ordered list of operations that run within a single database transaction. The `pipeline` attribute makes the execution sequence explicit: `save` is a step you include (or omit), not an implicit behavior.
3336
+
3337
+ #### Pipeline steps
3338
+
3339
+ | Step | Syntax | Description |
3340
+ |------|--------|-------------|
3341
+ | **save** | `save` | Save the record (create or update). Applies `set_fields` before saving. |
3342
+ | **transition** | `{ transition: <name> }` | Fire a workflow transition via `TransitionExecutor`. |
3343
+ | **action** | `{ action: <name> }` | Execute a custom action class. Resolved as `<model>/<name>`. |
3344
+
3345
+ #### Sugar attributes
3346
+
3347
+ For simple cases, `transition:` and `action:` top-level attributes expand into a pipeline automatically:
3348
+
3349
+ ```yaml
3350
+ # This:
3351
+ - name: save_and_submit
3352
+ transition: submit
3353
+
3354
+ # Expands to:
3355
+ - name: save_and_submit
3356
+ pipeline: [save, { transition: submit }]
3357
+ ```
3358
+
3359
+ When neither `pipeline`, `transition`, nor `action` is set, the default pipeline is `[save]`.
3360
+
3361
+ #### Execution semantics
3362
+
3363
+ 1. The entire pipeline runs inside a single `ActiveRecord::Base.transaction`.
3364
+ 2. Steps execute in order. If any step fails, the transaction rolls back.
3365
+ 3. `set_fields` is applied before the `save` step.
3366
+ 4. For custom action results: `result.redirect_to` takes precedence over the form action's `redirect`. `result.message` is displayed as flash. When multiple actions run, last non-nil values win.
3367
+ 5. Workflow events are deferred until after the transaction commits.
3368
+
3369
+ #### Pipeline on single actions
3370
+
3371
+ The `pipeline` attribute also works on `single` actions (show page buttons). On show pages there is no `save` step — the pipeline operates on the existing record:
3372
+
3373
+ ```yaml
3374
+ actions:
3375
+ single:
3376
+ - name: approve
3377
+ label: "Approve"
3378
+ style: success
3379
+ icon: check
3380
+ pipeline:
3381
+ - { transition: approve }
3382
+ visible_when: { field: status, operator: eq, value: pending_approval }
3383
+ only_roles: [admin, manager]
3384
+
3385
+ - name: reject_and_notify
3386
+ label: "Reject & Notify"
3387
+ style: danger
3388
+ pipeline:
3389
+ - { transition: reject }
3390
+ - { action: notify_rejection }
3391
+ confirm: true
3392
+ ```
3393
+
3394
+ Single actions also support `transition:` and `action:` sugar (without `save` step):
3395
+
3396
+ ```yaml
3397
+ # Sugar form — expands to pipeline: [{ transition: approve }]
3398
+ - name: approve
3399
+ transition: approve
3400
+ ```
3401
+
3402
+ For the step-by-step walkthrough, see the [Action Buttons & Pipelines Guide](../guides/action-buttons.md). For the full design rationale, see the [Form Submit Actions design doc](../design/form_submit_actions.md).
3403
+
3404
+ ## Export Configuration
3405
+
3406
+ Enables data export (CSV/XLSX) from the presenter. See [Export Reference](export.md) for full details.
3407
+
3408
+ ```yaml
3409
+ export:
3410
+ formats: [csv, xlsx]
3411
+ max_records: 5000
3412
+ save_history: true
3413
+ fields: [name, email, department_id, hire_date]
3414
+ csv_delimiter: ","
3415
+ ```
3416
+
3417
+ | Attribute | Type | Default | Description |
3418
+ |-----------|------|---------|-------------|
3419
+ | `formats` | array | `["csv"]` | Available formats: `csv`, `xlsx` (XLSX requires the `caxlsx` gem) |
3420
+ | `max_records` | integer | `10000` | Safety limit — export fails with error if count exceeds this |
3421
+ | `save_history` | boolean | `false` | Log each export to `export_log` model (requires `lcp_ruby:export` generator) |
3422
+ | `fields` | array | all readable | Field ceiling — restricts which fields appear in the picker |
3423
+ | `csv_delimiter` | string | `","` | CSV separator character |
3424
+
3425
+ The export action is a non-CRUD built-in action. Authorization is checked via the `actions` permission config (not the `crud` list). See [Permissions](permissions.md#actions).
3426
+
3427
+ ```yaml
3428
+ actions:
3429
+ collection:
3430
+ - { name: export, type: built_in, icon: download }
3431
+ batch:
3432
+ - { name: export, type: built_in, icon: download }
3433
+ ```
3434
+
3435
+ ## Import Configuration
3436
+
3437
+ Enables data import (CSV/XLSX) into the presenter's model. See [Import Reference](import.md) for full details.
3438
+
3439
+ ```yaml
3440
+ import:
3441
+ formats: [csv, xlsx]
3442
+ max_rows: 5000
3443
+ max_file_size: 10485760
3444
+ csv_delimiter: ","
3445
+ csv_encoding: auto
3446
+ nested_associations: [employee_profile]
3447
+ nested_blank_strategy: skip
3448
+ parsing:
3449
+ date_format: ["%d.%m.%Y", "%Y-%m-%d"]
3450
+ boolean_true: [true, yes, 1, ano]
3451
+ boolean_false: [false, no, 0, ne]
3452
+ ```
3453
+
3454
+ | Attribute | Type | Default | Description |
3455
+ |-----------|------|---------|-------------|
3456
+ | `formats` | array | `["csv"]` | Accepted formats: `csv`, `xlsx` (XLSX requires the `roo` gem) |
3457
+ | `max_rows` | integer | `10000` | Safety limit — import fails if row count exceeds this |
3458
+ | `max_file_size` | integer | `10485760` | Maximum file size in bytes (default 10 MiB) |
3459
+ | `csv_delimiter` | string | `","` | CSV separator character |
3460
+ | `csv_encoding` | string | `"auto"` | CSV encoding. `"auto"` detects via BOM and UTF-8 validity check |
3461
+ | `parsing` | hash | `{}` | Type coercion options: `date_format`, `datetime_format`, `decimal_separator`, `boolean_true`, `boolean_false` |
3462
+ | `nested_associations` | array | all | Whitelist of `has_one` association names exposed in import. When omitted, all `has_one` with `nested_attributes` are exposed |
3463
+ | `nested_blank_strategy` | string | `"null"` | How to handle blank nested values: `"null"` sets to nil, `"skip"` preserves existing |
3464
+
3465
+ The import action is a non-CRUD built-in action. Authorization is checked via the `actions` permission config (not the `crud` list). See [Permissions](permissions.md#actions).
3466
+
3467
+ ```yaml
3468
+ actions:
3469
+ collection:
3470
+ - { name: import, type: built_in, icon: upload }
3471
+ ```
3472
+
3473
+ ## Navigation
3474
+
3475
+ Navigation is configured through [view groups](view-groups.md), not directly on presenters. View groups control menu placement, ordering, and view switching between multiple presenters for the same model.
3476
+
3477
+ ## Complete Example
3478
+
3479
+ ```yaml
3480
+ presenter:
3481
+ name: deal
3482
+ model: deal
3483
+ label: "Deals"
3484
+ slug: deals
3485
+ icon: dollar-sign
3486
+
3487
+ index:
3488
+ default_view: table
3489
+ default_sort: { field: created_at, direction: desc }
3490
+ per_page: 25
3491
+ row_click: show
3492
+ empty_message: "No deals found. Create your first deal to get started."
3493
+ actions_position: dropdown
3494
+ table_columns:
3495
+ - field: title
3496
+ width: "30%"
3497
+ link_to: show
3498
+ sortable: true
3499
+ pinned: left
3500
+ - field: stage
3501
+ width: "15%"
3502
+ renderer: badge
3503
+ options:
3504
+ color_map:
3505
+ open: blue
3506
+ negotiation: yellow
3507
+ closed_won: green
3508
+ closed_lost: red
3509
+ sortable: true
3510
+ - field: value
3511
+ width: "15%"
3512
+ renderer: currency
3513
+ options:
3514
+ currency: "$"
3515
+ precision: 2
3516
+ sortable: true
3517
+ summary: sum
3518
+ - field: contact_name
3519
+ hidden_on: [mobile, tablet]
3520
+ - { field: updated_at, renderer: relative_date, hidden_on: mobile }
3521
+
3522
+ show:
3523
+ layout:
3524
+ - section: "Deal Information"
3525
+ columns: 3
3526
+ responsive:
3527
+ tablet:
3528
+ columns: 2
3529
+ mobile:
3530
+ columns: 1
3531
+ fields:
3532
+ - { field: title, renderer: heading, col_span: 3 }
3533
+ - field: stage
3534
+ renderer: badge
3535
+ options:
3536
+ color_map:
3537
+ open: blue
3538
+ negotiation: yellow
3539
+ closed_won: green
3540
+ closed_lost: red
3541
+ - field: value
3542
+ renderer: currency
3543
+ options:
3544
+ currency: "$"
3545
+ - { field: email, renderer: email_link }
3546
+ - { field: website, renderer: url_link, hidden_on: mobile }
3547
+ - section: "Contacts"
3548
+ type: association_list
3549
+ association: contacts
3550
+
3551
+ form:
3552
+ layout: tabs
3553
+ sections:
3554
+ - title: "Deal Details"
3555
+ columns: 2
3556
+ responsive:
3557
+ mobile:
3558
+ columns: 1
3559
+ fields:
3560
+ - field: title
3561
+ placeholder: "Deal title..."
3562
+ autofocus: true
3563
+ col_span: 2
3564
+ hint: "A short, descriptive name for the deal"
3565
+ - { field: stage, input_type: select }
3566
+ - field: value
3567
+ input_type: number
3568
+ prefix: "$"
3569
+ input_options:
3570
+ min: 0
3571
+ step: 0.01
3572
+ visible_when: { field: stage, operator: not_in, value: [lead] }
3573
+ disable_when: { field: stage, operator: in, value: [closed_won, closed_lost] }
3574
+ - { type: divider, label: "Relationships" }
3575
+ - { field: company_id, input_type: association_select }
3576
+ - { field: contact_id, input_type: association_select }
3577
+ - title: "Additional"
3578
+ collapsible: true
3579
+ visible_when: { field: stage, operator: not_eq, value: lead }
3580
+ fields:
3581
+ - field: probability
3582
+ input_type: slider
3583
+ input_options:
3584
+ min: 0
3585
+ max: 100
3586
+ step: 5
3587
+ show_value: true
3588
+ suffix: "%"
3589
+ - field: notes
3590
+ input_type: textarea
3591
+ input_options:
3592
+ rows: 4
3593
+ max_length: 2000
3594
+ show_counter: true
3595
+ - field: close_date
3596
+ input_type: date
3597
+ default: current_date
3598
+ visible_when: { service: persisted_check }
3599
+
3600
+ search:
3601
+ enabled: true
3602
+ searchable_fields: [title]
3603
+ placeholder: "Search deals..."
3604
+ predefined_filters:
3605
+ - { name: all, label: "All", default: true }
3606
+ - { name: open, label: "Open", scope: open_deals }
3607
+ - { name: won, label: "Won", scope: won }
3608
+ - { name: lost, label: "Lost", scope: lost }
3609
+
3610
+ actions:
3611
+ collection:
3612
+ - { name: create, type: built_in, label: "New Deal", icon: plus }
3613
+ single:
3614
+ - { name: show, type: built_in, icon: eye }
3615
+ - { name: edit, type: built_in, icon: pencil }
3616
+ - name: close_won
3617
+ type: custom
3618
+ label: "Close as Won"
3619
+ icon: check-circle
3620
+ confirm: true
3621
+ confirm_message: "Mark this deal as won?"
3622
+ visible_when: { field: stage, operator: not_in, value: [closed_won, closed_lost] }
3623
+ disable_when: { field: value, operator: blank }
3624
+ - { name: destroy, type: built_in, icon: trash, confirm: true, style: danger }
3625
+ ```
3626
+
3627
+ Source: `lib/lcp_ruby/metadata/presenter_definition.rb`, `lib/lcp_ruby/presenter/layout_builder.rb`, `lib/lcp_ruby/presenter/column_set.rb`, `lib/lcp_ruby/presenter/action_set.rb`