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.
- checksums.yaml +7 -0
- data/.claude/skills/lcp-custom-field/SKILL.md +205 -0
- data/.claude/skills/lcp-getting-started/SKILL.md +332 -0
- data/.claude/skills/lcp-host-binding/SKILL.md +287 -0
- data/.claude/skills/lcp-model/SKILL.md +185 -0
- data/.claude/skills/lcp-permissions/SKILL.md +176 -0
- data/.claude/skills/lcp-presenter/SKILL.md +194 -0
- data/.claude/skills/lcp-workflow/SKILL.md +281 -0
- data/CHANGELOG.md +69 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +17 -0
- data/app/assets/javascripts/lcp_ruby/application.js +58 -0
- data/app/assets/javascripts/lcp_ruby/controllers/advanced_filter_controller.js +1521 -0
- data/app/assets/javascripts/lcp_ruby/controllers/approval_actions_controller.js +24 -0
- data/app/assets/javascripts/lcp_ruby/controllers/array_input_controller.js +156 -0
- data/app/assets/javascripts/lcp_ruby/controllers/batch_select_controller.js +405 -0
- data/app/assets/javascripts/lcp_ruby/controllers/cascading_selects_controller.js +436 -0
- data/app/assets/javascripts/lcp_ruby/controllers/char_counter_controller.js +28 -0
- data/app/assets/javascripts/lcp_ruby/controllers/clipboard_controller.js +62 -0
- data/app/assets/javascripts/lcp_ruby/controllers/conditional_rendering_controller.js +178 -0
- data/app/assets/javascripts/lcp_ruby/controllers/confirm_dialog_controller.js +131 -0
- data/app/assets/javascripts/lcp_ruby/controllers/custom_fields_manage_controller.js +80 -0
- data/app/assets/javascripts/lcp_ruby/controllers/dialog_controller.js +421 -0
- data/app/assets/javascripts/lcp_ruby/controllers/dialog_nested_controller.js +182 -0
- data/app/assets/javascripts/lcp_ruby/controllers/direct_upload_controller.js +100 -0
- data/app/assets/javascripts/lcp_ruby/controllers/drawer_controller.js +205 -0
- data/app/assets/javascripts/lcp_ruby/controllers/dropdown_controller.js +160 -0
- data/app/assets/javascripts/lcp_ruby/controllers/export_dialog_controller.js +15 -0
- data/app/assets/javascripts/lcp_ruby/controllers/field_picker_controller.js +290 -0
- data/app/assets/javascripts/lcp_ruby/controllers/file_upload_controller.js +135 -0
- data/app/assets/javascripts/lcp_ruby/controllers/filter_auto_submit_controller.js +46 -0
- data/app/assets/javascripts/lcp_ruby/controllers/filter_dirty_controller.js +94 -0
- data/app/assets/javascripts/lcp_ruby/controllers/form_actions_controller.js +68 -0
- data/app/assets/javascripts/lcp_ruby/controllers/form_handling_controller.js +67 -0
- data/app/assets/javascripts/lcp_ruby/controllers/import_dialog_controller.js +142 -0
- data/app/assets/javascripts/lcp_ruby/controllers/import_mapper_controller.js +322 -0
- data/app/assets/javascripts/lcp_ruby/controllers/index_sortable_controller.js +388 -0
- data/app/assets/javascripts/lcp_ruby/controllers/inline_create_controller.js +137 -0
- data/app/assets/javascripts/lcp_ruby/controllers/kanban_board_controller.js +290 -0
- data/app/assets/javascripts/lcp_ruby/controllers/menu_controller.js +19 -0
- data/app/assets/javascripts/lcp_ruby/controllers/nested_forms_controller.js +261 -0
- data/app/assets/javascripts/lcp_ruby/controllers/responsive_sidebar_controller.js +161 -0
- data/app/assets/javascripts/lcp_ruby/controllers/responsive_top_nav_controller.js +394 -0
- data/app/assets/javascripts/lcp_ruby/controllers/saved_filters_controller.js +129 -0
- data/app/assets/javascripts/lcp_ruby/controllers/search_controller.js +82 -0
- data/app/assets/javascripts/lcp_ruby/controllers/selection_controller.js +33 -0
- data/app/assets/javascripts/lcp_ruby/controllers/sidebar_controller.js +83 -0
- data/app/assets/javascripts/lcp_ruby/controllers/sidebar_toggle_controller.js +66 -0
- data/app/assets/javascripts/lcp_ruby/controllers/slider_controller.js +26 -0
- data/app/assets/javascripts/lcp_ruby/controllers/submenu_controller.js +55 -0
- data/app/assets/javascripts/lcp_ruby/controllers/tom_select_controller.js +168 -0
- data/app/assets/javascripts/lcp_ruby/controllers/tree_index_controller.js +106 -0
- data/app/assets/javascripts/lcp_ruby/controllers/tree_reparent_controller.js +182 -0
- data/app/assets/javascripts/lcp_ruby/controllers/tree_select_controller.js +119 -0
- data/app/assets/javascripts/lcp_ruby/controllers/ui_components_controller.js +135 -0
- data/app/assets/javascripts/lcp_ruby/controllers/zone_tabs_controller.js +66 -0
- data/app/assets/javascripts/lcp_ruby/dev_toolbar.js +494 -0
- data/app/assets/javascripts/lcp_ruby/i18n.js.erb +29 -0
- data/app/assets/javascripts/lcp_ruby/lucide_init.js +24 -0
- data/app/assets/javascripts/lcp_ruby/stimulus_bootstrap.js +7 -0
- data/app/assets/javascripts/lcp_ruby/utils.js +119 -0
- data/app/assets/javascripts/lcp_ruby/xhr_fetch.js +411 -0
- data/app/assets/stylesheets/lcp_ruby/application.css +1940 -0
- data/app/assets/stylesheets/lcp_ruby/dev_toolbar.css +355 -0
- data/app/controllers/concerns/lcp_ruby/dialog_rendering.rb +167 -0
- data/app/controllers/concerns/lcp_ruby/form_action_execution.rb +249 -0
- data/app/controllers/concerns/lcp_ruby/page_authorization.rb +105 -0
- data/app/controllers/concerns/lcp_ruby/zone_resolution.rb +199 -0
- data/app/controllers/lcp_ruby/actions_controller.rb +481 -0
- data/app/controllers/lcp_ruby/application_controller.rb +59 -0
- data/app/controllers/lcp_ruby/approval_tasks_controller.rb +98 -0
- data/app/controllers/lcp_ruby/auth/base_controller.rb +58 -0
- data/app/controllers/lcp_ruby/auth/callbacks_controller.rb +103 -0
- data/app/controllers/lcp_ruby/auth/passwords_controller.rb +15 -0
- data/app/controllers/lcp_ruby/auth/registrations_controller.rb +37 -0
- data/app/controllers/lcp_ruby/auth/sessions_controller.rb +112 -0
- data/app/controllers/lcp_ruby/custom_fields_controller.rb +370 -0
- data/app/controllers/lcp_ruby/dev_toolbar_controller.rb +197 -0
- data/app/controllers/lcp_ruby/dialogs_controller.rb +199 -0
- data/app/controllers/lcp_ruby/health_controller.rb +23 -0
- data/app/controllers/lcp_ruby/impersonation_controller.rb +32 -0
- data/app/controllers/lcp_ruby/landing_controller.rb +49 -0
- data/app/controllers/lcp_ruby/metrics_controller.rb +17 -0
- data/app/controllers/lcp_ruby/resources_controller.rb +2218 -0
- data/app/controllers/lcp_ruby/saved_filters_controller.rb +221 -0
- data/app/helpers/lcp_ruby/condition_helper.rb +87 -0
- data/app/helpers/lcp_ruby/dashboard_helper.rb +45 -0
- data/app/helpers/lcp_ruby/dev_toolbar_helper.rb +17 -0
- data/app/helpers/lcp_ruby/dialog_helper.rb +27 -0
- data/app/helpers/lcp_ruby/display/card_helper.rb +317 -0
- data/app/helpers/lcp_ruby/display_helper.rb +63 -0
- data/app/helpers/lcp_ruby/display_template_helper.rb +100 -0
- data/app/helpers/lcp_ruby/form_helper.rb +1020 -0
- data/app/helpers/lcp_ruby/grouped_query_helper.rb +107 -0
- data/app/helpers/lcp_ruby/i18n_payload_helper.rb +197 -0
- data/app/helpers/lcp_ruby/layout_helper.rb +832 -0
- data/app/helpers/lcp_ruby/link_through_helper.rb +29 -0
- data/app/helpers/lcp_ruby/oidc_button_helper.rb +59 -0
- data/app/helpers/lcp_ruby/search_helper.rb +31 -0
- data/app/helpers/lcp_ruby/tree_helper.rb +140 -0
- data/app/helpers/lcp_ruby/view_slot_helper.rb +33 -0
- data/app/models/lcp_ruby/user.rb +61 -0
- data/app/views/layouts/lcp_ruby/application.html.erb +168 -0
- data/app/views/layouts/lcp_ruby/auth.html.erb +170 -0
- data/app/views/lcp_ruby/auth/callbacks/failure.html.erb +18 -0
- data/app/views/lcp_ruby/auth/mailer/reset_password_instructions.html.erb +9 -0
- data/app/views/lcp_ruby/auth/passwords/edit.html.erb +34 -0
- data/app/views/lcp_ruby/auth/passwords/new.html.erb +24 -0
- data/app/views/lcp_ruby/auth/registrations/edit.html.erb +48 -0
- data/app/views/lcp_ruby/auth/registrations/new.html.erb +46 -0
- data/app/views/lcp_ruby/auth/sessions/new.html.erb +55 -0
- data/app/views/lcp_ruby/auth/shared/_links.html.erb +13 -0
- data/app/views/lcp_ruby/custom_fields/_manage_row.html.erb +82 -0
- data/app/views/lcp_ruby/custom_fields/manage.html.erb +47 -0
- data/app/views/lcp_ruby/dialogs/_dialog_composite_frame.html.erb +87 -0
- data/app/views/lcp_ruby/dialogs/_dialog_frame.html.erb +60 -0
- data/app/views/lcp_ruby/dialogs/_dialog_show_frame.html.erb +14 -0
- data/app/views/lcp_ruby/dialogs/_show_secret.html.erb +10 -0
- data/app/views/lcp_ruby/dialogs/_success.html.erb +1 -0
- data/app/views/lcp_ruby/errors/not_found.html.erb +5 -0
- data/app/views/lcp_ruby/menu_renderers/_user_menu.html.erb +51 -0
- data/app/views/lcp_ruby/menu_renderers/_user_menu_panel.html.erb +32 -0
- data/app/views/lcp_ruby/navigation/_auto_top.html.erb +16 -0
- data/app/views/lcp_ruby/navigation/_both_sidebar.html.erb +1 -0
- data/app/views/lcp_ruby/navigation/_both_top.html.erb +1 -0
- data/app/views/lcp_ruby/navigation/_impersonation_banner.html.erb +14 -0
- data/app/views/lcp_ruby/navigation/_item_content.html.erb +23 -0
- data/app/views/lcp_ruby/navigation/_link_or_button.html.erb +51 -0
- data/app/views/lcp_ruby/navigation/_mobile_header.html.erb +23 -0
- data/app/views/lcp_ruby/navigation/_panel_item.html.erb +32 -0
- data/app/views/lcp_ruby/navigation/_sidebar.html.erb +38 -0
- data/app/views/lcp_ruby/navigation/_sidebar_item.html.erb +38 -0
- data/app/views/lcp_ruby/navigation/_sidebar_toggle.html.erb +32 -0
- data/app/views/lcp_ruby/navigation/_top.html.erb +26 -0
- data/app/views/lcp_ruby/navigation/_top_item.html.erb +112 -0
- data/app/views/lcp_ruby/navigation/_widget_item.html.erb +14 -0
- data/app/views/lcp_ruby/resources/_action_button.html.erb +138 -0
- data/app/views/lcp_ruby/resources/_advanced_filter.html.erb +55 -0
- data/app/views/lcp_ruby/resources/_api_status_banner.html.erb +11 -0
- data/app/views/lcp_ruby/resources/_association_list.html.erb +132 -0
- data/app/views/lcp_ruby/resources/_audit_history.html.erb +66 -0
- data/app/views/lcp_ruby/resources/_batch_toolbar.html.erb +54 -0
- data/app/views/lcp_ruby/resources/_filter_form.html.erb +57 -0
- data/app/views/lcp_ruby/resources/_form.html.erb +73 -0
- data/app/views/lcp_ruby/resources/_form_action_button.html.erb +20 -0
- data/app/views/lcp_ruby/resources/_form_action_dropdown_item.html.erb +17 -0
- data/app/views/lcp_ruby/resources/_form_actions.html.erb +38 -0
- data/app/views/lcp_ruby/resources/_form_section.html.erb +235 -0
- data/app/views/lcp_ruby/resources/_grid_page.html.erb +17 -0
- data/app/views/lcp_ruby/resources/_grouped_index.html.erb +61 -0
- data/app/views/lcp_ruby/resources/_inline_create_form.html.erb +50 -0
- data/app/views/lcp_ruby/resources/_json_items_list.html.erb +77 -0
- data/app/views/lcp_ruby/resources/_json_nested_fields.html.erb +30 -0
- data/app/views/lcp_ruby/resources/_kanban_card.html.erb +31 -0
- data/app/views/lcp_ruby/resources/_kanban_card_body.html.erb +17 -0
- data/app/views/lcp_ruby/resources/_kanban_column.html.erb +33 -0
- data/app/views/lcp_ruby/resources/_kanban_column_header.html.erb +10 -0
- data/app/views/lcp_ruby/resources/_kanban_index.html.erb +17 -0
- data/app/views/lcp_ruby/resources/_nested_field_cell.html.erb +33 -0
- data/app/views/lcp_ruby/resources/_nested_fields.html.erb +26 -0
- data/app/views/lcp_ruby/resources/_nested_row_content.html.erb +65 -0
- data/app/views/lcp_ruby/resources/_scope_filters.html.erb +71 -0
- data/app/views/lcp_ruby/resources/_semantic_index_page.html.erb +162 -0
- data/app/views/lcp_ruby/resources/_semantic_page.html.erb +121 -0
- data/app/views/lcp_ruby/resources/_show_sections.html.erb +128 -0
- data/app/views/lcp_ruby/resources/_table_index.html.erb +169 -0
- data/app/views/lcp_ruby/resources/_tile_card.html.erb +26 -0
- data/app/views/lcp_ruby/resources/_tile_card_body.html.erb +19 -0
- data/app/views/lcp_ruby/resources/_tiles_index.html.erb +15 -0
- data/app/views/lcp_ruby/resources/_transition_button.html.erb +33 -0
- data/app/views/lcp_ruby/resources/_tree_index.html.erb +61 -0
- data/app/views/lcp_ruby/resources/_view_switcher.html.erb +24 -0
- data/app/views/lcp_ruby/resources/edit.html.erb +9 -0
- data/app/views/lcp_ruby/resources/index.html.erb +80 -0
- data/app/views/lcp_ruby/resources/new.html.erb +9 -0
- data/app/views/lcp_ruby/resources/show.html.erb +37 -0
- data/app/views/lcp_ruby/shared/_breadcrumbs.html.erb +15 -0
- data/app/views/lcp_ruby/shared/_custom_partial_error.html.erb +3 -0
- data/app/views/lcp_ruby/shared/_flash_messages.html.erb +19 -0
- data/app/views/lcp_ruby/slots/index/_advanced_filter.html.erb +3 -0
- data/app/views/lcp_ruby/slots/index/_collection_actions.html.erb +50 -0
- data/app/views/lcp_ruby/slots/index/_manage_all.html.erb +4 -0
- data/app/views/lcp_ruby/slots/index/_pagination_footer.html.erb +71 -0
- data/app/views/lcp_ruby/slots/index/_predefined_filters.html.erb +8 -0
- data/app/views/lcp_ruby/slots/index/_saved_filters.html.erb +87 -0
- data/app/views/lcp_ruby/slots/index/_search.html.erb +52 -0
- data/app/views/lcp_ruby/slots/index/_search_parameter.html.erb +72 -0
- data/app/views/lcp_ruby/slots/index/_sort_dropdown.html.erb +21 -0
- data/app/views/lcp_ruby/slots/index/_summary_bar.html.erb +22 -0
- data/app/views/lcp_ruby/slots/index/_view_switcher.html.erb +1 -0
- data/app/views/lcp_ruby/slots/show/_approval_status.html.erb +8 -0
- data/app/views/lcp_ruby/slots/show/_back_to_list.html.erb +1 -0
- data/app/views/lcp_ruby/slots/show/_copy_url.html.erb +5 -0
- data/app/views/lcp_ruby/slots/show/_single_actions.html.erb +4 -0
- data/app/views/lcp_ruby/slots/show/_view_switcher.html.erb +1 -0
- data/app/views/lcp_ruby/widgets/_approval_status.html.erb +150 -0
- data/app/views/lcp_ruby/widgets/_chart.html.erb +36 -0
- data/app/views/lcp_ruby/widgets/_embed.html.erb +13 -0
- data/app/views/lcp_ruby/widgets/_kpi_card.html.erb +29 -0
- data/app/views/lcp_ruby/widgets/_list.html.erb +17 -0
- data/app/views/lcp_ruby/widgets/_presenter_zone.html.erb +84 -0
- data/app/views/lcp_ruby/widgets/_record_show_zone.html.erb +11 -0
- data/app/views/lcp_ruby/widgets/_text.html.erb +3 -0
- data/app/views/lcp_ruby/widgets/_workflow_graph.html.erb +32 -0
- data/app/views/lcp_ruby/zones/_custom_zone.html.erb +16 -0
- data/app/views/lcp_ruby/zones/_error.html.erb +5 -0
- data/app/views/lcp_ruby/zones/_zone_frame.html.erb +25 -0
- data/app/views/lcp_ruby/zones/_zone_search.html.erb +20 -0
- data/config/locales/cs.yml +859 -0
- data/config/locales/en.yml +731 -0
- data/config/routes.rb +119 -0
- data/docs/README.md +225 -0
- data/docs/architecture.md +212 -0
- data/docs/feature-catalog.md +763 -0
- data/docs/feature-catalog.yml +20911 -0
- data/docs/getting-started.md +1187 -0
- data/docs/guides/action-buttons.md +537 -0
- data/docs/guides/adding-locale.md +353 -0
- data/docs/guides/api-backed-models.md +478 -0
- data/docs/guides/attachments.md +399 -0
- data/docs/guides/auditing.md +333 -0
- data/docs/guides/batch-actions.md +342 -0
- data/docs/guides/composite-pages.md +1290 -0
- data/docs/guides/computed-fields.md +350 -0
- data/docs/guides/conditional-rendering.md +832 -0
- data/docs/guides/custom-actions.md +238 -0
- data/docs/guides/custom-fields.md +439 -0
- data/docs/guides/custom-renderers.md +234 -0
- data/docs/guides/custom-types.md +274 -0
- data/docs/guides/dashboards.md +504 -0
- data/docs/guides/debugging/README.md +38 -0
- data/docs/guides/debugging/controllers.md +147 -0
- data/docs/guides/debugging/data.md +165 -0
- data/docs/guides/debugging/metadata.md +143 -0
- data/docs/guides/debugging/models.md +126 -0
- data/docs/guides/debugging/permissions.md +147 -0
- data/docs/guides/debugging/presenters.md +151 -0
- data/docs/guides/developer-tools.md +418 -0
- data/docs/guides/dialogs.md +392 -0
- data/docs/guides/display-types.md +1430 -0
- data/docs/guides/eager-loading.md +230 -0
- data/docs/guides/event-handlers.md +135 -0
- data/docs/guides/export.md +305 -0
- data/docs/guides/extensibility.md +761 -0
- data/docs/guides/groups.md +220 -0
- data/docs/guides/hierarchical-authorization.md +427 -0
- data/docs/guides/host-application.md +556 -0
- data/docs/guides/host-controller-integration.md +473 -0
- data/docs/guides/impersonation.md +83 -0
- data/docs/guides/import.md +165 -0
- data/docs/guides/inherited-permissions.md +459 -0
- data/docs/guides/menu.md +373 -0
- data/docs/guides/monitoring.md +254 -0
- data/docs/guides/oidc-setup.md +399 -0
- data/docs/guides/permission-source.md +205 -0
- data/docs/guides/permissions.md +364 -0
- data/docs/guides/presenters.md +2324 -0
- data/docs/guides/record-aliases.md +303 -0
- data/docs/guides/rendering-extension-points.md +280 -0
- data/docs/guides/role-source.md +288 -0
- data/docs/guides/selectbox.md +516 -0
- data/docs/guides/sequences.md +291 -0
- data/docs/guides/soft-delete.md +460 -0
- data/docs/guides/theming.md +129 -0
- data/docs/guides/tiles.md +383 -0
- data/docs/guides/tree-structures.md +297 -0
- data/docs/guides/userstamps.md +288 -0
- data/docs/guides/view-groups.md +259 -0
- data/docs/guides/view-slots.md +352 -0
- data/docs/guides/virtual-columns.md +810 -0
- data/docs/guides/workflow.md +692 -0
- data/docs/reference/api-backed-models.md +404 -0
- data/docs/reference/api-tokens.md +128 -0
- data/docs/reference/auditing.md +277 -0
- data/docs/reference/boot_lifecycle.md +188 -0
- data/docs/reference/cascading_selects.md +189 -0
- data/docs/reference/condition-operators.md +445 -0
- data/docs/reference/custom-fields.md +483 -0
- data/docs/reference/dialogs.md +286 -0
- data/docs/reference/doctor.md +168 -0
- data/docs/reference/dynamic-references.md +95 -0
- data/docs/reference/eager-loading.md +192 -0
- data/docs/reference/engine-configuration.md +989 -0
- data/docs/reference/export.md +309 -0
- data/docs/reference/forms.md +68 -0
- data/docs/reference/groups.md +176 -0
- data/docs/reference/host-controller-integration.md +342 -0
- data/docs/reference/i18n.md +497 -0
- data/docs/reference/i18n_check.md +351 -0
- data/docs/reference/import.md +260 -0
- data/docs/reference/invariant_check.md +216 -0
- data/docs/reference/menu.md +985 -0
- data/docs/reference/model-dsl.md +1157 -0
- data/docs/reference/models.md +2972 -0
- data/docs/reference/monitoring.md +222 -0
- data/docs/reference/oidc-bearer.md +269 -0
- data/docs/reference/oidc.md +407 -0
- data/docs/reference/page_filters.md +328 -0
- data/docs/reference/pages.md +1375 -0
- data/docs/reference/permission-source.md +185 -0
- data/docs/reference/permissions.md +715 -0
- data/docs/reference/presenter-dsl.md +1719 -0
- data/docs/reference/presenters.md +3627 -0
- data/docs/reference/role-source.md +227 -0
- data/docs/reference/theme-variables.md +139 -0
- data/docs/reference/tree-structures.md +374 -0
- data/docs/reference/types.md +470 -0
- data/docs/reference/view-groups.md +347 -0
- data/docs/reference/view-slots.md +228 -0
- data/docs/reference/virtual_forms.md +196 -0
- data/docs/reference/workflow-approvals.md +387 -0
- data/docs/reference/workflow.md +651 -0
- data/examples/crm/Gemfile +9 -0
- data/examples/crm/Gemfile.lock +417 -0
- data/examples/crm/Rakefile +2 -0
- data/examples/crm/app/actions/activity/complete.rb +20 -0
- data/examples/crm/app/actions/deal/close_won.rb +20 -0
- data/examples/crm/app/assets/config/manifest.js +3 -0
- data/examples/crm/app/controllers/application_controller.rb +9 -0
- data/examples/crm/app/event_handlers/deal/on_stage_change.rb +17 -0
- data/examples/crm/app/lcp_services/computed/weighted_deal_value.rb +13 -0
- data/examples/crm/app/lcp_services/data_providers/active_contacts_count.rb +14 -0
- data/examples/crm/app/lcp_services/data_providers/open_deals_count.rb +14 -0
- data/examples/crm/app/lcp_services/data_providers/pending_activities_count.rb +15 -0
- data/examples/crm/app/lcp_services/data_providers/pipeline_value.rb +27 -0
- data/examples/crm/app/lcp_services/data_providers/won_deals_count.rb +14 -0
- data/examples/crm/app/lcp_services/defaults/thirty_days_out.rb +11 -0
- data/examples/crm/app/lcp_services/transforms/titlecase.rb +11 -0
- data/examples/crm/app/lcp_services/validators/deal_credit_limit.rb +19 -0
- data/examples/crm/app/lcp_services/validators/deal_documents_required.rb +17 -0
- data/examples/crm/app/renderers/conditional_badge.rb +39 -0
- data/examples/crm/bin/rails +4 -0
- data/examples/crm/bin/rake +4 -0
- data/examples/crm/config/application.rb +31 -0
- data/examples/crm/config/boot.rb +2 -0
- data/examples/crm/config/database.yml +12 -0
- data/examples/crm/config/environment.rb +2 -0
- data/examples/crm/config/initializers/lcp_ruby.rb +45 -0
- data/examples/crm/config/lcp_ruby/menu.yml +53 -0
- data/examples/crm/config/lcp_ruby/models/activity.rb +43 -0
- data/examples/crm/config/lcp_ruby/models/city.rb +19 -0
- data/examples/crm/config/lcp_ruby/models/company.rb +65 -0
- data/examples/crm/config/lcp_ruby/models/contact.rb +62 -0
- data/examples/crm/config/lcp_ruby/models/country.rb +22 -0
- data/examples/crm/config/lcp_ruby/models/custom_field_definition.rb +60 -0
- data/examples/crm/config/lcp_ruby/models/deal.rb +85 -0
- data/examples/crm/config/lcp_ruby/models/deal_category.rb +15 -0
- data/examples/crm/config/lcp_ruby/models/gapfree_sequence.rb +17 -0
- data/examples/crm/config/lcp_ruby/models/region.rb +15 -0
- data/examples/crm/config/lcp_ruby/models/saved_filter.rb +50 -0
- data/examples/crm/config/lcp_ruby/pages/activity_quick_log.yml +19 -0
- data/examples/crm/config/lcp_ruby/pages/company_detail.yml +127 -0
- data/examples/crm/config/lcp_ruby/pages/deals_overview.yml +65 -0
- data/examples/crm/config/lcp_ruby/permissions/activity.yml +40 -0
- data/examples/crm/config/lcp_ruby/permissions/custom_field_definition.yml +16 -0
- data/examples/crm/config/lcp_ruby/permissions/deal.yml +54 -0
- data/examples/crm/config/lcp_ruby/permissions/default.yml +16 -0
- data/examples/crm/config/lcp_ruby/permissions/gapfree_sequence.yml +6 -0
- data/examples/crm/config/lcp_ruby/permissions/saved_filter.yml +27 -0
- data/examples/crm/config/lcp_ruby/presenters/activity.rb +140 -0
- data/examples/crm/config/lcp_ruby/presenters/activity_quick_form.rb +36 -0
- data/examples/crm/config/lcp_ruby/presenters/activity_short.rb +16 -0
- data/examples/crm/config/lcp_ruby/presenters/activity_tiles.rb +35 -0
- data/examples/crm/config/lcp_ruby/presenters/city.rb +53 -0
- data/examples/crm/config/lcp_ruby/presenters/company.rb +147 -0
- data/examples/crm/config/lcp_ruby/presenters/company_activities_zone.rb +26 -0
- data/examples/crm/config/lcp_ruby/presenters/company_archive.rb +37 -0
- data/examples/crm/config/lcp_ruby/presenters/company_contacts_zone.rb +22 -0
- data/examples/crm/config/lcp_ruby/presenters/company_deals_zone.rb +36 -0
- data/examples/crm/config/lcp_ruby/presenters/company_short.rb +21 -0
- data/examples/crm/config/lcp_ruby/presenters/company_show_zone.rb +38 -0
- data/examples/crm/config/lcp_ruby/presenters/company_sidebar.rb +15 -0
- data/examples/crm/config/lcp_ruby/presenters/company_tiles.rb +35 -0
- data/examples/crm/config/lcp_ruby/presenters/contact.rb +120 -0
- data/examples/crm/config/lcp_ruby/presenters/contact_quick_form.rb +17 -0
- data/examples/crm/config/lcp_ruby/presenters/contact_short.rb +20 -0
- data/examples/crm/config/lcp_ruby/presenters/contact_tiles.rb +34 -0
- data/examples/crm/config/lcp_ruby/presenters/country.rb +44 -0
- data/examples/crm/config/lcp_ruby/presenters/custom_fields.rb +124 -0
- data/examples/crm/config/lcp_ruby/presenters/deal.rb +181 -0
- data/examples/crm/config/lcp_ruby/presenters/deal_category.rb +46 -0
- data/examples/crm/config/lcp_ruby/presenters/deal_overview.rb +6 -0
- data/examples/crm/config/lcp_ruby/presenters/deal_pipeline.rb +18 -0
- data/examples/crm/config/lcp_ruby/presenters/deal_short.rb +25 -0
- data/examples/crm/config/lcp_ruby/presenters/deal_tiles.rb +48 -0
- data/examples/crm/config/lcp_ruby/presenters/region.rb +42 -0
- data/examples/crm/config/lcp_ruby/presenters/save_filter_dialog.rb +17 -0
- data/examples/crm/config/lcp_ruby/presenters/saved_filters.rb +93 -0
- data/examples/crm/config/lcp_ruby/views/activities.yml +16 -0
- data/examples/crm/config/lcp_ruby/views/cities.yml +10 -0
- data/examples/crm/config/lcp_ruby/views/companies.yml +14 -0
- data/examples/crm/config/lcp_ruby/views/contacts.yml +16 -0
- data/examples/crm/config/lcp_ruby/views/countries.yml +8 -0
- data/examples/crm/config/lcp_ruby/views/custom_fields.rb +9 -0
- data/examples/crm/config/lcp_ruby/views/deal_categories.yml +8 -0
- data/examples/crm/config/lcp_ruby/views/deals.yml +19 -0
- data/examples/crm/config/lcp_ruby/views/pipeline.yml +10 -0
- data/examples/crm/config/lcp_ruby/views/regions.yml +10 -0
- data/examples/crm/config/lcp_ruby/views/saved_filters.yml +7 -0
- data/examples/crm/config/locales/cs.yml +338 -0
- data/examples/crm/config/locales/en.yml +353 -0
- data/examples/crm/config/routes.rb +4 -0
- data/examples/crm/config/storage.yml +3 -0
- data/examples/crm/config.ru +2 -0
- data/examples/crm/db/migrate/20260219104942_create_active_storage_tables.active_storage.rb +57 -0
- data/examples/crm/db/schema.rb +245 -0
- data/examples/crm/db/seeds.rb +1111 -0
- data/examples/crm/erd.md +163 -0
- data/examples/hr/Gemfile +9 -0
- data/examples/hr/Gemfile.lock +419 -0
- data/examples/hr/Rakefile +6 -0
- data/examples/hr/app/actions/asset/assign_asset.rb +20 -0
- data/examples/hr/app/actions/asset/return_asset.rb +20 -0
- data/examples/hr/app/actions/candidate/advance.rb +32 -0
- data/examples/hr/app/actions/candidate/hire.rb +20 -0
- data/examples/hr/app/actions/candidate/reject_candidate.rb +20 -0
- data/examples/hr/app/actions/expense_claim/approve.rb +21 -0
- data/examples/hr/app/actions/expense_claim/reject.rb +21 -0
- data/examples/hr/app/actions/expense_claim/submit.rb +20 -0
- data/examples/hr/app/actions/interview/complete_interview.rb +20 -0
- data/examples/hr/app/actions/leave_request/approve.rb +21 -0
- data/examples/hr/app/actions/leave_request/cancel.rb +20 -0
- data/examples/hr/app/actions/leave_request/reject.rb +21 -0
- data/examples/hr/app/assets/config/manifest.js +1 -0
- data/examples/hr/app/assets/stylesheets/application.css +10 -0
- data/examples/hr/app/condition_services/is_own_org_unit.rb +14 -0
- data/examples/hr/app/condition_services/is_own_record.rb +20 -0
- data/examples/hr/app/controllers/application_controller.rb +15 -0
- data/examples/hr/app/event_handlers/asset_assignment/on_create.rb +24 -0
- data/examples/hr/app/event_handlers/candidate/on_status_change.rb +18 -0
- data/examples/hr/app/event_handlers/leave_request/on_status_change.rb +45 -0
- data/examples/hr/app/helpers/application_helper.rb +2 -0
- data/examples/hr/app/javascript/application.js +1 -0
- data/examples/hr/app/jobs/application_job.rb +7 -0
- data/examples/hr/app/lcp_services/computed/employee_tenure.rb +28 -0
- data/examples/hr/app/lcp_services/computed/leave_remaining.rb +13 -0
- data/examples/hr/app/lcp_services/data_providers/headcount_text.rb +14 -0
- data/examples/hr/app/lcp_services/data_providers/open_positions_count.rb +14 -0
- data/examples/hr/app/lcp_services/data_providers/pending_expenses_count.rb +14 -0
- data/examples/hr/app/lcp_services/data_providers/pending_leaves_count.rb +14 -0
- data/examples/hr/app/lcp_services/defaults/current_year.rb +11 -0
- data/examples/hr/app/lcp_services/transforms/titlecase.rb +11 -0
- data/examples/hr/app/lcp_services/validators/expense_receipt_required.rb +17 -0
- data/examples/hr/app/lcp_services/validators/leave_balance_check.rb +37 -0
- data/examples/hr/app/models/application_record.rb +3 -0
- data/examples/hr/app/views/layouts/application.html.erb +29 -0
- data/examples/hr/app/views/pwa/manifest.json.erb +22 -0
- data/examples/hr/app/views/pwa/service-worker.js +26 -0
- data/examples/hr/bin/brakeman +7 -0
- data/examples/hr/bin/bundler-audit +6 -0
- data/examples/hr/bin/ci +6 -0
- data/examples/hr/bin/dev +2 -0
- data/examples/hr/bin/docker-entrypoint +8 -0
- data/examples/hr/bin/importmap +4 -0
- data/examples/hr/bin/jobs +6 -0
- data/examples/hr/bin/kamal +27 -0
- data/examples/hr/bin/rails +4 -0
- data/examples/hr/bin/rake +4 -0
- data/examples/hr/bin/rubocop +8 -0
- data/examples/hr/bin/setup +35 -0
- data/examples/hr/bin/thrust +5 -0
- data/examples/hr/config/application.rb +31 -0
- data/examples/hr/config/boot.rb +2 -0
- data/examples/hr/config/bundler-audit.yml +5 -0
- data/examples/hr/config/cache.yml +16 -0
- data/examples/hr/config/ci.rb +20 -0
- data/examples/hr/config/credentials.yml.enc +1 -0
- data/examples/hr/config/database.yml +36 -0
- data/examples/hr/config/deploy.yml +119 -0
- data/examples/hr/config/environment.rb +5 -0
- data/examples/hr/config/environments/development.rb +66 -0
- data/examples/hr/config/environments/production.rb +74 -0
- data/examples/hr/config/environments/test.rb +45 -0
- data/examples/hr/config/importmap.rb +3 -0
- data/examples/hr/config/initializers/assets.rb +7 -0
- data/examples/hr/config/initializers/content_security_policy.rb +29 -0
- data/examples/hr/config/initializers/filter_parameter_logging.rb +8 -0
- data/examples/hr/config/initializers/inflections.rb +16 -0
- data/examples/hr/config/initializers/lcp_ruby.rb +18 -0
- data/examples/hr/config/lcp_ruby/menu.yml +86 -0
- data/examples/hr/config/lcp_ruby/models/announcement.rb +33 -0
- data/examples/hr/config/lcp_ruby/models/asset.rb +73 -0
- data/examples/hr/config/lcp_ruby/models/asset_assignment.rb +39 -0
- data/examples/hr/config/lcp_ruby/models/audit_log.yml +57 -0
- data/examples/hr/config/lcp_ruby/models/candidate.rb +68 -0
- data/examples/hr/config/lcp_ruby/models/custom_field_definition.rb +60 -0
- data/examples/hr/config/lcp_ruby/models/dashboard.rb +17 -0
- data/examples/hr/config/lcp_ruby/models/document.rb +38 -0
- data/examples/hr/config/lcp_ruby/models/employee.rb +128 -0
- data/examples/hr/config/lcp_ruby/models/employee_skill.rb +29 -0
- data/examples/hr/config/lcp_ruby/models/expense_claim.rb +70 -0
- data/examples/hr/config/lcp_ruby/models/goal.rb +48 -0
- data/examples/hr/config/lcp_ruby/models/group.rb +37 -0
- data/examples/hr/config/lcp_ruby/models/group_membership.rb +26 -0
- data/examples/hr/config/lcp_ruby/models/interview.rb +54 -0
- data/examples/hr/config/lcp_ruby/models/job_posting.rb +67 -0
- data/examples/hr/config/lcp_ruby/models/leave_balance.rb +31 -0
- data/examples/hr/config/lcp_ruby/models/leave_request.rb +52 -0
- data/examples/hr/config/lcp_ruby/models/leave_type.rb +35 -0
- data/examples/hr/config/lcp_ruby/models/organization_unit.rb +42 -0
- data/examples/hr/config/lcp_ruby/models/performance_review.rb +59 -0
- data/examples/hr/config/lcp_ruby/models/position.rb +43 -0
- data/examples/hr/config/lcp_ruby/models/skill.rb +27 -0
- data/examples/hr/config/lcp_ruby/models/training_course.rb +53 -0
- data/examples/hr/config/lcp_ruby/models/training_enrollment.rb +35 -0
- data/examples/hr/config/lcp_ruby/pages/dashboard.yml +101 -0
- data/examples/hr/config/lcp_ruby/permissions/announcement.yml +25 -0
- data/examples/hr/config/lcp_ruby/permissions/asset.yml +35 -0
- data/examples/hr/config/lcp_ruby/permissions/audit_log.yml +28 -0
- data/examples/hr/config/lcp_ruby/permissions/candidate.yml +25 -0
- data/examples/hr/config/lcp_ruby/permissions/custom_field_definition.yml +28 -0
- data/examples/hr/config/lcp_ruby/permissions/dashboard.yml +25 -0
- data/examples/hr/config/lcp_ruby/permissions/default.yml +25 -0
- data/examples/hr/config/lcp_ruby/permissions/document.yml +37 -0
- data/examples/hr/config/lcp_ruby/permissions/employee.yml +55 -0
- data/examples/hr/config/lcp_ruby/permissions/expense_claim.yml +45 -0
- data/examples/hr/config/lcp_ruby/permissions/group.yml +27 -0
- data/examples/hr/config/lcp_ruby/permissions/job_posting.yml +34 -0
- data/examples/hr/config/lcp_ruby/permissions/leave_request.yml +45 -0
- data/examples/hr/config/lcp_ruby/permissions/performance_review.yml +42 -0
- data/examples/hr/config/lcp_ruby/presenters/announcement.rb +47 -0
- data/examples/hr/config/lcp_ruby/presenters/asset.rb +69 -0
- data/examples/hr/config/lcp_ruby/presenters/asset_assignment.rb +47 -0
- data/examples/hr/config/lcp_ruby/presenters/audit_logs.yml +43 -0
- data/examples/hr/config/lcp_ruby/presenters/candidate.rb +71 -0
- data/examples/hr/config/lcp_ruby/presenters/custom_fields.rb +124 -0
- data/examples/hr/config/lcp_ruby/presenters/dashboard.rb +37 -0
- data/examples/hr/config/lcp_ruby/presenters/document.rb +46 -0
- data/examples/hr/config/lcp_ruby/presenters/employee.rb +167 -0
- data/examples/hr/config/lcp_ruby/presenters/employee_archive.rb +38 -0
- data/examples/hr/config/lcp_ruby/presenters/employee_directory.rb +27 -0
- data/examples/hr/config/lcp_ruby/presenters/employee_skill.rb +57 -0
- data/examples/hr/config/lcp_ruby/presenters/expense_claim.rb +76 -0
- data/examples/hr/config/lcp_ruby/presenters/goal.rb +60 -0
- data/examples/hr/config/lcp_ruby/presenters/group.rb +48 -0
- data/examples/hr/config/lcp_ruby/presenters/interview.rb +59 -0
- data/examples/hr/config/lcp_ruby/presenters/job_posting.rb +73 -0
- data/examples/hr/config/lcp_ruby/presenters/leave_balance.rb +50 -0
- data/examples/hr/config/lcp_ruby/presenters/leave_request.rb +89 -0
- data/examples/hr/config/lcp_ruby/presenters/leave_type.rb +52 -0
- data/examples/hr/config/lcp_ruby/presenters/organization_unit.rb +56 -0
- data/examples/hr/config/lcp_ruby/presenters/performance_review.rb +87 -0
- data/examples/hr/config/lcp_ruby/presenters/position.rb +54 -0
- data/examples/hr/config/lcp_ruby/presenters/skill.rb +45 -0
- data/examples/hr/config/lcp_ruby/presenters/training_course.rb +61 -0
- data/examples/hr/config/lcp_ruby/presenters/training_enrollment.rb +49 -0
- data/examples/hr/config/lcp_ruby/views/announcements.yml +8 -0
- data/examples/hr/config/lcp_ruby/views/asset_assignments.yml +10 -0
- data/examples/hr/config/lcp_ruby/views/assets.yml +8 -0
- data/examples/hr/config/lcp_ruby/views/audit_logs.yml +8 -0
- data/examples/hr/config/lcp_ruby/views/candidates.yml +8 -0
- data/examples/hr/config/lcp_ruby/views/custom_fields.yml +8 -0
- data/examples/hr/config/lcp_ruby/views/dashboard.yml +6 -0
- data/examples/hr/config/lcp_ruby/views/documents.yml +10 -0
- data/examples/hr/config/lcp_ruby/views/employee_skills.yml +10 -0
- data/examples/hr/config/lcp_ruby/views/employees.yml +14 -0
- data/examples/hr/config/lcp_ruby/views/expense_claims.yml +10 -0
- data/examples/hr/config/lcp_ruby/views/goals.yml +10 -0
- data/examples/hr/config/lcp_ruby/views/groups.yml +8 -0
- data/examples/hr/config/lcp_ruby/views/interviews.yml +8 -0
- data/examples/hr/config/lcp_ruby/views/job_postings.yml +6 -0
- data/examples/hr/config/lcp_ruby/views/leave_balances.yml +10 -0
- data/examples/hr/config/lcp_ruby/views/leave_requests.yml +10 -0
- data/examples/hr/config/lcp_ruby/views/leave_types.yml +8 -0
- data/examples/hr/config/lcp_ruby/views/organization_units.yml +8 -0
- data/examples/hr/config/lcp_ruby/views/performance_reviews.yml +10 -0
- data/examples/hr/config/lcp_ruby/views/positions.yml +8 -0
- data/examples/hr/config/lcp_ruby/views/skills.yml +8 -0
- data/examples/hr/config/lcp_ruby/views/training_courses.yml +8 -0
- data/examples/hr/config/lcp_ruby/views/training_enrollments.yml +10 -0
- data/examples/hr/config/locales/cs.yml +496 -0
- data/examples/hr/config/locales/en.yml +740 -0
- data/examples/hr/config/locales/sk.yml +496 -0
- data/examples/hr/config/puma.rb +42 -0
- data/examples/hr/config/queue.yml +18 -0
- data/examples/hr/config/recurring.yml +15 -0
- data/examples/hr/config/routes.rb +4 -0
- data/examples/hr/config/storage.yml +27 -0
- data/examples/hr/config.ru +6 -0
- data/examples/hr/db/cache_schema.rb +12 -0
- data/examples/hr/db/migrate/20260303202825_create_active_storage_tables.active_storage.rb +57 -0
- data/examples/hr/db/queue_schema.rb +129 -0
- data/examples/hr/db/schema.rb +588 -0
- data/examples/hr/db/seeds.rb +932 -0
- data/examples/hr/erd.md +396 -0
- data/examples/hr/public/400.html +135 -0
- data/examples/hr/public/404.html +135 -0
- data/examples/hr/public/406-unsupported-browser.html +135 -0
- data/examples/hr/public/422.html +135 -0
- data/examples/hr/public/500.html +135 -0
- data/examples/hr/public/icon.svg +3 -0
- data/examples/hr/public/robots.txt +1 -0
- data/examples/showcase/Gemfile +15 -0
- data/examples/showcase/Gemfile.lock +425 -0
- data/examples/showcase/Rakefile +2 -0
- data/examples/showcase/app/actions/showcase_batch_task/assign_batch.rb +20 -0
- data/examples/showcase/app/actions/showcase_batch_task/close_task.rb +21 -0
- data/examples/showcase/app/actions/showcase_condition/approve.rb +20 -0
- data/examples/showcase/app/actions/showcase_permission/lock.rb +20 -0
- data/examples/showcase/app/assets/config/manifest.js +3 -0
- data/examples/showcase/app/assets/stylesheets/application.css +27 -0
- data/examples/showcase/app/condition_services/budget_threshold.rb +16 -0
- data/examples/showcase/app/condition_services/overdue_check.rb +11 -0
- data/examples/showcase/app/controllers/application_controller.rb +2 -0
- data/examples/showcase/app/controllers/docs_controller.rb +48 -0
- data/examples/showcase/app/controllers/host_inventory_items_managed_controller.rb +90 -0
- data/examples/showcase/app/controllers/host_inventory_items_report_controller.rb +39 -0
- data/examples/showcase/app/controllers/host_inventory_items_wizard_controller.rb +50 -0
- data/examples/showcase/app/data_providers/showcase_amount_provider.rb +52 -0
- data/examples/showcase/app/data_providers/weather_station_provider.rb +140 -0
- data/examples/showcase/app/event_handlers/showcase_model/on_status_change.rb +17 -0
- data/examples/showcase/app/event_handlers/showcase_permission/on_status_change.rb +17 -0
- data/examples/showcase/app/event_handlers/showcase_workflow/log_review_exit.rb +16 -0
- data/examples/showcase/app/event_handlers/showcase_workflow/notify_reviewers.rb +15 -0
- data/examples/showcase/app/event_handlers/showcase_workflow/request_submitted.rb +15 -0
- data/examples/showcase/app/lcp_metrics/showcase_metrics.rb +31 -0
- data/examples/showcase/app/lcp_services/computed/showcase_score.rb +18 -0
- data/examples/showcase/app/lcp_services/computed/showcase_total.rb +13 -0
- data/examples/showcase/app/lcp_services/defaults/one_week_from_now.rb +11 -0
- data/examples/showcase/app/lcp_services/menu_items/recent_announcements.rb +36 -0
- data/examples/showcase/app/lcp_services/virtual_columns/project_health.rb +26 -0
- data/examples/showcase/app/model_extensions/lcp_error_log_extension.rb +12 -0
- data/examples/showcase/app/models/host_inventory_item.rb +20 -0
- data/examples/showcase/app/models/platform_profile.rb +27 -0
- data/examples/showcase/app/models/platform_user.rb +5 -0
- data/examples/showcase/app/views/docs/show.html.erb +19 -0
- data/examples/showcase/app/views/host_inventory_items_report/index.html.erb +61 -0
- data/examples/showcase/app/views/host_inventory_items_report/show.html.erb +39 -0
- data/examples/showcase/app/views/layouts/docs.html.erb +46 -0
- data/examples/showcase/app/views/showcase/menu_renderers/_status_pill.html.erb +17 -0
- data/examples/showcase/app/views/showcase_custom/_activity_timeline.html.erb +37 -0
- data/examples/showcase/app/views/showcase_custom/_card_index.html.erb +74 -0
- data/examples/showcase/app/views/showcase_custom/_detail_show.html.erb +115 -0
- data/examples/showcase/app/views/showcase_custom/_location_map.html.erb +29 -0
- data/examples/showcase/app/views/showcase_custom/_location_view.html.erb +34 -0
- data/examples/showcase/app/views/showcase_custom/_quick_stats.html.erb +50 -0
- data/examples/showcase/app/views/showcase_custom/_stats_sidebar.html.erb +42 -0
- data/examples/showcase/app/views/showcase_custom/_tags_editor.html.erb +48 -0
- data/examples/showcase/bin/rails +4 -0
- data/examples/showcase/bin/rake +4 -0
- data/examples/showcase/config/application.rb +37 -0
- data/examples/showcase/config/boot.rb +2 -0
- data/examples/showcase/config/database.yml +12 -0
- data/examples/showcase/config/environment.rb +2 -0
- data/examples/showcase/config/initializers/devise.rb +5 -0
- data/examples/showcase/config/initializers/lcp_ruby.rb +175 -0
- data/examples/showcase/config/lcp_ruby/auth.yml +60 -0
- data/examples/showcase/config/lcp_ruby/jobs/data_import.yml +7 -0
- data/examples/showcase/config/lcp_ruby/jobs/showcase_cleanup.yml +11 -0
- data/examples/showcase/config/lcp_ruby/jobs/showcase_event_triggered.yml +11 -0
- data/examples/showcase/config/lcp_ruby/jobs/showcase_multi_step.yml +9 -0
- data/examples/showcase/config/lcp_ruby/jobs/showcase_webhook.yml +13 -0
- data/examples/showcase/config/lcp_ruby/menu.yml +249 -0
- data/examples/showcase/config/lcp_ruby/models/_base_document.rb +26 -0
- data/examples/showcase/config/lcp_ruby/models/_categorized_document.rb +19 -0
- data/examples/showcase/config/lcp_ruby/models/_contactable.rb +20 -0
- data/examples/showcase/config/lcp_ruby/models/api_token.rb +23 -0
- data/examples/showcase/config/lcp_ruby/models/article.rb +49 -0
- data/examples/showcase/config/lcp_ruby/models/article_tag.rb +10 -0
- data/examples/showcase/config/lcp_ruby/models/author.rb +16 -0
- data/examples/showcase/config/lcp_ruby/models/batch_operation.yml +79 -0
- data/examples/showcase/config/lcp_ruby/models/batch_operation_item.yml +45 -0
- data/examples/showcase/config/lcp_ruby/models/category.rb +23 -0
- data/examples/showcase/config/lcp_ruby/models/comment.rb +21 -0
- data/examples/showcase/config/lcp_ruby/models/custom_field_definition.rb +59 -0
- data/examples/showcase/config/lcp_ruby/models/department.rb +32 -0
- data/examples/showcase/config/lcp_ruby/models/employee.rb +52 -0
- data/examples/showcase/config/lcp_ruby/models/employee_emergency_contact.rb +24 -0
- data/examples/showcase/config/lcp_ruby/models/employee_profile.rb +18 -0
- data/examples/showcase/config/lcp_ruby/models/employee_skill.rb +10 -0
- data/examples/showcase/config/lcp_ruby/models/export_log.yml +27 -0
- data/examples/showcase/config/lcp_ruby/models/export_profile.yml +37 -0
- data/examples/showcase/config/lcp_ruby/models/feature.rb +76 -0
- data/examples/showcase/config/lcp_ruby/models/gapfree_sequence.rb +17 -0
- data/examples/showcase/config/lcp_ruby/models/group.rb +26 -0
- data/examples/showcase/config/lcp_ruby/models/group_membership.rb +21 -0
- data/examples/showcase/config/lcp_ruby/models/group_role_mapping.rb +14 -0
- data/examples/showcase/config/lcp_ruby/models/host_inventory_item.yml +111 -0
- data/examples/showcase/config/lcp_ruby/models/import_profile.rb +30 -0
- data/examples/showcase/config/lcp_ruby/models/import_row.rb +32 -0
- data/examples/showcase/config/lcp_ruby/models/ingredient_def.rb +11 -0
- data/examples/showcase/config/lcp_ruby/models/lcp_error_log.yml +61 -0
- data/examples/showcase/config/lcp_ruby/models/page_config.rb +19 -0
- data/examples/showcase/config/lcp_ruby/models/permission_config.rb +21 -0
- data/examples/showcase/config/lcp_ruby/models/pipeline.rb +15 -0
- data/examples/showcase/config/lcp_ruby/models/pipeline_stage.rb +17 -0
- data/examples/showcase/config/lcp_ruby/models/platform_profile.rb +104 -0
- data/examples/showcase/config/lcp_ruby/models/profile_setting.rb +17 -0
- data/examples/showcase/config/lcp_ruby/models/profile_tag.rb +19 -0
- data/examples/showcase/config/lcp_ruby/models/project.rb +23 -0
- data/examples/showcase/config/lcp_ruby/models/role.rb +24 -0
- data/examples/showcase/config/lcp_ruby/models/saved_filter.rb +50 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_aggregate.rb +93 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_aggregate_company.rb +14 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_aggregate_item.rb +25 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_announcement.rb +20 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_array.rb +45 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_attachment.rb +49 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_audit_log.yml +44 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_audited_record.rb +20 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_batch_task.rb +27 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_business_unit.rb +26 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_condition.rb +24 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_condition_category.rb +16 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_condition_task.rb +16 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_condition_threshold.rb +13 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_contact.rb +22 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_custom_render.rb +38 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_delete_reason.rb +11 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_division.rb +31 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_extensibility.rb +25 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_field.rb +53 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_form.rb +23 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_form_action.rb +38 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_grade.rb +18 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_hr_employee.rb +90 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_item_class.rb +31 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_job_execution.rb +49 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_memo.rb +23 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_model.rb +74 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_organization.rb +31 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_permission.rb +30 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_person.rb +23 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_positioning.rb +19 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_quick_note.rb +13 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_recipe.rb +17 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_report.rb +32 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_school_class.rb +17 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_search.rb +84 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_sequence.rb +46 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_soft_delete.rb +30 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_soft_delete_item.rb +21 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_student.rb +17 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_type_default.rb +49 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_userstamps.rb +26 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_virtual_field.rb +68 -0
- data/examples/showcase/config/lcp_ruby/models/showcase_workflow.rb +54 -0
- data/examples/showcase/config/lcp_ruby/models/skill.rb +22 -0
- data/examples/showcase/config/lcp_ruby/models/tag.rb +16 -0
- data/examples/showcase/config/lcp_ruby/models/user.rb +54 -0
- data/examples/showcase/config/lcp_ruby/models/weather_station.rb +20 -0
- data/examples/showcase/config/lcp_ruby/models/workflow_approval_request.yml +77 -0
- data/examples/showcase/config/lcp_ruby/models/workflow_approval_step.yml +56 -0
- data/examples/showcase/config/lcp_ruby/models/workflow_approval_task.yml +51 -0
- data/examples/showcase/config/lcp_ruby/models/workflow_audit_log.yml +74 -0
- data/examples/showcase/config/lcp_ruby/pages/article_detail.yml +32 -0
- data/examples/showcase/config/lcp_ruby/pages/author_detail.yml +27 -0
- data/examples/showcase/config/lcp_ruby/pages/category_detail.yml +24 -0
- data/examples/showcase/config/lcp_ruby/pages/chart_showcase.yml +151 -0
- data/examples/showcase/config/lcp_ruby/pages/department_detail.yml +55 -0
- data/examples/showcase/config/lcp_ruby/pages/department_explorer.yml +60 -0
- data/examples/showcase/config/lcp_ruby/pages/employee_overview.yml +106 -0
- data/examples/showcase/config/lcp_ruby/pages/employee_transfer_dialog.yml +24 -0
- data/examples/showcase/config/lcp_ruby/pages/hr_turnover_dashboard.yml +288 -0
- data/examples/showcase/config/lcp_ruby/pages/main_dashboard.yml +142 -0
- data/examples/showcase/config/lcp_ruby/pages/monitoring_dashboard.yml +58 -0
- data/examples/showcase/config/lcp_ruby/pages/pipeline_detail.yml +16 -0
- data/examples/showcase/config/lcp_ruby/pages/showcase_custom_zones.yml +39 -0
- data/examples/showcase/config/lcp_ruby/pages/showcase_form_action_dialog.yml +11 -0
- data/examples/showcase/config/lcp_ruby/pages/workflow_request_detail.yml +34 -0
- data/examples/showcase/config/lcp_ruby/permissions/api_token.yml +26 -0
- data/examples/showcase/config/lcp_ruby/permissions/batch_operation.yml +36 -0
- data/examples/showcase/config/lcp_ruby/permissions/custom_field_definition.yml +15 -0
- data/examples/showcase/config/lcp_ruby/permissions/default.yml +28 -0
- data/examples/showcase/config/lcp_ruby/permissions/export_log.yml +23 -0
- data/examples/showcase/config/lcp_ruby/permissions/export_profile.yml +25 -0
- data/examples/showcase/config/lcp_ruby/permissions/gapfree_sequence.yml +6 -0
- data/examples/showcase/config/lcp_ruby/permissions/group.yml +20 -0
- data/examples/showcase/config/lcp_ruby/permissions/group_membership.yml +20 -0
- data/examples/showcase/config/lcp_ruby/permissions/group_role_mapping.yml +20 -0
- data/examples/showcase/config/lcp_ruby/permissions/host_inventory_item.yml +34 -0
- data/examples/showcase/config/lcp_ruby/permissions/import_profile.yml +29 -0
- data/examples/showcase/config/lcp_ruby/permissions/import_row.yml +17 -0
- data/examples/showcase/config/lcp_ruby/permissions/lcp_error_log.yml +8 -0
- data/examples/showcase/config/lcp_ruby/permissions/page_config.yml +15 -0
- data/examples/showcase/config/lcp_ruby/permissions/permission_config.yml +15 -0
- data/examples/showcase/config/lcp_ruby/permissions/platform_profile.yml +23 -0
- data/examples/showcase/config/lcp_ruby/permissions/profile_setting.yml +23 -0
- data/examples/showcase/config/lcp_ruby/permissions/profile_tag.yml +23 -0
- data/examples/showcase/config/lcp_ruby/permissions/role.yml +20 -0
- data/examples/showcase/config/lcp_ruby/permissions/saved_filter.yml +39 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_announcement.yml +22 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_audit_log.yml +15 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_audited_record.yml +15 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_batch_task.yml +31 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_condition.yml +54 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_contact.yml +21 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_custom_render.yml +21 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_delete_reason.yml +12 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_form_action.yml +26 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_grade.yml +33 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_job_execution.yml +23 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_permission.yml +47 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_quick_note.yml +12 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_school_class.yml +37 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_student.yml +43 -0
- data/examples/showcase/config/lcp_ruby/permissions/showcase_workflow.yml +34 -0
- data/examples/showcase/config/lcp_ruby/permissions/workflow_approval_request.yml +18 -0
- data/examples/showcase/config/lcp_ruby/permissions/workflow_approval_step.yml +18 -0
- data/examples/showcase/config/lcp_ruby/permissions/workflow_approval_task.yml +18 -0
- data/examples/showcase/config/lcp_ruby/permissions/workflow_audit_log.yml +19 -0
- data/examples/showcase/config/lcp_ruby/presenters/announcements.rb +59 -0
- data/examples/showcase/config/lcp_ruby/presenters/approval_requests.rb +69 -0
- data/examples/showcase/config/lcp_ruby/presenters/approval_steps.rb +59 -0
- data/examples/showcase/config/lcp_ruby/presenters/approval_tasks.rb +65 -0
- data/examples/showcase/config/lcp_ruby/presenters/article_comments_zone.rb +12 -0
- data/examples/showcase/config/lcp_ruby/presenters/article_related_zone.rb +15 -0
- data/examples/showcase/config/lcp_ruby/presenters/articles.rb +149 -0
- data/examples/showcase/config/lcp_ruby/presenters/articles_tiles.rb +30 -0
- data/examples/showcase/config/lcp_ruby/presenters/author_articles_zone.rb +12 -0
- data/examples/showcase/config/lcp_ruby/presenters/author_show_zone.rb +12 -0
- data/examples/showcase/config/lcp_ruby/presenters/authors.rb +39 -0
- data/examples/showcase/config/lcp_ruby/presenters/batch_operation_items.yml +45 -0
- data/examples/showcase/config/lcp_ruby/presenters/batch_operations.yml +66 -0
- data/examples/showcase/config/lcp_ruby/presenters/categories.rb +50 -0
- data/examples/showcase/config/lcp_ruby/presenters/category_articles_zone.rb +20 -0
- data/examples/showcase/config/lcp_ruby/presenters/category_children_zone.rb +10 -0
- data/examples/showcase/config/lcp_ruby/presenters/comment_quick_add_dialog.rb +15 -0
- data/examples/showcase/config/lcp_ruby/presenters/custom_fields.rb +124 -0
- data/examples/showcase/config/lcp_ruby/presenters/dashboard_employees.rb +16 -0
- data/examples/showcase/config/lcp_ruby/presenters/delete_reason_dialog.rb +13 -0
- data/examples/showcase/config/lcp_ruby/presenters/departments.rb +55 -0
- data/examples/showcase/config/lcp_ruby/presenters/dept_add_employee_dialog.rb +19 -0
- data/examples/showcase/config/lcp_ruby/presenters/dept_children_zone.rb +10 -0
- data/examples/showcase/config/lcp_ruby/presenters/dept_detail_zone.rb +14 -0
- data/examples/showcase/config/lcp_ruby/presenters/dept_employees_zone.rb +16 -0
- data/examples/showcase/config/lcp_ruby/presenters/dept_list_selection_zone.rb +19 -0
- data/examples/showcase/config/lcp_ruby/presenters/employee_overview_index_zone.rb +36 -0
- data/examples/showcase/config/lcp_ruby/presenters/employee_quick_add_dialog.rb +14 -0
- data/examples/showcase/config/lcp_ruby/presenters/employee_show_zone.rb +20 -0
- data/examples/showcase/config/lcp_ruby/presenters/employee_transfer_form_zone.rb +19 -0
- data/examples/showcase/config/lcp_ruby/presenters/employees.rb +165 -0
- data/examples/showcase/config/lcp_ruby/presenters/employees_tiles.rb +33 -0
- data/examples/showcase/config/lcp_ruby/presenters/export_logs.yml +58 -0
- data/examples/showcase/config/lcp_ruby/presenters/export_profiles.yml +70 -0
- data/examples/showcase/config/lcp_ruby/presenters/extensibility_quick_edit_dialog.rb +13 -0
- data/examples/showcase/config/lcp_ruby/presenters/feature_kanban.rb +30 -0
- data/examples/showcase/config/lcp_ruby/presenters/features_card.rb +179 -0
- data/examples/showcase/config/lcp_ruby/presenters/features_hub.rb +44 -0
- data/examples/showcase/config/lcp_ruby/presenters/features_table.rb +37 -0
- data/examples/showcase/config/lcp_ruby/presenters/features_tiles.rb +48 -0
- data/examples/showcase/config/lcp_ruby/presenters/group_memberships.rb +54 -0
- data/examples/showcase/config/lcp_ruby/presenters/group_role_mappings.rb +48 -0
- data/examples/showcase/config/lcp_ruby/presenters/groups.rb +74 -0
- data/examples/showcase/config/lcp_ruby/presenters/host_inventory_items.rb +99 -0
- data/examples/showcase/config/lcp_ruby/presenters/host_inventory_items_managed.rb +84 -0
- data/examples/showcase/config/lcp_ruby/presenters/host_inventory_items_report.rb +40 -0
- data/examples/showcase/config/lcp_ruby/presenters/host_inventory_items_wizard.rb +76 -0
- data/examples/showcase/config/lcp_ruby/presenters/import_profiles.rb +61 -0
- data/examples/showcase/config/lcp_ruby/presenters/import_rows.rb +39 -0
- data/examples/showcase/config/lcp_ruby/presenters/lcp_error_logs.yml +46 -0
- data/examples/showcase/config/lcp_ruby/presenters/my_api_tokens.rb +35 -0
- data/examples/showcase/config/lcp_ruby/presenters/my_api_tokens_create_dialog.rb +27 -0
- data/examples/showcase/config/lcp_ruby/presenters/my_employee_profile.rb +24 -0
- data/examples/showcase/config/lcp_ruby/presenters/my_settings.rb +50 -0
- data/examples/showcase/config/lcp_ruby/presenters/page_configs.rb +56 -0
- data/examples/showcase/config/lcp_ruby/presenters/permission_configs.rb +58 -0
- data/examples/showcase/config/lcp_ruby/presenters/pipeline_edit_zone.rb +11 -0
- data/examples/showcase/config/lcp_ruby/presenters/pipeline_stages.rb +51 -0
- data/examples/showcase/config/lcp_ruby/presenters/pipeline_stages_zone.rb +11 -0
- data/examples/showcase/config/lcp_ruby/presenters/pipelines.rb +40 -0
- data/examples/showcase/config/lcp_ruby/presenters/platform_profiles.rb +90 -0
- data/examples/showcase/config/lcp_ruby/presenters/projects.rb +58 -0
- data/examples/showcase/config/lcp_ruby/presenters/quick_note_dialog.rb +14 -0
- data/examples/showcase/config/lcp_ruby/presenters/roles.rb +60 -0
- data/examples/showcase/config/lcp_ruby/presenters/save_filter_dialog.rb +17 -0
- data/examples/showcase/config/lcp_ruby/presenters/saved_filters.rb +94 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_aggregate_items.rb +61 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_aggregates.rb +115 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_aggregates_tiles.rb +44 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_amount_kanban.rb +31 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_arrays.rb +140 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_attachments.rb +66 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_audit_logs.rb +46 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_audited_records.rb +58 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_batch_tasks.rb +97 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_batch_tasks_archive.rb +52 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_business_units.rb +43 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_conditions.rb +195 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_contacts.rb +82 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_custom_render_with.rb +50 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_custom_sections.rb +88 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_custom_zones_main.rb +75 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_divisions.rb +53 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_extensibility.rb +48 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_fields_card.rb +34 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_fields_table.rb +128 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_fields_tiles.rb +42 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_form_action_dialog.rb +27 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_form_actions.rb +132 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_form_actions_overflow.rb +76 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_forms.rb +110 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_grades.rb +38 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_hr_employees.rb +84 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_item_classes.rb +120 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_job_executions.rb +150 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_memos.rb +96 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_models.rb +112 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_organizations.rb +106 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_people.rb +97 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_permissions.rb +84 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_positioning.rb +61 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_recipes.rb +105 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_recipes_raw.rb +32 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_reports.rb +109 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_school_classes.rb +34 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_searches.rb +226 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_sequences.rb +67 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_soft_delete.rb +88 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_soft_delete_archive.rb +52 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_soft_delete_items.rb +47 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_students.rb +33 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_type_defaults.rb +92 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_userstamps.rb +76 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_virtual_fields.rb +104 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_workflow_admin.rb +24 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_workflow_kanban.rb +36 -0
- data/examples/showcase/config/lcp_ruby/presenters/showcase_workflows.rb +103 -0
- data/examples/showcase/config/lcp_ruby/presenters/tags.rb +35 -0
- data/examples/showcase/config/lcp_ruby/presenters/users.rb +61 -0
- data/examples/showcase/config/lcp_ruby/presenters/weather_stations.rb +60 -0
- data/examples/showcase/config/lcp_ruby/presenters/workflow_audit_logs.rb +76 -0
- data/examples/showcase/config/lcp_ruby/presenters/workflow_request_audit_zone.rb +28 -0
- data/examples/showcase/config/lcp_ruby/theme.yml +2 -0
- data/examples/showcase/config/lcp_ruby/types/currency.yml +15 -0
- data/examples/showcase/config/lcp_ruby/types/percentage.yml +16 -0
- data/examples/showcase/config/lcp_ruby/types/rating.yml +20 -0
- data/examples/showcase/config/lcp_ruby/views/announcements.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/approval_requests.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/approval_steps.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/approval_tasks.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/articles.yml +12 -0
- data/examples/showcase/config/lcp_ruby/views/authors.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/batch_operation_items.yml +6 -0
- data/examples/showcase/config/lcp_ruby/views/batch_operations.yml +10 -0
- data/examples/showcase/config/lcp_ruby/views/categories.yml +9 -0
- data/examples/showcase/config/lcp_ruby/views/custom_fields.rb +8 -0
- data/examples/showcase/config/lcp_ruby/views/dashboard.yml +10 -0
- data/examples/showcase/config/lcp_ruby/views/department_explorer.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/departments.yml +9 -0
- data/examples/showcase/config/lcp_ruby/views/employee_overview.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/employees.yml +12 -0
- data/examples/showcase/config/lcp_ruby/views/export_logs.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/export_profiles.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/features.yml +19 -0
- data/examples/showcase/config/lcp_ruby/views/group_memberships.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/group_role_mappings.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/groups.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/host_inventory_items.yml +16 -0
- data/examples/showcase/config/lcp_ruby/views/hr_turnover_dashboard.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/import_profiles.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/import_rows.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/lcp_error_logs.yml +10 -0
- data/examples/showcase/config/lcp_ruby/views/monitoring.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/my_api_tokens.rb +9 -0
- data/examples/showcase/config/lcp_ruby/views/page_configs.yml +11 -0
- data/examples/showcase/config/lcp_ruby/views/permission_configs.yml +11 -0
- data/examples/showcase/config/lcp_ruby/views/pipeline_stages.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/pipelines.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/platform_profiles.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/projects.yml +9 -0
- data/examples/showcase/config/lcp_ruby/views/roles.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/saved_filters.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_aggregate_items.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_aggregates.yml +10 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_arrays.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_attachments.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_audit_logs.yml +11 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_audited_records.yml +10 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_batch_tasks.yml +10 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_business_units.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_conditions.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_contacts.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_custom_render_with.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_custom_sections.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_custom_zones.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_divisions.yml +9 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_extensibility.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_fields.yml +13 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_form_actions.yml +10 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_forms.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_grades.yml +9 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_hr_employees.yml +9 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_item_classes.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_job_executions.yml +9 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_memos.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_models.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_organizations.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_people.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_permissions.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_positioning.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_recipes.yml +10 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_reports.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_school_classes.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_searches.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_sequences.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_soft_delete.yml +10 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_soft_delete_items.yml +8 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_students.yml +9 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_userstamps.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_virtual_fields.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/showcase_workflows.yml +16 -0
- data/examples/showcase/config/lcp_ruby/views/tags.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/users.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/weather_stations.yml +7 -0
- data/examples/showcase/config/lcp_ruby/views/workflow_audit_logs.yml +9 -0
- data/examples/showcase/config/lcp_ruby/workflows/showcase_form_action.rb +48 -0
- data/examples/showcase/config/lcp_ruby/workflows/showcase_workflow.rb +199 -0
- data/examples/showcase/config/locales/cs.yml +2818 -0
- data/examples/showcase/config/locales/en.yml +67 -0
- data/examples/showcase/config/locales/lcp_ruby/api_tokens.cs.yml +46 -0
- data/examples/showcase/config/locales/lcp_ruby/api_tokens.en.yml +49 -0
- data/examples/showcase/config/routes.rb +21 -0
- data/examples/showcase/config/storage.yml +3 -0
- data/examples/showcase/config.ru +2 -0
- data/examples/showcase/db/migrate/20260219124321_create_active_storage_tables.active_storage.rb +57 -0
- data/examples/showcase/db/migrate/20260220072723_create_lcp_ruby_users.rb +42 -0
- data/examples/showcase/db/migrate/20260406120000_create_host_inventory_items.rb +32 -0
- data/examples/showcase/db/migrate/20260428134542_add_oidc_columns_to_lcp_ruby_users.rb +13 -0
- data/examples/showcase/db/migrate/20260502120000_create_platform_profiles.rb +19 -0
- data/examples/showcase/db/schema.rb +1222 -0
- data/examples/showcase/db/seeds.rb +3085 -0
- data/examples/showcase/erd.md +567 -0
- data/examples/showcase/test/fixtures/import_articles.csv +4 -0
- data/examples/showcase/test/fixtures/import_employees.csv +4 -0
- data/examples/todo/Gemfile +8 -0
- data/examples/todo/Gemfile.lock +415 -0
- data/examples/todo/Rakefile +2 -0
- data/examples/todo/app/assets/config/manifest.js +1 -0
- data/examples/todo/app/controllers/application_controller.rb +6 -0
- data/examples/todo/app/lcp_services/defaults/one_week_from_now.rb +11 -0
- data/examples/todo/bin/rails +4 -0
- data/examples/todo/bin/rake +4 -0
- data/examples/todo/config/application.rb +31 -0
- data/examples/todo/config/boot.rb +2 -0
- data/examples/todo/config/database.yml +12 -0
- data/examples/todo/config/environment.rb +2 -0
- data/examples/todo/config/lcp_ruby/models/todo_item.yml +67 -0
- data/examples/todo/config/lcp_ruby/models/todo_list.yml +49 -0
- data/examples/todo/config/lcp_ruby/permissions/default.yml +10 -0
- data/examples/todo/config/lcp_ruby/permissions/todo_item.yml +21 -0
- data/examples/todo/config/lcp_ruby/presenters/todo_item.yml +75 -0
- data/examples/todo/config/lcp_ruby/presenters/todo_list.yml +68 -0
- data/examples/todo/config/lcp_ruby/views/todo_items.yml +11 -0
- data/examples/todo/config/lcp_ruby/views/todo_lists.yml +9 -0
- data/examples/todo/config/routes.rb +4 -0
- data/examples/todo/config/storage.yml +3 -0
- data/examples/todo/config.ru +2 -0
- data/examples/todo/db/migrate/20260219103416_create_active_storage_tables.active_storage.rb +57 -0
- data/examples/todo/db/schema.rb +63 -0
- data/examples/todo/db/seeds.rb +24 -0
- data/examples/todo/erd.md +21 -0
- data/exe/lcp +33 -0
- data/lib/generators/lcp_ruby/agent_setup_generator.rb +102 -0
- data/lib/generators/lcp_ruby/api_tokens_generator.rb +102 -0
- data/lib/generators/lcp_ruby/auditing_generator.rb +54 -0
- data/lib/generators/lcp_ruby/background_jobs_generator.rb +61 -0
- data/lib/generators/lcp_ruby/batch_operations_generator.rb +62 -0
- data/lib/generators/lcp_ruby/claude_skills_generator.rb +47 -0
- data/lib/generators/lcp_ruby/custom_fields_generator.rb +54 -0
- data/lib/generators/lcp_ruby/dsl_to_yaml.rb +72 -0
- data/lib/generators/lcp_ruby/entity/color_palette.rb +22 -0
- data/lib/generators/lcp_ruby/entity/field_descriptor.rb +24 -0
- data/lib/generators/lcp_ruby/entity/field_token_parser.rb +101 -0
- data/lib/generators/lcp_ruby/entity/role_discovery.rb +45 -0
- data/lib/generators/lcp_ruby/entity_generator.rb +1104 -0
- data/lib/generators/lcp_ruby/export_generator.rb +71 -0
- data/lib/generators/lcp_ruby/format_support.rb +56 -0
- data/lib/generators/lcp_ruby/gapfree_sequences_generator.rb +64 -0
- data/lib/generators/lcp_ruby/groups_generator.rb +94 -0
- data/lib/generators/lcp_ruby/host_controller_generator.rb +202 -0
- data/lib/generators/lcp_ruby/import_generator.rb +96 -0
- data/lib/generators/lcp_ruby/install_auth_generator.rb +432 -0
- data/lib/generators/lcp_ruby/install_generator.rb +319 -0
- data/lib/generators/lcp_ruby/monitoring_generator.rb +58 -0
- data/lib/generators/lcp_ruby/oidc_role_mappings_generator.rb +60 -0
- data/lib/generators/lcp_ruby/pages_generator.rb +66 -0
- data/lib/generators/lcp_ruby/permission_source_generator.rb +66 -0
- data/lib/generators/lcp_ruby/role_model_generator.rb +73 -0
- data/lib/generators/lcp_ruby/saved_filters_generator.rb +62 -0
- data/lib/generators/lcp_ruby/templates/add_oidc_columns_to_lcp_ruby_users.rb.erb +13 -0
- data/lib/generators/lcp_ruby/templates/agent_setup/agents_md.md +3 -0
- data/lib/generators/lcp_ruby/templates/agent_setup/claude_md.md +12 -0
- data/lib/generators/lcp_ruby/templates/api_tokens/create_dialog.rb +31 -0
- data/lib/generators/lcp_ruby/templates/api_tokens/locales.en.yml +43 -0
- data/lib/generators/lcp_ruby/templates/api_tokens/model.rb +23 -0
- data/lib/generators/lcp_ruby/templates/api_tokens/permissions.yml +30 -0
- data/lib/generators/lcp_ruby/templates/api_tokens/presenter.rb +38 -0
- data/lib/generators/lcp_ruby/templates/api_tokens/view_group.rb +13 -0
- data/lib/generators/lcp_ruby/templates/auditing/model.rb +34 -0
- data/lib/generators/lcp_ruby/templates/auditing/permissions.yml +19 -0
- data/lib/generators/lcp_ruby/templates/auditing/presenter.rb +41 -0
- data/lib/generators/lcp_ruby/templates/auditing/view_group.rb +10 -0
- data/lib/generators/lcp_ruby/templates/background_jobs/model.rb +59 -0
- data/lib/generators/lcp_ruby/templates/background_jobs/permissions.yml +22 -0
- data/lib/generators/lcp_ruby/templates/background_jobs/presenter.rb +82 -0
- data/lib/generators/lcp_ruby/templates/background_jobs/view_group.rb +9 -0
- data/lib/generators/lcp_ruby/templates/batch_operations/item_model.rb +28 -0
- data/lib/generators/lcp_ruby/templates/batch_operations/item_presenter.rb +43 -0
- data/lib/generators/lcp_ruby/templates/batch_operations/model.rb +42 -0
- data/lib/generators/lcp_ruby/templates/batch_operations/permissions.yml +44 -0
- data/lib/generators/lcp_ruby/templates/batch_operations/presenter.rb +61 -0
- data/lib/generators/lcp_ruby/templates/batch_operations/view_group.rb +9 -0
- data/lib/generators/lcp_ruby/templates/create_lcp_ruby_users.rb.erb +50 -0
- data/lib/generators/lcp_ruby/templates/custom_fields/model.rb +60 -0
- data/lib/generators/lcp_ruby/templates/custom_fields/permissions.yml +19 -0
- data/lib/generators/lcp_ruby/templates/custom_fields/presenter.rb +128 -0
- data/lib/generators/lcp_ruby/templates/custom_fields/view_group.rb +9 -0
- data/lib/generators/lcp_ruby/templates/entity/model.rb +42 -0
- data/lib/generators/lcp_ruby/templates/entity/permissions.yml +20 -0
- data/lib/generators/lcp_ruby/templates/entity/presenter.rb +55 -0
- data/lib/generators/lcp_ruby/templates/entity/view_group.rb +8 -0
- data/lib/generators/lcp_ruby/templates/export/export_log_model.rb +30 -0
- data/lib/generators/lcp_ruby/templates/export/export_log_permissions.yml +24 -0
- data/lib/generators/lcp_ruby/templates/export/export_logs_presenter.rb +51 -0
- data/lib/generators/lcp_ruby/templates/export/export_profile_model.rb +28 -0
- data/lib/generators/lcp_ruby/templates/export/export_profile_permissions.yml +26 -0
- data/lib/generators/lcp_ruby/templates/export/export_profiles_presenter.rb +59 -0
- data/lib/generators/lcp_ruby/templates/gapfree_sequences/model.rb +18 -0
- data/lib/generators/lcp_ruby/templates/gapfree_sequences/permissions.yml +19 -0
- data/lib/generators/lcp_ruby/templates/gapfree_sequences/presenter.rb +51 -0
- data/lib/generators/lcp_ruby/templates/gapfree_sequences/view_group.rb +9 -0
- data/lib/generators/lcp_ruby/templates/groups/group_membership_model.rb +17 -0
- data/lib/generators/lcp_ruby/templates/groups/group_model.rb +28 -0
- data/lib/generators/lcp_ruby/templates/groups/group_permissions.yml +19 -0
- data/lib/generators/lcp_ruby/templates/groups/group_presenter.rb +53 -0
- data/lib/generators/lcp_ruby/templates/groups/group_role_mapping_model.rb +15 -0
- data/lib/generators/lcp_ruby/templates/groups/group_view_group.rb +9 -0
- data/lib/generators/lcp_ruby/templates/host_controller/controller.rb.erb +131 -0
- data/lib/generators/lcp_ruby/templates/import/data_import_job.yml +7 -0
- data/lib/generators/lcp_ruby/templates/import/import_profile_model.rb +30 -0
- data/lib/generators/lcp_ruby/templates/import/import_profile_permissions.yml +29 -0
- data/lib/generators/lcp_ruby/templates/import/import_profiles_presenter.rb +57 -0
- data/lib/generators/lcp_ruby/templates/import/import_row_model.rb +32 -0
- data/lib/generators/lcp_ruby/templates/import/import_row_permissions.yml +11 -0
- data/lib/generators/lcp_ruby/templates/import/import_rows_presenter.rb +35 -0
- data/lib/generators/lcp_ruby/templates/install/default_permissions.yml +26 -0
- data/lib/generators/lcp_ruby/templates/install/menu.yml.tt +71 -0
- data/lib/generators/lcp_ruby/templates/install/model.rb +20 -0
- data/lib/generators/lcp_ruby/templates/install/permissions.yml +25 -0
- data/lib/generators/lcp_ruby/templates/install/presenter.rb +44 -0
- data/lib/generators/lcp_ruby/templates/install/view_group.rb +10 -0
- data/lib/generators/lcp_ruby/templates/install_auth/oidc/entra.yml.erb +46 -0
- data/lib/generators/lcp_ruby/templates/install_auth/oidc/generic.yml.erb +55 -0
- data/lib/generators/lcp_ruby/templates/install_auth/oidc/google.yml.erb +42 -0
- data/lib/generators/lcp_ruby/templates/install_auth/oidc/keycloak.yml.erb +45 -0
- data/lib/generators/lcp_ruby/templates/install_auth/oidc/okta.yml.erb +45 -0
- data/lib/generators/lcp_ruby/templates/install_auth/user.rb +36 -0
- data/lib/generators/lcp_ruby/templates/monitoring/model.rb +34 -0
- data/lib/generators/lcp_ruby/templates/monitoring/model_extension.rb +12 -0
- data/lib/generators/lcp_ruby/templates/monitoring/page.yml +49 -0
- data/lib/generators/lcp_ruby/templates/monitoring/permissions.yml +8 -0
- data/lib/generators/lcp_ruby/templates/monitoring/presenter.rb +44 -0
- data/lib/generators/lcp_ruby/templates/oidc_role_mappings/locales.en.yml +15 -0
- data/lib/generators/lcp_ruby/templates/oidc_role_mappings/model.rb +32 -0
- data/lib/generators/lcp_ruby/templates/oidc_role_mappings/permissions.yml +21 -0
- data/lib/generators/lcp_ruby/templates/oidc_role_mappings/presenter.rb +41 -0
- data/lib/generators/lcp_ruby/templates/pages/model.rb +19 -0
- data/lib/generators/lcp_ruby/templates/pages/permissions.yml +19 -0
- data/lib/generators/lcp_ruby/templates/pages/presenter.rb +51 -0
- data/lib/generators/lcp_ruby/templates/pages/view_group.rb +10 -0
- data/lib/generators/lcp_ruby/templates/permission_source/model.rb +21 -0
- data/lib/generators/lcp_ruby/templates/permission_source/permissions.yml +19 -0
- data/lib/generators/lcp_ruby/templates/permission_source/presenter.rb +51 -0
- data/lib/generators/lcp_ruby/templates/permission_source/view_group.rb +10 -0
- data/lib/generators/lcp_ruby/templates/role_model/model.rb +24 -0
- data/lib/generators/lcp_ruby/templates/role_model/permissions.yml +19 -0
- data/lib/generators/lcp_ruby/templates/role_model/presenter.rb +62 -0
- data/lib/generators/lcp_ruby/templates/role_model/view_group.rb +10 -0
- data/lib/generators/lcp_ruby/templates/saved_filters/model.rb +51 -0
- data/lib/generators/lcp_ruby/templates/saved_filters/permissions.yml +44 -0
- data/lib/generators/lcp_ruby/templates/saved_filters/presenter.rb +96 -0
- data/lib/generators/lcp_ruby/templates/saved_filters/save_dialog_presenter.rb +19 -0
- data/lib/generators/lcp_ruby/templates/workflow_approvals/request_model.rb +50 -0
- data/lib/generators/lcp_ruby/templates/workflow_approvals/request_permissions.yml +10 -0
- data/lib/generators/lcp_ruby/templates/workflow_approvals/request_presenter.rb +44 -0
- data/lib/generators/lcp_ruby/templates/workflow_approvals/request_view_group.rb +10 -0
- data/lib/generators/lcp_ruby/templates/workflow_approvals/step_model.rb +36 -0
- data/lib/generators/lcp_ruby/templates/workflow_approvals/step_permissions.yml +10 -0
- data/lib/generators/lcp_ruby/templates/workflow_approvals/task_model.rb +34 -0
- data/lib/generators/lcp_ruby/templates/workflow_approvals/task_permissions.yml +10 -0
- data/lib/generators/lcp_ruby/templates/workflow_approvals/task_presenter.rb +39 -0
- data/lib/generators/lcp_ruby/templates/workflow_approvals/task_view_group.rb +10 -0
- data/lib/generators/lcp_ruby/templates/workflow_audit_log/model.rb +48 -0
- data/lib/generators/lcp_ruby/templates/workflow_audit_log/permissions.yml +10 -0
- data/lib/generators/lcp_ruby/templates/workflow_audit_log/presenter.rb +44 -0
- data/lib/generators/lcp_ruby/templates/workflow_audit_log/view_group.rb +10 -0
- data/lib/generators/lcp_ruby/templates/workflow_definition/model.rb +43 -0
- data/lib/generators/lcp_ruby/templates/workflow_definition/permissions.yml +19 -0
- data/lib/generators/lcp_ruby/templates/workflow_definition/presenter.rb +70 -0
- data/lib/generators/lcp_ruby/templates/workflow_definition/view_group.rb +10 -0
- data/lib/generators/lcp_ruby/workflow_approvals_generator.rb +93 -0
- data/lib/generators/lcp_ruby/workflow_audit_log_generator.rb +54 -0
- data/lib/generators/lcp_ruby/workflow_definition_generator.rb +77 -0
- data/lib/lcp.rb +6 -0
- data/lib/lcp_ruby/actions/action_executor.rb +66 -0
- data/lib/lcp_ruby/actions/action_registry.rb +48 -0
- data/lib/lcp_ruby/actions/api_tokens/revoke.rb +13 -0
- data/lib/lcp_ruby/actions/base_action.rb +79 -0
- data/lib/lcp_ruby/actions/form_action_pipeline.rb +138 -0
- data/lib/lcp_ruby/aggregates/query_builder.rb +6 -0
- data/lib/lcp_ruby/api_tokens/model_extension.rb +41 -0
- data/lib/lcp_ruby/api_tokens/resolver_registry.rb +53 -0
- data/lib/lcp_ruby/api_tokens/token_generator.rb +27 -0
- data/lib/lcp_ruby/api_tokens/verifier.rb +38 -0
- data/lib/lcp_ruby/app_template.rb +181 -0
- data/lib/lcp_ruby/array_query.rb +120 -0
- data/lib/lcp_ruby/asset_copier.rb +62 -0
- data/lib/lcp_ruby/association_fk_type.rb +191 -0
- data/lib/lcp_ruby/association_join_column.rb +28 -0
- data/lib/lcp_ruby/association_options_builder.rb +231 -0
- data/lib/lcp_ruby/auditing/audit_writer.rb +258 -0
- data/lib/lcp_ruby/auditing/contract_validator.rb +95 -0
- data/lib/lcp_ruby/auditing/registry.rb +29 -0
- data/lib/lcp_ruby/auditing/setup.rb +49 -0
- data/lib/lcp_ruby/authentication/audit_subscriber.rb +51 -0
- data/lib/lcp_ruby/authentication/bearer_jwt_verifier.rb +139 -0
- data/lib/lcp_ruby/authentication/devise_setup.rb +47 -0
- data/lib/lcp_ruby/authentication/errors.rb +27 -0
- data/lib/lcp_ruby/authentication/http_fetcher.rb +36 -0
- data/lib/lcp_ruby/authentication/jwks_cache.rb +91 -0
- data/lib/lcp_ruby/authentication/oidc_bearer_resolver.rb +84 -0
- data/lib/lcp_ruby/authentication/omniauth_builder.rb +147 -0
- data/lib/lcp_ruby/authentication/provider.rb +108 -0
- data/lib/lcp_ruby/authentication/provider_registry.rb +227 -0
- data/lib/lcp_ruby/authentication/role_mapper.rb +94 -0
- data/lib/lcp_ruby/authentication/test_support.rb +257 -0
- data/lib/lcp_ruby/authentication/user_resolver.rb +169 -0
- data/lib/lcp_ruby/authentication.rb +40 -0
- data/lib/lcp_ruby/authorization/association_lookup.rb +56 -0
- data/lib/lcp_ruby/authorization/authorization_error.rb +12 -0
- data/lib/lcp_ruby/authorization/cache.rb +89 -0
- data/lib/lcp_ruby/authorization/codes.rb +17 -0
- data/lib/lcp_ruby/authorization/impersonated_user.rb +29 -0
- data/lib/lcp_ruby/authorization/includes_hint.rb +110 -0
- data/lib/lcp_ruby/authorization/inherited_parent_validator.rb +142 -0
- data/lib/lcp_ruby/authorization/invariant_check/configuration.rb +132 -0
- data/lib/lcp_ruby/authorization/invariant_error.rb +15 -0
- data/lib/lcp_ruby/authorization/misconfigured_page_error.rb +30 -0
- data/lib/lcp_ruby/authorization/page_gate.rb +57 -0
- data/lib/lcp_ruby/authorization/permission_evaluator.rb +343 -0
- data/lib/lcp_ruby/authorization/policy_factory.rb +91 -0
- data/lib/lcp_ruby/authorization/runtime_invariant_validator.rb +421 -0
- data/lib/lcp_ruby/authorization/scope_builder.rb +227 -0
- data/lib/lcp_ruby/authorization/scope_resolver.rb +28 -0
- data/lib/lcp_ruby/authorized_controller.rb +44 -0
- data/lib/lcp_ruby/background_jobs/base_handler.rb +113 -0
- data/lib/lcp_ruby/background_jobs/change_handler.rb +17 -0
- data/lib/lcp_ruby/background_jobs/contract.rb +16 -0
- data/lib/lcp_ruby/background_jobs/contract_validator.rb +112 -0
- data/lib/lcp_ruby/background_jobs/declarative/base_action.rb +11 -0
- data/lib/lcp_ruby/background_jobs/declarative/call_webhook_action.rb +174 -0
- data/lib/lcp_ruby/background_jobs/declarative/fire_event_action.rb +24 -0
- data/lib/lcp_ruby/background_jobs/declarative/registry.rb +34 -0
- data/lib/lcp_ruby/background_jobs/declarative/run_scope_action.rb +98 -0
- data/lib/lcp_ruby/background_jobs/declarative/send_notification_action.rb +13 -0
- data/lib/lcp_ruby/background_jobs/definition.rb +134 -0
- data/lib/lcp_ruby/background_jobs/enqueue.rb +83 -0
- data/lib/lcp_ruby/background_jobs/errors.rb +9 -0
- data/lib/lcp_ruby/background_jobs/executor_job.rb +111 -0
- data/lib/lcp_ruby/background_jobs/handler_factory.rb +46 -0
- data/lib/lcp_ruby/background_jobs/host_source.rb +33 -0
- data/lib/lcp_ruby/background_jobs/model_source.rb +93 -0
- data/lib/lcp_ruby/background_jobs/registry.rb +81 -0
- data/lib/lcp_ruby/background_jobs/resolver.rb +29 -0
- data/lib/lcp_ruby/background_jobs/schedule_adapter.rb +14 -0
- data/lib/lcp_ruby/background_jobs/setup.rb +145 -0
- data/lib/lcp_ruby/background_jobs/static_source.rb +19 -0
- data/lib/lcp_ruby/background_jobs/steps_executor.rb +52 -0
- data/lib/lcp_ruby/background_jobs/triggers/event_trigger.rb +97 -0
- data/lib/lcp_ruby/background_jobs/triggers/trigger_installer.rb +20 -0
- data/lib/lcp_ruby/background_jobs/unique_key_builder.rb +31 -0
- data/lib/lcp_ruby/batch_actions/base_service.rb +70 -0
- data/lib/lcp_ruby/batch_actions/batch_action_handler.rb +200 -0
- data/lib/lcp_ruby/batch_actions/custom_action_dispatcher.rb +133 -0
- data/lib/lcp_ruby/batch_actions/destroy_service.rb +37 -0
- data/lib/lcp_ruby/batch_actions/permanently_destroy_service.rb +33 -0
- data/lib/lcp_ruby/batch_actions/restore_service.rb +33 -0
- data/lib/lcp_ruby/batch_actions.rb +5 -0
- data/lib/lcp_ruby/bulk_updater.rb +25 -0
- data/lib/lcp_ruby/cli/docs_command.rb +29 -0
- data/lib/lcp_ruby/cli/examples_command.rb +29 -0
- data/lib/lcp_ruby/cli/new_command.rb +509 -0
- data/lib/lcp_ruby/cli/run_command.rb +155 -0
- data/lib/lcp_ruby/cli/skills_command.rb +54 -0
- data/lib/lcp_ruby/cli.rb +74 -0
- data/lib/lcp_ruby/condition_evaluator.rb +366 -0
- data/lib/lcp_ruby/condition_service_registry.rb +58 -0
- data/lib/lcp_ruby/condition_services/current_user_role.rb +28 -0
- data/lib/lcp_ruby/condition_services/feature_flag.rb +63 -0
- data/lib/lcp_ruby/condition_services/impersonating.rb +24 -0
- data/lib/lcp_ruby/conditions/validator.rb +35 -0
- data/lib/lcp_ruby/configuration.rb +431 -0
- data/lib/lcp_ruby/controller/authentication.rb +118 -0
- data/lib/lcp_ruby/controller/authorization.rb +198 -0
- data/lib/lcp_ruby/controller/bearer_authentication.rb +76 -0
- data/lib/lcp_ruby/controller/crud_helpers.rb +233 -0
- data/lib/lcp_ruby/controller/error_handling.rb +94 -0
- data/lib/lcp_ruby/controller/impersonation.rb +70 -0
- data/lib/lcp_ruby/controller/locale_binding.rb +62 -0
- data/lib/lcp_ruby/controller/path_helpers.rb +125 -0
- data/lib/lcp_ruby/controller/presenter_setup.rb +89 -0
- data/lib/lcp_ruby/controller/search.rb +321 -0
- data/lib/lcp_ruby/controller/view_helpers.rb +105 -0
- data/lib/lcp_ruby/current.rb +13 -0
- data/lib/lcp_ruby/custom_fields/applicator.rb +194 -0
- data/lib/lcp_ruby/custom_fields/contract_validator.rb +77 -0
- data/lib/lcp_ruby/custom_fields/definition_change_handler.rb +21 -0
- data/lib/lcp_ruby/custom_fields/query.rb +112 -0
- data/lib/lcp_ruby/custom_fields/registry.rb +70 -0
- data/lib/lcp_ruby/custom_fields/setup.rb +58 -0
- data/lib/lcp_ruby/custom_fields/utils.rb +40 -0
- data/lib/lcp_ruby/custom_fields.rb +9 -0
- data/lib/lcp_ruby/data_source/api_error_placeholder.rb +47 -0
- data/lib/lcp_ruby/data_source/api_filter_translator.rb +72 -0
- data/lib/lcp_ruby/data_source/api_model_concern.rb +131 -0
- data/lib/lcp_ruby/data_source/api_preloader.rb +44 -0
- data/lib/lcp_ruby/data_source/base.rb +85 -0
- data/lib/lcp_ruby/data_source/cached_wrapper.rb +107 -0
- data/lib/lcp_ruby/data_source/host.rb +71 -0
- data/lib/lcp_ruby/data_source/registry.rb +39 -0
- data/lib/lcp_ruby/data_source/resilient_wrapper.rb +67 -0
- data/lib/lcp_ruby/data_source/rest_json.rb +247 -0
- data/lib/lcp_ruby/data_source/setup.rb +57 -0
- data/lib/lcp_ruby/dev_toolbar.rb +8 -0
- data/lib/lcp_ruby/display/base_renderer.rb +21 -0
- data/lib/lcp_ruby/display/count_badge.rb +11 -0
- data/lib/lcp_ruby/display/icon_badge.rb +17 -0
- data/lib/lcp_ruby/display/renderer_registry.rb +138 -0
- data/lib/lcp_ruby/display/renderers/attachment_link.rb +26 -0
- data/lib/lcp_ruby/display/renderers/attachment_list.rb +32 -0
- data/lib/lcp_ruby/display/renderers/attachment_preview.rb +26 -0
- data/lib/lcp_ruby/display/renderers/avatar.rb +14 -0
- data/lib/lcp_ruby/display/renderers/badge.rb +43 -0
- data/lib/lcp_ruby/display/renderers/boolean_icon.rb +54 -0
- data/lib/lcp_ruby/display/renderers/code.rb +39 -0
- data/lib/lcp_ruby/display/renderers/collection.rb +34 -0
- data/lib/lcp_ruby/display/renderers/color_swatch.rb +25 -0
- data/lib/lcp_ruby/display/renderers/concerns/attachment_helpers.rb +73 -0
- data/lib/lcp_ruby/display/renderers/concerns/workflow_helpers.rb +35 -0
- data/lib/lcp_ruby/display/renderers/copy_code.rb +33 -0
- data/lib/lcp_ruby/display/renderers/currency.rb +14 -0
- data/lib/lcp_ruby/display/renderers/date.rb +17 -0
- data/lib/lcp_ruby/display/renderers/datetime.rb +17 -0
- data/lib/lcp_ruby/display/renderers/email_link.rb +15 -0
- data/lib/lcp_ruby/display/renderers/file_size.rb +11 -0
- data/lib/lcp_ruby/display/renderers/heading.rb +11 -0
- data/lib/lcp_ruby/display/renderers/image.rb +17 -0
- data/lib/lcp_ruby/display/renderers/internal_link.rb +23 -0
- data/lib/lcp_ruby/display/renderers/link.rb +15 -0
- data/lib/lcp_ruby/display/renderers/link_list.rb +90 -0
- data/lib/lcp_ruby/display/renderers/markdown.rb +33 -0
- data/lib/lcp_ruby/display/renderers/number.rb +14 -0
- data/lib/lcp_ruby/display/renderers/percentage.rb +12 -0
- data/lib/lcp_ruby/display/renderers/phone_link.rb +15 -0
- data/lib/lcp_ruby/display/renderers/progress_bar.rb +15 -0
- data/lib/lcp_ruby/display/renderers/rating.rb +15 -0
- data/lib/lcp_ruby/display/renderers/record_link.rb +101 -0
- data/lib/lcp_ruby/display/renderers/relative_date.rb +15 -0
- data/lib/lcp_ruby/display/renderers/rich_text.rb +15 -0
- data/lib/lcp_ruby/display/renderers/text.rb +12 -0
- data/lib/lcp_ruby/display/renderers/truncate.rb +17 -0
- data/lib/lcp_ruby/display/renderers/url_link.rb +22 -0
- data/lib/lcp_ruby/display/renderers/workflow_badge.rb +37 -0
- data/lib/lcp_ruby/display/renderers/workflow_timeline.rb +173 -0
- data/lib/lcp_ruby/display/renderers.rb +3 -0
- data/lib/lcp_ruby/display/text_badge.rb +15 -0
- data/lib/lcp_ruby/dsl/condition_builder.rb +190 -0
- data/lib/lcp_ruby/dsl/dsl_loader.rb +365 -0
- data/lib/lcp_ruby/dsl/field_builder.rb +35 -0
- data/lib/lcp_ruby/dsl/job_builder.rb +92 -0
- data/lib/lcp_ruby/dsl/model_builder.rb +544 -0
- data/lib/lcp_ruby/dsl/presenter_builder.rb +1272 -0
- data/lib/lcp_ruby/dsl/source_location_capture.rb +52 -0
- data/lib/lcp_ruby/dsl/type_builder.rb +88 -0
- data/lib/lcp_ruby/dsl/view_group_builder.rb +92 -0
- data/lib/lcp_ruby/dsl/workflow_builder.rb +319 -0
- data/lib/lcp_ruby/dynamic.rb +7 -0
- data/lib/lcp_ruby/dynamic_references/resolver.rb +154 -0
- data/lib/lcp_ruby/dynamic_references/validator.rb +92 -0
- data/lib/lcp_ruby/embed_providers/base.rb +18 -0
- data/lib/lcp_ruby/embed_providers/grafana.rb +38 -0
- data/lib/lcp_ruby/embed_providers/metabase.rb +37 -0
- data/lib/lcp_ruby/engine.rb +680 -0
- data/lib/lcp_ruby/events/async_handler_job.rb +21 -0
- data/lib/lcp_ruby/events/dispatcher.rb +52 -0
- data/lib/lcp_ruby/events/handler_base.rb +51 -0
- data/lib/lcp_ruby/events/handler_registry.rb +49 -0
- data/lib/lcp_ruby/export/data_generator.rb +158 -0
- data/lib/lcp_ruby/export/export_handler.rb +315 -0
- data/lib/lcp_ruby/export/field_tree_builder.rb +219 -0
- data/lib/lcp_ruby/export/setup.rb +94 -0
- data/lib/lcp_ruby/export/value_formatter.rb +223 -0
- data/lib/lcp_ruby/export.rb +9 -0
- data/lib/lcp_ruby/gem_paths.rb +51 -0
- data/lib/lcp_ruby/generators/entity_menu_writer.rb +258 -0
- data/lib/lcp_ruby/generators/feature_registry.rb +208 -0
- data/lib/lcp_ruby/generators/prerequisites.rb +90 -0
- data/lib/lcp_ruby/grouped_query/builder.rb +206 -0
- data/lib/lcp_ruby/grouped_query/result_wrapper.rb +72 -0
- data/lib/lcp_ruby/grouped_query/row.rb +31 -0
- data/lib/lcp_ruby/groups/change_handler.rb +18 -0
- data/lib/lcp_ruby/groups/contract.rb +42 -0
- data/lib/lcp_ruby/groups/contract_validator.rb +110 -0
- data/lib/lcp_ruby/groups/host_loader.rb +54 -0
- data/lib/lcp_ruby/groups/model_loader.rb +186 -0
- data/lib/lcp_ruby/groups/registry.rb +113 -0
- data/lib/lcp_ruby/groups/setup.rb +129 -0
- data/lib/lcp_ruby/groups/yaml_loader.rb +97 -0
- data/lib/lcp_ruby/hash_utils.rb +42 -0
- data/lib/lcp_ruby/i18n_check/configuration.rb +104 -0
- data/lib/lcp_ruby/i18n_check/heuristics.rb +26 -0
- data/lib/lcp_ruby/i18n_check/key_deriver.rb +136 -0
- data/lib/lcp_ruby/i18n_check/offense.rb +29 -0
- data/lib/lcp_ruby/i18n_check/registry_walker.rb +621 -0
- data/lib/lcp_ruby/i18n_check/reporter.rb +96 -0
- data/lib/lcp_ruby/i18n_check/runner.rb +46 -0
- data/lib/lcp_ruby/i18n_check.rb +15 -0
- data/lib/lcp_ruby/i18n_lint.rb +145 -0
- data/lib/lcp_ruby/import/auto_mapper.rb +98 -0
- data/lib/lcp_ruby/import/field_tree_builder.rb +178 -0
- data/lib/lcp_ruby/import/file_parser.rb +223 -0
- data/lib/lcp_ruby/import/import_dialog_handler.rb +410 -0
- data/lib/lcp_ruby/import/import_job_handler.rb +224 -0
- data/lib/lcp_ruby/import/row_processor.rb +281 -0
- data/lib/lcp_ruby/import/setup.rb +277 -0
- data/lib/lcp_ruby/import/value_coercer.rb +143 -0
- data/lib/lcp_ruby/import.rb +14 -0
- data/lib/lcp_ruby/json_item_wrapper.rb +152 -0
- data/lib/lcp_ruby/kanban/board.rb +28 -0
- data/lib/lcp_ruby/kanban/column.rb +28 -0
- data/lib/lcp_ruby/kanban/default_provider.rb +376 -0
- data/lib/lcp_ruby/kanban/host_provider.rb +94 -0
- data/lib/lcp_ruby/kanban/move_result.rb +52 -0
- data/lib/lcp_ruby/kanban/provider_test_harness.rb +54 -0
- data/lib/lcp_ruby/kanban/swimlane.rb +21 -0
- data/lib/lcp_ruby/menu.rb +46 -0
- data/lib/lcp_ruby/metadata/aggregate_definition.rb +6 -0
- data/lib/lcp_ruby/metadata/association_definition.rb +196 -0
- data/lib/lcp_ruby/metadata/auth_validator.rb +222 -0
- data/lib/lcp_ruby/metadata/configuration_validator.rb +7958 -0
- data/lib/lcp_ruby/metadata/contract_result.rb +9 -0
- data/lib/lcp_ruby/metadata/display_template_definition.rb +77 -0
- data/lib/lcp_ruby/metadata/enum_label_resolver.rb +27 -0
- data/lib/lcp_ruby/metadata/erd_generator.rb +274 -0
- data/lib/lcp_ruby/metadata/event_definition.rb +55 -0
- data/lib/lcp_ruby/metadata/field_definition.rb +267 -0
- data/lib/lcp_ruby/metadata/group_definition.rb +31 -0
- data/lib/lcp_ruby/metadata/i18n_label.rb +29 -0
- data/lib/lcp_ruby/metadata/loader.rb +916 -0
- data/lib/lcp_ruby/metadata/menu_definition.rb +116 -0
- data/lib/lcp_ruby/metadata/menu_item.rb +792 -0
- data/lib/lcp_ruby/metadata/menu_item_resolver.rb +105 -0
- data/lib/lcp_ruby/metadata/model_definition.rb +612 -0
- data/lib/lcp_ruby/metadata/model_hash_merger.rb +88 -0
- data/lib/lcp_ruby/metadata/model_inheritance_resolver.rb +165 -0
- data/lib/lcp_ruby/metadata/page_definition.rb +245 -0
- data/lib/lcp_ruby/metadata/path_template.rb +231 -0
- data/lib/lcp_ruby/metadata/permission_definition.rb +237 -0
- data/lib/lcp_ruby/metadata/permission_merger.rb +81 -0
- data/lib/lcp_ruby/metadata/presenter_definition.rb +689 -0
- data/lib/lcp_ruby/metadata/reserved_names.rb +79 -0
- data/lib/lcp_ruby/metadata/responsive_policy.rb +95 -0
- data/lib/lcp_ruby/metadata/schema_validator.rb +172 -0
- data/lib/lcp_ruby/metadata/validation_definition.rb +69 -0
- data/lib/lcp_ruby/metadata/view_group_definition.rb +208 -0
- data/lib/lcp_ruby/metadata/virtual_column_definition.rb +154 -0
- data/lib/lcp_ruby/metadata/zone_definition.rb +423 -0
- data/lib/lcp_ruby/metrics/collector.rb +123 -0
- data/lib/lcp_ruby/metrics/collector_registry.rb +57 -0
- data/lib/lcp_ruby/metrics/error_recorder.rb +70 -0
- data/lib/lcp_ruby/metrics/fingerprint.rb +33 -0
- data/lib/lcp_ruby/metrics/json_query.rb +47 -0
- data/lib/lcp_ruby/metrics/metric_definitions.rb +105 -0
- data/lib/lcp_ruby/metrics/prometheus_check.rb +9 -0
- data/lib/lcp_ruby/metrics/rate_limiter.rb +99 -0
- data/lib/lcp_ruby/metrics/setup.rb +71 -0
- data/lib/lcp_ruby/metrics/subscriber.rb +126 -0
- data/lib/lcp_ruby/model_factory/aggregate_applicator.rb +14 -0
- data/lib/lcp_ruby/model_factory/api_association_applicator.rb +119 -0
- data/lib/lcp_ruby/model_factory/api_builder.rb +85 -0
- data/lib/lcp_ruby/model_factory/array_type.rb +78 -0
- data/lib/lcp_ruby/model_factory/array_type_applicator.rb +33 -0
- data/lib/lcp_ruby/model_factory/association_applicator.rb +201 -0
- data/lib/lcp_ruby/model_factory/attachment_applicator.rb +160 -0
- data/lib/lcp_ruby/model_factory/auditing_applicator.rb +72 -0
- data/lib/lcp_ruby/model_factory/builder.rb +235 -0
- data/lib/lcp_ruby/model_factory/callback_applicator.rb +63 -0
- data/lib/lcp_ruby/model_factory/computed_applicator.rb +55 -0
- data/lib/lcp_ruby/model_factory/default_applicator.rb +85 -0
- data/lib/lcp_ruby/model_factory/enum_applicator.rb +24 -0
- data/lib/lcp_ruby/model_factory/inherited_parent_validator_applicator.rb +39 -0
- data/lib/lcp_ruby/model_factory/label_method_builder.rb +82 -0
- data/lib/lcp_ruby/model_factory/managed_tracking.rb +71 -0
- data/lib/lcp_ruby/model_factory/positioning_applicator.rb +23 -0
- data/lib/lcp_ruby/model_factory/ransack_applicator.rb +66 -0
- data/lib/lcp_ruby/model_factory/registry.rb +33 -0
- data/lib/lcp_ruby/model_factory/schema_manager.rb +655 -0
- data/lib/lcp_ruby/model_factory/scope_applicator.rb +87 -0
- data/lib/lcp_ruby/model_factory/sequence_applicator.rb +173 -0
- data/lib/lcp_ruby/model_factory/service_accessor_applicator.rb +40 -0
- data/lib/lcp_ruby/model_factory/soft_delete_applicator.rb +141 -0
- data/lib/lcp_ruby/model_factory/transform_applicator.rb +51 -0
- data/lib/lcp_ruby/model_factory/tree_applicator.rb +239 -0
- data/lib/lcp_ruby/model_factory/userstamps_applicator.rb +73 -0
- data/lib/lcp_ruby/model_factory/validation_applicator.rb +319 -0
- data/lib/lcp_ruby/model_factory/virtual_column_applicator.rb +79 -0
- data/lib/lcp_ruby/model_factory/workflow_applicator.rb +141 -0
- data/lib/lcp_ruby/pages/change_handler.rb +15 -0
- data/lib/lcp_ruby/pages/contract_validator.rb +74 -0
- data/lib/lcp_ruby/pages/definition_validator.rb +42 -0
- data/lib/lcp_ruby/pages/filter_form.rb +200 -0
- data/lib/lcp_ruby/pages/filter_form_validator.rb +636 -0
- data/lib/lcp_ruby/pages/registry.rb +133 -0
- data/lib/lcp_ruby/pages/resolver.rb +32 -0
- data/lib/lcp_ruby/pages/scope_context_resolver.rb +37 -0
- data/lib/lcp_ruby/pages/scope_filter_set.rb +57 -0
- data/lib/lcp_ruby/pages/setup.rb +46 -0
- data/lib/lcp_ruby/path_utils.rb +12 -0
- data/lib/lcp_ruby/permissions/change_handler.rb +22 -0
- data/lib/lcp_ruby/permissions/contract_validator.rb +74 -0
- data/lib/lcp_ruby/permissions/definition_validator.rb +119 -0
- data/lib/lcp_ruby/permissions/registry.rb +135 -0
- data/lib/lcp_ruby/permissions/setup.rb +51 -0
- data/lib/lcp_ruby/permissions/source_resolver.rb +56 -0
- data/lib/lcp_ruby/presenter/action_set.rb +236 -0
- data/lib/lcp_ruby/presenter/breadcrumb_builder.rb +183 -0
- data/lib/lcp_ruby/presenter/breadcrumb_path_helper.rb +17 -0
- data/lib/lcp_ruby/presenter/column_set.rb +268 -0
- data/lib/lcp_ruby/presenter/enrichment.rb +136 -0
- data/lib/lcp_ruby/presenter/field_value_resolver.rb +237 -0
- data/lib/lcp_ruby/presenter/includes_resolver/association_dependency.rb +59 -0
- data/lib/lcp_ruby/presenter/includes_resolver/dependency_collector.rb +394 -0
- data/lib/lcp_ruby/presenter/includes_resolver/loading_strategy.rb +70 -0
- data/lib/lcp_ruby/presenter/includes_resolver/strategy_resolver.rb +123 -0
- data/lib/lcp_ruby/presenter/includes_resolver.rb +42 -0
- data/lib/lcp_ruby/presenter/layout_builder.rb +467 -0
- data/lib/lcp_ruby/presenter/link_resolver.rb +65 -0
- data/lib/lcp_ruby/presenter/metadata_lookup.rb +28 -0
- data/lib/lcp_ruby/presenter/resolver.rb +25 -0
- data/lib/lcp_ruby/record_aliases/metadata_checker.rb +213 -0
- data/lib/lcp_ruby/record_aliases/setup.rb +212 -0
- data/lib/lcp_ruby/reserved_route_segments.rb +37 -0
- data/lib/lcp_ruby/roles/change_handler.rb +11 -0
- data/lib/lcp_ruby/roles/contract_validator.rb +67 -0
- data/lib/lcp_ruby/roles/registry.rb +89 -0
- data/lib/lcp_ruby/roles/setup.rb +50 -0
- data/lib/lcp_ruby/routing/presenter_routes.rb +104 -0
- data/lib/lcp_ruby/saved_filters/change_handler.rb +13 -0
- data/lib/lcp_ruby/saved_filters/contract_validator.rb +85 -0
- data/lib/lcp_ruby/saved_filters/registry.rb +36 -0
- data/lib/lcp_ruby/saved_filters/resolver.rb +108 -0
- data/lib/lcp_ruby/saved_filters/setup.rb +42 -0
- data/lib/lcp_ruby/saved_filters/stale_field_validator.rb +84 -0
- data/lib/lcp_ruby/schemas/auth.json +208 -0
- data/lib/lcp_ruby/schemas/menu.json +338 -0
- data/lib/lcp_ruby/schemas/model.json +1161 -0
- data/lib/lcp_ruby/schemas/page.json +877 -0
- data/lib/lcp_ruby/schemas/permission.json +454 -0
- data/lib/lcp_ruby/schemas/presenter.json +2274 -0
- data/lib/lcp_ruby/schemas/theme.json +62 -0
- data/lib/lcp_ruby/schemas/type.json +146 -0
- data/lib/lcp_ruby/schemas/view_group.json +163 -0
- data/lib/lcp_ruby/search/custom_field_filter.rb +171 -0
- data/lib/lcp_ruby/search/custom_filter_interceptor.rb +40 -0
- data/lib/lcp_ruby/search/filter_metadata_builder.rb +409 -0
- data/lib/lcp_ruby/search/filter_param_builder.rb +177 -0
- data/lib/lcp_ruby/search/operator_registry.rb +79 -0
- data/lib/lcp_ruby/search/param_sanitizer.rb +25 -0
- data/lib/lcp_ruby/search/parameter_definition.rb +187 -0
- data/lib/lcp_ruby/search/parameterized_scope_applicator.rb +129 -0
- data/lib/lcp_ruby/search/query_builder.rb +143 -0
- data/lib/lcp_ruby/search/query_language_parser.rb +549 -0
- data/lib/lcp_ruby/search/query_language_serializer.rb +193 -0
- data/lib/lcp_ruby/search/quick_search.rb +162 -0
- data/lib/lcp_ruby/search/relative_date_expander.rb +57 -0
- data/lib/lcp_ruby/search_result.rb +70 -0
- data/lib/lcp_ruby/sequences/sequence_manager.rb +51 -0
- data/lib/lcp_ruby/services/accessors/json_field.rb +23 -0
- data/lib/lcp_ruby/services/built_in_accessors.rb +17 -0
- data/lib/lcp_ruby/services/built_in_defaults.rb +22 -0
- data/lib/lcp_ruby/services/built_in_transforms.rb +20 -0
- data/lib/lcp_ruby/services/checker.rb +133 -0
- data/lib/lcp_ruby/services/registry.rb +83 -0
- data/lib/lcp_ruby/skills_installer.rb +73 -0
- data/lib/lcp_ruby/sort/enum_sort_order.rb +38 -0
- data/lib/lcp_ruby/tasks/destroy_order_resolver.rb +57 -0
- data/lib/lcp_ruby/tasks/doctor.rb +294 -0
- data/lib/lcp_ruby/tasks/permission_resolve_formatter.rb +245 -0
- data/lib/lcp_ruby/types/built_in_types.rb +157 -0
- data/lib/lcp_ruby/types/transforms/base_transform.rb +11 -0
- data/lib/lcp_ruby/types/transforms/downcase.rb +11 -0
- data/lib/lcp_ruby/types/transforms/normalize_phone.rb +19 -0
- data/lib/lcp_ruby/types/transforms/normalize_url.rb +16 -0
- data/lib/lcp_ruby/types/transforms/strip.rb +11 -0
- data/lib/lcp_ruby/types/type_definition.rb +112 -0
- data/lib/lcp_ruby/types/type_registry.rb +75 -0
- data/lib/lcp_ruby/url_safety.rb +97 -0
- data/lib/lcp_ruby/user_snapshot.rb +15 -0
- data/lib/lcp_ruby/version.rb +3 -0
- data/lib/lcp_ruby/view_slots/registry.rb +71 -0
- data/lib/lcp_ruby/view_slots/slot_component.rb +22 -0
- data/lib/lcp_ruby/view_slots/slot_context.rb +20 -0
- data/lib/lcp_ruby/virtual_columns/builder.rb +234 -0
- data/lib/lcp_ruby/virtual_columns/collector.rb +186 -0
- data/lib/lcp_ruby/virtual_columns.rb +4 -0
- data/lib/lcp_ruby/virtual_fields/synthetic_marker.rb +17 -0
- data/lib/lcp_ruby/virtual_fields/types/array_of.rb +49 -0
- data/lib/lcp_ruby/virtual_fields/virtual_field.rb +107 -0
- data/lib/lcp_ruby/virtual_fields/virtual_form.rb +144 -0
- data/lib/lcp_ruby/widgets/chart_palette.rb +25 -0
- data/lib/lcp_ruby/widgets/chartkick_check.rb +9 -0
- data/lib/lcp_ruby/widgets/data_resolver.rb +676 -0
- data/lib/lcp_ruby/widgets/date_grouper.rb +54 -0
- data/lib/lcp_ruby/widgets/presenter_zone_resolver.rb +170 -0
- data/lib/lcp_ruby/widgets/record_source_resolver.rb +56 -0
- data/lib/lcp_ruby/widgets/scope_applicator.rb +187 -0
- data/lib/lcp_ruby/workflow/approval/activation_handler.rb +39 -0
- data/lib/lcp_ruby/workflow/approval/approval_definition.rb +117 -0
- data/lib/lcp_ruby/workflow/approval/approver_resolver.rb +98 -0
- data/lib/lcp_ruby/workflow/approval/cleanup_handler.rb +37 -0
- data/lib/lcp_ruby/workflow/approval/contract_validator.rb +96 -0
- data/lib/lcp_ruby/workflow/approval/data_builder.rb +53 -0
- data/lib/lcp_ruby/workflow/approval/discard_handler.rb +51 -0
- data/lib/lcp_ruby/workflow/approval/engine.rb +314 -0
- data/lib/lcp_ruby/workflow/approval/registry.rb +40 -0
- data/lib/lcp_ruby/workflow/approval/resolution_handler.rb +103 -0
- data/lib/lcp_ruby/workflow/approval/setup.rb +138 -0
- data/lib/lcp_ruby/workflow/approval/step_definition.rb +52 -0
- data/lib/lcp_ruby/workflow/approval/step_evaluator.rb +163 -0
- data/lib/lcp_ruby/workflow/approval/system_evaluator.rb +29 -0
- data/lib/lcp_ruby/workflow/approval/task_manager.rb +202 -0
- data/lib/lcp_ruby/workflow/audit_contract_validator.rb +64 -0
- data/lib/lcp_ruby/workflow/audit_registry.rb +24 -0
- data/lib/lcp_ruby/workflow/audit_writer.rb +51 -0
- data/lib/lcp_ruby/workflow/change_handler.rb +14 -0
- data/lib/lcp_ruby/workflow/contract.rb +21 -0
- data/lib/lcp_ruby/workflow/contract_validator.rb +44 -0
- data/lib/lcp_ruby/workflow/errors.rb +12 -0
- data/lib/lcp_ruby/workflow/host_source.rb +19 -0
- data/lib/lcp_ruby/workflow/mermaid_builder.rb +217 -0
- data/lib/lcp_ruby/workflow/model_source.rb +79 -0
- data/lib/lcp_ruby/workflow/registry.rb +113 -0
- data/lib/lcp_ruby/workflow/resolver.rb +32 -0
- data/lib/lcp_ruby/workflow/setup.rb +135 -0
- data/lib/lcp_ruby/workflow/state_definition.rb +59 -0
- data/lib/lcp_ruby/workflow/state_machine.rb +78 -0
- data/lib/lcp_ruby/workflow/static_source.rb +20 -0
- data/lib/lcp_ruby/workflow/transition_action_builder.rb +46 -0
- data/lib/lcp_ruby/workflow/transition_definition.rb +70 -0
- data/lib/lcp_ruby/workflow/transition_executor.rb +140 -0
- data/lib/lcp_ruby/workflow/transition_label_resolver.rb +21 -0
- data/lib/lcp_ruby/workflow/transition_result.rb +20 -0
- data/lib/lcp_ruby/workflow/value_resolver.rb +58 -0
- data/lib/lcp_ruby/workflow/workflow_definition.rb +195 -0
- data/lib/lcp_ruby.rb +764 -0
- data/lib/rubocop/cop/lcp_ruby/no_hardcoded_i18n_string.rb +249 -0
- data/lib/tasks/lcp_ruby.rake +432 -0
- data/lib/tasks/lcp_ruby_assets.rake +37 -0
- data/lib/tasks/lcp_ruby_auth.rake +49 -0
- data/lib/tasks/lcp_ruby_db.rake +76 -0
- data/lib/tasks/lcp_ruby_doctor.rake +20 -0
- data/lib/tasks/lcp_ruby_feature_catalog.rake +61 -0
- data/lib/tasks/lcp_ruby_gapfree_sequences.rake +39 -0
- data/lib/tasks/lcp_ruby_i18n_check.rake +23 -0
- data/lib/tasks/lcp_ruby_i18n_lint.rake +20 -0
- data/lib/tasks/lcp_ruby_invariant_check.rake +72 -0
- data/vendor/assets/javascripts/lcp_ruby/activestorage.min.js +866 -0
- data/vendor/assets/javascripts/lcp_ruby/highlight.min.js +1244 -0
- data/vendor/assets/javascripts/lcp_ruby/lucide.min.js +12 -0
- data/vendor/assets/javascripts/lcp_ruby/stimulus.umd.js +2588 -0
- data/vendor/assets/javascripts/lcp_ruby/tom-select.complete.min.js +444 -0
- data/vendor/assets/stylesheets/lcp_ruby/highlight-github.min.css +12 -0
- data/vendor/assets/stylesheets/lcp_ruby/tom-select.css +412 -0
- 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`
|