loading
Generated 2026-07-23T15:10:59+00:00

All Files ( 31.04% covered at 0.82 hits/line )

659 files in total.
14511 relevant lines, 4504 lines covered and 10007 lines missed. ( 31.04% )
4728 total branches, 3 branches covered and 4725 branches missed. ( 0.06% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/components/loopos_ui/accordion.rb 60.71 % 54 28 17 11 0.61 0.00 % 8 0 8
app/components/loopos_ui/accordion/accordion.html.erb 0.00 % 32 13 0 13 0.00 0.00 % 4 0 4
app/components/loopos_ui/action_bar.rb 35.29 % 102 51 18 33 0.35 0.00 % 24 0 24
app/components/loopos_ui/action_bar/action_bar.html.erb 0.00 % 9 6 0 6 0.00 0.00 % 2 0 2
app/components/loopos_ui/action_bar/item_navigation.html.erb 0.00 % 9 6 0 6 0.00 100.00 % 0 0 0
app/components/loopos_ui/action_buttons.rb 100.00 % 11 5 5 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/action_buttons/action_buttons.html.erb 0.00 % 5 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/action_buttons/button_group.html.erb 0.00 % 5 3 0 3 0.00 100.00 % 0 0 0
app/components/loopos_ui/action_menu.rb 62.50 % 30 16 10 6 0.63 0.00 % 2 0 2
app/components/loopos_ui/action_menu/action_menu.html.erb 0.00 % 44 25 0 25 0.00 0.00 % 20 0 20
app/components/loopos_ui/app_instance/card.rb 42.86 % 61 21 9 12 0.43 0.00 % 11 0 11
app/components/loopos_ui/app_instance/card/card.html.erb 0.00 % 48 23 0 23 0.00 0.00 % 17 0 17
app/components/loopos_ui/app_layout_component.rb 59.09 % 89 44 26 18 0.59 0.00 % 2 0 2
app/components/loopos_ui/app_layout_component/app_layout_component.html.erb 0.00 % 48 22 0 22 0.00 0.00 % 10 0 10
app/components/loopos_ui/app_layout_component/root_styles.html.erb 0.00 % 15 10 0 10 0.00 100.00 % 0 0 0
app/components/loopos_ui/app_logo.rb 60.00 % 26 15 9 6 0.60 0.00 % 4 0 4
app/components/loopos_ui/app_logo/app_logo.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/apps/list.rb 100.00 % 9 5 5 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/apps/list/list.html.erb 0.00 % 14 7 0 7 0.00 0.00 % 2 0 2
app/components/loopos_ui/assign/assign_component.html.erb 0.00 % 8 3 0 3 0.00 100.00 % 0 0 0
app/components/loopos_ui/assign/assign_component.rb 100.00 % 13 7 7 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/assign/assign_section.html.erb 0.00 % 112 57 0 57 0.00 0.00 % 44 0 44
app/components/loopos_ui/assign/assign_section.rb 33.33 % 55 27 9 18 0.33 100.00 % 0 0 0
app/components/loopos_ui/assign/elements.html.erb 0.00 % 29 16 0 16 0.00 0.00 % 26 0 26
app/components/loopos_ui/assign/elements.rb 35.00 % 40 20 7 13 0.35 100.00 % 0 0 0
app/components/loopos_ui/assign/empty.html.erb 0.00 % 7 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/assign/empty.rb 83.33 % 13 6 5 1 0.83 100.00 % 0 0 0
app/components/loopos_ui/assign/update.rb 25.93 % 55 27 7 20 0.26 100.00 % 0 0 0
app/components/loopos_ui/assign/update.turbo_stream.erb 0.00 % 25 3 0 3 0.00 0.00 % 2 0 2
app/components/loopos_ui/association_overlay.rb 87.88 % 58 33 29 4 0.88 0.00 % 2 0 2
app/components/loopos_ui/association_overlay/association_overlay.html.erb 0.00 % 16 9 0 9 0.00 0.00 % 8 0 8
app/components/loopos_ui/association_overlay/header.html.erb 0.00 % 4 3 0 3 0.00 0.00 % 4 0 4
app/components/loopos_ui/association_overlay/results_container.html.erb 0.00 % 12 7 0 7 0.00 0.00 % 6 0 6
app/components/loopos_ui/association_overlay/selected_container.html.erb 0.00 % 7 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/async_select/async_select_component.html.erb 0.00 % 29 9 0 9 0.00 0.00 % 2 0 2
app/components/loopos_ui/async_select/async_select_component.rb 55.00 % 46 20 11 9 0.55 100.00 % 0 0 0
app/components/loopos_ui/async_select/async_select_option_component.html.erb 0.00 % 22 12 0 12 0.00 0.00 % 10 0 10
app/components/loopos_ui/async_select/async_select_option_component.rb 52.00 % 45 25 13 12 0.52 0.00 % 14 0 14
app/components/loopos_ui/async_select/filter_component.rb 70.00 % 18 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/async_select/filter_component.turbo_stream.erb 0.00 % 9 5 0 5 0.00 0.00 % 2 0 2
app/components/loopos_ui/async_select/update_list_component.rb 77.78 % 17 9 7 2 0.78 100.00 % 0 0 0
app/components/loopos_ui/async_select/update_list_component.turbo_stream.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/avatar.rb 71.43 % 43 21 15 6 0.71 0.00 % 4 0 4
app/components/loopos_ui/avatar/avatar.html.erb 0.00 % 4 3 0 3 0.00 100.00 % 0 0 0
app/components/loopos_ui/basic_radio_button.rb 92.31 % 19 13 12 1 0.92 100.00 % 0 0 0
app/components/loopos_ui/basic_radio_button/basic_radio_button.html.erb 0.00 % 11 7 0 7 0.00 0.00 % 2 0 2
app/components/loopos_ui/bottom_bar.rb 100.00 % 6 4 4 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/bottom_bar/bottom_bar.html.erb 0.00 % 10 6 0 6 0.00 100.00 % 0 0 0
app/components/loopos_ui/breadcrumb_list.rb 53.33 % 34 15 8 7 0.53 0.00 % 2 0 2
app/components/loopos_ui/breadcrumb_list/breadcrumb_list.html.erb 0.00 % 10 8 0 8 0.00 0.00 % 2 0 2
app/components/loopos_ui/button.rb 32.26 % 296 124 40 84 0.32 0.00 % 75 0 75
app/components/loopos_ui/button/button.html.erb 0.00 % 37 27 0 27 0.00 0.00 % 22 0 22
app/components/loopos_ui/card/card_component.html.erb 0.00 % 25 14 0 14 0.00 0.00 % 6 0 6
app/components/loopos_ui/card/card_component.rb 90.91 % 17 11 10 1 0.91 100.00 % 0 0 0
app/components/loopos_ui/carousel.rb 77.78 % 39 18 14 4 0.78 100.00 % 0 0 0
app/components/loopos_ui/carousel/carousel.html.erb 0.00 % 46 14 0 14 0.00 100.00 % 0 0 0
app/components/loopos_ui/charts/chart_component.html.erb 0.00 % 57 21 0 21 0.00 0.00 % 10 0 10
app/components/loopos_ui/charts/chart_component.rb 47.37 % 70 19 9 10 0.47 0.00 % 2 0 2
app/components/loopos_ui/chat.rb 100.00 % 14 12 12 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/chat/chat.html.erb 0.00 % 14 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/chip.rb 100.00 % 7 4 4 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/chip/chip.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/chip_list.rb 83.33 % 14 6 5 1 0.83 100.00 % 0 0 0
app/components/loopos_ui/chip_list/chip_list.html.erb 0.00 % 5 3 0 3 0.00 100.00 % 0 0 0
app/components/loopos_ui/code_editor.rb 52.27 % 83 44 23 21 0.52 0.00 % 15 0 15
app/components/loopos_ui/code_editor/code_editor.html.erb 0.00 % 3 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/context_menu.rb 53.33 % 60 30 16 14 0.53 0.00 % 12 0 12
app/components/loopos_ui/context_menu/context_menu.html.erb 0.00 % 20 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/context_menu/item.html.erb 0.00 % 55 19 0 19 0.00 0.00 % 2 0 2
app/components/loopos_ui/core/product_show_header.rb 100.00 % 6 3 3 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/counter.rb 31.25 % 44 16 5 11 0.31 0.00 % 8 0 8
app/components/loopos_ui/counter/counter.html.erb 0.00 % 11 7 0 7 0.00 0.00 % 6 0 6
app/components/loopos_ui/counter_label.rb 50.00 % 43 12 6 6 0.50 0.00 % 6 0 6
app/components/loopos_ui/dashboard/dashboard_component.html.erb 0.00 % 48 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/dashboard/dashboard_component.rb 88.89 % 17 9 8 1 0.89 100.00 % 0 0 0
app/components/loopos_ui/data_card.rb 100.00 % 11 9 9 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/data_card/data_card.html.erb 0.00 % 10 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker.rb 37.50 % 248 104 39 65 0.38 0.00 % 63 0 63
app/components/loopos_ui/date_picker/date_picker.html.erb 0.00 % 53 15 0 15 0.00 0.00 % 9 0 9
app/components/loopos_ui/date_picker/field.rb 85.71 % 22 14 12 2 0.86 100.00 % 0 0 0
app/components/loopos_ui/date_picker/field/field.html.erb 0.00 % 15 6 0 6 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/month.rb 100.00 % 13 9 9 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/month/month.html.erb 0.00 % 9 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/presets.rb 90.91 % 17 11 10 1 0.91 100.00 % 0 0 0
app/components/loopos_ui/date_picker/presets/presets.html.erb 0.00 % 25 6 0 6 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/presets_inline_calendar.rb 100.00 % 8 4 4 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/presets_inline_calendar/presets_inline_calendar.html.erb 0.00 % 5 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/presets_menu.rb 85.71 % 13 7 6 1 0.86 100.00 % 0 0 0
app/components/loopos_ui/date_picker/presets_menu/presets_menu.html.erb 0.00 % 90 12 0 12 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/range.rb 100.00 % 12 8 8 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/range/range.html.erb 0.00 % 9 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/single.rb 100.00 % 13 9 9 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/single/single.html.erb 0.00 % 9 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/year.rb 100.00 % 13 9 9 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/year/year.html.erb 0.00 % 9 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_show.rb 55.56 % 19 9 5 4 0.56 0.00 % 4 0 4
app/components/loopos_ui/date_show/date_show.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/dots.rb 81.82 % 18 11 9 2 0.82 100.00 % 0 0 0
app/components/loopos_ui/dots/dots.html.erb 0.00 % 5 3 0 3 0.00 0.00 % 2 0 2
app/components/loopos_ui/double_state_label.html.erb 0.00 % 14 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/double_state_label.rb 86.67 % 22 15 13 2 0.87 100.00 % 0 0 0
app/components/loopos_ui/drawer_bar.rb 56.86 % 89 51 29 22 0.57 0.00 % 6 0 6
app/components/loopos_ui/drawer_bar/drawer_bar.html.erb 0.00 % 27 9 0 9 0.00 100.00 % 0 0 0
app/components/loopos_ui/dummy_slot.rb 75.00 % 30 12 9 3 0.75 0.00 % 12 0 12
app/components/loopos_ui/dummy_slot/dummy_slot.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/email_preview.rb 100.00 % 5 3 3 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/email_preview/email_preview.html.erb 0.00 % 8 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/entities/app_instance.rb 57.14 % 25 14 8 6 0.57 100.00 % 0 0 0
app/components/loopos_ui/entities/brand.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/catalog_node.rb 66.67 % 14 9 6 3 0.67 100.00 % 0 0 0
app/components/loopos_ui/entities/catalog_node/catalog_node.html.erb 0.00 % 8 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/entities/catalog_section_inherited_protocol.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/catalog_section_protocol.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/category.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/email.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/flow.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/identifier.rb 36.36 % 36 22 8 14 0.36 0.00 % 2 0 2
app/components/loopos_ui/entities/incoming_payment.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/inherited_protocol.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/invoice.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/item.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/partnable.rb 53.85 % 24 13 7 6 0.54 0.00 % 5 0 5
app/components/loopos_ui/entities/partner_group.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/payment.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/pricing_rule.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/pricing_rule_inheritance.rb 72.73 % 16 11 8 3 0.73 100.00 % 0 0 0
app/components/loopos_ui/entities/product.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/protocol.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/provider.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/script.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/script_kind.rb 66.67 % 14 9 6 3 0.67 100.00 % 0 0 0
app/components/loopos_ui/entities/shipping.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/sms.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/template.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/user_group.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entity.rb 59.52 % 99 42 25 17 4.21 0.00 % 14 0 14
app/components/loopos_ui/entity_token.rb 29.73 % 80 37 11 26 0.30 0.00 % 20 0 20
app/components/loopos_ui/error_page.rb 83.33 % 31 6 5 1 0.83 100.00 % 0 0 0
app/components/loopos_ui/error_page/error_page.html.erb 0.00 % 27 10 0 10 0.00 0.00 % 2 0 2
app/components/loopos_ui/extra_data_viewer.rb 76.00 % 39 25 19 6 0.76 0.00 % 5 0 5
app/components/loopos_ui/extra_data_viewer/extra_data_viewer.html.erb 0.00 % 64 20 0 20 0.00 0.00 % 10 0 10
app/components/loopos_ui/features/list.rb 100.00 % 19 6 6 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/features/list/list.html.erb 0.00 % 17 8 0 8 0.00 0.00 % 2 0 2
app/components/loopos_ui/filter/filters_dropdown_component.html.erb 0.00 % 48 19 0 19 0.00 0.00 % 4 0 4
app/components/loopos_ui/filter/filters_dropdown_component.rb 46.15 % 25 13 6 7 0.46 100.00 % 0 0 0
app/components/loopos_ui/filter/search_component.html.erb 0.00 % 13 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/filter/search_component.rb 66.67 % 15 9 6 3 0.67 100.00 % 0 0 0
app/components/loopos_ui/filter_bar.rb 45.45 % 95 55 25 30 0.45 0.00 % 4 0 4
app/components/loopos_ui/filter_bar/filter_bar.html.erb 0.00 % 71 30 0 30 0.00 0.00 % 18 0 18
app/components/loopos_ui/filter_button.rb 46.51 % 192 86 40 46 0.47 0.00 % 26 0 26
app/components/loopos_ui/filter_button/filter_button.html.erb 0.00 % 34 18 0 18 0.00 0.00 % 14 0 14
app/components/loopos_ui/filter_pill.rb 81.82 % 49 22 18 4 0.82 100.00 % 0 0 0
app/components/loopos_ui/filter_pill/filter_pill.html.erb 0.00 % 18 8 0 8 0.00 0.00 % 4 0 4
app/components/loopos_ui/flex_layout.rb 83.33 % 25 12 10 2 0.83 0.00 % 2 0 2
app/components/loopos_ui/flex_layout/flex_layout.html.erb 0.00 % 5 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/flex_layout/media_query_sizes_concern.rb 70.00 % 41 20 14 6 0.70 0.00 % 10 0 10
app/components/loopos_ui/flex_layout/section.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/float_bar.rb 36.36 % 132 55 20 35 1.15 20.00 % 10 2 8
app/components/loopos_ui/float_bar/float_bar.html.erb 0.00 % 34 13 0 13 0.00 100.00 % 0 0 0
app/components/loopos_ui/flows/drawer_bar.rb 57.89 % 106 38 22 16 0.58 0.00 % 4 0 4
app/components/loopos_ui/flows/drawer_bar/drawer_bar.html.erb 0.00 % 70 29 0 29 0.00 0.00 % 10 0 10
app/components/loopos_ui/form_entry.rb 73.08 % 46 26 19 7 0.73 0.00 % 2 0 2
app/components/loopos_ui/form_entry/form_entry.html.erb 0.00 % 35 20 0 20 0.00 0.00 % 14 0 14
app/components/loopos_ui/form_entry_ai.rb 100.00 % 11 9 9 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/form_entry_ai/form_entry_ai.html.erb 0.00 % 37 11 0 11 0.00 0.00 % 2 0 2
app/components/loopos_ui/grid_layout.rb 77.78 % 28 9 7 2 0.78 100.00 % 0 0 0
app/components/loopos_ui/grid_layout/grid_layout.html.erb 0.00 % 5 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/grid_layout/section.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/group_avatar.rb 100.00 % 6 4 4 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/group_avatar/group_avatar.html.erb 0.00 % 6 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/header.rb 83.33 % 42 30 25 5 0.83 0.00 % 4 0 4
app/components/loopos_ui/header/header.html.erb 0.00 % 36 30 0 30 0.00 0.00 % 22 0 22
app/components/loopos_ui/header_component.rb 39.58 % 106 48 19 29 0.40 0.00 % 6 0 6
app/components/loopos_ui/header_component/header_component.html.erb 0.00 % 138 62 0 62 0.00 0.00 % 56 0 56
app/components/loopos_ui/header_component/partnable_applications_component.html.erb 0.00 % 5 2 0 2 0.00 0.00 % 2 0 2
app/components/loopos_ui/header_component/user_menu_component.html.erb 0.00 % 96 38 0 38 0.00 0.00 % 26 0 26
app/components/loopos_ui/history_card.rb 100.00 % 5 3 3 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/history_card/history_card.html.erb 0.00 % 4 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/human_view.rb 55.56 % 51 27 15 12 0.56 0.00 % 11 0 11
app/components/loopos_ui/human_view/human_view.html.erb 0.00 % 27 12 0 12 0.00 0.00 % 6 0 6
app/components/loopos_ui/human_view/tree_node.rb 47.83 % 43 23 11 12 0.48 0.00 % 9 0 9
app/components/loopos_ui/human_view/tree_node/tree_node.html.erb 0.00 % 37 19 0 19 0.00 0.00 % 2 0 2
app/components/loopos_ui/icon.rb 75.00 % 25 12 9 3 0.75 100.00 % 0 0 0
app/components/loopos_ui/icon/icon.html.erb 0.00 % 4 3 0 3 0.00 0.00 % 4 0 4
app/components/loopos_ui/icon_tooltip.rb 40.00 % 14 10 4 6 0.40 0.00 % 2 0 2
app/components/loopos_ui/icon_tooltip/icon_tooltip.html.erb 0.00 % 19 12 0 12 0.00 0.00 % 8 0 8
app/components/loopos_ui/identity_token.rb 43.59 % 109 39 17 22 0.44 0.00 % 14 0 14
app/components/loopos_ui/image.rb 46.15 % 31 13 6 7 0.46 0.00 % 2 0 2
app/components/loopos_ui/image/image.html.erb 0.00 % 26 16 0 16 0.00 0.00 % 8 0 8
app/components/loopos_ui/index_header.rb 75.00 % 26 12 9 3 0.75 0.00 % 4 0 4
app/components/loopos_ui/index_header/index_header.html.erb 0.00 % 11 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/index_layout.rb 66.67 % 24 12 8 4 0.67 0.00 % 4 0 4
app/components/loopos_ui/index_layout/index_layout.html.erb 0.00 % 15 9 0 9 0.00 0.00 % 2 0 2
app/components/loopos_ui/inline_edit_component.rb 39.02 % 109 41 16 25 0.39 0.00 % 6 0 6
app/components/loopos_ui/inline_edit_component/base.html.erb 0.00 % 23 13 0 13 0.00 0.00 % 10 0 10
app/components/loopos_ui/inline_text_edit.rb 84.62 % 23 13 11 2 0.85 100.00 % 0 0 0
app/components/loopos_ui/inline_text_edit/inline_text_edit.html.erb 0.00 % 62 15 0 15 0.00 0.00 % 2 0 2
app/components/loopos_ui/input.rb 54.55 % 51 22 12 10 0.55 0.00 % 8 0 8
app/components/loopos_ui/input/input.html.erb 0.00 % 77 27 0 27 0.00 0.00 % 14 0 14
app/components/loopos_ui/inputs/ai.rb 80.00 % 15 10 8 2 0.80 0.00 % 2 0 2
app/components/loopos_ui/inputs/ai/ai.html.erb 0.00 % 41 14 0 14 0.00 0.00 % 12 0 12
app/components/loopos_ui/inputs/base_input_concern.rb 77.78 % 92 36 28 8 6.28 0.00 % 8 0 8
app/components/loopos_ui/inputs/checkbox.rb 75.00 % 35 24 18 6 0.75 0.00 % 8 0 8
app/components/loopos_ui/inputs/checkbox/checkbox.html.erb 0.00 % 41 18 0 18 0.00 0.00 % 18 0 18
app/components/loopos_ui/inputs/combobox.rb 45.09 % 300 173 78 95 0.45 0.00 % 81 0 81
app/components/loopos_ui/inputs/combobox/combobox.html.erb 0.00 % 82 27 0 27 0.00 0.00 % 14 0 14
app/components/loopos_ui/inputs/date.rb 86.67 % 42 15 13 2 0.87 0.00 % 2 0 2
app/components/loopos_ui/inputs/date/date.html.erb 0.00 % 24 11 0 11 0.00 0.00 % 2 0 2
app/components/loopos_ui/inputs/file.rb 42.86 % 117 70 30 40 0.81 0.00 % 36 0 36
app/components/loopos_ui/inputs/file/direct_upload_concern.rb 30.93 % 209 97 30 67 0.43 0.00 % 54 0 54
app/components/loopos_ui/inputs/file/file_actions.html.erb 0.00 % 32 8 0 8 0.00 100.00 % 0 0 0
app/components/loopos_ui/inputs/file/file_actions.rb 100.00 % 11 7 7 0 1.00 0.00 % 2 0 2
app/components/loopos_ui/inputs/file/form_file.html.erb 0.00 % 99 32 0 32 0.00 0.00 % 10 0 10
app/components/loopos_ui/inputs/file/form_file.rb 38.89 % 93 36 14 22 0.39 0.00 % 22 0 22
app/components/loopos_ui/inputs/file/multiple.html.erb 0.00 % 142 53 0 53 0.00 0.00 % 26 0 26
app/components/loopos_ui/inputs/file/multiple.rb 37.50 % 134 56 21 35 0.38 0.00 % 34 0 34
app/components/loopos_ui/inputs/file/option.html.erb 0.00 % 7 3 0 3 0.00 100.00 % 0 0 0
app/components/loopos_ui/inputs/file/option.rb 100.00 % 11 6 6 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/inputs/file/single.html.erb 0.00 % 77 30 0 30 0.00 0.00 % 24 0 24
app/components/loopos_ui/inputs/file/single.rb 46.88 % 72 32 15 17 0.47 0.00 % 16 0 16
app/components/loopos_ui/inputs/money.rb 66.67 % 41 18 12 6 0.67 0.00 % 5 0 5
app/components/loopos_ui/inputs/money/money.html.erb 0.00 % 13 7 0 7 0.00 100.00 % 0 0 0
app/components/loopos_ui/inputs/number.rb 76.92 % 32 13 10 3 0.77 0.00 % 2 0 2
app/components/loopos_ui/inputs/number/number.html.erb 0.00 % 14 7 0 7 0.00 0.00 % 2 0 2
app/components/loopos_ui/inputs/phone_number.rb 61.76 % 89 34 21 13 0.62 0.00 % 14 0 14
app/components/loopos_ui/inputs/phone_number/phone_number.html.erb 0.00 % 7 5 0 5 0.00 0.00 % 4 0 4
app/components/loopos_ui/inputs/rich_text.rb 85.71 % 18 7 6 1 0.86 100.00 % 0 0 0
app/components/loopos_ui/inputs/rich_text/rich_text.html.erb 0.00 % 18 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/inputs/search.rb 84.62 % 31 13 11 2 0.85 100.00 % 0 0 0
app/components/loopos_ui/inputs/search/search.html.erb 0.00 % 24 12 0 12 0.00 0.00 % 2 0 2
app/components/loopos_ui/inputs/select.rb 64.71 % 69 34 22 12 0.65 0.00 % 13 0 13
app/components/loopos_ui/inputs/select/select.html.erb 0.00 % 92 33 0 33 0.00 0.00 % 28 0 28
app/components/loopos_ui/inputs/select2.rb 48.96 % 195 96 47 49 0.49 0.00 % 45 0 45
app/components/loopos_ui/inputs/select2/select2.html.erb 0.00 % 188 57 0 57 0.00 0.00 % 42 0 42
app/components/loopos_ui/inputs/text.rb 72.73 % 25 11 8 3 0.73 0.00 % 2 0 2
app/components/loopos_ui/inputs/text/text.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/inputs/text_area.rb 72.22 % 38 18 13 5 0.72 0.00 % 4 0 4
app/components/loopos_ui/inputs/text_area/text_area.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/item/state_label.rb 57.14 % 74 35 20 15 0.57 0.00 % 8 0 8
app/components/loopos_ui/item/state_label/state_label.html.erb 0.00 % 24 7 0 7 0.00 0.00 % 2 0 2
app/components/loopos_ui/item_value.rb 86.67 % 23 15 13 2 0.87 100.00 % 0 0 0
app/components/loopos_ui/item_value/item_value.html.erb 0.00 % 10 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/label.rb 42.11 % 36 19 8 11 0.42 0.00 % 6 0 6
app/components/loopos_ui/label/label.html.erb 0.00 % 14 8 0 8 0.00 0.00 % 11 0 11
app/components/loopos_ui/layout_component.html.erb 0.00 % 19 9 0 9 0.00 0.00 % 6 0 6
app/components/loopos_ui/layout_component.rb 36.36 % 18 11 4 7 0.36 100.00 % 0 0 0
app/components/loopos_ui/layout_loading.rb 100.00 % 5 2 2 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/layout_loading/layout_loading.html.erb 0.00 % 7 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/link.rb 90.00 % 15 10 9 1 0.90 100.00 % 0 0 0
app/components/loopos_ui/link/link.html.erb 0.00 % 12 8 0 8 0.00 0.00 % 4 0 4
app/components/loopos_ui/loading_ai.rb 100.00 % 5 2 2 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/loading_ai/loading_ai.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/loadings/skeleton.rb 80.00 % 17 10 8 2 0.80 100.00 % 0 0 0
app/components/loopos_ui/loadings/skeleton/skeleton.html.erb 0.00 % 12 5 0 5 0.00 0.00 % 2 0 2
app/components/loopos_ui/log.rb 29.20 % 272 113 33 80 0.30 0.00 % 40 0 40
app/components/loopos_ui/log/factories.rb 63.16 % 39 19 12 7 0.74 0.00 % 6 0 6
app/components/loopos_ui/log/factories/base.rb 44.44 % 34 18 8 10 0.44 0.00 % 2 0 2
app/components/loopos_ui/log/factories/core_item_log_presenter_hash.rb 23.64 % 128 55 13 42 0.24 0.00 % 22 0 22
app/components/loopos_ui/log/factories/script_log.rb 34.69 % 126 49 17 32 0.35 0.00 % 6 0 6
app/components/loopos_ui/log/log.html.erb 0.00 % 50 29 0 29 0.00 0.00 % 6 0 6
app/components/loopos_ui/log/script/info_modal_body.html.erb 0.00 % 63 39 0 39 0.00 0.00 % 44 0 44
app/components/loopos_ui/log/script/info_modal_body.rb 100.00 % 9 5 5 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/log_list.rb 35.80 % 183 81 29 52 0.40 3.33 % 30 1 29
app/components/loopos_ui/log_list/factories.rb 66.67 % 37 18 12 6 0.78 0.00 % 6 0 6
app/components/loopos_ui/log_list/factories/base.rb 50.00 % 40 22 11 11 0.50 0.00 % 2 0 2
app/components/loopos_ui/log_list/factories/core_item_log_list_presenter_hash.rb 26.92 % 53 26 7 19 0.27 0.00 % 6 0 6
app/components/loopos_ui/log_list/factories/script.rb 42.86 % 35 14 6 8 0.43 0.00 % 4 0 4
app/components/loopos_ui/log_list/group.html.erb 0.00 % 7 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/log_list/group.rb 87.50 % 14 8 7 1 0.88 100.00 % 0 0 0
app/components/loopos_ui/log_list/log_list.html.erb 0.00 % 20 11 0 11 0.00 0.00 % 4 0 4
app/components/loopos_ui/logo.rb 42.86 % 10 7 3 4 0.43 100.00 % 0 0 0
app/components/loopos_ui/logo/logo.html.erb 0.00 % 8 4 0 4 0.00 0.00 % 2 0 2
app/components/loopos_ui/logs_component/log_content_component.html.erb 0.00 % 26 15 0 15 0.00 0.00 % 10 0 10
app/components/loopos_ui/logs_component/log_content_component.rb 75.00 % 18 12 9 3 0.75 0.00 % 2 0 2
app/components/loopos_ui/logs_component/log_group_component.html.erb 0.00 % 5 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/logs_component/logs_component.html.erb 0.00 % 19 11 0 11 0.00 0.00 % 2 0 2
app/components/loopos_ui/m_icon.rb 74.29 % 79 35 26 9 112.29 0.00 % 6 0 6
app/components/loopos_ui/m_icon/m_icon.html.erb 0.00 % 7 4 0 4 0.00 0.00 % 2 0 2
app/components/loopos_ui/main_component.rb 100.00 % 8 4 4 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/main_component/main_component.html.erb 0.00 % 8 3 0 3 0.00 100.00 % 0 0 0
app/components/loopos_ui/main_layout.rb 45.45 % 45 22 10 12 0.45 0.00 % 12 0 12
app/components/loopos_ui/main_layout/main_layout.html.erb 0.00 % 17 8 0 8 0.00 0.00 % 4 0 4
app/components/loopos_ui/marketing_layout.rb 50.00 % 28 16 8 8 0.50 0.00 % 2 0 2
app/components/loopos_ui/marketing_layout/marketing_layout.html.erb 0.00 % 24 10 0 10 0.00 100.00 % 0 0 0
app/components/loopos_ui/marketplace/footer.rb 90.00 % 16 10 9 1 0.90 0.00 % 2 0 2
app/components/loopos_ui/marketplace/footer/footer.html.erb 0.00 % 15 9 0 9 0.00 0.00 % 4 0 4
app/components/loopos_ui/marketplace/header.rb 88.89 % 16 9 8 1 0.89 0.00 % 2 0 2
app/components/loopos_ui/marketplace/header/header.html.erb 0.00 % 10 7 0 7 0.00 0.00 % 2 0 2
app/components/loopos_ui/marketplace/item_card.rb 41.67 % 129 72 30 42 0.42 0.00 % 28 0 28
app/components/loopos_ui/marketplace/item_card/item_card.html.erb 0.00 % 35 24 0 24 0.00 0.00 % 18 0 18
app/components/loopos_ui/marketplace/item_details.rb 58.82 % 30 17 10 7 0.59 0.00 % 6 0 6
app/components/loopos_ui/marketplace/item_details/item_details.html.erb 0.00 % 22 14 0 14 0.00 0.00 % 2 0 2
app/components/loopos_ui/marketplace/payment_summary.rb 90.00 % 17 10 9 1 0.90 100.00 % 0 0 0
app/components/loopos_ui/marketplace/payment_summary/payment_summary.html.erb 0.00 % 19 10 0 10 0.00 100.00 % 0 0 0
app/components/loopos_ui/mini_app.rb 90.91 % 19 11 10 1 0.91 100.00 % 0 0 0
app/components/loopos_ui/mini_app/mini_app.html.erb 0.00 % 11 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/modal.rb 79.31 % 64 29 23 6 0.79 0.00 % 2 0 2
app/components/loopos_ui/modal/modal.html.erb 0.00 % 48 25 0 25 0.00 0.00 % 14 0 14
app/components/loopos_ui/modal_component.rb 33.33 % 20 15 5 10 0.33 100.00 % 0 0 0
app/components/loopos_ui/modal_component/modal_component.html.erb 0.00 % 29 16 0 16 0.00 0.00 % 32 0 32
app/components/loopos_ui/model_association_list.rb 47.37 % 200 76 36 40 0.50 0.00 % 27 0 27
app/components/loopos_ui/model_association_list/model_association_list.html.erb 0.00 % 35 19 0 19 0.00 0.00 % 4 0 4
app/components/loopos_ui/model_association_overlay.rb 54.41 % 173 68 37 31 0.54 0.00 % 18 0 18
app/components/loopos_ui/model_association_overlay/attached_item.html.erb 0.00 % 7 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/model_association_overlay/context.rb 72.09 % 115 43 31 12 0.81 0.00 % 10 0 10
app/components/loopos_ui/model_association_overlay/item_factory.rb 38.10 % 206 84 32 52 0.38 0.00 % 60 0 60
app/components/loopos_ui/model_association_overlay/missing_item.html.erb 0.00 % 9 6 0 6 0.00 100.00 % 0 0 0
app/components/loopos_ui/model_association_overlay/model_association_overlay.html.erb 0.00 % 31 17 0 17 0.00 0.00 % 4 0 4
app/components/loopos_ui/model_association_overlay/new_item.html.erb 0.00 % 27 10 0 10 0.00 100.00 % 0 0 0
app/components/loopos_ui/multiple_list.rb 50.00 % 9 6 3 3 0.50 100.00 % 0 0 0
app/components/loopos_ui/page_header.rb 65.52 % 63 29 19 10 0.66 0.00 % 4 0 4
app/components/loopos_ui/page_header/page_header.html.erb 0.00 % 27 16 0 16 0.00 0.00 % 4 0 4
app/components/loopos_ui/page_section/identifier_editor.rb 56.25 % 79 32 18 14 0.56 0.00 % 9 0 9
app/components/loopos_ui/page_section/identifier_editor/identifier_editor.html.erb 0.00 % 19 10 0 10 0.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/identifier_editor/result.html.erb 0.00 % 35 16 0 16 0.00 0.00 % 2 0 2
app/components/loopos_ui/page_section/identifier_editor/result.rb 90.91 % 18 11 10 1 0.91 100.00 % 0 0 0
app/components/loopos_ui/page_section/item_tab.rb 55.56 % 16 9 5 4 0.56 0.00 % 2 0 2
app/components/loopos_ui/page_section/item_tab/advanced_details.rb 53.85 % 55 26 14 12 0.54 0.00 % 4 0 4
app/components/loopos_ui/page_section/item_tab/advanced_details/advanced_details.html.erb 0.00 % 87 46 0 46 0.00 0.00 % 4 0 4
app/components/loopos_ui/page_section/item_tab/chat.rb 100.00 % 11 6 6 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/item_tab/chat/chat.html.erb 0.00 % 123 30 0 30 0.00 0.00 % 12 0 12
app/components/loopos_ui/page_section/item_tab/customer_info.rb 32.88 % 171 73 24 49 0.33 0.00 % 27 0 27
app/components/loopos_ui/page_section/item_tab/customer_info/customer_info.html.erb 0.00 % 124 51 0 51 0.00 0.00 % 8 0 8
app/components/loopos_ui/page_section/item_tab/financial_data.rb 53.61 % 264 97 52 45 0.54 0.00 % 38 0 38
app/components/loopos_ui/page_section/item_tab/financial_data/financial_data.html.erb 0.00 % 165 88 0 88 0.00 0.00 % 30 0 30
app/components/loopos_ui/page_section/item_tab/financial_data/item_marketplace_value_out_card.html.erb 0.00 % 25 12 0 12 0.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/item_tab/financial_data/item_value_card.html.erb 0.00 % 313 158 0 158 0.00 0.00 % 126 0 126
app/components/loopos_ui/page_section/item_tab/financial_data/item_value_table.html.erb 0.00 % 141 64 0 64 0.00 0.00 % 40 0 40
app/components/loopos_ui/page_section/item_tab/financial_data/logs_list.html.erb 0.00 % 11 5 0 5 0.00 0.00 % 2 0 2
app/components/loopos_ui/page_section/item_tab/financial_data/other_values_section.html.erb 0.00 % 128 57 0 57 0.00 0.00 % 14 0 14
app/components/loopos_ui/page_section/item_tab/financial_data/transactions_list.html.erb 0.00 % 22 12 0 12 0.00 0.00 % 6 0 6
app/components/loopos_ui/page_section/item_tab/flow.rb 76.92 % 26 13 10 3 0.77 100.00 % 0 0 0
app/components/loopos_ui/page_section/item_tab/flow/flow.html.erb 0.00 % 38 15 0 15 0.00 0.00 % 4 0 4
app/components/loopos_ui/page_section/item_tab/impact_widget.rb 64.29 % 27 14 9 5 0.64 100.00 % 0 0 0
app/components/loopos_ui/page_section/item_tab/impact_widget/impact_widget.html.erb 0.00 % 31 15 0 15 0.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/item_tab/info.rb 68.00 % 41 25 17 8 0.68 0.00 % 4 0 4
app/components/loopos_ui/page_section/item_tab/info/identifiers_table.html.erb 0.00 % 83 29 0 29 0.00 0.00 % 4 0 4
app/components/loopos_ui/page_section/item_tab/info/info.html.erb 0.00 % 150 88 0 88 0.00 0.00 % 34 0 34
app/components/loopos_ui/page_section/item_tab/logs.rb 26.47 % 206 102 27 75 0.26 0.00 % 46 0 46
app/components/loopos_ui/page_section/item_tab/logs/logs.html.erb 0.00 % 108 43 0 43 0.00 0.00 % 10 0 10
app/components/loopos_ui/page_section/item_tab/notes.rb 72.22 % 33 18 13 5 0.72 0.00 % 2 0 2
app/components/loopos_ui/page_section/item_tab/notes/notes.html.erb 0.00 % 61 30 0 30 0.00 0.00 % 6 0 6
app/components/loopos_ui/page_section/item_tab/protocol_responses.rb 66.67 % 38 18 12 6 0.67 0.00 % 4 0 4
app/components/loopos_ui/page_section/item_tab/protocol_responses/protocol_answer_value.html.erb 0.00 % 20 10 0 10 0.00 0.00 % 9 0 9
app/components/loopos_ui/page_section/item_tab/protocol_responses/protocol_responses.html.erb 0.00 % 96 46 0 46 0.00 0.00 % 34 0 34
app/components/loopos_ui/page_section/item_tab/rfi.rb 80.43 % 77 46 37 9 0.80 0.00 % 2 0 2
app/components/loopos_ui/page_section/item_tab/rfi/rfi.html.erb 0.00 % 49 20 0 20 0.00 0.00 % 2 0 2
app/components/loopos_ui/page_section/item_tab/rfi/rfi_answer_history_table.html.erb 0.00 % 90 40 0 40 0.00 0.00 % 20 0 20
app/components/loopos_ui/page_section/item_tab/rfi/rfi_answers_index_table.html.erb 0.00 % 112 44 0 44 0.00 0.00 % 12 0 12
app/components/loopos_ui/page_section/item_tab/rfi/rfi_answers_step_table.html.erb 0.00 % 134 58 0 58 0.00 0.00 % 22 0 22
app/components/loopos_ui/page_section/item_tab/rfi/rfi_responders_without_rfi_answers_table.html.erb 0.00 % 79 32 0 32 0.00 0.00 % 12 0 12
app/components/loopos_ui/page_section/item_tab/services.rb 30.19 % 100 53 16 37 0.30 0.00 % 34 0 34
app/components/loopos_ui/page_section/item_tab/services/services.html.erb 0.00 % 139 55 0 55 0.00 0.00 % 12 0 12
app/components/loopos_ui/page_section/item_tab/trade_in.rb 66.67 % 35 18 12 6 0.67 0.00 % 4 0 4
app/components/loopos_ui/page_section/item_tab/trade_in/trade_in.html.erb 0.00 % 75 42 0 42 0.00 0.00 % 6 0 6
app/components/loopos_ui/page_section/item_tab/validation_widget.rb 100.00 % 13 6 6 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/item_tab/validation_widget/validation_widget.html.erb 0.00 % 44 21 0 21 0.00 0.00 % 2 0 2
app/components/loopos_ui/page_section/rfi_answer_tab.rb 55.56 % 16 9 5 4 0.56 0.00 % 2 0 2
app/components/loopos_ui/page_section/rfi_answer_tab/history.rb 100.00 % 13 8 8 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/rfi_answer_tab/history/history.html.erb 0.00 % 16 7 0 7 0.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/rfi_answer_tab/info.rb 100.00 % 13 8 8 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/rfi_answer_tab/info/info.html.erb 0.00 % 87 49 0 49 0.00 0.00 % 24 0 24
app/components/loopos_ui/page_section/rfi_tab.rb 55.56 % 16 9 5 4 0.56 0.00 % 2 0 2
app/components/loopos_ui/page_section/rfi_tab/advanced_details.rb 100.00 % 11 6 6 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/rfi_tab/advanced_details/advanced_details.html.erb 0.00 % 16 8 0 8 0.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/rfi_tab/info.rb 100.00 % 13 8 8 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/rfi_tab/info/info.html.erb 0.00 % 161 93 0 93 0.00 0.00 % 32 0 32
app/components/loopos_ui/page_section/rfi_tab/rfi_answers.rb 100.00 % 12 7 7 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/rfi_tab/rfi_answers/rfi_answers.html.erb 0.00 % 17 7 0 7 0.00 100.00 % 0 0 0
app/components/loopos_ui/pages/item_show.html.erb 0.00 % 78 15 0 15 0.00 0.00 % 18 0 18
app/components/loopos_ui/pages/item_show.rb 53.70 % 120 54 29 25 0.54 0.00 % 8 0 8
app/components/loopos_ui/pages/item_show_header.html.erb 0.00 % 25 15 0 15 0.00 0.00 % 4 0 4
app/components/loopos_ui/pages/item_show_header.rb 70.00 % 47 20 14 6 0.70 100.00 % 0 0 0
app/components/loopos_ui/pages/reports_index.html.erb 0.00 % 45 20 0 20 0.00 0.00 % 4 0 4
app/components/loopos_ui/pages/reports_index.rb 59.38 % 79 32 19 13 0.59 0.00 % 12 0 12
app/components/loopos_ui/pages/reports_index_table.html.erb 0.00 % 48 19 0 19 0.00 0.00 % 4 0 4
app/components/loopos_ui/pages/rfi/rfi_show.html.erb 0.00 % 13 8 0 8 0.00 0.00 % 2 0 2
app/components/loopos_ui/pages/rfi/rfi_show.rb 70.00 % 41 20 14 6 0.70 0.00 % 5 0 5
app/components/loopos_ui/pages/rfi_answer/show.html.erb 0.00 % 10 6 0 6 0.00 100.00 % 0 0 0
app/components/loopos_ui/pages/rfi_answer/show.rb 69.57 % 46 23 16 7 0.70 0.00 % 5 0 5
app/components/loopos_ui/pagination.rb 50.79 % 137 63 32 31 0.51 0.00 % 8 0 8
app/components/loopos_ui/pagination/pagination.html.erb 0.00 % 11 7 0 7 0.00 0.00 % 2 0 2
app/components/loopos_ui/popover.rb 76.00 % 100 25 19 6 0.84 100.00 % 0 0 0
app/components/loopos_ui/popover/popover.html.erb 0.00 % 10 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/popover_template.rb 100.00 % 7 5 5 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/popover_template/popover_template.html.erb 0.00 % 14 8 0 8 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/add_element/add_element_component.html.erb 0.00 % 36 10 0 10 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/add_element/add_element_component.rb 55.00 % 48 20 11 9 0.55 100.00 % 0 0 0
app/components/loopos_ui/protocol/add_element/filter_component.rb 72.73 % 20 11 8 3 0.73 100.00 % 0 0 0
app/components/loopos_ui/protocol/add_element/filter_component.turbo_stream.erb 0.00 % 35 14 0 14 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/add_element/select_option_component.html.erb 0.00 % 13 9 0 9 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/add_element/select_option_component.rb 50.00 % 50 28 14 14 0.50 0.00 % 14 0 14
app/components/loopos_ui/protocol/add_protocol.html.erb 0.00 % 98 40 0 40 0.00 0.00 % 14 0 14
app/components/loopos_ui/protocol/attach_node/filter_component.rb 72.73 % 20 11 8 3 0.73 100.00 % 0 0 0
app/components/loopos_ui/protocol/attach_node/filter_component.turbo_stream.erb 0.00 % 9 5 0 5 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/attach_node/select_option_component.html.erb 0.00 % 32 17 0 17 0.00 0.00 % 20 0 20
app/components/loopos_ui/protocol/attach_node/select_option_component.rb 53.85 % 47 26 14 12 0.54 0.00 % 14 0 14
app/components/loopos_ui/protocol/drawer_bar.rb 87.50 % 14 8 7 1 0.88 100.00 % 0 0 0
app/components/loopos_ui/protocol/drawer_bar/drawer_bar.html.erb 0.00 % 73 38 0 38 0.00 0.00 % 6 0 6
app/components/loopos_ui/protocol/elements/fields/array.html.erb 0.00 % 79 23 0 23 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/elements/fields/array.rb 50.00 % 24 16 8 8 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/fields/boolean.html.erb 0.00 % 26 17 0 17 0.00 0.00 % 4 0 4
app/components/loopos_ui/protocol/elements/fields/boolean.rb 50.00 % 24 16 8 8 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/fields/custom.html.erb 0.00 % 60 19 0 19 0.00 0.00 % 6 0 6
app/components/loopos_ui/protocol/elements/fields/custom.rb 50.00 % 24 16 8 8 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/fields/date.html.erb 0.00 % 38 16 0 16 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/elements/fields/date.rb 50.00 % 24 16 8 8 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/fields/enum.html.erb 0.00 % 23 14 0 14 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/elements/fields/enum.rb 50.00 % 24 16 8 8 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/fields/granularity.html.erb 0.00 % 21 14 0 14 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/elements/fields/granularity.rb 50.00 % 24 16 8 8 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/fields/image.html.erb 0.00 % 21 13 0 13 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/elements/fields/image.rb 50.00 % 24 16 8 8 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/fields/integer.html.erb 0.00 % 19 9 0 9 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/fields/integer.rb 50.00 % 24 16 8 8 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/fields/number.html.erb 0.00 % 32 16 0 16 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/elements/fields/number.rb 50.00 % 23 16 8 8 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/fields/option.html.erb 0.00 % 75 33 0 33 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/elements/fields/option.rb 44.44 % 27 18 8 10 0.44 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/fields/options.html.erb 0.00 % 45 22 0 22 0.00 0.00 % 6 0 6
app/components/loopos_ui/protocol/elements/fields/options.rb 50.00 % 24 16 8 8 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/fields/string.html.erb 0.00 % 32 17 0 17 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/elements/fields/string.rb 50.00 % 24 16 8 8 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/break.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/break.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/html.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/html.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/image.html.erb 0.00 % 2 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/image.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/text.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/text.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/title.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/title.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/url.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/url.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/bool.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/bool.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/custom.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/custom.rb 58.82 % 28 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/date.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/date.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/date_range.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/date_range.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/email.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/email.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/files.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/files.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/iban.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/iban.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/identifier.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/identifier.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/images.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/images.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/money.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/money.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/number.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/number.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/phone_number.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/phone_number.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/postal_code.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/postal_code.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/select.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/select.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/text.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/text.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/settings.html.erb 0.00 % 72 30 0 30 0.00 0.00 % 4 0 4
app/components/loopos_ui/protocol/elements/settings.rb 53.57 % 51 28 15 13 0.54 0.00 % 6 0 6
app/components/loopos_ui/protocol/elements/structure/divider.html.erb 0.00 % 94 40 0 40 0.00 0.00 % 22 0 22
app/components/loopos_ui/protocol/elements/structure/divider.rb 58.82 % 28 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/protocol_builder_component.erb 0.00 % 9 7 0 7 0.00 0.00 % 4 0 4
app/components/loopos_ui/protocol/protocol_builder_component.rb 65.00 % 36 20 13 7 0.65 100.00 % 0 0 0
app/components/loopos_ui/protocol/protocol_component.html.erb 0.00 % 64 30 0 30 0.00 0.00 % 20 0 20
app/components/loopos_ui/protocol/protocol_component.rb 55.00 % 30 20 11 9 0.55 100.00 % 0 0 0
app/components/loopos_ui/protocol/protocol_element_component.html.erb 0.00 % 206 102 0 102 0.00 0.00 % 58 0 58
app/components/loopos_ui/protocol/protocol_element_component.rb 50.00 % 24 16 8 8 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/protocol_filter_component.html.erb 0.00 % 121 54 0 54 0.00 0.00 % 18 0 18
app/components/loopos_ui/protocol/protocol_filter_component.rb 50.00 % 15 10 5 5 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/protocol_header.html.erb 0.00 % 201 105 0 105 0.00 0.00 % 46 0 46
app/components/loopos_ui/protocol/protocol_sidebar_component.erb 0.00 % 14 7 0 7 0.00 0.00 % 4 0 4
app/components/loopos_ui/protocol/protocol_sidebar_component.rb 87.50 % 14 8 7 1 0.88 100.00 % 0 0 0
app/components/loopos_ui/protocol/protocols_area_component.erb 0.00 % 47 25 0 25 0.00 0.00 % 8 0 8
app/components/loopos_ui/protocol/protocols_area_component.rb 46.67 % 65 30 14 16 0.47 100.00 % 0 0 0
app/components/loopos_ui/protocol/settings.html.erb 0.00 % 155 57 0 57 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/settings.rb 53.85 % 48 26 14 12 0.54 0.00 % 4 0 4
app/components/loopos_ui/protocol_answer_value.rb 100.00 % 4 2 2 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/protocol_answer_value/protocol_answer_value.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol_element/drawer_bar.rb 90.91 % 17 11 10 1 0.91 100.00 % 0 0 0
app/components/loopos_ui/protocol_element/drawer_bar/drawer_bar.html.erb 0.00 % 60 26 0 26 0.00 0.00 % 8 0 8
app/components/loopos_ui/protocol_form.rb 30.41 % 725 388 118 270 0.30 0.00 % 214 0 214
app/components/loopos_ui/protocol_form/protocol_form.html.erb 0.00 % 374 151 0 151 0.00 0.00 % 81 0 81
app/components/loopos_ui/radio_card.rb 81.82 % 38 22 18 4 0.82 0.00 % 3 0 3
app/components/loopos_ui/radio_card/content.html.erb 0.00 % 26 12 0 12 0.00 0.00 % 2 0 2
app/components/loopos_ui/radio_card/radio_card.html.erb 0.00 % 6 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/script_editor.rb 100.00 % 22 17 17 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/script_editor/script_editor.html.erb 0.00 % 17 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/script_input_form.rb 42.86 % 58 28 12 16 0.43 0.00 % 10 0 10
app/components/loopos_ui/script_input_form/script_input_form.html.erb 0.00 % 82 31 0 31 0.00 0.00 % 13 0 13
app/components/loopos_ui/services/base_concern.rb 73.68 % 42 19 14 5 1.00 0.00 % 2 0 2
app/components/loopos_ui/services/email_messages/table.rb 38.00 % 114 50 19 31 0.42 0.00 % 22 0 22
app/components/loopos_ui/services/email_messages/table/table.html.erb 0.00 % 83 47 0 47 0.00 0.00 % 22 0 22
app/components/loopos_ui/services/email_messages/tabs/info.rb 84.62 % 26 13 11 2 0.85 0.00 % 2 0 2
app/components/loopos_ui/services/email_messages/tabs/info/info.html.erb 0.00 % 95 57 0 57 0.00 0.00 % 18 0 18
app/components/loopos_ui/services/email_templates/table.rb 45.95 % 96 37 17 20 0.46 0.00 % 10 0 10
app/components/loopos_ui/services/email_templates/table/table.html.erb 0.00 % 110 49 0 49 0.00 0.00 % 22 0 22
app/components/loopos_ui/services/email_templates/tabs/info.rb 86.67 % 29 15 13 2 0.87 0.00 % 2 0 2
app/components/loopos_ui/services/email_templates/tabs/info/info.html.erb 0.00 % 77 33 0 33 0.00 0.00 % 8 0 8
app/components/loopos_ui/services/extra_data.rb 100.00 % 9 4 4 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/services/extra_data/extra_data.html.erb 0.00 % 25 12 0 12 0.00 0.00 % 2 0 2
app/components/loopos_ui/services/incoming_payments/table.rb 41.30 % 87 46 19 27 0.43 0.00 % 16 0 16
app/components/loopos_ui/services/incoming_payments/table/table.html.erb 0.00 % 109 67 0 67 0.00 0.00 % 26 0 26
app/components/loopos_ui/services/incoming_payments/tabs/info.rb 84.62 % 26 13 11 2 0.85 0.00 % 2 0 2
app/components/loopos_ui/services/incoming_payments/tabs/info/info.html.erb 0.00 % 109 69 0 69 0.00 0.00 % 18 0 18
app/components/loopos_ui/services/invoices/show.rb 100.00 % 11 6 6 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/services/invoices/show/show.html.erb 0.00 % 28 16 0 16 0.00 0.00 % 8 0 8
app/components/loopos_ui/services/invoices/table.rb 37.74 % 99 53 20 33 0.40 0.00 % 18 0 18
app/components/loopos_ui/services/invoices/table/table.html.erb 0.00 % 125 70 0 70 0.00 0.00 % 26 0 26
app/components/loopos_ui/services/invoices/tabs/info.rb 73.08 % 48 26 19 7 0.73 0.00 % 2 0 2
app/components/loopos_ui/services/invoices/tabs/info/info.html.erb 0.00 % 106 60 0 60 0.00 0.00 % 14 0 14
app/components/loopos_ui/services/issues_area.rb 100.00 % 7 4 4 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/services/issues_area/issues_area.html.erb 0.00 % 11 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/services/list.rb 100.00 % 18 6 6 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/services/list/list.html.erb 0.00 % 14 7 0 7 0.00 0.00 % 2 0 2
app/components/loopos_ui/services/payments/table.rb 40.43 % 104 47 19 28 0.45 0.00 % 16 0 16
app/components/loopos_ui/services/payments/table/table.html.erb 0.00 % 147 78 0 78 0.00 0.00 % 30 0 30
app/components/loopos_ui/services/payments/tabs/info.rb 84.62 % 26 13 11 2 0.85 0.00 % 2 0 2
app/components/loopos_ui/services/payments/tabs/info/info.html.erb 0.00 % 105 64 0 64 0.00 0.00 % 20 0 20
app/components/loopos_ui/services/shipping_pickups/table.rb 45.45 % 70 33 15 18 0.45 0.00 % 10 0 10
app/components/loopos_ui/services/shipping_pickups/table/table.html.erb 0.00 % 50 31 0 31 0.00 0.00 % 10 0 10
app/components/loopos_ui/services/shipping_pickups/tabs/info.rb 88.89 % 16 9 8 1 0.89 100.00 % 0 0 0
app/components/loopos_ui/services/shipping_pickups/tabs/info/info.html.erb 0.00 % 47 29 0 29 0.00 100.00 % 0 0 0
app/components/loopos_ui/services/shippings/table.rb 38.46 % 118 52 20 32 0.40 0.00 % 22 0 22
app/components/loopos_ui/services/shippings/table/table.html.erb 0.00 % 248 120 0 120 0.00 0.00 % 40 0 40
app/components/loopos_ui/services/shippings/tabs/info.rb 90.91 % 22 11 10 1 0.91 0.00 % 2 0 2
app/components/loopos_ui/services/shippings/tabs/info/info.html.erb 0.00 % 137 87 0 87 0.00 0.00 % 16 0 16
app/components/loopos_ui/services/shippings/tabs/packages.rb 90.00 % 18 10 9 1 1.10 100.00 % 0 0 0
app/components/loopos_ui/services/shippings/tabs/packages/packages.html.erb 0.00 % 153 83 0 83 0.00 0.00 % 20 0 20
app/components/loopos_ui/services/sms_messages/table.rb 38.00 % 105 50 19 31 0.40 0.00 % 20 0 20
app/components/loopos_ui/services/sms_messages/table/table.html.erb 0.00 % 82 46 0 46 0.00 0.00 % 24 0 24
app/components/loopos_ui/services/sms_messages/tabs/info.rb 84.62 % 26 13 11 2 0.85 0.00 % 2 0 2
app/components/loopos_ui/services/sms_messages/tabs/info/info.html.erb 0.00 % 109 69 0 69 0.00 0.00 % 18 0 18
app/components/loopos_ui/services/tabs_layout.rb 100.00 % 7 4 4 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/services/tabs_layout/tabs_layout.htm.erb 0.00 % 47 20 0 20 0.00 0.00 % 17 0 17
app/components/loopos_ui/show_header.rb 56.25 % 72 32 18 14 0.56 0.00 % 6 0 6
app/components/loopos_ui/show_header/show_header.html.erb 0.00 % 67 35 0 35 0.00 0.00 % 16 0 16
app/components/loopos_ui/show_layout.rb 75.00 % 28 8 6 2 0.75 100.00 % 0 0 0
app/components/loopos_ui/show_layout/show_layout.html.erb 0.00 % 18 8 0 8 0.00 0.00 % 2 0 2
app/components/loopos_ui/show_layout_component/show_layout_component.html.erb 0.00 % 6 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar.rb 80.00 % 31 15 12 3 0.80 0.00 % 2 0 2
app/components/loopos_ui/sidebar/configuration.rb 100.00 % 23 14 14 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar/v1/divider.html.erb 0.00 % 2 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar/v1/divider.rb 100.00 % 10 4 4 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar/v1/drawer_divider.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar/v1/drawer_divider.rb 100.00 % 10 4 4 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar/v1/drawer_entry.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar/v1/drawer_item.html.erb 0.00 % 52 25 0 25 0.00 0.00 % 12 0 12
app/components/loopos_ui/sidebar/v1/drawer_item.rb 37.97 % 156 79 30 49 0.38 0.00 % 47 0 47
app/components/loopos_ui/sidebar/v1/item.rb 34.69 % 194 98 34 64 0.35 0.00 % 52 0 52
app/components/loopos_ui/sidebar/v1/sidebar.html.erb 0.00 % 16 10 0 10 0.00 0.00 % 2 0 2
app/components/loopos_ui/sidebar/v1/sidebar.rb 83.33 % 25 12 10 2 0.83 100.00 % 0 0 0
app/components/loopos_ui/sidebar/v1/single_item.html.erb 0.00 % 22 12 0 12 0.00 0.00 % 8 0 8
app/components/loopos_ui/sidebar/v1/single_item.rb 36.84 % 40 19 7 12 0.37 0.00 % 12 0 12
app/components/loopos_ui/sidebar/v2/group.html.erb 0.00 % 9 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar/v2/item.html.erb 0.00 % 37 17 0 17 0.00 0.00 % 8 0 8
app/components/loopos_ui/sidebar/v2/sidebar.html.erb 0.00 % 39 22 0 22 0.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar/v2/sidebar.rb 61.11 % 153 54 33 21 0.61 0.00 % 11 0 11
app/components/loopos_ui/sidebar_layout_component.rb 58.82 % 31 17 10 7 0.59 0.00 % 4 0 4
app/components/loopos_ui/sidebar_layout_component/sidebar_old_component.html.erb 0.00 % 3 2 0 2 0.00 0.00 % 2 0 2
app/components/loopos_ui/sidebar_old_component.rb 53.85 % 30 13 7 6 0.54 0.00 % 4 0 4
app/components/loopos_ui/spinner.rb 46.67 % 32 15 7 8 0.47 0.00 % 5 0 5
app/components/loopos_ui/spinner/spinner.html.erb 0.00 % 12 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/state_label.rb 62.96 % 79 27 17 10 0.63 0.00 % 12 0 12
app/components/loopos_ui/status_dot.rb 80.00 % 9 5 4 1 0.80 100.00 % 0 0 0
app/components/loopos_ui/status_dot/status_dot.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/stepper.rb 60.53 % 102 38 23 15 0.61 0.00 % 7 0 7
app/components/loopos_ui/stepper/step.html.erb 0.00 % 21 15 0 15 0.00 0.00 % 2 0 2
app/components/loopos_ui/stepper/stepper.html.erb 0.00 % 8 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/stepper_panel.rb 35.00 % 46 20 7 13 0.35 100.00 % 0 0 0
app/components/loopos_ui/stepper_panel/stepper_panel.html.erb 0.00 % 13 9 0 9 0.00 0.00 % 2 0 2
app/components/loopos_ui/table/pagination_component.html.erb 0.00 % 39 28 0 28 0.00 0.00 % 8 0 8
app/components/loopos_ui/table/pagination_component.rb 46.15 % 21 13 6 7 0.46 100.00 % 0 0 0
app/components/loopos_ui/table/table_component.html.erb 0.00 % 61 35 0 35 0.00 0.00 % 26 0 26
app/components/loopos_ui/table/table_component.rb 31.71 % 84 41 13 28 0.32 0.00 % 4 0 4
app/components/loopos_ui/table_data_list.rb 64.00 % 42 25 16 9 0.64 0.00 % 6 0 6
app/components/loopos_ui/table_data_list/label_header_row.html.erb 0.00 % 4 3 0 3 0.00 100.00 % 0 0 0
app/components/loopos_ui/table_data_list/table_data_list.html.erb 0.00 % 49 21 0 21 0.00 100.00 % 0 0 0
app/components/loopos_ui/table_filter.rb 72.73 % 17 11 8 3 0.73 0.00 % 6 0 6
app/components/loopos_ui/table_filter/table_filter.html.erb 0.00 % 11 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/tabs_component/tabs_component.html.erb 0.00 % 14 8 0 8 0.00 100.00 % 0 0 0
app/components/loopos_ui/tabs_component/tabs_component.rb 34.38 % 50 32 11 21 0.34 0.00 % 6 0 6
app/components/loopos_ui/tabs_content.rb 64.29 % 25 14 9 5 0.64 0.00 % 2 0 2
app/components/loopos_ui/tabs_content/tabs_content.html.erb 0.00 % 7 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/tabs_layout.rb 49.21 % 122 63 31 32 0.49 0.00 % 16 0 16
app/components/loopos_ui/tabs_layout/request_focus.html.erb 0.00 % 7 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/tabs_layout/tab.html.erb 0.00 % 14 8 0 8 0.00 0.00 % 2 0 2
app/components/loopos_ui/tabs_layout/tabs_layout.html.erb 0.00 % 55 19 0 19 0.00 0.00 % 14 0 14
app/components/loopos_ui/tabs_section.rb 100.00 % 14 11 11 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/tabs_section/tabs_section.html.erb 0.00 % 18 11 0 11 0.00 0.00 % 6 0 6
app/components/loopos_ui/tag_token.rb 38.10 % 55 21 8 13 0.38 0.00 % 6 0 6
app/components/loopos_ui/theme_context.rb 62.86 % 70 35 22 13 0.63 0.00 % 2 0 2
app/components/loopos_ui/timeline.rb 100.00 % 6 4 4 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/timeline/timeline.html.erb 0.00 % 7 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/title_description.rb 92.31 % 20 13 12 1 0.92 100.00 % 0 0 0
app/components/loopos_ui/title_description/title_description.html.erb 0.00 % 29 20 0 20 0.00 0.00 % 14 0 14
app/components/loopos_ui/toast.rb 63.64 % 20 11 7 4 0.64 0.00 % 3 0 3
app/components/loopos_ui/toast/toast.html.erb 0.00 % 31 12 0 12 0.00 0.00 % 8 0 8
app/components/loopos_ui/toaster.rb 62.50 % 118 56 35 21 0.70 0.00 % 14 0 14
app/components/loopos_ui/toaster/toaster.html.erb 0.00 % 13 8 0 8 0.00 0.00 % 2 0 2
app/components/loopos_ui/toggle.rb 69.23 % 48 26 18 8 0.69 0.00 % 4 0 4
app/components/loopos_ui/toggle/toggle.html.erb 0.00 % 32 19 0 19 0.00 0.00 % 22 0 22
app/components/loopos_ui/token.rb 30.16 % 133 63 19 44 0.30 0.00 % 26 0 26
app/components/loopos_ui/token/token.html.erb 0.00 % 69 43 0 43 0.00 0.00 % 34 0 34
app/components/loopos_ui/token_list.rb 65.91 % 86 44 29 15 0.66 0.00 % 4 0 4
app/components/loopos_ui/token_list/list.html.erb 0.00 % 29 19 0 19 0.00 0.00 % 6 0 6
app/components/loopos_ui/token_list/token_list.html.erb 0.00 % 13 8 0 8 0.00 100.00 % 0 0 0
app/components/loopos_ui/tooltip.rb 86.67 % 41 15 13 2 0.87 100.00 % 0 0 0
app/components/loopos_ui/tooltip/tooltip.html.erb 0.00 % 28 18 0 18 0.00 0.00 % 4 0 4
app/components/loopos_ui/top_page_component/breadcrumbs.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/top_page_component/top_page_actions_component.html.erb 0.00 % 15 7 0 7 0.00 0.00 % 6 0 6
app/components/loopos_ui/top_page_component/top_page_info_component.html.erb 0.00 % 65 45 0 45 0.00 0.00 % 24 0 24
app/components/loopos_ui/universal_search_component/universal_search_filters.html.erb 0.00 % 18 12 0 12 0.00 0.00 % 4 0 4
app/components/loopos_ui/universal_search_component/universal_search_filters.rb 87.50 % 13 8 7 1 0.88 100.00 % 0 0 0
app/components/loopos_ui/universal_search_component/universal_search_footer.html.erb 0.00 % 12 8 0 8 0.00 0.00 % 4 0 4
app/components/loopos_ui/universal_search_component/universal_search_footer.rb 100.00 % 11 6 6 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/universal_search_component/universal_search_global_search.html.erb 0.00 % 5 4 0 4 0.00 0.00 % 2 0 2
app/components/loopos_ui/universal_search_component/universal_search_global_search.rb 100.00 % 10 5 5 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/universal_search_component/universal_search_results.html.erb 0.00 % 24 10 0 10 0.00 0.00 % 4 0 4
app/components/loopos_ui/universal_search_component/universal_search_results.rb 88.89 % 18 9 8 1 0.89 100.00 % 0 0 0
app/components/loopos_ui/user_menu.rb 50.00 % 69 34 17 17 0.50 0.00 % 5 0 5
app/components/loopos_ui/user_menu/user_menu.html.erb 0.00 % 89 44 0 44 0.00 0.00 % 8 0 8
app/components/loopos_ui/v2/app_layout.rb 74.07 % 49 27 20 7 0.74 0.00 % 4 0 4
app/components/loopos_ui/v2/app_layout/app_layout.html.erb 0.00 % 73 29 0 29 0.00 0.00 % 10 0 10
app/components/loopos_ui/v2/app_layout/root_styles.html.erb 0.00 % 15 10 0 10 0.00 100.00 % 0 0 0
app/components/loopos_ui/v2/card.rb 75.00 % 71 40 30 10 1.10 0.00 % 12 0 12
app/components/loopos_ui/v2/card/app_instance.html.erb 0.00 % 53 20 0 20 0.00 100.00 % 0 0 0
app/components/loopos_ui/v2/card/app_instance.rb 76.00 % 62 25 19 6 0.76 0.00 % 4 0 4
app/components/loopos_ui/v2/card/card.html.erb 0.00 % 26 17 0 17 0.00 0.00 % 14 0 14
app/components/loopos_ui/v2/card/dashboard.html.erb 0.00 % 7 5 0 5 0.00 0.00 % 4 0 4
app/components/loopos_ui/v2/card/dashboard.rb 92.86 % 30 14 13 1 0.93 100.00 % 0 0 0
app/components/loopos_ui/v2/card/draft.html.erb 0.00 % 5 4 0 4 0.00 0.00 % 2 0 2
app/components/loopos_ui/v2/card/draft.rb 92.31 % 27 13 12 1 0.92 100.00 % 0 0 0
app/components/loopos_ui/v2/card/favorites.html.erb 0.00 % 21 11 0 11 0.00 0.00 % 2 0 2
app/components/loopos_ui/v2/card/favorites.rb 100.00 % 9 5 5 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/v2/card/financial_log.html.erb 0.00 % 29 16 0 16 0.00 0.00 % 12 0 12
app/components/loopos_ui/v2/card/financial_log.rb 65.79 % 87 38 25 13 0.66 0.00 % 9 0 9
app/components/loopos_ui/v2/card/financial_transaction.html.erb 0.00 % 43 21 0 21 0.00 0.00 % 12 0 12
app/components/loopos_ui/v2/card/financial_transaction.rb 37.21 % 81 43 16 27 0.37 0.00 % 23 0 23
app/components/loopos_ui/v2/card/script.html.erb 0.00 % 17 6 0 6 0.00 0.00 % 2 0 2
app/components/loopos_ui/v2/card/script.rb 63.33 % 62 30 19 11 0.63 0.00 % 4 0 4
app/components/loopos_ui/v2/image.rb 45.27 % 355 148 67 81 20.90 0.00 % 80 0 80
app/components/loopos_ui/v2/image/image.html.erb 0.00 % 291 84 0 84 0.00 0.00 % 48 0 48
app/components/loopos_ui/v2/select_bar.rb 45.16 % 61 31 14 17 0.45 100.00 % 0 0 0
app/components/loopos_ui/v2/table.html.erb 0.00 % 116 39 0 39 0.00 0.00 % 18 0 18
app/components/loopos_ui/v2/table.rb 39.07 % 517 215 84 131 0.39 0.00 % 81 0 81
app/components/loopos_ui/v2/table/cell.html.erb 0.00 % 27 15 0 15 0.00 0.00 % 8 0 8
app/components/loopos_ui/v2/table/cell.rb 50.00 % 72 36 18 18 0.50 0.00 % 22 0 22
app/components/loopos_ui/v2/table/header.html.erb 0.00 % 22 9 0 9 0.00 0.00 % 4 0 4
app/components/loopos_ui/v2/table/header.rb 100.00 % 14 9 9 0 1.00 0.00 % 2 0 2
app/components/loopos_ui/wysiwyg.rb 85.71 % 22 14 12 2 0.86 100.00 % 0 0 0
app/components/loopos_ui/wysiwyg/wysiwyg.html.erb 0.00 % 15 5 0 5 0.00 100.00 % 0 0 0

Components ( 31.04% covered at 0.82 hits/line )

659 files in total.
14511 relevant lines, 4504 lines covered and 10007 lines missed. ( 31.04% )
4728 total branches, 3 branches covered and 4725 branches missed. ( 0.06% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/components/loopos_ui/accordion.rb 60.71 % 54 28 17 11 0.61 0.00 % 8 0 8
app/components/loopos_ui/accordion/accordion.html.erb 0.00 % 32 13 0 13 0.00 0.00 % 4 0 4
app/components/loopos_ui/action_bar.rb 35.29 % 102 51 18 33 0.35 0.00 % 24 0 24
app/components/loopos_ui/action_bar/action_bar.html.erb 0.00 % 9 6 0 6 0.00 0.00 % 2 0 2
app/components/loopos_ui/action_bar/item_navigation.html.erb 0.00 % 9 6 0 6 0.00 100.00 % 0 0 0
app/components/loopos_ui/action_buttons.rb 100.00 % 11 5 5 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/action_buttons/action_buttons.html.erb 0.00 % 5 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/action_buttons/button_group.html.erb 0.00 % 5 3 0 3 0.00 100.00 % 0 0 0
app/components/loopos_ui/action_menu.rb 62.50 % 30 16 10 6 0.63 0.00 % 2 0 2
app/components/loopos_ui/action_menu/action_menu.html.erb 0.00 % 44 25 0 25 0.00 0.00 % 20 0 20
app/components/loopos_ui/app_instance/card.rb 42.86 % 61 21 9 12 0.43 0.00 % 11 0 11
app/components/loopos_ui/app_instance/card/card.html.erb 0.00 % 48 23 0 23 0.00 0.00 % 17 0 17
app/components/loopos_ui/app_layout_component.rb 59.09 % 89 44 26 18 0.59 0.00 % 2 0 2
app/components/loopos_ui/app_layout_component/app_layout_component.html.erb 0.00 % 48 22 0 22 0.00 0.00 % 10 0 10
app/components/loopos_ui/app_layout_component/root_styles.html.erb 0.00 % 15 10 0 10 0.00 100.00 % 0 0 0
app/components/loopos_ui/app_logo.rb 60.00 % 26 15 9 6 0.60 0.00 % 4 0 4
app/components/loopos_ui/app_logo/app_logo.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/apps/list.rb 100.00 % 9 5 5 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/apps/list/list.html.erb 0.00 % 14 7 0 7 0.00 0.00 % 2 0 2
app/components/loopos_ui/assign/assign_component.html.erb 0.00 % 8 3 0 3 0.00 100.00 % 0 0 0
app/components/loopos_ui/assign/assign_component.rb 100.00 % 13 7 7 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/assign/assign_section.html.erb 0.00 % 112 57 0 57 0.00 0.00 % 44 0 44
app/components/loopos_ui/assign/assign_section.rb 33.33 % 55 27 9 18 0.33 100.00 % 0 0 0
app/components/loopos_ui/assign/elements.html.erb 0.00 % 29 16 0 16 0.00 0.00 % 26 0 26
app/components/loopos_ui/assign/elements.rb 35.00 % 40 20 7 13 0.35 100.00 % 0 0 0
app/components/loopos_ui/assign/empty.html.erb 0.00 % 7 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/assign/empty.rb 83.33 % 13 6 5 1 0.83 100.00 % 0 0 0
app/components/loopos_ui/assign/update.rb 25.93 % 55 27 7 20 0.26 100.00 % 0 0 0
app/components/loopos_ui/assign/update.turbo_stream.erb 0.00 % 25 3 0 3 0.00 0.00 % 2 0 2
app/components/loopos_ui/association_overlay.rb 87.88 % 58 33 29 4 0.88 0.00 % 2 0 2
app/components/loopos_ui/association_overlay/association_overlay.html.erb 0.00 % 16 9 0 9 0.00 0.00 % 8 0 8
app/components/loopos_ui/association_overlay/header.html.erb 0.00 % 4 3 0 3 0.00 0.00 % 4 0 4
app/components/loopos_ui/association_overlay/results_container.html.erb 0.00 % 12 7 0 7 0.00 0.00 % 6 0 6
app/components/loopos_ui/association_overlay/selected_container.html.erb 0.00 % 7 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/async_select/async_select_component.html.erb 0.00 % 29 9 0 9 0.00 0.00 % 2 0 2
app/components/loopos_ui/async_select/async_select_component.rb 55.00 % 46 20 11 9 0.55 100.00 % 0 0 0
app/components/loopos_ui/async_select/async_select_option_component.html.erb 0.00 % 22 12 0 12 0.00 0.00 % 10 0 10
app/components/loopos_ui/async_select/async_select_option_component.rb 52.00 % 45 25 13 12 0.52 0.00 % 14 0 14
app/components/loopos_ui/async_select/filter_component.rb 70.00 % 18 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/async_select/filter_component.turbo_stream.erb 0.00 % 9 5 0 5 0.00 0.00 % 2 0 2
app/components/loopos_ui/async_select/update_list_component.rb 77.78 % 17 9 7 2 0.78 100.00 % 0 0 0
app/components/loopos_ui/async_select/update_list_component.turbo_stream.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/avatar.rb 71.43 % 43 21 15 6 0.71 0.00 % 4 0 4
app/components/loopos_ui/avatar/avatar.html.erb 0.00 % 4 3 0 3 0.00 100.00 % 0 0 0
app/components/loopos_ui/basic_radio_button.rb 92.31 % 19 13 12 1 0.92 100.00 % 0 0 0
app/components/loopos_ui/basic_radio_button/basic_radio_button.html.erb 0.00 % 11 7 0 7 0.00 0.00 % 2 0 2
app/components/loopos_ui/bottom_bar.rb 100.00 % 6 4 4 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/bottom_bar/bottom_bar.html.erb 0.00 % 10 6 0 6 0.00 100.00 % 0 0 0
app/components/loopos_ui/breadcrumb_list.rb 53.33 % 34 15 8 7 0.53 0.00 % 2 0 2
app/components/loopos_ui/breadcrumb_list/breadcrumb_list.html.erb 0.00 % 10 8 0 8 0.00 0.00 % 2 0 2
app/components/loopos_ui/button.rb 32.26 % 296 124 40 84 0.32 0.00 % 75 0 75
app/components/loopos_ui/button/button.html.erb 0.00 % 37 27 0 27 0.00 0.00 % 22 0 22
app/components/loopos_ui/card/card_component.html.erb 0.00 % 25 14 0 14 0.00 0.00 % 6 0 6
app/components/loopos_ui/card/card_component.rb 90.91 % 17 11 10 1 0.91 100.00 % 0 0 0
app/components/loopos_ui/carousel.rb 77.78 % 39 18 14 4 0.78 100.00 % 0 0 0
app/components/loopos_ui/carousel/carousel.html.erb 0.00 % 46 14 0 14 0.00 100.00 % 0 0 0
app/components/loopos_ui/charts/chart_component.html.erb 0.00 % 57 21 0 21 0.00 0.00 % 10 0 10
app/components/loopos_ui/charts/chart_component.rb 47.37 % 70 19 9 10 0.47 0.00 % 2 0 2
app/components/loopos_ui/chat.rb 100.00 % 14 12 12 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/chat/chat.html.erb 0.00 % 14 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/chip.rb 100.00 % 7 4 4 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/chip/chip.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/chip_list.rb 83.33 % 14 6 5 1 0.83 100.00 % 0 0 0
app/components/loopos_ui/chip_list/chip_list.html.erb 0.00 % 5 3 0 3 0.00 100.00 % 0 0 0
app/components/loopos_ui/code_editor.rb 52.27 % 83 44 23 21 0.52 0.00 % 15 0 15
app/components/loopos_ui/code_editor/code_editor.html.erb 0.00 % 3 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/context_menu.rb 53.33 % 60 30 16 14 0.53 0.00 % 12 0 12
app/components/loopos_ui/context_menu/context_menu.html.erb 0.00 % 20 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/context_menu/item.html.erb 0.00 % 55 19 0 19 0.00 0.00 % 2 0 2
app/components/loopos_ui/core/product_show_header.rb 100.00 % 6 3 3 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/counter.rb 31.25 % 44 16 5 11 0.31 0.00 % 8 0 8
app/components/loopos_ui/counter/counter.html.erb 0.00 % 11 7 0 7 0.00 0.00 % 6 0 6
app/components/loopos_ui/counter_label.rb 50.00 % 43 12 6 6 0.50 0.00 % 6 0 6
app/components/loopos_ui/dashboard/dashboard_component.html.erb 0.00 % 48 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/dashboard/dashboard_component.rb 88.89 % 17 9 8 1 0.89 100.00 % 0 0 0
app/components/loopos_ui/data_card.rb 100.00 % 11 9 9 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/data_card/data_card.html.erb 0.00 % 10 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker.rb 37.50 % 248 104 39 65 0.38 0.00 % 63 0 63
app/components/loopos_ui/date_picker/date_picker.html.erb 0.00 % 53 15 0 15 0.00 0.00 % 9 0 9
app/components/loopos_ui/date_picker/field.rb 85.71 % 22 14 12 2 0.86 100.00 % 0 0 0
app/components/loopos_ui/date_picker/field/field.html.erb 0.00 % 15 6 0 6 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/month.rb 100.00 % 13 9 9 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/month/month.html.erb 0.00 % 9 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/presets.rb 90.91 % 17 11 10 1 0.91 100.00 % 0 0 0
app/components/loopos_ui/date_picker/presets/presets.html.erb 0.00 % 25 6 0 6 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/presets_inline_calendar.rb 100.00 % 8 4 4 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/presets_inline_calendar/presets_inline_calendar.html.erb 0.00 % 5 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/presets_menu.rb 85.71 % 13 7 6 1 0.86 100.00 % 0 0 0
app/components/loopos_ui/date_picker/presets_menu/presets_menu.html.erb 0.00 % 90 12 0 12 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/range.rb 100.00 % 12 8 8 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/range/range.html.erb 0.00 % 9 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/single.rb 100.00 % 13 9 9 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/single/single.html.erb 0.00 % 9 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/year.rb 100.00 % 13 9 9 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/year/year.html.erb 0.00 % 9 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_show.rb 55.56 % 19 9 5 4 0.56 0.00 % 4 0 4
app/components/loopos_ui/date_show/date_show.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/dots.rb 81.82 % 18 11 9 2 0.82 100.00 % 0 0 0
app/components/loopos_ui/dots/dots.html.erb 0.00 % 5 3 0 3 0.00 0.00 % 2 0 2
app/components/loopos_ui/double_state_label.html.erb 0.00 % 14 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/double_state_label.rb 86.67 % 22 15 13 2 0.87 100.00 % 0 0 0
app/components/loopos_ui/drawer_bar.rb 56.86 % 89 51 29 22 0.57 0.00 % 6 0 6
app/components/loopos_ui/drawer_bar/drawer_bar.html.erb 0.00 % 27 9 0 9 0.00 100.00 % 0 0 0
app/components/loopos_ui/dummy_slot.rb 75.00 % 30 12 9 3 0.75 0.00 % 12 0 12
app/components/loopos_ui/dummy_slot/dummy_slot.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/email_preview.rb 100.00 % 5 3 3 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/email_preview/email_preview.html.erb 0.00 % 8 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/entities/app_instance.rb 57.14 % 25 14 8 6 0.57 100.00 % 0 0 0
app/components/loopos_ui/entities/brand.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/catalog_node.rb 66.67 % 14 9 6 3 0.67 100.00 % 0 0 0
app/components/loopos_ui/entities/catalog_node/catalog_node.html.erb 0.00 % 8 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/entities/catalog_section_inherited_protocol.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/catalog_section_protocol.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/category.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/email.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/flow.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/identifier.rb 36.36 % 36 22 8 14 0.36 0.00 % 2 0 2
app/components/loopos_ui/entities/incoming_payment.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/inherited_protocol.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/invoice.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/item.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/partnable.rb 53.85 % 24 13 7 6 0.54 0.00 % 5 0 5
app/components/loopos_ui/entities/partner_group.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/payment.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/pricing_rule.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/pricing_rule_inheritance.rb 72.73 % 16 11 8 3 0.73 100.00 % 0 0 0
app/components/loopos_ui/entities/product.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/protocol.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/provider.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/script.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/script_kind.rb 66.67 % 14 9 6 3 0.67 100.00 % 0 0 0
app/components/loopos_ui/entities/shipping.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/sms.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/template.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entities/user_group.rb 70.00 % 15 10 7 3 0.70 100.00 % 0 0 0
app/components/loopos_ui/entity.rb 59.52 % 99 42 25 17 4.21 0.00 % 14 0 14
app/components/loopos_ui/entity_token.rb 29.73 % 80 37 11 26 0.30 0.00 % 20 0 20
app/components/loopos_ui/error_page.rb 83.33 % 31 6 5 1 0.83 100.00 % 0 0 0
app/components/loopos_ui/error_page/error_page.html.erb 0.00 % 27 10 0 10 0.00 0.00 % 2 0 2
app/components/loopos_ui/extra_data_viewer.rb 76.00 % 39 25 19 6 0.76 0.00 % 5 0 5
app/components/loopos_ui/extra_data_viewer/extra_data_viewer.html.erb 0.00 % 64 20 0 20 0.00 0.00 % 10 0 10
app/components/loopos_ui/features/list.rb 100.00 % 19 6 6 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/features/list/list.html.erb 0.00 % 17 8 0 8 0.00 0.00 % 2 0 2
app/components/loopos_ui/filter/filters_dropdown_component.html.erb 0.00 % 48 19 0 19 0.00 0.00 % 4 0 4
app/components/loopos_ui/filter/filters_dropdown_component.rb 46.15 % 25 13 6 7 0.46 100.00 % 0 0 0
app/components/loopos_ui/filter/search_component.html.erb 0.00 % 13 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/filter/search_component.rb 66.67 % 15 9 6 3 0.67 100.00 % 0 0 0
app/components/loopos_ui/filter_bar.rb 45.45 % 95 55 25 30 0.45 0.00 % 4 0 4
app/components/loopos_ui/filter_bar/filter_bar.html.erb 0.00 % 71 30 0 30 0.00 0.00 % 18 0 18
app/components/loopos_ui/filter_button.rb 46.51 % 192 86 40 46 0.47 0.00 % 26 0 26
app/components/loopos_ui/filter_button/filter_button.html.erb 0.00 % 34 18 0 18 0.00 0.00 % 14 0 14
app/components/loopos_ui/filter_pill.rb 81.82 % 49 22 18 4 0.82 100.00 % 0 0 0
app/components/loopos_ui/filter_pill/filter_pill.html.erb 0.00 % 18 8 0 8 0.00 0.00 % 4 0 4
app/components/loopos_ui/flex_layout.rb 83.33 % 25 12 10 2 0.83 0.00 % 2 0 2
app/components/loopos_ui/flex_layout/flex_layout.html.erb 0.00 % 5 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/flex_layout/media_query_sizes_concern.rb 70.00 % 41 20 14 6 0.70 0.00 % 10 0 10
app/components/loopos_ui/flex_layout/section.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/float_bar.rb 36.36 % 132 55 20 35 1.15 20.00 % 10 2 8
app/components/loopos_ui/float_bar/float_bar.html.erb 0.00 % 34 13 0 13 0.00 100.00 % 0 0 0
app/components/loopos_ui/flows/drawer_bar.rb 57.89 % 106 38 22 16 0.58 0.00 % 4 0 4
app/components/loopos_ui/flows/drawer_bar/drawer_bar.html.erb 0.00 % 70 29 0 29 0.00 0.00 % 10 0 10
app/components/loopos_ui/form_entry.rb 73.08 % 46 26 19 7 0.73 0.00 % 2 0 2
app/components/loopos_ui/form_entry/form_entry.html.erb 0.00 % 35 20 0 20 0.00 0.00 % 14 0 14
app/components/loopos_ui/form_entry_ai.rb 100.00 % 11 9 9 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/form_entry_ai/form_entry_ai.html.erb 0.00 % 37 11 0 11 0.00 0.00 % 2 0 2
app/components/loopos_ui/grid_layout.rb 77.78 % 28 9 7 2 0.78 100.00 % 0 0 0
app/components/loopos_ui/grid_layout/grid_layout.html.erb 0.00 % 5 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/grid_layout/section.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/group_avatar.rb 100.00 % 6 4 4 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/group_avatar/group_avatar.html.erb 0.00 % 6 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/header.rb 83.33 % 42 30 25 5 0.83 0.00 % 4 0 4
app/components/loopos_ui/header/header.html.erb 0.00 % 36 30 0 30 0.00 0.00 % 22 0 22
app/components/loopos_ui/header_component.rb 39.58 % 106 48 19 29 0.40 0.00 % 6 0 6
app/components/loopos_ui/header_component/header_component.html.erb 0.00 % 138 62 0 62 0.00 0.00 % 56 0 56
app/components/loopos_ui/header_component/partnable_applications_component.html.erb 0.00 % 5 2 0 2 0.00 0.00 % 2 0 2
app/components/loopos_ui/header_component/user_menu_component.html.erb 0.00 % 96 38 0 38 0.00 0.00 % 26 0 26
app/components/loopos_ui/history_card.rb 100.00 % 5 3 3 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/history_card/history_card.html.erb 0.00 % 4 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/human_view.rb 55.56 % 51 27 15 12 0.56 0.00 % 11 0 11
app/components/loopos_ui/human_view/human_view.html.erb 0.00 % 27 12 0 12 0.00 0.00 % 6 0 6
app/components/loopos_ui/human_view/tree_node.rb 47.83 % 43 23 11 12 0.48 0.00 % 9 0 9
app/components/loopos_ui/human_view/tree_node/tree_node.html.erb 0.00 % 37 19 0 19 0.00 0.00 % 2 0 2
app/components/loopos_ui/icon.rb 75.00 % 25 12 9 3 0.75 100.00 % 0 0 0
app/components/loopos_ui/icon/icon.html.erb 0.00 % 4 3 0 3 0.00 0.00 % 4 0 4
app/components/loopos_ui/icon_tooltip.rb 40.00 % 14 10 4 6 0.40 0.00 % 2 0 2
app/components/loopos_ui/icon_tooltip/icon_tooltip.html.erb 0.00 % 19 12 0 12 0.00 0.00 % 8 0 8
app/components/loopos_ui/identity_token.rb 43.59 % 109 39 17 22 0.44 0.00 % 14 0 14
app/components/loopos_ui/image.rb 46.15 % 31 13 6 7 0.46 0.00 % 2 0 2
app/components/loopos_ui/image/image.html.erb 0.00 % 26 16 0 16 0.00 0.00 % 8 0 8
app/components/loopos_ui/index_header.rb 75.00 % 26 12 9 3 0.75 0.00 % 4 0 4
app/components/loopos_ui/index_header/index_header.html.erb 0.00 % 11 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/index_layout.rb 66.67 % 24 12 8 4 0.67 0.00 % 4 0 4
app/components/loopos_ui/index_layout/index_layout.html.erb 0.00 % 15 9 0 9 0.00 0.00 % 2 0 2
app/components/loopos_ui/inline_edit_component.rb 39.02 % 109 41 16 25 0.39 0.00 % 6 0 6
app/components/loopos_ui/inline_edit_component/base.html.erb 0.00 % 23 13 0 13 0.00 0.00 % 10 0 10
app/components/loopos_ui/inline_text_edit.rb 84.62 % 23 13 11 2 0.85 100.00 % 0 0 0
app/components/loopos_ui/inline_text_edit/inline_text_edit.html.erb 0.00 % 62 15 0 15 0.00 0.00 % 2 0 2
app/components/loopos_ui/input.rb 54.55 % 51 22 12 10 0.55 0.00 % 8 0 8
app/components/loopos_ui/input/input.html.erb 0.00 % 77 27 0 27 0.00 0.00 % 14 0 14
app/components/loopos_ui/inputs/ai.rb 80.00 % 15 10 8 2 0.80 0.00 % 2 0 2
app/components/loopos_ui/inputs/ai/ai.html.erb 0.00 % 41 14 0 14 0.00 0.00 % 12 0 12
app/components/loopos_ui/inputs/base_input_concern.rb 77.78 % 92 36 28 8 6.28 0.00 % 8 0 8
app/components/loopos_ui/inputs/checkbox.rb 75.00 % 35 24 18 6 0.75 0.00 % 8 0 8
app/components/loopos_ui/inputs/checkbox/checkbox.html.erb 0.00 % 41 18 0 18 0.00 0.00 % 18 0 18
app/components/loopos_ui/inputs/combobox.rb 45.09 % 300 173 78 95 0.45 0.00 % 81 0 81
app/components/loopos_ui/inputs/combobox/combobox.html.erb 0.00 % 82 27 0 27 0.00 0.00 % 14 0 14
app/components/loopos_ui/inputs/date.rb 86.67 % 42 15 13 2 0.87 0.00 % 2 0 2
app/components/loopos_ui/inputs/date/date.html.erb 0.00 % 24 11 0 11 0.00 0.00 % 2 0 2
app/components/loopos_ui/inputs/file.rb 42.86 % 117 70 30 40 0.81 0.00 % 36 0 36
app/components/loopos_ui/inputs/file/direct_upload_concern.rb 30.93 % 209 97 30 67 0.43 0.00 % 54 0 54
app/components/loopos_ui/inputs/file/file_actions.html.erb 0.00 % 32 8 0 8 0.00 100.00 % 0 0 0
app/components/loopos_ui/inputs/file/file_actions.rb 100.00 % 11 7 7 0 1.00 0.00 % 2 0 2
app/components/loopos_ui/inputs/file/form_file.html.erb 0.00 % 99 32 0 32 0.00 0.00 % 10 0 10
app/components/loopos_ui/inputs/file/form_file.rb 38.89 % 93 36 14 22 0.39 0.00 % 22 0 22
app/components/loopos_ui/inputs/file/multiple.html.erb 0.00 % 142 53 0 53 0.00 0.00 % 26 0 26
app/components/loopos_ui/inputs/file/multiple.rb 37.50 % 134 56 21 35 0.38 0.00 % 34 0 34
app/components/loopos_ui/inputs/file/option.html.erb 0.00 % 7 3 0 3 0.00 100.00 % 0 0 0
app/components/loopos_ui/inputs/file/option.rb 100.00 % 11 6 6 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/inputs/file/single.html.erb 0.00 % 77 30 0 30 0.00 0.00 % 24 0 24
app/components/loopos_ui/inputs/file/single.rb 46.88 % 72 32 15 17 0.47 0.00 % 16 0 16
app/components/loopos_ui/inputs/money.rb 66.67 % 41 18 12 6 0.67 0.00 % 5 0 5
app/components/loopos_ui/inputs/money/money.html.erb 0.00 % 13 7 0 7 0.00 100.00 % 0 0 0
app/components/loopos_ui/inputs/number.rb 76.92 % 32 13 10 3 0.77 0.00 % 2 0 2
app/components/loopos_ui/inputs/number/number.html.erb 0.00 % 14 7 0 7 0.00 0.00 % 2 0 2
app/components/loopos_ui/inputs/phone_number.rb 61.76 % 89 34 21 13 0.62 0.00 % 14 0 14
app/components/loopos_ui/inputs/phone_number/phone_number.html.erb 0.00 % 7 5 0 5 0.00 0.00 % 4 0 4
app/components/loopos_ui/inputs/rich_text.rb 85.71 % 18 7 6 1 0.86 100.00 % 0 0 0
app/components/loopos_ui/inputs/rich_text/rich_text.html.erb 0.00 % 18 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/inputs/search.rb 84.62 % 31 13 11 2 0.85 100.00 % 0 0 0
app/components/loopos_ui/inputs/search/search.html.erb 0.00 % 24 12 0 12 0.00 0.00 % 2 0 2
app/components/loopos_ui/inputs/select.rb 64.71 % 69 34 22 12 0.65 0.00 % 13 0 13
app/components/loopos_ui/inputs/select/select.html.erb 0.00 % 92 33 0 33 0.00 0.00 % 28 0 28
app/components/loopos_ui/inputs/select2.rb 48.96 % 195 96 47 49 0.49 0.00 % 45 0 45
app/components/loopos_ui/inputs/select2/select2.html.erb 0.00 % 188 57 0 57 0.00 0.00 % 42 0 42
app/components/loopos_ui/inputs/text.rb 72.73 % 25 11 8 3 0.73 0.00 % 2 0 2
app/components/loopos_ui/inputs/text/text.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/inputs/text_area.rb 72.22 % 38 18 13 5 0.72 0.00 % 4 0 4
app/components/loopos_ui/inputs/text_area/text_area.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/item/state_label.rb 57.14 % 74 35 20 15 0.57 0.00 % 8 0 8
app/components/loopos_ui/item/state_label/state_label.html.erb 0.00 % 24 7 0 7 0.00 0.00 % 2 0 2
app/components/loopos_ui/item_value.rb 86.67 % 23 15 13 2 0.87 100.00 % 0 0 0
app/components/loopos_ui/item_value/item_value.html.erb 0.00 % 10 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/label.rb 42.11 % 36 19 8 11 0.42 0.00 % 6 0 6
app/components/loopos_ui/label/label.html.erb 0.00 % 14 8 0 8 0.00 0.00 % 11 0 11
app/components/loopos_ui/layout_component.html.erb 0.00 % 19 9 0 9 0.00 0.00 % 6 0 6
app/components/loopos_ui/layout_component.rb 36.36 % 18 11 4 7 0.36 100.00 % 0 0 0
app/components/loopos_ui/layout_loading.rb 100.00 % 5 2 2 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/layout_loading/layout_loading.html.erb 0.00 % 7 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/link.rb 90.00 % 15 10 9 1 0.90 100.00 % 0 0 0
app/components/loopos_ui/link/link.html.erb 0.00 % 12 8 0 8 0.00 0.00 % 4 0 4
app/components/loopos_ui/loading_ai.rb 100.00 % 5 2 2 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/loading_ai/loading_ai.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/loadings/skeleton.rb 80.00 % 17 10 8 2 0.80 100.00 % 0 0 0
app/components/loopos_ui/loadings/skeleton/skeleton.html.erb 0.00 % 12 5 0 5 0.00 0.00 % 2 0 2
app/components/loopos_ui/log.rb 29.20 % 272 113 33 80 0.30 0.00 % 40 0 40
app/components/loopos_ui/log/factories.rb 63.16 % 39 19 12 7 0.74 0.00 % 6 0 6
app/components/loopos_ui/log/factories/base.rb 44.44 % 34 18 8 10 0.44 0.00 % 2 0 2
app/components/loopos_ui/log/factories/core_item_log_presenter_hash.rb 23.64 % 128 55 13 42 0.24 0.00 % 22 0 22
app/components/loopos_ui/log/factories/script_log.rb 34.69 % 126 49 17 32 0.35 0.00 % 6 0 6
app/components/loopos_ui/log/log.html.erb 0.00 % 50 29 0 29 0.00 0.00 % 6 0 6
app/components/loopos_ui/log/script/info_modal_body.html.erb 0.00 % 63 39 0 39 0.00 0.00 % 44 0 44
app/components/loopos_ui/log/script/info_modal_body.rb 100.00 % 9 5 5 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/log_list.rb 35.80 % 183 81 29 52 0.40 3.33 % 30 1 29
app/components/loopos_ui/log_list/factories.rb 66.67 % 37 18 12 6 0.78 0.00 % 6 0 6
app/components/loopos_ui/log_list/factories/base.rb 50.00 % 40 22 11 11 0.50 0.00 % 2 0 2
app/components/loopos_ui/log_list/factories/core_item_log_list_presenter_hash.rb 26.92 % 53 26 7 19 0.27 0.00 % 6 0 6
app/components/loopos_ui/log_list/factories/script.rb 42.86 % 35 14 6 8 0.43 0.00 % 4 0 4
app/components/loopos_ui/log_list/group.html.erb 0.00 % 7 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/log_list/group.rb 87.50 % 14 8 7 1 0.88 100.00 % 0 0 0
app/components/loopos_ui/log_list/log_list.html.erb 0.00 % 20 11 0 11 0.00 0.00 % 4 0 4
app/components/loopos_ui/logo.rb 42.86 % 10 7 3 4 0.43 100.00 % 0 0 0
app/components/loopos_ui/logo/logo.html.erb 0.00 % 8 4 0 4 0.00 0.00 % 2 0 2
app/components/loopos_ui/logs_component/log_content_component.html.erb 0.00 % 26 15 0 15 0.00 0.00 % 10 0 10
app/components/loopos_ui/logs_component/log_content_component.rb 75.00 % 18 12 9 3 0.75 0.00 % 2 0 2
app/components/loopos_ui/logs_component/log_group_component.html.erb 0.00 % 5 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/logs_component/logs_component.html.erb 0.00 % 19 11 0 11 0.00 0.00 % 2 0 2
app/components/loopos_ui/m_icon.rb 74.29 % 79 35 26 9 112.29 0.00 % 6 0 6
app/components/loopos_ui/m_icon/m_icon.html.erb 0.00 % 7 4 0 4 0.00 0.00 % 2 0 2
app/components/loopos_ui/main_component.rb 100.00 % 8 4 4 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/main_component/main_component.html.erb 0.00 % 8 3 0 3 0.00 100.00 % 0 0 0
app/components/loopos_ui/main_layout.rb 45.45 % 45 22 10 12 0.45 0.00 % 12 0 12
app/components/loopos_ui/main_layout/main_layout.html.erb 0.00 % 17 8 0 8 0.00 0.00 % 4 0 4
app/components/loopos_ui/marketing_layout.rb 50.00 % 28 16 8 8 0.50 0.00 % 2 0 2
app/components/loopos_ui/marketing_layout/marketing_layout.html.erb 0.00 % 24 10 0 10 0.00 100.00 % 0 0 0
app/components/loopos_ui/marketplace/footer.rb 90.00 % 16 10 9 1 0.90 0.00 % 2 0 2
app/components/loopos_ui/marketplace/footer/footer.html.erb 0.00 % 15 9 0 9 0.00 0.00 % 4 0 4
app/components/loopos_ui/marketplace/header.rb 88.89 % 16 9 8 1 0.89 0.00 % 2 0 2
app/components/loopos_ui/marketplace/header/header.html.erb 0.00 % 10 7 0 7 0.00 0.00 % 2 0 2
app/components/loopos_ui/marketplace/item_card.rb 41.67 % 129 72 30 42 0.42 0.00 % 28 0 28
app/components/loopos_ui/marketplace/item_card/item_card.html.erb 0.00 % 35 24 0 24 0.00 0.00 % 18 0 18
app/components/loopos_ui/marketplace/item_details.rb 58.82 % 30 17 10 7 0.59 0.00 % 6 0 6
app/components/loopos_ui/marketplace/item_details/item_details.html.erb 0.00 % 22 14 0 14 0.00 0.00 % 2 0 2
app/components/loopos_ui/marketplace/payment_summary.rb 90.00 % 17 10 9 1 0.90 100.00 % 0 0 0
app/components/loopos_ui/marketplace/payment_summary/payment_summary.html.erb 0.00 % 19 10 0 10 0.00 100.00 % 0 0 0
app/components/loopos_ui/mini_app.rb 90.91 % 19 11 10 1 0.91 100.00 % 0 0 0
app/components/loopos_ui/mini_app/mini_app.html.erb 0.00 % 11 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/modal.rb 79.31 % 64 29 23 6 0.79 0.00 % 2 0 2
app/components/loopos_ui/modal/modal.html.erb 0.00 % 48 25 0 25 0.00 0.00 % 14 0 14
app/components/loopos_ui/modal_component.rb 33.33 % 20 15 5 10 0.33 100.00 % 0 0 0
app/components/loopos_ui/modal_component/modal_component.html.erb 0.00 % 29 16 0 16 0.00 0.00 % 32 0 32
app/components/loopos_ui/model_association_list.rb 47.37 % 200 76 36 40 0.50 0.00 % 27 0 27
app/components/loopos_ui/model_association_list/model_association_list.html.erb 0.00 % 35 19 0 19 0.00 0.00 % 4 0 4
app/components/loopos_ui/model_association_overlay.rb 54.41 % 173 68 37 31 0.54 0.00 % 18 0 18
app/components/loopos_ui/model_association_overlay/attached_item.html.erb 0.00 % 7 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/model_association_overlay/context.rb 72.09 % 115 43 31 12 0.81 0.00 % 10 0 10
app/components/loopos_ui/model_association_overlay/item_factory.rb 38.10 % 206 84 32 52 0.38 0.00 % 60 0 60
app/components/loopos_ui/model_association_overlay/missing_item.html.erb 0.00 % 9 6 0 6 0.00 100.00 % 0 0 0
app/components/loopos_ui/model_association_overlay/model_association_overlay.html.erb 0.00 % 31 17 0 17 0.00 0.00 % 4 0 4
app/components/loopos_ui/model_association_overlay/new_item.html.erb 0.00 % 27 10 0 10 0.00 100.00 % 0 0 0
app/components/loopos_ui/multiple_list.rb 50.00 % 9 6 3 3 0.50 100.00 % 0 0 0
app/components/loopos_ui/page_header.rb 65.52 % 63 29 19 10 0.66 0.00 % 4 0 4
app/components/loopos_ui/page_header/page_header.html.erb 0.00 % 27 16 0 16 0.00 0.00 % 4 0 4
app/components/loopos_ui/page_section/identifier_editor.rb 56.25 % 79 32 18 14 0.56 0.00 % 9 0 9
app/components/loopos_ui/page_section/identifier_editor/identifier_editor.html.erb 0.00 % 19 10 0 10 0.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/identifier_editor/result.html.erb 0.00 % 35 16 0 16 0.00 0.00 % 2 0 2
app/components/loopos_ui/page_section/identifier_editor/result.rb 90.91 % 18 11 10 1 0.91 100.00 % 0 0 0
app/components/loopos_ui/page_section/item_tab.rb 55.56 % 16 9 5 4 0.56 0.00 % 2 0 2
app/components/loopos_ui/page_section/item_tab/advanced_details.rb 53.85 % 55 26 14 12 0.54 0.00 % 4 0 4
app/components/loopos_ui/page_section/item_tab/advanced_details/advanced_details.html.erb 0.00 % 87 46 0 46 0.00 0.00 % 4 0 4
app/components/loopos_ui/page_section/item_tab/chat.rb 100.00 % 11 6 6 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/item_tab/chat/chat.html.erb 0.00 % 123 30 0 30 0.00 0.00 % 12 0 12
app/components/loopos_ui/page_section/item_tab/customer_info.rb 32.88 % 171 73 24 49 0.33 0.00 % 27 0 27
app/components/loopos_ui/page_section/item_tab/customer_info/customer_info.html.erb 0.00 % 124 51 0 51 0.00 0.00 % 8 0 8
app/components/loopos_ui/page_section/item_tab/financial_data.rb 53.61 % 264 97 52 45 0.54 0.00 % 38 0 38
app/components/loopos_ui/page_section/item_tab/financial_data/financial_data.html.erb 0.00 % 165 88 0 88 0.00 0.00 % 30 0 30
app/components/loopos_ui/page_section/item_tab/financial_data/item_marketplace_value_out_card.html.erb 0.00 % 25 12 0 12 0.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/item_tab/financial_data/item_value_card.html.erb 0.00 % 313 158 0 158 0.00 0.00 % 126 0 126
app/components/loopos_ui/page_section/item_tab/financial_data/item_value_table.html.erb 0.00 % 141 64 0 64 0.00 0.00 % 40 0 40
app/components/loopos_ui/page_section/item_tab/financial_data/logs_list.html.erb 0.00 % 11 5 0 5 0.00 0.00 % 2 0 2
app/components/loopos_ui/page_section/item_tab/financial_data/other_values_section.html.erb 0.00 % 128 57 0 57 0.00 0.00 % 14 0 14
app/components/loopos_ui/page_section/item_tab/financial_data/transactions_list.html.erb 0.00 % 22 12 0 12 0.00 0.00 % 6 0 6
app/components/loopos_ui/page_section/item_tab/flow.rb 76.92 % 26 13 10 3 0.77 100.00 % 0 0 0
app/components/loopos_ui/page_section/item_tab/flow/flow.html.erb 0.00 % 38 15 0 15 0.00 0.00 % 4 0 4
app/components/loopos_ui/page_section/item_tab/impact_widget.rb 64.29 % 27 14 9 5 0.64 100.00 % 0 0 0
app/components/loopos_ui/page_section/item_tab/impact_widget/impact_widget.html.erb 0.00 % 31 15 0 15 0.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/item_tab/info.rb 68.00 % 41 25 17 8 0.68 0.00 % 4 0 4
app/components/loopos_ui/page_section/item_tab/info/identifiers_table.html.erb 0.00 % 83 29 0 29 0.00 0.00 % 4 0 4
app/components/loopos_ui/page_section/item_tab/info/info.html.erb 0.00 % 150 88 0 88 0.00 0.00 % 34 0 34
app/components/loopos_ui/page_section/item_tab/logs.rb 26.47 % 206 102 27 75 0.26 0.00 % 46 0 46
app/components/loopos_ui/page_section/item_tab/logs/logs.html.erb 0.00 % 108 43 0 43 0.00 0.00 % 10 0 10
app/components/loopos_ui/page_section/item_tab/notes.rb 72.22 % 33 18 13 5 0.72 0.00 % 2 0 2
app/components/loopos_ui/page_section/item_tab/notes/notes.html.erb 0.00 % 61 30 0 30 0.00 0.00 % 6 0 6
app/components/loopos_ui/page_section/item_tab/protocol_responses.rb 66.67 % 38 18 12 6 0.67 0.00 % 4 0 4
app/components/loopos_ui/page_section/item_tab/protocol_responses/protocol_answer_value.html.erb 0.00 % 20 10 0 10 0.00 0.00 % 9 0 9
app/components/loopos_ui/page_section/item_tab/protocol_responses/protocol_responses.html.erb 0.00 % 96 46 0 46 0.00 0.00 % 34 0 34
app/components/loopos_ui/page_section/item_tab/rfi.rb 80.43 % 77 46 37 9 0.80 0.00 % 2 0 2
app/components/loopos_ui/page_section/item_tab/rfi/rfi.html.erb 0.00 % 49 20 0 20 0.00 0.00 % 2 0 2
app/components/loopos_ui/page_section/item_tab/rfi/rfi_answer_history_table.html.erb 0.00 % 90 40 0 40 0.00 0.00 % 20 0 20
app/components/loopos_ui/page_section/item_tab/rfi/rfi_answers_index_table.html.erb 0.00 % 112 44 0 44 0.00 0.00 % 12 0 12
app/components/loopos_ui/page_section/item_tab/rfi/rfi_answers_step_table.html.erb 0.00 % 134 58 0 58 0.00 0.00 % 22 0 22
app/components/loopos_ui/page_section/item_tab/rfi/rfi_responders_without_rfi_answers_table.html.erb 0.00 % 79 32 0 32 0.00 0.00 % 12 0 12
app/components/loopos_ui/page_section/item_tab/services.rb 30.19 % 100 53 16 37 0.30 0.00 % 34 0 34
app/components/loopos_ui/page_section/item_tab/services/services.html.erb 0.00 % 139 55 0 55 0.00 0.00 % 12 0 12
app/components/loopos_ui/page_section/item_tab/trade_in.rb 66.67 % 35 18 12 6 0.67 0.00 % 4 0 4
app/components/loopos_ui/page_section/item_tab/trade_in/trade_in.html.erb 0.00 % 75 42 0 42 0.00 0.00 % 6 0 6
app/components/loopos_ui/page_section/item_tab/validation_widget.rb 100.00 % 13 6 6 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/item_tab/validation_widget/validation_widget.html.erb 0.00 % 44 21 0 21 0.00 0.00 % 2 0 2
app/components/loopos_ui/page_section/rfi_answer_tab.rb 55.56 % 16 9 5 4 0.56 0.00 % 2 0 2
app/components/loopos_ui/page_section/rfi_answer_tab/history.rb 100.00 % 13 8 8 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/rfi_answer_tab/history/history.html.erb 0.00 % 16 7 0 7 0.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/rfi_answer_tab/info.rb 100.00 % 13 8 8 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/rfi_answer_tab/info/info.html.erb 0.00 % 87 49 0 49 0.00 0.00 % 24 0 24
app/components/loopos_ui/page_section/rfi_tab.rb 55.56 % 16 9 5 4 0.56 0.00 % 2 0 2
app/components/loopos_ui/page_section/rfi_tab/advanced_details.rb 100.00 % 11 6 6 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/rfi_tab/advanced_details/advanced_details.html.erb 0.00 % 16 8 0 8 0.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/rfi_tab/info.rb 100.00 % 13 8 8 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/rfi_tab/info/info.html.erb 0.00 % 161 93 0 93 0.00 0.00 % 32 0 32
app/components/loopos_ui/page_section/rfi_tab/rfi_answers.rb 100.00 % 12 7 7 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/rfi_tab/rfi_answers/rfi_answers.html.erb 0.00 % 17 7 0 7 0.00 100.00 % 0 0 0
app/components/loopos_ui/pages/item_show.html.erb 0.00 % 78 15 0 15 0.00 0.00 % 18 0 18
app/components/loopos_ui/pages/item_show.rb 53.70 % 120 54 29 25 0.54 0.00 % 8 0 8
app/components/loopos_ui/pages/item_show_header.html.erb 0.00 % 25 15 0 15 0.00 0.00 % 4 0 4
app/components/loopos_ui/pages/item_show_header.rb 70.00 % 47 20 14 6 0.70 100.00 % 0 0 0
app/components/loopos_ui/pages/reports_index.html.erb 0.00 % 45 20 0 20 0.00 0.00 % 4 0 4
app/components/loopos_ui/pages/reports_index.rb 59.38 % 79 32 19 13 0.59 0.00 % 12 0 12
app/components/loopos_ui/pages/reports_index_table.html.erb 0.00 % 48 19 0 19 0.00 0.00 % 4 0 4
app/components/loopos_ui/pages/rfi/rfi_show.html.erb 0.00 % 13 8 0 8 0.00 0.00 % 2 0 2
app/components/loopos_ui/pages/rfi/rfi_show.rb 70.00 % 41 20 14 6 0.70 0.00 % 5 0 5
app/components/loopos_ui/pages/rfi_answer/show.html.erb 0.00 % 10 6 0 6 0.00 100.00 % 0 0 0
app/components/loopos_ui/pages/rfi_answer/show.rb 69.57 % 46 23 16 7 0.70 0.00 % 5 0 5
app/components/loopos_ui/pagination.rb 50.79 % 137 63 32 31 0.51 0.00 % 8 0 8
app/components/loopos_ui/pagination/pagination.html.erb 0.00 % 11 7 0 7 0.00 0.00 % 2 0 2
app/components/loopos_ui/popover.rb 76.00 % 100 25 19 6 0.84 100.00 % 0 0 0
app/components/loopos_ui/popover/popover.html.erb 0.00 % 10 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/popover_template.rb 100.00 % 7 5 5 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/popover_template/popover_template.html.erb 0.00 % 14 8 0 8 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/add_element/add_element_component.html.erb 0.00 % 36 10 0 10 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/add_element/add_element_component.rb 55.00 % 48 20 11 9 0.55 100.00 % 0 0 0
app/components/loopos_ui/protocol/add_element/filter_component.rb 72.73 % 20 11 8 3 0.73 100.00 % 0 0 0
app/components/loopos_ui/protocol/add_element/filter_component.turbo_stream.erb 0.00 % 35 14 0 14 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/add_element/select_option_component.html.erb 0.00 % 13 9 0 9 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/add_element/select_option_component.rb 50.00 % 50 28 14 14 0.50 0.00 % 14 0 14
app/components/loopos_ui/protocol/add_protocol.html.erb 0.00 % 98 40 0 40 0.00 0.00 % 14 0 14
app/components/loopos_ui/protocol/attach_node/filter_component.rb 72.73 % 20 11 8 3 0.73 100.00 % 0 0 0
app/components/loopos_ui/protocol/attach_node/filter_component.turbo_stream.erb 0.00 % 9 5 0 5 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/attach_node/select_option_component.html.erb 0.00 % 32 17 0 17 0.00 0.00 % 20 0 20
app/components/loopos_ui/protocol/attach_node/select_option_component.rb 53.85 % 47 26 14 12 0.54 0.00 % 14 0 14
app/components/loopos_ui/protocol/drawer_bar.rb 87.50 % 14 8 7 1 0.88 100.00 % 0 0 0
app/components/loopos_ui/protocol/drawer_bar/drawer_bar.html.erb 0.00 % 73 38 0 38 0.00 0.00 % 6 0 6
app/components/loopos_ui/protocol/elements/fields/array.html.erb 0.00 % 79 23 0 23 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/elements/fields/array.rb 50.00 % 24 16 8 8 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/fields/boolean.html.erb 0.00 % 26 17 0 17 0.00 0.00 % 4 0 4
app/components/loopos_ui/protocol/elements/fields/boolean.rb 50.00 % 24 16 8 8 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/fields/custom.html.erb 0.00 % 60 19 0 19 0.00 0.00 % 6 0 6
app/components/loopos_ui/protocol/elements/fields/custom.rb 50.00 % 24 16 8 8 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/fields/date.html.erb 0.00 % 38 16 0 16 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/elements/fields/date.rb 50.00 % 24 16 8 8 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/fields/enum.html.erb 0.00 % 23 14 0 14 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/elements/fields/enum.rb 50.00 % 24 16 8 8 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/fields/granularity.html.erb 0.00 % 21 14 0 14 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/elements/fields/granularity.rb 50.00 % 24 16 8 8 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/fields/image.html.erb 0.00 % 21 13 0 13 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/elements/fields/image.rb 50.00 % 24 16 8 8 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/fields/integer.html.erb 0.00 % 19 9 0 9 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/fields/integer.rb 50.00 % 24 16 8 8 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/fields/number.html.erb 0.00 % 32 16 0 16 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/elements/fields/number.rb 50.00 % 23 16 8 8 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/fields/option.html.erb 0.00 % 75 33 0 33 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/elements/fields/option.rb 44.44 % 27 18 8 10 0.44 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/fields/options.html.erb 0.00 % 45 22 0 22 0.00 0.00 % 6 0 6
app/components/loopos_ui/protocol/elements/fields/options.rb 50.00 % 24 16 8 8 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/fields/string.html.erb 0.00 % 32 17 0 17 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/elements/fields/string.rb 50.00 % 24 16 8 8 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/break.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/break.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/html.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/html.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/image.html.erb 0.00 % 2 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/image.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/text.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/text.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/title.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/title.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/url.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/url.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/bool.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/bool.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/custom.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/custom.rb 58.82 % 28 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/date.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/date.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/date_range.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/date_range.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/email.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/email.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/files.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/files.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/iban.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/iban.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/identifier.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/identifier.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/images.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/images.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/money.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/money.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/number.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/number.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/phone_number.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/phone_number.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/postal_code.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/postal_code.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/select.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/select.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/text.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/text.rb 58.82 % 27 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/settings.html.erb 0.00 % 72 30 0 30 0.00 0.00 % 4 0 4
app/components/loopos_ui/protocol/elements/settings.rb 53.57 % 51 28 15 13 0.54 0.00 % 6 0 6
app/components/loopos_ui/protocol/elements/structure/divider.html.erb 0.00 % 94 40 0 40 0.00 0.00 % 22 0 22
app/components/loopos_ui/protocol/elements/structure/divider.rb 58.82 % 28 17 10 7 0.59 100.00 % 0 0 0
app/components/loopos_ui/protocol/protocol_builder_component.erb 0.00 % 9 7 0 7 0.00 0.00 % 4 0 4
app/components/loopos_ui/protocol/protocol_builder_component.rb 65.00 % 36 20 13 7 0.65 100.00 % 0 0 0
app/components/loopos_ui/protocol/protocol_component.html.erb 0.00 % 64 30 0 30 0.00 0.00 % 20 0 20
app/components/loopos_ui/protocol/protocol_component.rb 55.00 % 30 20 11 9 0.55 100.00 % 0 0 0
app/components/loopos_ui/protocol/protocol_element_component.html.erb 0.00 % 206 102 0 102 0.00 0.00 % 58 0 58
app/components/loopos_ui/protocol/protocol_element_component.rb 50.00 % 24 16 8 8 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/protocol_filter_component.html.erb 0.00 % 121 54 0 54 0.00 0.00 % 18 0 18
app/components/loopos_ui/protocol/protocol_filter_component.rb 50.00 % 15 10 5 5 0.50 100.00 % 0 0 0
app/components/loopos_ui/protocol/protocol_header.html.erb 0.00 % 201 105 0 105 0.00 0.00 % 46 0 46
app/components/loopos_ui/protocol/protocol_sidebar_component.erb 0.00 % 14 7 0 7 0.00 0.00 % 4 0 4
app/components/loopos_ui/protocol/protocol_sidebar_component.rb 87.50 % 14 8 7 1 0.88 100.00 % 0 0 0
app/components/loopos_ui/protocol/protocols_area_component.erb 0.00 % 47 25 0 25 0.00 0.00 % 8 0 8
app/components/loopos_ui/protocol/protocols_area_component.rb 46.67 % 65 30 14 16 0.47 100.00 % 0 0 0
app/components/loopos_ui/protocol/settings.html.erb 0.00 % 155 57 0 57 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/settings.rb 53.85 % 48 26 14 12 0.54 0.00 % 4 0 4
app/components/loopos_ui/protocol_answer_value.rb 100.00 % 4 2 2 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/protocol_answer_value/protocol_answer_value.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol_element/drawer_bar.rb 90.91 % 17 11 10 1 0.91 100.00 % 0 0 0
app/components/loopos_ui/protocol_element/drawer_bar/drawer_bar.html.erb 0.00 % 60 26 0 26 0.00 0.00 % 8 0 8
app/components/loopos_ui/protocol_form.rb 30.41 % 725 388 118 270 0.30 0.00 % 214 0 214
app/components/loopos_ui/protocol_form/protocol_form.html.erb 0.00 % 374 151 0 151 0.00 0.00 % 81 0 81
app/components/loopos_ui/radio_card.rb 81.82 % 38 22 18 4 0.82 0.00 % 3 0 3
app/components/loopos_ui/radio_card/content.html.erb 0.00 % 26 12 0 12 0.00 0.00 % 2 0 2
app/components/loopos_ui/radio_card/radio_card.html.erb 0.00 % 6 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/script_editor.rb 100.00 % 22 17 17 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/script_editor/script_editor.html.erb 0.00 % 17 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/script_input_form.rb 42.86 % 58 28 12 16 0.43 0.00 % 10 0 10
app/components/loopos_ui/script_input_form/script_input_form.html.erb 0.00 % 82 31 0 31 0.00 0.00 % 13 0 13
app/components/loopos_ui/services/base_concern.rb 73.68 % 42 19 14 5 1.00 0.00 % 2 0 2
app/components/loopos_ui/services/email_messages/table.rb 38.00 % 114 50 19 31 0.42 0.00 % 22 0 22
app/components/loopos_ui/services/email_messages/table/table.html.erb 0.00 % 83 47 0 47 0.00 0.00 % 22 0 22
app/components/loopos_ui/services/email_messages/tabs/info.rb 84.62 % 26 13 11 2 0.85 0.00 % 2 0 2
app/components/loopos_ui/services/email_messages/tabs/info/info.html.erb 0.00 % 95 57 0 57 0.00 0.00 % 18 0 18
app/components/loopos_ui/services/email_templates/table.rb 45.95 % 96 37 17 20 0.46 0.00 % 10 0 10
app/components/loopos_ui/services/email_templates/table/table.html.erb 0.00 % 110 49 0 49 0.00 0.00 % 22 0 22
app/components/loopos_ui/services/email_templates/tabs/info.rb 86.67 % 29 15 13 2 0.87 0.00 % 2 0 2
app/components/loopos_ui/services/email_templates/tabs/info/info.html.erb 0.00 % 77 33 0 33 0.00 0.00 % 8 0 8
app/components/loopos_ui/services/extra_data.rb 100.00 % 9 4 4 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/services/extra_data/extra_data.html.erb 0.00 % 25 12 0 12 0.00 0.00 % 2 0 2
app/components/loopos_ui/services/incoming_payments/table.rb 41.30 % 87 46 19 27 0.43 0.00 % 16 0 16
app/components/loopos_ui/services/incoming_payments/table/table.html.erb 0.00 % 109 67 0 67 0.00 0.00 % 26 0 26
app/components/loopos_ui/services/incoming_payments/tabs/info.rb 84.62 % 26 13 11 2 0.85 0.00 % 2 0 2
app/components/loopos_ui/services/incoming_payments/tabs/info/info.html.erb 0.00 % 109 69 0 69 0.00 0.00 % 18 0 18
app/components/loopos_ui/services/invoices/show.rb 100.00 % 11 6 6 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/services/invoices/show/show.html.erb 0.00 % 28 16 0 16 0.00 0.00 % 8 0 8
app/components/loopos_ui/services/invoices/table.rb 37.74 % 99 53 20 33 0.40 0.00 % 18 0 18
app/components/loopos_ui/services/invoices/table/table.html.erb 0.00 % 125 70 0 70 0.00 0.00 % 26 0 26
app/components/loopos_ui/services/invoices/tabs/info.rb 73.08 % 48 26 19 7 0.73 0.00 % 2 0 2
app/components/loopos_ui/services/invoices/tabs/info/info.html.erb 0.00 % 106 60 0 60 0.00 0.00 % 14 0 14
app/components/loopos_ui/services/issues_area.rb 100.00 % 7 4 4 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/services/issues_area/issues_area.html.erb 0.00 % 11 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/services/list.rb 100.00 % 18 6 6 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/services/list/list.html.erb 0.00 % 14 7 0 7 0.00 0.00 % 2 0 2
app/components/loopos_ui/services/payments/table.rb 40.43 % 104 47 19 28 0.45 0.00 % 16 0 16
app/components/loopos_ui/services/payments/table/table.html.erb 0.00 % 147 78 0 78 0.00 0.00 % 30 0 30
app/components/loopos_ui/services/payments/tabs/info.rb 84.62 % 26 13 11 2 0.85 0.00 % 2 0 2
app/components/loopos_ui/services/payments/tabs/info/info.html.erb 0.00 % 105 64 0 64 0.00 0.00 % 20 0 20
app/components/loopos_ui/services/shipping_pickups/table.rb 45.45 % 70 33 15 18 0.45 0.00 % 10 0 10
app/components/loopos_ui/services/shipping_pickups/table/table.html.erb 0.00 % 50 31 0 31 0.00 0.00 % 10 0 10
app/components/loopos_ui/services/shipping_pickups/tabs/info.rb 88.89 % 16 9 8 1 0.89 100.00 % 0 0 0
app/components/loopos_ui/services/shipping_pickups/tabs/info/info.html.erb 0.00 % 47 29 0 29 0.00 100.00 % 0 0 0
app/components/loopos_ui/services/shippings/table.rb 38.46 % 118 52 20 32 0.40 0.00 % 22 0 22
app/components/loopos_ui/services/shippings/table/table.html.erb 0.00 % 248 120 0 120 0.00 0.00 % 40 0 40
app/components/loopos_ui/services/shippings/tabs/info.rb 90.91 % 22 11 10 1 0.91 0.00 % 2 0 2
app/components/loopos_ui/services/shippings/tabs/info/info.html.erb 0.00 % 137 87 0 87 0.00 0.00 % 16 0 16
app/components/loopos_ui/services/shippings/tabs/packages.rb 90.00 % 18 10 9 1 1.10 100.00 % 0 0 0
app/components/loopos_ui/services/shippings/tabs/packages/packages.html.erb 0.00 % 153 83 0 83 0.00 0.00 % 20 0 20
app/components/loopos_ui/services/sms_messages/table.rb 38.00 % 105 50 19 31 0.40 0.00 % 20 0 20
app/components/loopos_ui/services/sms_messages/table/table.html.erb 0.00 % 82 46 0 46 0.00 0.00 % 24 0 24
app/components/loopos_ui/services/sms_messages/tabs/info.rb 84.62 % 26 13 11 2 0.85 0.00 % 2 0 2
app/components/loopos_ui/services/sms_messages/tabs/info/info.html.erb 0.00 % 109 69 0 69 0.00 0.00 % 18 0 18
app/components/loopos_ui/services/tabs_layout.rb 100.00 % 7 4 4 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/services/tabs_layout/tabs_layout.htm.erb 0.00 % 47 20 0 20 0.00 0.00 % 17 0 17
app/components/loopos_ui/show_header.rb 56.25 % 72 32 18 14 0.56 0.00 % 6 0 6
app/components/loopos_ui/show_header/show_header.html.erb 0.00 % 67 35 0 35 0.00 0.00 % 16 0 16
app/components/loopos_ui/show_layout.rb 75.00 % 28 8 6 2 0.75 100.00 % 0 0 0
app/components/loopos_ui/show_layout/show_layout.html.erb 0.00 % 18 8 0 8 0.00 0.00 % 2 0 2
app/components/loopos_ui/show_layout_component/show_layout_component.html.erb 0.00 % 6 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar.rb 80.00 % 31 15 12 3 0.80 0.00 % 2 0 2
app/components/loopos_ui/sidebar/configuration.rb 100.00 % 23 14 14 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar/v1/divider.html.erb 0.00 % 2 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar/v1/divider.rb 100.00 % 10 4 4 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar/v1/drawer_divider.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar/v1/drawer_divider.rb 100.00 % 10 4 4 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar/v1/drawer_entry.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar/v1/drawer_item.html.erb 0.00 % 52 25 0 25 0.00 0.00 % 12 0 12
app/components/loopos_ui/sidebar/v1/drawer_item.rb 37.97 % 156 79 30 49 0.38 0.00 % 47 0 47
app/components/loopos_ui/sidebar/v1/item.rb 34.69 % 194 98 34 64 0.35 0.00 % 52 0 52
app/components/loopos_ui/sidebar/v1/sidebar.html.erb 0.00 % 16 10 0 10 0.00 0.00 % 2 0 2
app/components/loopos_ui/sidebar/v1/sidebar.rb 83.33 % 25 12 10 2 0.83 100.00 % 0 0 0
app/components/loopos_ui/sidebar/v1/single_item.html.erb 0.00 % 22 12 0 12 0.00 0.00 % 8 0 8
app/components/loopos_ui/sidebar/v1/single_item.rb 36.84 % 40 19 7 12 0.37 0.00 % 12 0 12
app/components/loopos_ui/sidebar/v2/group.html.erb 0.00 % 9 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar/v2/item.html.erb 0.00 % 37 17 0 17 0.00 0.00 % 8 0 8
app/components/loopos_ui/sidebar/v2/sidebar.html.erb 0.00 % 39 22 0 22 0.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar/v2/sidebar.rb 61.11 % 153 54 33 21 0.61 0.00 % 11 0 11
app/components/loopos_ui/sidebar_layout_component.rb 58.82 % 31 17 10 7 0.59 0.00 % 4 0 4
app/components/loopos_ui/sidebar_layout_component/sidebar_old_component.html.erb 0.00 % 3 2 0 2 0.00 0.00 % 2 0 2
app/components/loopos_ui/sidebar_old_component.rb 53.85 % 30 13 7 6 0.54 0.00 % 4 0 4
app/components/loopos_ui/spinner.rb 46.67 % 32 15 7 8 0.47 0.00 % 5 0 5
app/components/loopos_ui/spinner/spinner.html.erb 0.00 % 12 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/state_label.rb 62.96 % 79 27 17 10 0.63 0.00 % 12 0 12
app/components/loopos_ui/status_dot.rb 80.00 % 9 5 4 1 0.80 100.00 % 0 0 0
app/components/loopos_ui/status_dot/status_dot.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/stepper.rb 60.53 % 102 38 23 15 0.61 0.00 % 7 0 7
app/components/loopos_ui/stepper/step.html.erb 0.00 % 21 15 0 15 0.00 0.00 % 2 0 2
app/components/loopos_ui/stepper/stepper.html.erb 0.00 % 8 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/stepper_panel.rb 35.00 % 46 20 7 13 0.35 100.00 % 0 0 0
app/components/loopos_ui/stepper_panel/stepper_panel.html.erb 0.00 % 13 9 0 9 0.00 0.00 % 2 0 2
app/components/loopos_ui/table/pagination_component.html.erb 0.00 % 39 28 0 28 0.00 0.00 % 8 0 8
app/components/loopos_ui/table/pagination_component.rb 46.15 % 21 13 6 7 0.46 100.00 % 0 0 0
app/components/loopos_ui/table/table_component.html.erb 0.00 % 61 35 0 35 0.00 0.00 % 26 0 26
app/components/loopos_ui/table/table_component.rb 31.71 % 84 41 13 28 0.32 0.00 % 4 0 4
app/components/loopos_ui/table_data_list.rb 64.00 % 42 25 16 9 0.64 0.00 % 6 0 6
app/components/loopos_ui/table_data_list/label_header_row.html.erb 0.00 % 4 3 0 3 0.00 100.00 % 0 0 0
app/components/loopos_ui/table_data_list/table_data_list.html.erb 0.00 % 49 21 0 21 0.00 100.00 % 0 0 0
app/components/loopos_ui/table_filter.rb 72.73 % 17 11 8 3 0.73 0.00 % 6 0 6
app/components/loopos_ui/table_filter/table_filter.html.erb 0.00 % 11 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/tabs_component/tabs_component.html.erb 0.00 % 14 8 0 8 0.00 100.00 % 0 0 0
app/components/loopos_ui/tabs_component/tabs_component.rb 34.38 % 50 32 11 21 0.34 0.00 % 6 0 6
app/components/loopos_ui/tabs_content.rb 64.29 % 25 14 9 5 0.64 0.00 % 2 0 2
app/components/loopos_ui/tabs_content/tabs_content.html.erb 0.00 % 7 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/tabs_layout.rb 49.21 % 122 63 31 32 0.49 0.00 % 16 0 16
app/components/loopos_ui/tabs_layout/request_focus.html.erb 0.00 % 7 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/tabs_layout/tab.html.erb 0.00 % 14 8 0 8 0.00 0.00 % 2 0 2
app/components/loopos_ui/tabs_layout/tabs_layout.html.erb 0.00 % 55 19 0 19 0.00 0.00 % 14 0 14
app/components/loopos_ui/tabs_section.rb 100.00 % 14 11 11 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/tabs_section/tabs_section.html.erb 0.00 % 18 11 0 11 0.00 0.00 % 6 0 6
app/components/loopos_ui/tag_token.rb 38.10 % 55 21 8 13 0.38 0.00 % 6 0 6
app/components/loopos_ui/theme_context.rb 62.86 % 70 35 22 13 0.63 0.00 % 2 0 2
app/components/loopos_ui/timeline.rb 100.00 % 6 4 4 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/timeline/timeline.html.erb 0.00 % 7 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/title_description.rb 92.31 % 20 13 12 1 0.92 100.00 % 0 0 0
app/components/loopos_ui/title_description/title_description.html.erb 0.00 % 29 20 0 20 0.00 0.00 % 14 0 14
app/components/loopos_ui/toast.rb 63.64 % 20 11 7 4 0.64 0.00 % 3 0 3
app/components/loopos_ui/toast/toast.html.erb 0.00 % 31 12 0 12 0.00 0.00 % 8 0 8
app/components/loopos_ui/toaster.rb 62.50 % 118 56 35 21 0.70 0.00 % 14 0 14
app/components/loopos_ui/toaster/toaster.html.erb 0.00 % 13 8 0 8 0.00 0.00 % 2 0 2
app/components/loopos_ui/toggle.rb 69.23 % 48 26 18 8 0.69 0.00 % 4 0 4
app/components/loopos_ui/toggle/toggle.html.erb 0.00 % 32 19 0 19 0.00 0.00 % 22 0 22
app/components/loopos_ui/token.rb 30.16 % 133 63 19 44 0.30 0.00 % 26 0 26
app/components/loopos_ui/token/token.html.erb 0.00 % 69 43 0 43 0.00 0.00 % 34 0 34
app/components/loopos_ui/token_list.rb 65.91 % 86 44 29 15 0.66 0.00 % 4 0 4
app/components/loopos_ui/token_list/list.html.erb 0.00 % 29 19 0 19 0.00 0.00 % 6 0 6
app/components/loopos_ui/token_list/token_list.html.erb 0.00 % 13 8 0 8 0.00 100.00 % 0 0 0
app/components/loopos_ui/tooltip.rb 86.67 % 41 15 13 2 0.87 100.00 % 0 0 0
app/components/loopos_ui/tooltip/tooltip.html.erb 0.00 % 28 18 0 18 0.00 0.00 % 4 0 4
app/components/loopos_ui/top_page_component/breadcrumbs.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/top_page_component/top_page_actions_component.html.erb 0.00 % 15 7 0 7 0.00 0.00 % 6 0 6
app/components/loopos_ui/top_page_component/top_page_info_component.html.erb 0.00 % 65 45 0 45 0.00 0.00 % 24 0 24
app/components/loopos_ui/universal_search_component/universal_search_filters.html.erb 0.00 % 18 12 0 12 0.00 0.00 % 4 0 4
app/components/loopos_ui/universal_search_component/universal_search_filters.rb 87.50 % 13 8 7 1 0.88 100.00 % 0 0 0
app/components/loopos_ui/universal_search_component/universal_search_footer.html.erb 0.00 % 12 8 0 8 0.00 0.00 % 4 0 4
app/components/loopos_ui/universal_search_component/universal_search_footer.rb 100.00 % 11 6 6 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/universal_search_component/universal_search_global_search.html.erb 0.00 % 5 4 0 4 0.00 0.00 % 2 0 2
app/components/loopos_ui/universal_search_component/universal_search_global_search.rb 100.00 % 10 5 5 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/universal_search_component/universal_search_results.html.erb 0.00 % 24 10 0 10 0.00 0.00 % 4 0 4
app/components/loopos_ui/universal_search_component/universal_search_results.rb 88.89 % 18 9 8 1 0.89 100.00 % 0 0 0
app/components/loopos_ui/user_menu.rb 50.00 % 69 34 17 17 0.50 0.00 % 5 0 5
app/components/loopos_ui/user_menu/user_menu.html.erb 0.00 % 89 44 0 44 0.00 0.00 % 8 0 8
app/components/loopos_ui/v2/app_layout.rb 74.07 % 49 27 20 7 0.74 0.00 % 4 0 4
app/components/loopos_ui/v2/app_layout/app_layout.html.erb 0.00 % 73 29 0 29 0.00 0.00 % 10 0 10
app/components/loopos_ui/v2/app_layout/root_styles.html.erb 0.00 % 15 10 0 10 0.00 100.00 % 0 0 0
app/components/loopos_ui/v2/card.rb 75.00 % 71 40 30 10 1.10 0.00 % 12 0 12
app/components/loopos_ui/v2/card/app_instance.html.erb 0.00 % 53 20 0 20 0.00 100.00 % 0 0 0
app/components/loopos_ui/v2/card/app_instance.rb 76.00 % 62 25 19 6 0.76 0.00 % 4 0 4
app/components/loopos_ui/v2/card/card.html.erb 0.00 % 26 17 0 17 0.00 0.00 % 14 0 14
app/components/loopos_ui/v2/card/dashboard.html.erb 0.00 % 7 5 0 5 0.00 0.00 % 4 0 4
app/components/loopos_ui/v2/card/dashboard.rb 92.86 % 30 14 13 1 0.93 100.00 % 0 0 0
app/components/loopos_ui/v2/card/draft.html.erb 0.00 % 5 4 0 4 0.00 0.00 % 2 0 2
app/components/loopos_ui/v2/card/draft.rb 92.31 % 27 13 12 1 0.92 100.00 % 0 0 0
app/components/loopos_ui/v2/card/favorites.html.erb 0.00 % 21 11 0 11 0.00 0.00 % 2 0 2
app/components/loopos_ui/v2/card/favorites.rb 100.00 % 9 5 5 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/v2/card/financial_log.html.erb 0.00 % 29 16 0 16 0.00 0.00 % 12 0 12
app/components/loopos_ui/v2/card/financial_log.rb 65.79 % 87 38 25 13 0.66 0.00 % 9 0 9
app/components/loopos_ui/v2/card/financial_transaction.html.erb 0.00 % 43 21 0 21 0.00 0.00 % 12 0 12
app/components/loopos_ui/v2/card/financial_transaction.rb 37.21 % 81 43 16 27 0.37 0.00 % 23 0 23
app/components/loopos_ui/v2/card/script.html.erb 0.00 % 17 6 0 6 0.00 0.00 % 2 0 2
app/components/loopos_ui/v2/card/script.rb 63.33 % 62 30 19 11 0.63 0.00 % 4 0 4
app/components/loopos_ui/v2/image.rb 45.27 % 355 148 67 81 20.90 0.00 % 80 0 80
app/components/loopos_ui/v2/image/image.html.erb 0.00 % 291 84 0 84 0.00 0.00 % 48 0 48
app/components/loopos_ui/v2/select_bar.rb 45.16 % 61 31 14 17 0.45 100.00 % 0 0 0
app/components/loopos_ui/v2/table.html.erb 0.00 % 116 39 0 39 0.00 0.00 % 18 0 18
app/components/loopos_ui/v2/table.rb 39.07 % 517 215 84 131 0.39 0.00 % 81 0 81
app/components/loopos_ui/v2/table/cell.html.erb 0.00 % 27 15 0 15 0.00 0.00 % 8 0 8
app/components/loopos_ui/v2/table/cell.rb 50.00 % 72 36 18 18 0.50 0.00 % 22 0 22
app/components/loopos_ui/v2/table/header.html.erb 0.00 % 22 9 0 9 0.00 0.00 % 4 0 4
app/components/loopos_ui/v2/table/header.rb 100.00 % 14 9 9 0 1.00 0.00 % 2 0 2
app/components/loopos_ui/wysiwyg.rb 85.71 % 22 14 12 2 0.86 100.00 % 0 0 0
app/components/loopos_ui/wysiwyg/wysiwyg.html.erb 0.00 % 15 5 0 5 0.00 100.00 % 0 0 0

Views ( 0.0% covered at 0.0 hits/line )

313 files in total.
6009 relevant lines, 0 lines covered and 6009 lines missed. ( 0.0% )
2428 total branches, 0 branches covered and 2428 branches missed. ( 0.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/components/loopos_ui/accordion/accordion.html.erb 0.00 % 32 13 0 13 0.00 0.00 % 4 0 4
app/components/loopos_ui/action_bar/action_bar.html.erb 0.00 % 9 6 0 6 0.00 0.00 % 2 0 2
app/components/loopos_ui/action_bar/item_navigation.html.erb 0.00 % 9 6 0 6 0.00 100.00 % 0 0 0
app/components/loopos_ui/action_buttons/action_buttons.html.erb 0.00 % 5 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/action_buttons/button_group.html.erb 0.00 % 5 3 0 3 0.00 100.00 % 0 0 0
app/components/loopos_ui/action_menu/action_menu.html.erb 0.00 % 44 25 0 25 0.00 0.00 % 20 0 20
app/components/loopos_ui/app_instance/card/card.html.erb 0.00 % 48 23 0 23 0.00 0.00 % 17 0 17
app/components/loopos_ui/app_layout_component/app_layout_component.html.erb 0.00 % 48 22 0 22 0.00 0.00 % 10 0 10
app/components/loopos_ui/app_layout_component/root_styles.html.erb 0.00 % 15 10 0 10 0.00 100.00 % 0 0 0
app/components/loopos_ui/app_logo/app_logo.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/apps/list/list.html.erb 0.00 % 14 7 0 7 0.00 0.00 % 2 0 2
app/components/loopos_ui/assign/assign_component.html.erb 0.00 % 8 3 0 3 0.00 100.00 % 0 0 0
app/components/loopos_ui/assign/assign_section.html.erb 0.00 % 112 57 0 57 0.00 0.00 % 44 0 44
app/components/loopos_ui/assign/elements.html.erb 0.00 % 29 16 0 16 0.00 0.00 % 26 0 26
app/components/loopos_ui/assign/empty.html.erb 0.00 % 7 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/association_overlay/association_overlay.html.erb 0.00 % 16 9 0 9 0.00 0.00 % 8 0 8
app/components/loopos_ui/association_overlay/header.html.erb 0.00 % 4 3 0 3 0.00 0.00 % 4 0 4
app/components/loopos_ui/association_overlay/results_container.html.erb 0.00 % 12 7 0 7 0.00 0.00 % 6 0 6
app/components/loopos_ui/association_overlay/selected_container.html.erb 0.00 % 7 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/async_select/async_select_component.html.erb 0.00 % 29 9 0 9 0.00 0.00 % 2 0 2
app/components/loopos_ui/async_select/async_select_option_component.html.erb 0.00 % 22 12 0 12 0.00 0.00 % 10 0 10
app/components/loopos_ui/avatar/avatar.html.erb 0.00 % 4 3 0 3 0.00 100.00 % 0 0 0
app/components/loopos_ui/basic_radio_button/basic_radio_button.html.erb 0.00 % 11 7 0 7 0.00 0.00 % 2 0 2
app/components/loopos_ui/bottom_bar/bottom_bar.html.erb 0.00 % 10 6 0 6 0.00 100.00 % 0 0 0
app/components/loopos_ui/breadcrumb_list/breadcrumb_list.html.erb 0.00 % 10 8 0 8 0.00 0.00 % 2 0 2
app/components/loopos_ui/button/button.html.erb 0.00 % 37 27 0 27 0.00 0.00 % 22 0 22
app/components/loopos_ui/card/card_component.html.erb 0.00 % 25 14 0 14 0.00 0.00 % 6 0 6
app/components/loopos_ui/carousel/carousel.html.erb 0.00 % 46 14 0 14 0.00 100.00 % 0 0 0
app/components/loopos_ui/charts/chart_component.html.erb 0.00 % 57 21 0 21 0.00 0.00 % 10 0 10
app/components/loopos_ui/chat/chat.html.erb 0.00 % 14 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/chip/chip.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/chip_list/chip_list.html.erb 0.00 % 5 3 0 3 0.00 100.00 % 0 0 0
app/components/loopos_ui/code_editor/code_editor.html.erb 0.00 % 3 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/context_menu/context_menu.html.erb 0.00 % 20 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/context_menu/item.html.erb 0.00 % 55 19 0 19 0.00 0.00 % 2 0 2
app/components/loopos_ui/counter/counter.html.erb 0.00 % 11 7 0 7 0.00 0.00 % 6 0 6
app/components/loopos_ui/dashboard/dashboard_component.html.erb 0.00 % 48 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/data_card/data_card.html.erb 0.00 % 10 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/date_picker.html.erb 0.00 % 53 15 0 15 0.00 0.00 % 9 0 9
app/components/loopos_ui/date_picker/field/field.html.erb 0.00 % 15 6 0 6 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/month/month.html.erb 0.00 % 9 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/presets/presets.html.erb 0.00 % 25 6 0 6 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/presets_inline_calendar/presets_inline_calendar.html.erb 0.00 % 5 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/presets_menu/presets_menu.html.erb 0.00 % 90 12 0 12 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/range/range.html.erb 0.00 % 9 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/single/single.html.erb 0.00 % 9 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/year/year.html.erb 0.00 % 9 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_show/date_show.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/dots/dots.html.erb 0.00 % 5 3 0 3 0.00 0.00 % 2 0 2
app/components/loopos_ui/double_state_label.html.erb 0.00 % 14 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/drawer_bar/drawer_bar.html.erb 0.00 % 27 9 0 9 0.00 100.00 % 0 0 0
app/components/loopos_ui/dummy_slot/dummy_slot.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/email_preview/email_preview.html.erb 0.00 % 8 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/entities/catalog_node/catalog_node.html.erb 0.00 % 8 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/error_page/error_page.html.erb 0.00 % 27 10 0 10 0.00 0.00 % 2 0 2
app/components/loopos_ui/extra_data_viewer/extra_data_viewer.html.erb 0.00 % 64 20 0 20 0.00 0.00 % 10 0 10
app/components/loopos_ui/features/list/list.html.erb 0.00 % 17 8 0 8 0.00 0.00 % 2 0 2
app/components/loopos_ui/filter/filters_dropdown_component.html.erb 0.00 % 48 19 0 19 0.00 0.00 % 4 0 4
app/components/loopos_ui/filter/search_component.html.erb 0.00 % 13 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/filter_bar/filter_bar.html.erb 0.00 % 71 30 0 30 0.00 0.00 % 18 0 18
app/components/loopos_ui/filter_button/filter_button.html.erb 0.00 % 34 18 0 18 0.00 0.00 % 14 0 14
app/components/loopos_ui/filter_pill/filter_pill.html.erb 0.00 % 18 8 0 8 0.00 0.00 % 4 0 4
app/components/loopos_ui/flex_layout/flex_layout.html.erb 0.00 % 5 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/flex_layout/section.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/float_bar/float_bar.html.erb 0.00 % 34 13 0 13 0.00 100.00 % 0 0 0
app/components/loopos_ui/flows/drawer_bar/drawer_bar.html.erb 0.00 % 70 29 0 29 0.00 0.00 % 10 0 10
app/components/loopos_ui/form_entry/form_entry.html.erb 0.00 % 35 20 0 20 0.00 0.00 % 14 0 14
app/components/loopos_ui/form_entry_ai/form_entry_ai.html.erb 0.00 % 37 11 0 11 0.00 0.00 % 2 0 2
app/components/loopos_ui/grid_layout/grid_layout.html.erb 0.00 % 5 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/grid_layout/section.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/group_avatar/group_avatar.html.erb 0.00 % 6 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/header/header.html.erb 0.00 % 36 30 0 30 0.00 0.00 % 22 0 22
app/components/loopos_ui/header_component/header_component.html.erb 0.00 % 138 62 0 62 0.00 0.00 % 56 0 56
app/components/loopos_ui/header_component/partnable_applications_component.html.erb 0.00 % 5 2 0 2 0.00 0.00 % 2 0 2
app/components/loopos_ui/header_component/user_menu_component.html.erb 0.00 % 96 38 0 38 0.00 0.00 % 26 0 26
app/components/loopos_ui/history_card/history_card.html.erb 0.00 % 4 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/human_view/human_view.html.erb 0.00 % 27 12 0 12 0.00 0.00 % 6 0 6
app/components/loopos_ui/human_view/tree_node/tree_node.html.erb 0.00 % 37 19 0 19 0.00 0.00 % 2 0 2
app/components/loopos_ui/icon/icon.html.erb 0.00 % 4 3 0 3 0.00 0.00 % 4 0 4
app/components/loopos_ui/icon_tooltip/icon_tooltip.html.erb 0.00 % 19 12 0 12 0.00 0.00 % 8 0 8
app/components/loopos_ui/image/image.html.erb 0.00 % 26 16 0 16 0.00 0.00 % 8 0 8
app/components/loopos_ui/index_header/index_header.html.erb 0.00 % 11 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/index_layout/index_layout.html.erb 0.00 % 15 9 0 9 0.00 0.00 % 2 0 2
app/components/loopos_ui/inline_edit_component/base.html.erb 0.00 % 23 13 0 13 0.00 0.00 % 10 0 10
app/components/loopos_ui/inline_text_edit/inline_text_edit.html.erb 0.00 % 62 15 0 15 0.00 0.00 % 2 0 2
app/components/loopos_ui/input/input.html.erb 0.00 % 77 27 0 27 0.00 0.00 % 14 0 14
app/components/loopos_ui/inputs/ai/ai.html.erb 0.00 % 41 14 0 14 0.00 0.00 % 12 0 12
app/components/loopos_ui/inputs/checkbox/checkbox.html.erb 0.00 % 41 18 0 18 0.00 0.00 % 18 0 18
app/components/loopos_ui/inputs/combobox/combobox.html.erb 0.00 % 82 27 0 27 0.00 0.00 % 14 0 14
app/components/loopos_ui/inputs/date/date.html.erb 0.00 % 24 11 0 11 0.00 0.00 % 2 0 2
app/components/loopos_ui/inputs/file/file_actions.html.erb 0.00 % 32 8 0 8 0.00 100.00 % 0 0 0
app/components/loopos_ui/inputs/file/form_file.html.erb 0.00 % 99 32 0 32 0.00 0.00 % 10 0 10
app/components/loopos_ui/inputs/file/multiple.html.erb 0.00 % 142 53 0 53 0.00 0.00 % 26 0 26
app/components/loopos_ui/inputs/file/option.html.erb 0.00 % 7 3 0 3 0.00 100.00 % 0 0 0
app/components/loopos_ui/inputs/file/single.html.erb 0.00 % 77 30 0 30 0.00 0.00 % 24 0 24
app/components/loopos_ui/inputs/money/money.html.erb 0.00 % 13 7 0 7 0.00 100.00 % 0 0 0
app/components/loopos_ui/inputs/number/number.html.erb 0.00 % 14 7 0 7 0.00 0.00 % 2 0 2
app/components/loopos_ui/inputs/phone_number/phone_number.html.erb 0.00 % 7 5 0 5 0.00 0.00 % 4 0 4
app/components/loopos_ui/inputs/rich_text/rich_text.html.erb 0.00 % 18 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/inputs/search/search.html.erb 0.00 % 24 12 0 12 0.00 0.00 % 2 0 2
app/components/loopos_ui/inputs/select/select.html.erb 0.00 % 92 33 0 33 0.00 0.00 % 28 0 28
app/components/loopos_ui/inputs/select2/select2.html.erb 0.00 % 188 57 0 57 0.00 0.00 % 42 0 42
app/components/loopos_ui/inputs/text/text.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/inputs/text_area/text_area.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/item/state_label/state_label.html.erb 0.00 % 24 7 0 7 0.00 0.00 % 2 0 2
app/components/loopos_ui/item_value/item_value.html.erb 0.00 % 10 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/label/label.html.erb 0.00 % 14 8 0 8 0.00 0.00 % 11 0 11
app/components/loopos_ui/layout_component.html.erb 0.00 % 19 9 0 9 0.00 0.00 % 6 0 6
app/components/loopos_ui/layout_loading/layout_loading.html.erb 0.00 % 7 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/link/link.html.erb 0.00 % 12 8 0 8 0.00 0.00 % 4 0 4
app/components/loopos_ui/loading_ai/loading_ai.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/loadings/skeleton/skeleton.html.erb 0.00 % 12 5 0 5 0.00 0.00 % 2 0 2
app/components/loopos_ui/log/log.html.erb 0.00 % 50 29 0 29 0.00 0.00 % 6 0 6
app/components/loopos_ui/log/script/info_modal_body.html.erb 0.00 % 63 39 0 39 0.00 0.00 % 44 0 44
app/components/loopos_ui/log_list/group.html.erb 0.00 % 7 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/log_list/log_list.html.erb 0.00 % 20 11 0 11 0.00 0.00 % 4 0 4
app/components/loopos_ui/logo/logo.html.erb 0.00 % 8 4 0 4 0.00 0.00 % 2 0 2
app/components/loopos_ui/logs_component/log_content_component.html.erb 0.00 % 26 15 0 15 0.00 0.00 % 10 0 10
app/components/loopos_ui/logs_component/log_group_component.html.erb 0.00 % 5 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/logs_component/logs_component.html.erb 0.00 % 19 11 0 11 0.00 0.00 % 2 0 2
app/components/loopos_ui/m_icon/m_icon.html.erb 0.00 % 7 4 0 4 0.00 0.00 % 2 0 2
app/components/loopos_ui/main_component/main_component.html.erb 0.00 % 8 3 0 3 0.00 100.00 % 0 0 0
app/components/loopos_ui/main_layout/main_layout.html.erb 0.00 % 17 8 0 8 0.00 0.00 % 4 0 4
app/components/loopos_ui/marketing_layout/marketing_layout.html.erb 0.00 % 24 10 0 10 0.00 100.00 % 0 0 0
app/components/loopos_ui/marketplace/footer/footer.html.erb 0.00 % 15 9 0 9 0.00 0.00 % 4 0 4
app/components/loopos_ui/marketplace/header/header.html.erb 0.00 % 10 7 0 7 0.00 0.00 % 2 0 2
app/components/loopos_ui/marketplace/item_card/item_card.html.erb 0.00 % 35 24 0 24 0.00 0.00 % 18 0 18
app/components/loopos_ui/marketplace/item_details/item_details.html.erb 0.00 % 22 14 0 14 0.00 0.00 % 2 0 2
app/components/loopos_ui/marketplace/payment_summary/payment_summary.html.erb 0.00 % 19 10 0 10 0.00 100.00 % 0 0 0
app/components/loopos_ui/mini_app/mini_app.html.erb 0.00 % 11 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/modal/modal.html.erb 0.00 % 48 25 0 25 0.00 0.00 % 14 0 14
app/components/loopos_ui/modal_component/modal_component.html.erb 0.00 % 29 16 0 16 0.00 0.00 % 32 0 32
app/components/loopos_ui/model_association_list/model_association_list.html.erb 0.00 % 35 19 0 19 0.00 0.00 % 4 0 4
app/components/loopos_ui/model_association_overlay/attached_item.html.erb 0.00 % 7 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/model_association_overlay/missing_item.html.erb 0.00 % 9 6 0 6 0.00 100.00 % 0 0 0
app/components/loopos_ui/model_association_overlay/model_association_overlay.html.erb 0.00 % 31 17 0 17 0.00 0.00 % 4 0 4
app/components/loopos_ui/model_association_overlay/new_item.html.erb 0.00 % 27 10 0 10 0.00 100.00 % 0 0 0
app/components/loopos_ui/page_header/page_header.html.erb 0.00 % 27 16 0 16 0.00 0.00 % 4 0 4
app/components/loopos_ui/page_section/identifier_editor/identifier_editor.html.erb 0.00 % 19 10 0 10 0.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/identifier_editor/result.html.erb 0.00 % 35 16 0 16 0.00 0.00 % 2 0 2
app/components/loopos_ui/page_section/item_tab/advanced_details/advanced_details.html.erb 0.00 % 87 46 0 46 0.00 0.00 % 4 0 4
app/components/loopos_ui/page_section/item_tab/chat/chat.html.erb 0.00 % 123 30 0 30 0.00 0.00 % 12 0 12
app/components/loopos_ui/page_section/item_tab/customer_info/customer_info.html.erb 0.00 % 124 51 0 51 0.00 0.00 % 8 0 8
app/components/loopos_ui/page_section/item_tab/financial_data/financial_data.html.erb 0.00 % 165 88 0 88 0.00 0.00 % 30 0 30
app/components/loopos_ui/page_section/item_tab/financial_data/item_marketplace_value_out_card.html.erb 0.00 % 25 12 0 12 0.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/item_tab/financial_data/item_value_card.html.erb 0.00 % 313 158 0 158 0.00 0.00 % 126 0 126
app/components/loopos_ui/page_section/item_tab/financial_data/item_value_table.html.erb 0.00 % 141 64 0 64 0.00 0.00 % 40 0 40
app/components/loopos_ui/page_section/item_tab/financial_data/logs_list.html.erb 0.00 % 11 5 0 5 0.00 0.00 % 2 0 2
app/components/loopos_ui/page_section/item_tab/financial_data/other_values_section.html.erb 0.00 % 128 57 0 57 0.00 0.00 % 14 0 14
app/components/loopos_ui/page_section/item_tab/financial_data/transactions_list.html.erb 0.00 % 22 12 0 12 0.00 0.00 % 6 0 6
app/components/loopos_ui/page_section/item_tab/flow/flow.html.erb 0.00 % 38 15 0 15 0.00 0.00 % 4 0 4
app/components/loopos_ui/page_section/item_tab/impact_widget/impact_widget.html.erb 0.00 % 31 15 0 15 0.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/item_tab/info/identifiers_table.html.erb 0.00 % 83 29 0 29 0.00 0.00 % 4 0 4
app/components/loopos_ui/page_section/item_tab/info/info.html.erb 0.00 % 150 88 0 88 0.00 0.00 % 34 0 34
app/components/loopos_ui/page_section/item_tab/logs/logs.html.erb 0.00 % 108 43 0 43 0.00 0.00 % 10 0 10
app/components/loopos_ui/page_section/item_tab/notes/notes.html.erb 0.00 % 61 30 0 30 0.00 0.00 % 6 0 6
app/components/loopos_ui/page_section/item_tab/protocol_responses/protocol_answer_value.html.erb 0.00 % 20 10 0 10 0.00 0.00 % 9 0 9
app/components/loopos_ui/page_section/item_tab/protocol_responses/protocol_responses.html.erb 0.00 % 96 46 0 46 0.00 0.00 % 34 0 34
app/components/loopos_ui/page_section/item_tab/rfi/rfi.html.erb 0.00 % 49 20 0 20 0.00 0.00 % 2 0 2
app/components/loopos_ui/page_section/item_tab/rfi/rfi_answer_history_table.html.erb 0.00 % 90 40 0 40 0.00 0.00 % 20 0 20
app/components/loopos_ui/page_section/item_tab/rfi/rfi_answers_index_table.html.erb 0.00 % 112 44 0 44 0.00 0.00 % 12 0 12
app/components/loopos_ui/page_section/item_tab/rfi/rfi_answers_step_table.html.erb 0.00 % 134 58 0 58 0.00 0.00 % 22 0 22
app/components/loopos_ui/page_section/item_tab/rfi/rfi_responders_without_rfi_answers_table.html.erb 0.00 % 79 32 0 32 0.00 0.00 % 12 0 12
app/components/loopos_ui/page_section/item_tab/services/services.html.erb 0.00 % 139 55 0 55 0.00 0.00 % 12 0 12
app/components/loopos_ui/page_section/item_tab/trade_in/trade_in.html.erb 0.00 % 75 42 0 42 0.00 0.00 % 6 0 6
app/components/loopos_ui/page_section/item_tab/validation_widget/validation_widget.html.erb 0.00 % 44 21 0 21 0.00 0.00 % 2 0 2
app/components/loopos_ui/page_section/rfi_answer_tab/history/history.html.erb 0.00 % 16 7 0 7 0.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/rfi_answer_tab/info/info.html.erb 0.00 % 87 49 0 49 0.00 0.00 % 24 0 24
app/components/loopos_ui/page_section/rfi_tab/advanced_details/advanced_details.html.erb 0.00 % 16 8 0 8 0.00 100.00 % 0 0 0
app/components/loopos_ui/page_section/rfi_tab/info/info.html.erb 0.00 % 161 93 0 93 0.00 0.00 % 32 0 32
app/components/loopos_ui/page_section/rfi_tab/rfi_answers/rfi_answers.html.erb 0.00 % 17 7 0 7 0.00 100.00 % 0 0 0
app/components/loopos_ui/pages/item_show.html.erb 0.00 % 78 15 0 15 0.00 0.00 % 18 0 18
app/components/loopos_ui/pages/item_show_header.html.erb 0.00 % 25 15 0 15 0.00 0.00 % 4 0 4
app/components/loopos_ui/pages/reports_index.html.erb 0.00 % 45 20 0 20 0.00 0.00 % 4 0 4
app/components/loopos_ui/pages/reports_index_table.html.erb 0.00 % 48 19 0 19 0.00 0.00 % 4 0 4
app/components/loopos_ui/pages/rfi/rfi_show.html.erb 0.00 % 13 8 0 8 0.00 0.00 % 2 0 2
app/components/loopos_ui/pages/rfi_answer/show.html.erb 0.00 % 10 6 0 6 0.00 100.00 % 0 0 0
app/components/loopos_ui/pagination/pagination.html.erb 0.00 % 11 7 0 7 0.00 0.00 % 2 0 2
app/components/loopos_ui/popover/popover.html.erb 0.00 % 10 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/popover_template/popover_template.html.erb 0.00 % 14 8 0 8 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/add_element/add_element_component.html.erb 0.00 % 36 10 0 10 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/add_element/select_option_component.html.erb 0.00 % 13 9 0 9 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/add_protocol.html.erb 0.00 % 98 40 0 40 0.00 0.00 % 14 0 14
app/components/loopos_ui/protocol/attach_node/select_option_component.html.erb 0.00 % 32 17 0 17 0.00 0.00 % 20 0 20
app/components/loopos_ui/protocol/drawer_bar/drawer_bar.html.erb 0.00 % 73 38 0 38 0.00 0.00 % 6 0 6
app/components/loopos_ui/protocol/elements/fields/array.html.erb 0.00 % 79 23 0 23 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/elements/fields/boolean.html.erb 0.00 % 26 17 0 17 0.00 0.00 % 4 0 4
app/components/loopos_ui/protocol/elements/fields/custom.html.erb 0.00 % 60 19 0 19 0.00 0.00 % 6 0 6
app/components/loopos_ui/protocol/elements/fields/date.html.erb 0.00 % 38 16 0 16 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/elements/fields/enum.html.erb 0.00 % 23 14 0 14 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/elements/fields/granularity.html.erb 0.00 % 21 14 0 14 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/elements/fields/image.html.erb 0.00 % 21 13 0 13 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/elements/fields/integer.html.erb 0.00 % 19 9 0 9 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/fields/number.html.erb 0.00 % 32 16 0 16 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/elements/fields/option.html.erb 0.00 % 75 33 0 33 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/elements/fields/options.html.erb 0.00 % 45 22 0 22 0.00 0.00 % 6 0 6
app/components/loopos_ui/protocol/elements/fields/string.html.erb 0.00 % 32 17 0 17 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol/elements/info/break.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/html.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/image.html.erb 0.00 % 2 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/text.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/title.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/info/url.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/bool.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/custom.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/date.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/date_range.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/email.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/files.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/iban.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/identifier.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/images.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/money.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/number.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/postal_code.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/select.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/input/text.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/elements/settings.html.erb 0.00 % 72 30 0 30 0.00 0.00 % 4 0 4
app/components/loopos_ui/protocol/elements/structure/divider.html.erb 0.00 % 94 40 0 40 0.00 0.00 % 22 0 22
app/components/loopos_ui/protocol/protocol_component.html.erb 0.00 % 64 30 0 30 0.00 0.00 % 20 0 20
app/components/loopos_ui/protocol/protocol_element_component.html.erb 0.00 % 206 102 0 102 0.00 0.00 % 58 0 58
app/components/loopos_ui/protocol/protocol_filter_component.html.erb 0.00 % 121 54 0 54 0.00 0.00 % 18 0 18
app/components/loopos_ui/protocol/protocol_header.html.erb 0.00 % 201 105 0 105 0.00 0.00 % 46 0 46
app/components/loopos_ui/protocol/settings.html.erb 0.00 % 155 57 0 57 0.00 0.00 % 2 0 2
app/components/loopos_ui/protocol_answer_value/protocol_answer_value.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol_element/drawer_bar/drawer_bar.html.erb 0.00 % 60 26 0 26 0.00 0.00 % 8 0 8
app/components/loopos_ui/protocol_form/protocol_form.html.erb 0.00 % 374 151 0 151 0.00 0.00 % 81 0 81
app/components/loopos_ui/radio_card/content.html.erb 0.00 % 26 12 0 12 0.00 0.00 % 2 0 2
app/components/loopos_ui/radio_card/radio_card.html.erb 0.00 % 6 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/script_editor/script_editor.html.erb 0.00 % 17 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/script_input_form/script_input_form.html.erb 0.00 % 82 31 0 31 0.00 0.00 % 13 0 13
app/components/loopos_ui/services/email_messages/table/table.html.erb 0.00 % 83 47 0 47 0.00 0.00 % 22 0 22
app/components/loopos_ui/services/email_messages/tabs/info/info.html.erb 0.00 % 95 57 0 57 0.00 0.00 % 18 0 18
app/components/loopos_ui/services/email_templates/table/table.html.erb 0.00 % 110 49 0 49 0.00 0.00 % 22 0 22
app/components/loopos_ui/services/email_templates/tabs/info/info.html.erb 0.00 % 77 33 0 33 0.00 0.00 % 8 0 8
app/components/loopos_ui/services/extra_data/extra_data.html.erb 0.00 % 25 12 0 12 0.00 0.00 % 2 0 2
app/components/loopos_ui/services/incoming_payments/table/table.html.erb 0.00 % 109 67 0 67 0.00 0.00 % 26 0 26
app/components/loopos_ui/services/incoming_payments/tabs/info/info.html.erb 0.00 % 109 69 0 69 0.00 0.00 % 18 0 18
app/components/loopos_ui/services/invoices/show/show.html.erb 0.00 % 28 16 0 16 0.00 0.00 % 8 0 8
app/components/loopos_ui/services/invoices/table/table.html.erb 0.00 % 125 70 0 70 0.00 0.00 % 26 0 26
app/components/loopos_ui/services/invoices/tabs/info/info.html.erb 0.00 % 106 60 0 60 0.00 0.00 % 14 0 14
app/components/loopos_ui/services/issues_area/issues_area.html.erb 0.00 % 11 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/services/list/list.html.erb 0.00 % 14 7 0 7 0.00 0.00 % 2 0 2
app/components/loopos_ui/services/payments/table/table.html.erb 0.00 % 147 78 0 78 0.00 0.00 % 30 0 30
app/components/loopos_ui/services/payments/tabs/info/info.html.erb 0.00 % 105 64 0 64 0.00 0.00 % 20 0 20
app/components/loopos_ui/services/shipping_pickups/table/table.html.erb 0.00 % 50 31 0 31 0.00 0.00 % 10 0 10
app/components/loopos_ui/services/shipping_pickups/tabs/info/info.html.erb 0.00 % 47 29 0 29 0.00 100.00 % 0 0 0
app/components/loopos_ui/services/shippings/table/table.html.erb 0.00 % 248 120 0 120 0.00 0.00 % 40 0 40
app/components/loopos_ui/services/shippings/tabs/info/info.html.erb 0.00 % 137 87 0 87 0.00 0.00 % 16 0 16
app/components/loopos_ui/services/shippings/tabs/packages/packages.html.erb 0.00 % 153 83 0 83 0.00 0.00 % 20 0 20
app/components/loopos_ui/services/sms_messages/table/table.html.erb 0.00 % 82 46 0 46 0.00 0.00 % 24 0 24
app/components/loopos_ui/services/sms_messages/tabs/info/info.html.erb 0.00 % 109 69 0 69 0.00 0.00 % 18 0 18
app/components/loopos_ui/show_header/show_header.html.erb 0.00 % 67 35 0 35 0.00 0.00 % 16 0 16
app/components/loopos_ui/show_layout/show_layout.html.erb 0.00 % 18 8 0 8 0.00 0.00 % 2 0 2
app/components/loopos_ui/show_layout_component/show_layout_component.html.erb 0.00 % 6 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar/v1/divider.html.erb 0.00 % 2 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar/v1/drawer_divider.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar/v1/drawer_entry.html.erb 0.00 % 3 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar/v1/drawer_item.html.erb 0.00 % 52 25 0 25 0.00 0.00 % 12 0 12
app/components/loopos_ui/sidebar/v1/sidebar.html.erb 0.00 % 16 10 0 10 0.00 0.00 % 2 0 2
app/components/loopos_ui/sidebar/v1/single_item.html.erb 0.00 % 22 12 0 12 0.00 0.00 % 8 0 8
app/components/loopos_ui/sidebar/v2/group.html.erb 0.00 % 9 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar/v2/item.html.erb 0.00 % 37 17 0 17 0.00 0.00 % 8 0 8
app/components/loopos_ui/sidebar/v2/sidebar.html.erb 0.00 % 39 22 0 22 0.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar_layout_component/sidebar_old_component.html.erb 0.00 % 3 2 0 2 0.00 0.00 % 2 0 2
app/components/loopos_ui/spinner/spinner.html.erb 0.00 % 12 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/status_dot/status_dot.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/stepper/step.html.erb 0.00 % 21 15 0 15 0.00 0.00 % 2 0 2
app/components/loopos_ui/stepper/stepper.html.erb 0.00 % 8 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/stepper_panel/stepper_panel.html.erb 0.00 % 13 9 0 9 0.00 0.00 % 2 0 2
app/components/loopos_ui/table/pagination_component.html.erb 0.00 % 39 28 0 28 0.00 0.00 % 8 0 8
app/components/loopos_ui/table/table_component.html.erb 0.00 % 61 35 0 35 0.00 0.00 % 26 0 26
app/components/loopos_ui/table_data_list/label_header_row.html.erb 0.00 % 4 3 0 3 0.00 100.00 % 0 0 0
app/components/loopos_ui/table_data_list/table_data_list.html.erb 0.00 % 49 21 0 21 0.00 100.00 % 0 0 0
app/components/loopos_ui/table_filter/table_filter.html.erb 0.00 % 11 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/tabs_component/tabs_component.html.erb 0.00 % 14 8 0 8 0.00 100.00 % 0 0 0
app/components/loopos_ui/tabs_content/tabs_content.html.erb 0.00 % 7 5 0 5 0.00 100.00 % 0 0 0
app/components/loopos_ui/tabs_layout/request_focus.html.erb 0.00 % 7 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/tabs_layout/tab.html.erb 0.00 % 14 8 0 8 0.00 0.00 % 2 0 2
app/components/loopos_ui/tabs_layout/tabs_layout.html.erb 0.00 % 55 19 0 19 0.00 0.00 % 14 0 14
app/components/loopos_ui/tabs_section/tabs_section.html.erb 0.00 % 18 11 0 11 0.00 0.00 % 6 0 6
app/components/loopos_ui/timeline/timeline.html.erb 0.00 % 7 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/title_description/title_description.html.erb 0.00 % 29 20 0 20 0.00 0.00 % 14 0 14
app/components/loopos_ui/toast/toast.html.erb 0.00 % 31 12 0 12 0.00 0.00 % 8 0 8
app/components/loopos_ui/toaster/toaster.html.erb 0.00 % 13 8 0 8 0.00 0.00 % 2 0 2
app/components/loopos_ui/toggle/toggle.html.erb 0.00 % 32 19 0 19 0.00 0.00 % 22 0 22
app/components/loopos_ui/token/token.html.erb 0.00 % 69 43 0 43 0.00 0.00 % 34 0 34
app/components/loopos_ui/token_list/list.html.erb 0.00 % 29 19 0 19 0.00 0.00 % 6 0 6
app/components/loopos_ui/token_list/token_list.html.erb 0.00 % 13 8 0 8 0.00 100.00 % 0 0 0
app/components/loopos_ui/tooltip/tooltip.html.erb 0.00 % 28 18 0 18 0.00 0.00 % 4 0 4
app/components/loopos_ui/top_page_component/breadcrumbs.html.erb 0.00 % 1 1 0 1 0.00 100.00 % 0 0 0
app/components/loopos_ui/top_page_component/top_page_actions_component.html.erb 0.00 % 15 7 0 7 0.00 0.00 % 6 0 6
app/components/loopos_ui/top_page_component/top_page_info_component.html.erb 0.00 % 65 45 0 45 0.00 0.00 % 24 0 24
app/components/loopos_ui/universal_search_component/universal_search_filters.html.erb 0.00 % 18 12 0 12 0.00 0.00 % 4 0 4
app/components/loopos_ui/universal_search_component/universal_search_footer.html.erb 0.00 % 12 8 0 8 0.00 0.00 % 4 0 4
app/components/loopos_ui/universal_search_component/universal_search_global_search.html.erb 0.00 % 5 4 0 4 0.00 0.00 % 2 0 2
app/components/loopos_ui/universal_search_component/universal_search_results.html.erb 0.00 % 24 10 0 10 0.00 0.00 % 4 0 4
app/components/loopos_ui/user_menu/user_menu.html.erb 0.00 % 89 44 0 44 0.00 0.00 % 8 0 8
app/components/loopos_ui/v2/app_layout/app_layout.html.erb 0.00 % 73 29 0 29 0.00 0.00 % 10 0 10
app/components/loopos_ui/v2/app_layout/root_styles.html.erb 0.00 % 15 10 0 10 0.00 100.00 % 0 0 0
app/components/loopos_ui/v2/card/app_instance.html.erb 0.00 % 53 20 0 20 0.00 100.00 % 0 0 0
app/components/loopos_ui/v2/card/card.html.erb 0.00 % 26 17 0 17 0.00 0.00 % 14 0 14
app/components/loopos_ui/v2/card/dashboard.html.erb 0.00 % 7 5 0 5 0.00 0.00 % 4 0 4
app/components/loopos_ui/v2/card/draft.html.erb 0.00 % 5 4 0 4 0.00 0.00 % 2 0 2
app/components/loopos_ui/v2/card/favorites.html.erb 0.00 % 21 11 0 11 0.00 0.00 % 2 0 2
app/components/loopos_ui/v2/card/financial_log.html.erb 0.00 % 29 16 0 16 0.00 0.00 % 12 0 12
app/components/loopos_ui/v2/card/financial_transaction.html.erb 0.00 % 43 21 0 21 0.00 0.00 % 12 0 12
app/components/loopos_ui/v2/card/script.html.erb 0.00 % 17 6 0 6 0.00 0.00 % 2 0 2
app/components/loopos_ui/v2/image/image.html.erb 0.00 % 291 84 0 84 0.00 0.00 % 48 0 48
app/components/loopos_ui/v2/table.html.erb 0.00 % 116 39 0 39 0.00 0.00 % 18 0 18
app/components/loopos_ui/v2/table/cell.html.erb 0.00 % 27 15 0 15 0.00 0.00 % 8 0 8
app/components/loopos_ui/v2/table/header.html.erb 0.00 % 22 9 0 9 0.00 0.00 % 4 0 4
app/components/loopos_ui/wysiwyg/wysiwyg.html.erb 0.00 % 15 5 0 5 0.00 100.00 % 0 0 0

app/components/loopos_ui/accordion.rb

60.71% lines covered

0.0% branches covered

28 relevant lines. 17 lines covered and 11 lines missed.
8 total branches, 0 branches covered and 8 branches missed.
    
  1. 1 module LooposUi
  2. 1 class Accordion < LoopComponent
  3. 1 renders_one :body # TODO: deprecate this. We have the default content slot
  4. 1 renders_many :accordions, LooposUi::Accordion
  5. 1 renders_one :action_buttons, LooposUi::ActionButtons
  6. 1 renders_one :header, ->(title: nil, description: nil, tooltip: nil, size: nil, **kwargs) {
  7. @size = size || :medium
  8. @size = @size.to_sym
  9. then: 0 else: 0 @size = :medium if @size == :normal # TODO: backwards compatibility with TitleDescription
  10. LooposUi::Header.new(
  11. title: title,
  12. description: description,
  13. tooltip: tooltip,
  14. size: @size,
  15. **kwargs,
  16. )
  17. }
  18. # shorthand_syntax, forwarded to the header slot
  19. 1 option :title, optional: true
  20. 1 option :description, optional: true
  21. 1 option :tooltip, optional: true
  22. 1 option :size, Types::Coercible::Symbol.enum(:normal, :small, :tiny), optional: true, default: -> { :normal }
  23. 1 option :open, default: proc { false }
  24. 1 option :overflow_x_auto, default: proc { false }, type: Types::Bool
  25. 1 def before_render
  26. else: 0 then: 0 unless header?
  27. with_header(title: title, description: description, tooltip: tooltip, size: size)
  28. end
  29. end
  30. 1 private
  31. 1 def accordion_id
  32. "accordion_#{SecureRandom.hex(6)}"
  33. end
  34. 1 def classes
  35. [
  36. "lui-accordion",
  37. then: 0 else: 0 open ? "lui-accordion--open" : nil,
  38. then: 0 else: 0 overflow_x_auto ? "lui-accordion--overflow-x-auto" : nil,
  39. ].compact.join(" ")
  40. end
  41. 1 def content_or_body
  42. content.presence || body
  43. end
  44. end
  45. end

app/components/loopos_ui/accordion/accordion.html.erb

0.0% lines covered

0.0% branches covered

13 relevant lines. 0 lines covered and 13 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <div class="<%= classes %>" data-controller="accordion" data-accordion-open-value="<%= open %>">
  2. <div class="lui-accordion__header">
  3. <button class="cursor-pointer flex justify-start gap-1 flex-1 items-center" type="button" data-action="click->accordion#toggle"
  4. data-accordion-target="trigger"
  5. aria-controls="<%= accordion_id %>">
  6. <%# TODO: this will be changed in V2, quickfix now%>
  7. then: 0 else: 0 <div class="lui-accordion__icon <%= [:small, :tiny].include?(size.to_sym) ? "mt-[2px]" : "mt-1" %>">
  8. <%= render LooposUi::Icon.new(icon: "fa-regular fa-chevron-down", size: 8) %>
  9. </div>
  10. <span class="lui-accordion__title flex justify-start gap-1">
  11. <%= header %>
  12. </span>
  13. </button>
  14. <div aria-controls="<%= accordion_id %>" class="lui-accordion__buttons">
  15. <%= action_buttons %>
  16. </div>
  17. </div>
  18. <div
  19. id="<%= accordion_id %>"
  20. class="lui-accordion__content"
  21. data-accordion-target="content"
  22. >
  23. <div class="lui-accordion__content-inner">
  24. then: 0 else: 0 <%= tag.div(content_or_body, class: "pb-4") if content_or_body.present? %>
  25. <% accordions.each do |accordion| %>
  26. <div class="lui-accordion__accordion-content">
  27. <%= accordion %>
  28. </div>
  29. <% end %>
  30. </div>
  31. </div>
  32. </div>

app/components/loopos_ui/action_bar.rb

35.29% lines covered

0.0% branches covered

51 relevant lines. 18 lines covered and 33 lines missed.
24 total branches, 0 branches covered and 24 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class ActionBar < LoopComponent
  4. 1 include ActionView::Helpers::UrlHelper
  5. 1 include LooposUi::ResourceAware
  6. 1 renders_one :action_buttons, LooposUi::ActionButtons
  7. 1 renders_one :breadcrumbs_list, LooposUi::BreadcrumbList
  8. 1 renders_one :item_navigation, "LooposUi::ActionBar::ItemNavigation"
  9. 1 renders_one :issue_card
  10. 1 option :current_action, optional: true
  11. 1 def before_render
  12. then: 0 else: 0 if breadcrumbs_list.nil? && LooposUi.config.auto_breadcrumbs
  13. auto_breadcrumbs = find_current_breadcrumbs
  14. with_breadcrumbs_list do |list|
  15. auto_breadcrumbs.each do |breadcrumb|
  16. list.with_breadcrumb(href: breadcrumb[:path]).with_content(breadcrumb[:title])
  17. end
  18. end
  19. end
  20. end
  21. 1 private
  22. 1 def find_current_breadcrumbs
  23. current_action = self.current_action.to_s || action_name
  24. target_path = case current_action
  25. when: 0 when "index"
  26. request.path
  27. # Sometimes we render the show view as a response to a POST request, or inline editing
  28. when "show"
  29. when: 0 # Not supported auto breadcrumbs for show action without resource
  30. then: 0 else: 0 if resource.nil?
  31. LooposUi.logger.warn(<<~ERROR)
  32. Auto breadcrumbs for show action without resource is not supported.
  33. You passed model: #{model.inspect} yet no resource was found.
  34. ERROR
  35. return []
  36. end
  37. request.path.gsub(%r{/\d+$}, "")
  38. else
  39. else: 0 # TODO: support for other actions
  40. return []
  41. end
  42. items = LooposUi.config.sidebar.items
  43. breadcrumbs = []
  44. find_breadcrumbs_dfs(items, breadcrumbs, target_path, current_action)
  45. if breadcrumbs.count == 1 && current_action == "index"
  46. then: 0 # TODO: translations for "Overview"
  47. else: 0 breadcrumbs
  48. then: 0 else: 0 elsif current_action == "show"
  49. breadcrumbs << { title: resource.model_title || "", path: "#" }
  50. end
  51. breadcrumbs
  52. end
  53. 1 def find_breadcrumbs_dfs(items, breadcrumbs, target_path, action)
  54. items.each do |item|
  55. else: 0 then: 0 next unless item.is_a?(Hash)
  56. else: 0 path = case item[:path]
  57. when: 0 when String
  58. item[:path]
  59. when: 0 when Proc
  60. instance_exec(&item[:path])
  61. end || "/"
  62. breadcrumbs << {
  63. title: string_or_callable(item[:title] || item[:short_title]),
  64. path: path,
  65. }
  66. then: 0 else: 0 return true if path == target_path
  67. then: 0 else: 0 if item[:menu_items]
  68. found = find_breadcrumbs_dfs(item[:menu_items], breadcrumbs, target_path, action)
  69. then: 0 else: 0 return true if found
  70. end
  71. breadcrumbs.pop
  72. end
  73. false
  74. end
  75. 1 def string_or_callable(value)
  76. then: 0 else: 0 value.respond_to?(:call) ? instance_exec(&value) : value
  77. end
  78. 1 class ItemNavigation < LoopComponent
  79. 1 option :title, Types::String
  80. 1 option :next_path, Types::String
  81. 1 option :previous_path, Types::String
  82. end
  83. end
  84. end

app/components/loopos_ui/action_bar/action_bar.html.erb

0.0% lines covered

0.0% branches covered

6 relevant lines. 0 lines covered and 6 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <div class="lui-action_bar">
  2. <%= breadcrumbs_list %>
  3. then: 0 else: 0 <% if issue_card.present? %>
  4. <%= issue_card %>
  5. <% end %>
  6. <%= item_navigation %>
  7. <%= action_buttons %>
  8. </div>

app/components/loopos_ui/action_bar/item_navigation.html.erb

0.0% lines covered

100.0% branches covered

6 relevant lines. 0 lines covered and 6 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div(class: "lui-action_bar__item_navigation") do %>
  2. <%= tag.a(href: previous_path, class: "lui-action_bar__item_navigation__previous") do %>
  3. <%= render LooposUi::MIcon.new(:chevron_left) %>
  4. <% end %>
  5. <%= tag.span(title, class: "lui-action_bar__item_navigation__title") %>
  6. <%= tag.a(href: next_path, class: "lui-action_bar__item_navigation__next") do %>
  7. <%= render LooposUi::MIcon.new(:chevron_right) %>
  8. <% end %>
  9. <% end %>

app/components/loopos_ui/action_buttons.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class ActionButtons < ViewComponent::Base
  4. 1 renders_many :button_groups, "LooposUi::ActionButtons::ButtonGroup"
  5. 1 class ButtonGroup < ViewComponent::Base
  6. 1 renders_many :buttons, types: LooposUi::Button.presets
  7. end
  8. end
  9. end

app/components/loopos_ui/action_buttons/action_buttons.html.erb

0.0% lines covered

100.0% branches covered

4 relevant lines. 0 lines covered and 4 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <div class="lui-action_buttons">
  2. <% button_groups.each do |bg| %>
  3. <%= bg %>
  4. <% end %>
  5. </div>

app/components/loopos_ui/action_buttons/button_group.html.erb

0.0% lines covered

100.0% branches covered

3 relevant lines. 0 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div(class: "lui-action_buttons__button-group") do %>
  2. <% buttons.each do |button| %>
  3. <%= button %>
  4. <% end %>
  5. <% end %>

app/components/loopos_ui/action_menu.rb

62.5% lines covered

0.0% branches covered

16 relevant lines. 10 lines covered and 6 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. # app/components/loopos_ui/action_menu.rb
  2. 1 module LooposUi
  3. 1 class ActionMenu < LoopComponent
  4. 1 include LooposUi::FaviconAware
  5. 1 renders_one :trigger, types: {
  6. button: { renders: Button, as: :trigger_button },
  7. slot: { renders: ->(&block) { capture(&block) }, as: :trigger },
  8. }
  9. 1 option :placement, default: -> { "bottom" }, type: Types::String, optional: true
  10. 1 option :options, Types::Array.of(Types::Hash)
  11. 1 option :portal_data_controllers, default: -> { [] }, type: Types::Array.of(Types::String)
  12. 1 def data_controllers
  13. ["modal", "form-submit", "pubsub"].push(*portal_data_controllers).join(" ")
  14. end
  15. 1 def option_attributes(option)
  16. then: 0 if option[:disabled].present?
  17. {}
  18. else: 0 else
  19. option[:attributes] || {}
  20. end
  21. end
  22. 1 def toggle_attributes(option)
  23. option[:toggle_attributes] || {}
  24. end
  25. end
  26. end

app/components/loopos_ui/action_menu/action_menu.html.erb

0.0% lines covered

0.0% branches covered

25 relevant lines. 0 lines covered and 25 lines missed.
20 total branches, 0 branches covered and 20 branches missed.
    
  1. <div
  2. data-controller="action-menu"
  3. data-action="modal:open@window->action-menu#disableTippy modal:close@window->action-menu#enableTippy"
  4. data-action-menu-placement-value="<%= placement %>"
  5. class="lui-action-menu">
  6. <div data-action-menu-target="trigger" class="flex">
  7. <%= trigger %>
  8. </div>
  9. <div data-action-menu-target="menu" class="hidden lui-action-menu__wrapper" data-controller="<%= data_controllers %>">
  10. <div class="lui-action-menu__options" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
  11. <% options.each do |option| %>
  12. then: 0 else: 0 <%= tag.div(class: "contents #{option[:disabled] ? 'cursor-not-allowed' : 'cursor-pointer'}") do %>
  13. then: 0 <% if option[:url] %>
  14. <a class="lui-action-menu__option" href="<%= option[:url] %>" <%= tag.attributes(option_attributes(option)) %>>
  15. <div class="lui-action-menu__option-text">
  16. then: 0 else: 0 <%= tag.i(class: faviconize(option[:icon])) if option[:icon] %>
  17. <span><%= option[:text] %></span>
  18. then: 0 else: 0 <%= render LooposUi::IconTooltip.new(icon: "fa-regular fa-circle-info", size: "12", text: option[:tooltip]) if option[:tooltip]%>
  19. </div>
  20. then: 0 else: 0 <% if option[:toggle] %>
  21. <%= render LooposUi::Toggle.new(
  22. **toggle_attributes(option).merge(color: LooposUi::Colors.find("apps-800-primary", LooposUi.config.app_type))
  23. ) %>
  24. <% end %>
  25. </a>
  26. else: 0 <% else %>
  27. then: 0 else: 0 <div class="lui-action-menu__option <%= option_attributes(option)[:'data-action'] ? 'cursor-pointer' : '' %>" <%= tag.attributes(option_attributes(option)) %>>
  28. then: 0 else: 0 <div class="lui-action-menu__option-text <%= option_attributes(option)[:'data-action'] ? 'cursor-pointer' : 'cursor-default' %>">
  29. then: 0 else: 0 <%= tag.i(class: faviconize(option[:icon])) if option[:icon] %>
  30. <span><%= option[:text] %></span>
  31. then: 0 else: 0 <%= render LooposUi::IconTooltip.new(icon: "fa-regular fa-circle-info", size: "12", text: option[:tooltip]) if option[:tooltip] %>
  32. </div>
  33. then: 0 else: 0 <% if option[:toggle] %>
  34. <%= render LooposUi::Toggle.new(
  35. **toggle_attributes(option).merge(color: LooposUi::Colors.find("apps-800-primary"))
  36. ) %>
  37. <% end %>
  38. </div>
  39. <% end %>
  40. <% end %>
  41. <% end %>
  42. </div>
  43. </div>
  44. </div>

app/components/loopos_ui/app_instance/card.rb

42.86% lines covered

0.0% branches covered

21 relevant lines. 9 lines covered and 12 lines missed.
11 total branches, 0 branches covered and 11 branches missed.
    
  1. 1 module LooposUi
  2. 1 module AppInstance
  3. 1 class Card < LoopComponent
  4. 1 option :app_instance
  5. 1 option :policy,
  6. Types::Bool |
  7. Types.Interface(:view_details?) |
  8. Types::Hash.schema(
  9. view_details?: Types::Bool | Types.Interface(:call),
  10. ),
  11. optional: true,
  12. default: -> { true }
  13. 1 private
  14. # Used policy methods, only has one method for now
  15. # TODO: abstract policy options to a concern. Duplicated in ModelAssociationList
  16. 1 [:view_details].each do |method|
  17. 1 define_method("can_#{method}?") do
  18. case policy
  19. when: 0 when TrueClass, FalseClass
  20. policy
  21. when: 0 when Hash
  22. then: 0 else: 0 then: 0 if policy[method]&.respond_to?(:call)
  23. policy[method].call
  24. else: 0 else
  25. !!policy[method]
  26. end
  27. else: 0 else # Assume object responds to method
  28. policy.public_send("#{method}?")
  29. end
  30. end
  31. end
  32. 1 def tag_status
  33. case app_instance.status
  34. when: 0 when "creating", "updating", "deleting", "stopped"
  35. {
  36. kind: "warning",
  37. icon: "fa-regular fa-circle-exclamation",
  38. }
  39. when: 0 when "created", "to_create", "running"
  40. {
  41. kind: "success",
  42. icon: "fa-regular fa-check-circle",
  43. }
  44. when: 0 when "error", "error_creating", "error_running", "error_updating", "error_deleting", "to_delete", "deleted", "partially_deleted"
  45. {
  46. kind: "danger",
  47. icon: "fa-regular fa-circle-xmark",
  48. }
  49. else: 0 else
  50. {
  51. kind: "informative",
  52. icon: "fa-regular fa-circle",
  53. }
  54. end
  55. end
  56. end
  57. end
  58. end

app/components/loopos_ui/app_instance/card/card.html.erb

0.0% lines covered

0.0% branches covered

23 relevant lines. 0 lines covered and 23 lines missed.
17 total branches, 0 branches covered and 17 branches missed.
    
  1. <%# TODO: migrate fully to UI, this is just a wrapper over an existing partial %>
  2. <%
  3. text_color =
  4. when: 0 case app_instance.app.name
  5. when: 0 when "core" then "text-orange-700!"
  6. when: 0 when "submission" then "text-yellow-700!"
  7. when: 0 when "validation" then "text-teal-800!"
  8. else: 0 when "handling" then "text-[#19007D]!"
  9. else "text-[#0069CA]!"
  10. end %>
  11. <div class="app-card" data-controller="app-cards" data-app-cards-target="overlay">
  12. <div class="app-card__background"></div>
  13. <div class="app-card__background-image-wrapper">
  14. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 <%= image_tag(app_instance.app.icon&.url || "loopos-icon.png", class:"app-card__background-image #{app_instance.app.icon&.url.present? ? "" : "gray-image"}")%>
  15. </div>
  16. <div class="absolute top-4 left-2 z-10 flex flex-col gap-1">
  17. <p class="copy-12-medium max-w-[70%]"><%= app_instance.app_release.version_slug %></p>
  18. then: 0 else: 0 <%= react_component("Tag", { text: app_instance.partnable&.name, variant: 'primary', kind: "manager", tSize: "large", line: "solid", imgURL: app_instance.partnable.logo.url }) %>
  19. </div>
  20. then: 0 else: 0 <% if helpers.current_user.can_view_app_instance_status? %>
  21. <div class="absolute top-4 right-2 z-10">
  22. <%= react_component("Tag", { text: app_instance.status, variant: 'primary', kind: tag_status.dig(:kind), tSize: "large", line: "solid", icon: tag_status.dig(:icon) }) %>
  23. </div>
  24. <% end %>
  25. <div class="card__overlay ">
  26. <div class="bg-glassify_intense rounded-full flex items-center absolute -top-6 right-2 p-1 cursor-pointer" data-action="click->app-cards#toggleOverlay" data-app-cards-target="icon"><i class="fa-regular fa-chevron-up text-xs text-black font-bold"></i></div>
  27. <div class="card__header app-card__details-wrapper">
  28. <div class="card__header-text">
  29. <h3 class="app-card__name"><%= app_instance.displayed_label %></h3>
  30. <span class="card__status">id: <%= app_instance.id %> </span>
  31. </div>
  32. <input type="checkbox" selected-id="<%= app_instance.id %>" class="<%= text_color %>">
  33. </div>
  34. <div class="card__description">
  35. then: 0 else: 0 <% if can_view_details? %>
  36. <a href="<%= admin_app_management_app_instance_path(app_instance) %>" target="_blank" >
  37. <%= react_component("Tag", { text:"View Details", variant: 'primary', kind: "neutral", tSize: "extraLarge", line: "solid", icon:"fa-sharp fa-regular fa-arrow-up-right-from-square" }) %>
  38. </a>
  39. <% end %>
  40. <a href="<%= app_instance.app_url %>" target="_blank" >
  41. <%= react_component("Tag", { text:"Open", variant: 'primary', kind: "neutral", tSize: "extraLarge", line: "solid", icon:"fa-sharp fa-regular fa-arrow-up-right-from-square" }) %>
  42. </a>
  43. </div>
  44. </div>
  45. </div>

app/components/loopos_ui/app_layout_component.rb

59.09% lines covered

0.0% branches covered

44 relevant lines. 26 lines covered and 18 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class AppLayoutComponent < LoopComponent
  4. 1 include Turbo::FramesHelper
  5. 1 include Turbo::StreamsHelper
  6. 1 renders_one :header
  7. 1 renders_one :select_bar, "SelectBar"
  8. 1 renders_one :bottom_bar, LooposUi::BottomBar
  9. # FIXME: When refactoring Sidebar, ensure that it gets the configuration from the initializer
  10. 1 renders_one :sidebar, ->(**kwargs) { LooposUi::Sidebar.new(**kwargs) }
  11. 1 option :title, optional: true # Not used yet, not documented
  12. 1 option :main_layout, default: -> { true }
  13. 1 def before_render
  14. else: 0 then: 0 with_sidebar unless sidebar?
  15. end
  16. # TODO: document this and possibly refactor to real component
  17. # But time is of essence, and the public facing API is good enough (check app_layout_preview/default.html.erb)
  18. 1 class SelectBar < LoopComponent
  19. 1 erb_template <<~ERB
  20. <nav class="flex w-full min-h-8 box-content py-1.5 pr-4 pl-6 items-center space-between bg-secondary border-b border-gray-200">
  21. <%= content %>
  22. </nav>
  23. ERB
  24. 1 def render?
  25. has_any_selectors?
  26. end
  27. 1 def has_any_selectors?
  28. content.present? && Nokogiri::HTML(content.to_s).search(".lui-select-bar__selector").any?
  29. end
  30. 1 class Selector < LoopComponent
  31. 1 erb_template <<-ERB
  32. <div class="lui-select-bar__selector">
  33. <%= render LooposUi::FormEntry.new(label: label, orientation: :horizontal) do |fe| %>
  34. <% fe.with_input do %>
  35. <%= render LooposUi::Inputs::Select.new(
  36. name: name,
  37. options: options,
  38. value: value,
  39. placeholder: "Select..",
  40. mode: :autosubmit,
  41. )
  42. %>
  43. <% end %>
  44. <% end %>
  45. </div>
  46. ERB
  47. 1 option :type, Types::Coercible::Symbol.enum(:core), default: -> { :core }
  48. 1 option :options, Types::Array.of(Types::Hash.schema(
  49. value: Types::Coercible::String,
  50. text: Types::String,
  51. ))
  52. 1 option :value, Types::Coercible::String
  53. 1 option :name, Types::Coercible::String
  54. # TODO: these components are kinda hacked together
  55. # and should be refactored internally. The FormEntry label does not support colors or icons, but
  56. # with html_safe trick it works
  57. 1 def label
  58. helpers.content_tag(:span, class: "text-app-800-primary") do
  59. concat(helpers.content_tag(:icon, "", class: "fa-regular fa-map-pin mr-1"))
  60. concat(type.to_s.capitalize)
  61. concat(":")
  62. end.html_safe
  63. end
  64. 1 def render?
  65. options.present? && options.count > 1
  66. end
  67. end
  68. end
  69. 1 class RootStyles < LoopComponent
  70. end
  71. end
  72. 1 AppLayout = LooposUi::AppLayoutComponent # TODO: Alias for the component, deprecate next major version
  73. # TODO: Alias for the component so it can be used as LooposUi::SelectBar Maybe we'll move it outside
  74. 1 SelectBar = LooposUi::AppLayoutComponent::SelectBar
  75. end

app/components/loopos_ui/app_layout_component/app_layout_component.html.erb

0.0% lines covered

0.0% branches covered

22 relevant lines. 0 lines covered and 22 lines missed.
10 total branches, 0 branches covered and 10 branches missed.
    
  1. <%# DEPRECATED: Use LooposUi::V2::AppLayout instead %>
  2. <div class="flex grow flex-col w-full h-full"
  3. data-controller="miniapps lui--global-modal"
  4. data-action="showModal@window->lui--global-modal#showModal"
  5. >
  6. <%= render LooposUi::AppLayout::RootStyles.new %>
  7. <div class="w-full lui-header-slot">
  8. <%= header %>
  9. </div>
  10. <div class="flex grow flex-row overflow-hidden relative">
  11. <%= sidebar %>
  12. then: 0 else: 0 <div class="relative flex flex-col flex-[1_0_0] self-stretch items-start overflow-scroll lui-app-layout_id" style="<%= bottom_bar.present? ? 'height: calc(100% - 48px)' : '' %>">
  13. <div class="h-full flex flex-col absolute left-0" style="width: 100%; transition: width 25ms ease-in;">
  14. <%= select_bar %>
  15. <%#= TODO: After migrating apps to not use main_layout, remove this conditional %>
  16. then: 0 <% if main_layout.present? %>
  17. then: 0 else: 0 <div class="<%= bottom_bar.present? ? 'pb-16' : '' %>">
  18. <%= content %>
  19. </div>
  20. else: 0 <% else %>
  21. <turbo-frame data-turbo-action="advance" class="h-full" id="lui-main-layout" data-turbo-frame="lui-main-layout">
  22. <%
  23. # I want to cal active_item but internaly it calls helpers (path etc) but I dont want to render the component just for that
  24. # So we pass in the current view context manually
  25. then: 0 else: 0 if LooposUi::Sidebar::USE_UI_2
  26. sd = LooposUi::Sidebar::V2::Sidebar.new
  27. sd.instance_variable_set(:@view_context, view_context)
  28. end
  29. %>
  30. then: 0 else: 0 <%= helpers.turbo_stream.update("lui-sidebar-active-item-id", sd.active_item) if LooposUi::Sidebar::USE_UI_2 %>
  31. <%= render LooposUi::LayoutLoading.new %>
  32. <div class="lui-main_layout__content" data-skeleton-loading="<%= LooposUi.config.enable_loading_skeletons %>">
  33. <%= content %>
  34. </div>
  35. </turbo-frame>
  36. <% end %>
  37. </div>
  38. <div class="fixed bottom-0" style="width: calc(100% - var(--sidebar-width));">
  39. <%= bottom_bar %>
  40. </div>
  41. </div>
  42. </div>
  43. <%= turbo_stream_from "lui-app-layout" %>
  44. <%= turbo_stream_from :toasters %>
  45. <div id="lui-toasters" popover="manual" data-controller="toasters" data-toasters-new-toaster-url-value="<%= LooposUi::Engine.routes.url_helpers.toasters_path %>">
  46. </div>
  47. </div>

app/components/loopos_ui/app_layout_component/root_styles.html.erb

0.0% lines covered

100.0% branches covered

10 relevant lines. 0 lines covered and 10 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <style>
  2. :root {
  3. --app-900-hover: <%= LooposUi::Colors.find("apps-900-hover") %>;
  4. --app-800-primary: <%= LooposUi::Colors.find("apps-800-primary") %>;
  5. --app-400: <%= LooposUi::Colors.find("apps-400") %>;
  6. --app-300: <%= LooposUi::Colors.find("apps-300") %>;
  7. --app-200: <%= LooposUi::Colors.find("apps-200") %>;
  8. --app-100: <%= LooposUi::Colors.find("apps-100") %>;
  9. --app-opacity5: <%= LooposUi::Colors.find("apps-opacity5") %>;
  10. /* Current app variables, for semantic colors */
  11. --current-surface-app: var(--color-surface-apps-<%= LooposUi.config.app_type %>);
  12. --current-text-app: var(--color-text-apps-<%= LooposUi.config.app_type %>);
  13. }
  14. </style>

app/components/loopos_ui/app_logo.rb

60.0% lines covered

0.0% branches covered

15 relevant lines. 9 lines covered and 6 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. 1 module LooposUi
  2. 1 class AppLogo < LoopComponent
  3. 1 require "stringio"
  4. 1 APPS = [:manager, :core, :submission, :validation, :handling, :hubs].freeze
  5. 1 option :app, Types::Coercible::Symbol.enum(*APPS)
  6. 1 option :expanded, Types::Strict::Bool, default: -> { false }
  7. 1 def app_logo_file
  8. app_logo_file = StringIO.new
  9. app_logo_file << "app_logos/logo_"
  10. then: 0 else: 0 app_logo_file << (app == :manager ? "loopos" : app.to_s)
  11. then: 0 else: 0 app_logo_file << (expanded ? "_expanded.svg" : ".svg")
  12. app_logo_file.string
  13. end
  14. 1 class << self
  15. 1 def apps
  16. APPS
  17. end
  18. end
  19. end
  20. end

app/components/loopos_ui/app_logo/app_logo.html.erb

0.0% lines covered

100.0% branches covered

2 relevant lines. 0 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <div class="lui-app_logo">
  2. <%= image_tag app_logo_file, class: "lui-app_logo_image" %>
  3. </div>

app/components/loopos_ui/apps/list.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Apps
  3. 1 class List < LoopComponent
  4. 1 ORDER = [:manager, :core, :submission, :validation, :hubs, :handling, :exit]
  5. 1 option :list
  6. end
  7. end
  8. end

app/components/loopos_ui/apps/list/list.html.erb

0.0% lines covered

0.0% branches covered

7 relevant lines. 0 lines covered and 7 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%= render LooposUi::TokenList.new do |token_list| %>
  2. <% ORDER.each do |app| %>
  3. <% details = list[app] %>
  4. then: 0 else: 0 <% if details.present? %>
  5. <% token_list.with_token_manual do %>
  6. <%= render LooposUi::IconTooltip.new(
  7. app: app.to_s,
  8. count: details[:count],
  9. text: details[:tooltip]
  10. ) %>
  11. <% end %>
  12. <% end %>
  13. <% end %>
  14. <% end %>

app/components/loopos_ui/assign/assign_component.html.erb

0.0% lines covered

100.0% branches covered

3 relevant lines. 0 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <div class="loopui-assign-component">
  2. <div class="loopui-assign-component__left">
  3. <%= left_section %>
  4. </div>
  5. <div class="loopui-assign-component__right">
  6. <%= right_section %>
  7. </div>
  8. </div>

app/components/loopos_ui/assign/assign_component.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 module Assign
  4. 1 class AssignComponent < ViewComponent::Base
  5. 1 include Turbo::FramesHelper
  6. 1 include Turbo::StreamsHelper
  7. 1 renders_one :left_section
  8. 1 renders_one :right_section
  9. end
  10. end
  11. end

app/components/loopos_ui/assign/assign_section.html.erb

0.0% lines covered

0.0% branches covered

57 relevant lines. 0 lines covered and 57 lines missed.
44 total branches, 0 branches covered and 44 branches missed.
    
  1. <div class="loopui-assign-section">
  2. <div class="loopui-assign-section__top-wrapper">
  3. <%= render LooposUi::TitleDescription.new(title: title, size: "small") %>
  4. <%# FIXME: fix cancel btn considering the cancel and save logics %>
  5. then: 0 else: 0 <% if @selected_resource.present? %>
  6. <a href="" data-turbo-frame="tabs_content"><i class="fa-regular fa-times"></i></a>
  7. <% end %>
  8. </div>
  9. <div class="loopui-assign-section__wrapper" data-controller="assign-search">
  10. <% if @assign_mode %>
  11. then: 0 <%# submit form wrapping %>
  12. then: 0 else: 0 <% if !@single_side %>
  13. <div class="loopui-assign-section__elements-section">
  14. then: 0 <% if @assigned.present? %>
  15. <% @assigned.each do |resource| %>
  16. <%= render LooposUi::Assign::Elements.new(main_resource: @main_resource, selected_resource: @selected_resource, resource: resource, edit_mode: @edit_mode, assign_mode: @assign_mode, edit_path: @edit_path, unassign_path: @unassign_path, card: @card, assigned: true, method: @method, turbo_frame_prefix: @turbo_frame_prefix)%>
  17. <% end %>
  18. else: 0 <% else %>
  19. <%= render LooposUi::Assign::Empty.new(title: "No assigned resources") %>
  20. <% end %>
  21. </div>
  22. <div class="loopui-assign-section__separator">
  23. <hr class="loopui-assign-section__line">
  24. <span class="loopui-assign-section__separator-text"> Add more <%= title %></span>
  25. <hr class="loopui-assign-section__line">
  26. </div>
  27. <% end %>
  28. <%# search form wrapping %>
  29. then: 0 else: 0 <% if @search_path.present? %>
  30. then: 0 else: 0 <%= form_tag @search_path&.call(@main_resource), method: @search_method, data:{ action: "submit->assign-search#setSectionLoading"} do %>
  31. then: 0 else: 0 <%= hidden_field_tag :selected_resource_id, @selected_resource&.id %>
  32. <%= hidden_field_tag :turbo_frame_prefix, @turbo_frame_prefix %>
  33. then: 0 else: 0 <% if @single_side %>
  34. <%= hidden_field_tag :format, :turbo_stream %>
  35. <% end %>
  36. <%= render LooposUi::Filter::SearchComponent.new(param: @search_param, search_query: @search_query, placeholder: t(".search")) %>
  37. <% end %>
  38. <% end %>
  39. <div class="loopui-assign-section__elements-section" data-assign-search-target="result">
  40. then: 0 <% if single_side %>
  41. then: 0 <% if @assigned.present? || @unassigned.present? %>
  42. then: 0 else: 0 <% resources = @assigned.present? ? @assigned : @unassigned %>
  43. <% assigned_status = @assigned.present? %>
  44. <% resources.each do |resource| %>
  45. <%= render LooposUi::Assign::Elements.new(
  46. main_resource: @main_resource,
  47. selected_resource: @selected_resource,
  48. resource: resource,
  49. edit_mode: false,
  50. assign_mode: @assign_mode,
  51. edit_path: @edit_path,
  52. then: 0 else: 0 assign_path: assigned_status ? nil : @assign_path,
  53. then: 0 else: 0 unassign_path: assigned_status ? @unassign_path : nil,
  54. card: @card,
  55. assigned: true,
  56. method: @method,
  57. turbo_frame_prefix: @turbo_frame_prefix,
  58. ) %>
  59. <% end %>
  60. else: 0 <% else %>
  61. then: 0 else: 0 <%= render LooposUi::Assign::Empty.new(title: @assigned.present? ? "No assigned resources" : "No more resources to assign") %>
  62. <% end %>
  63. else: 0 <% else %>
  64. then: 0 <% if @unassigned.present? %>
  65. <% resources = @unassigned %>
  66. <% assigned_status = false %>
  67. <% resources.each do |resource| %>
  68. <%= render LooposUi::Assign::Elements.new(
  69. main_resource: @main_resource,
  70. selected_resource: @selected_resource,
  71. resource: resource,
  72. edit_mode: false,
  73. assign_mode: @assign_mode,
  74. edit_path: @edit_path,
  75. then: 0 else: 0 assign_path: assigned_status ? nil : @assign_path,
  76. then: 0 else: 0 unassign_path: assigned_status ? @unassign_path : nil,
  77. card: @card,
  78. assigned: true,
  79. method: @method,
  80. turbo_frame_prefix: @turbo_frame_prefix,
  81. ) %>
  82. <% end %>
  83. else: 0 <% else %>
  84. then: 0 else: 0 <%= render LooposUi::Assign::Empty.new(title: @assigned.present? ? "No assigned resources" : "No more resources to assign") %>
  85. <% end %>
  86. <% end %>
  87. </div>
  88. else: 0 <% else %>
  89. then: 0 else: 0 <% if @search_path.present? %>
  90. <%= form_tag @search_path.call(@main_resource), method: @search_method, data:{ action: "submit->assign-search#setSectionLoading"} do %>
  91. then: 0 else: 0 <%= hidden_field_tag :selected_resource_id, @selected_resource&.id %>
  92. then: 0 else: 0 <% if @single_side %>
  93. <%= hidden_field_tag :format, :turbo_stream %>
  94. <% end %>
  95. <%= render LooposUi::Filter::SearchComponent.new(param: @search_param, search_query: @search_query, placeholder: t(".search")) %>
  96. <% end %>
  97. <% end %>
  98. <div class="loopui-assign-section__elements-section" data-assign-search-target="result">
  99. then: 0 <% if @assigned.present? %>
  100. <% @assigned.each do |resource| %>
  101. <%= render LooposUi::Assign::Elements.new(main_resource: @main_resource, selected_resource: @selected_resource, resource: resource, edit_mode: @edit_mode, assign_mode: @assign_mode, edit_path: @edit_path, assign_path: @assign_path, unassign_path: @unassign_path, card: @card, assigned: true, method: @method, turbo_frame_prefix: @turbo_frame_prefix)%>
  102. <% end %>
  103. else: 0 <% else %>
  104. <%= render LooposUi::Assign::Empty.new(title: "No assigned resources") %>
  105. <% end %>
  106. </div>
  107. <% end %>
  108. </div>
  109. </div>

app/components/loopos_ui/assign/assign_section.rb

33.33% lines covered

100.0% branches covered

27 relevant lines. 9 lines covered and 18 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 module Assign
  4. 1 class AssignSection < ViewComponent::Base
  5. 1 include Turbo::FramesHelper
  6. 1 include Turbo::StreamsHelper
  7. 1 renders_one :assigned_area
  8. 1 renders_one :unassigned_area
  9. 1 attr_reader :card,
  10. :main_resource,
  11. :selected_resource ,
  12. :title,
  13. :assigned,
  14. :unassigned,
  15. :edit_mode,
  16. :assign_mode,
  17. :edit_path,
  18. :assign_path,
  19. :unassign_path,
  20. :search_path,
  21. :single_side,
  22. :method,
  23. :search_param,
  24. :search_query,
  25. :search_method,
  26. :turbo_frame_prefix
  27. 1 def initialize(card: "", main_resource: nil, selected_resource: nil, title: "", assigned: nil, unassigned: nil,
  28. edit_mode: false, assign_mode: false, edit_path: nil, assign_path: nil, unassign_path: nil, search_path: nil, single_side: false, method: nil, search_param: "q[search]", search_query: "", search_method: :post, turbo_frame_prefix: "")
  29. @card = card
  30. @main_resource = main_resource
  31. @selected_resource = selected_resource
  32. @title = title
  33. @assigned = assigned
  34. @unassigned = unassigned
  35. @edit_mode = edit_mode
  36. @assign_mode = assign_mode
  37. @edit_path = edit_path
  38. @assign_path = assign_path
  39. @unassign_path = unassign_path
  40. @search_path = search_path
  41. @single_side = single_side
  42. @method = method
  43. @search_param = search_param
  44. @search_query = search_query
  45. @search_method = search_method
  46. @turbo_frame_prefix = turbo_frame_prefix
  47. # @args = args
  48. end
  49. end
  50. end
  51. end

app/components/loopos_ui/assign/elements.html.erb

0.0% lines covered

0.0% branches covered

16 relevant lines. 0 lines covered and 16 lines missed.
26 total branches, 0 branches covered and 26 branches missed.
    
  1. <%
  2. then: 0 card_partial = if @card.present?
  3. render partial: @card, locals: { resource: @resource, assigned: @assigned , main_resource: @main_resource}
  4. else: 0 else
  5. "Missing card partial for: #{resource.class.name}"
  6. end
  7. %>
  8. <div class="relative">
  9. then: 0 <% if @edit_mode %>
  10. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 <%= link_to @edit_path.call(selected_resource_id: @selected_resource&.id , resource_id: @resource.id, turbo_frame: @turbo_frame_prefix.present? ? "#{@turbo_frame_prefix}__assign__right" : "assign__right", turbo_frame_prefix: @turbo_frame_prefix), method: :post, data: { turbo_frame: @turbo_frame_prefix.present? ? "#{@turbo_frame_prefix}__assign__right" : "assign__right", controller: "assign" }, class: "group", id: "your-link-id" do %>
  11. <%= card_partial %>
  12. <% end %>
  13. else: 0 <% else %>
  14. <%= card_partial %>
  15. <% end %>
  16. else: 0 <% if @assign_mode && !@resource.try(:inherited?) && !@resource.try(:unassignable?) %>
  17. then: 0 <%
  18. assign = @assign_path.present?
  19. then: 0 else: 0 change_path = assign ? @assign_path : @unassign_path
  20. then: 0 else: 0 icon = assign ? "fa-plus" : "fa-minus"
  21. then: 0 else: 0 then: 0 else: 0 method = @method.present? ? @method : (assign ? :put : :delete)
  22. %>
  23. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 <%= link_to change_path.call(selected_resource_id: @selected_resource&.id , resource_id: @resource.id, turbo_frame: @turbo_frame_prefix.present? ? "#{@turbo_frame_prefix}__assign__left" : "assign__left", turbo_frame_prefix: @turbo_frame_prefix), data:{ turbo_frame: @turbo_frame_prefix.present? ? "#{@turbo_frame_prefix}__assign__left" : "assign__left" }, method: method, class:"loopui-center-buttn-temp" do %>
  24. <div class="w-8 h-8 p-3 bg-gray-50 rounded-md border border-zinc-200 justify-center items-center gap-2 inline-flex">
  25. <i class="fa-regular <%= icon %> text-gray-900"></i>
  26. </div>
  27. <% end %>
  28. <% end %>
  29. </div>

app/components/loopos_ui/assign/elements.rb

35.0% lines covered

100.0% branches covered

20 relevant lines. 7 lines covered and 13 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 module Assign
  4. 1 class Elements < ViewComponent::Base
  5. 1 include Turbo::FramesHelper
  6. 1 include Turbo::StreamsHelper
  7. 1 attr_reader :card,
  8. :main_resource,
  9. :selected_resource,
  10. :resource,
  11. :edit_mode,
  12. :edit_path,
  13. :assign_mode,
  14. :assign_path,
  15. :unassign_path,
  16. :assigned,
  17. :method,
  18. :turbo_frame_prefix
  19. 1 def initialize(card: "", main_resource: nil, selected_resource: nil, resource: nil,edited_resource: nil,
  20. edit_mode: false, edit_path: nil, assign_mode: false, assign_path: nil, unassign_path: nil, assigned: false, method: nil, turbo_frame_prefix: "")
  21. @card = card
  22. @main_resource = main_resource
  23. @selected_resource = selected_resource
  24. @resource = resource
  25. @edited_resource = edited_resource
  26. @edit_mode = edit_mode
  27. @assign_mode = assign_mode
  28. @edit_path = edit_path
  29. @assign_path = assign_path
  30. @unassign_path = unassign_path
  31. @assigned = assigned
  32. @method = method
  33. @turbo_frame_prefix = turbo_frame_prefix
  34. end
  35. end
  36. end
  37. end

app/components/loopos_ui/assign/empty.html.erb

0.0% lines covered

100.0% branches covered

2 relevant lines. 0 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <div class="w-full h-[72px] p-2 bg-gray-50 rounded border border-gray-50 justify-center items-center gap-2 inline-flex">
  2. <div class="grow shrink basis-0 flex-col justify-start items-center gap-2 inline-flex">
  3. <div class="self-stretch h-[17px] flex-col justify-start items-center gap-2 flex">
  4. <div class="text-neutral-800 copy-14"><%= title %></div>
  5. </div>
  6. </div>
  7. </div>

app/components/loopos_ui/assign/empty.rb

83.33% lines covered

100.0% branches covered

6 relevant lines. 5 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 module Assign
  4. 1 class Empty < ViewComponent::Base
  5. 1 attr_reader :title
  6. 1 def initialize(title: "")
  7. @title = title
  8. end
  9. end
  10. end
  11. end

app/components/loopos_ui/assign/update.rb

25.93% lines covered

100.0% branches covered

27 relevant lines. 7 lines covered and 20 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 module Assign
  4. 1 class Update < ViewComponent::Base
  5. 1 include Turbo::FramesHelper
  6. 1 include Turbo::StreamsHelper
  7. 1 attr_reader :card,
  8. :update_wrapper,
  9. :main_resource,
  10. :selected_resource,
  11. :title,
  12. :assigned,
  13. :unassigned,
  14. :edit_mode,
  15. :edit_path,
  16. :assign_mode,
  17. :assign_path,
  18. :unassign_path,
  19. :change_turbo_stream,
  20. :search_path,
  21. :single_side,
  22. :method,
  23. :search_param,
  24. :search_query,
  25. :search_method,
  26. :turbo_frame_prefix
  27. 1 def initialize(card: "",update_wrapper:, main_resource:, selected_resource: nil, title: "", assigned: nil,
  28. unassigned: nil, edit_mode: false, edit_path: nil, assign_mode: false, assign_path: nil, unassign_path: nil, change_turbo_stream: false, search_path: nil, single_side: false, method: nil, search_param: "q[search]", search_query: "", search_method: :post, turbo_frame_prefix: "")
  29. @card = card
  30. @change_turbo_stream = change_turbo_stream
  31. @update_wrapper = update_wrapper
  32. @main_resource = main_resource
  33. @selected_resource = selected_resource
  34. @title = title
  35. @assigned = assigned
  36. @unassigned = unassigned
  37. @edit_mode = edit_mode
  38. @edit_path = edit_path
  39. @assign_mode = assign_mode
  40. @assign_path = assign_path
  41. @unassign_path = unassign_path
  42. @search_path = search_path
  43. @single_side = single_side
  44. @method = method
  45. @search_param = search_param
  46. @search_query = search_query
  47. @search_method = search_method
  48. @turbo_frame_prefix = turbo_frame_prefix
  49. end
  50. end
  51. end
  52. end

app/components/loopos_ui/assign/update.turbo_stream.erb

0.0% lines covered

0.0% branches covered

3 relevant lines. 0 lines covered and 3 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <% section =
  2. LooposUi::Assign::AssignSection.new(
  3. card: @card,
  4. main_resource: @main_resource,
  5. selected_resource: @selected_resource,
  6. title: @title,
  7. assigned: @assigned,
  8. unassigned: @unassigned,
  9. edit_path: @edit_path,
  10. edit_mode: @edit_mode,
  11. assign_mode: @assign_mode,
  12. assign_path: @assign_path,
  13. unassign_path: @unassign_path,
  14. search_path: @search_path,
  15. single_side: @single_side,
  16. method: @method,
  17. search_param: @search_param,
  18. search_query: @search_query,
  19. search_method: @search_method,
  20. turbo_frame_prefix: @turbo_frame_prefix,
  21. )
  22. %>
  23. then: 0 else: 0 <%= turbo_stream.update @turbo_frame_prefix.present? ? "#{@turbo_frame_prefix}__#{@update_wrapper}" : @update_wrapper do %>
  24. <%= render section %>
  25. <% end %>

app/components/loopos_ui/association_overlay.rb

87.88% lines covered

0.0% branches covered

33 relevant lines. 29 lines covered and 4 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class AssociationOverlay < LoopComponent
  4. 1 include Turbo::FramesHelper
  5. 1 renders_one :header, "LooposUi::AssociationOverlay::Header"
  6. 1 renders_one :selected_container
  7. 1 renders_one :results_container
  8. 1 renders_one :new_item
  9. 1 option :id, optional: true, default: -> { "association-overlay-#{Random.hex(10)}" }
  10. 1 option :hidden, default: -> { false }
  11. 1 option :tag_options, default: -> { {} }
  12. 1 option :draggable, default: -> { false }
  13. 1 option :can_search, default: -> { true }
  14. 1 option :show_selected, default: -> { true }
  15. 1 option :show_results, default: -> { true }
  16. 1 class SelectedContainer < LoopComponent
  17. 1 option :id, optional: true
  18. 1 renders_many :selected_items
  19. end
  20. 1 class ResultsContainer < LoopComponent
  21. 1 option :id, optional: true
  22. 1 option :show_results, default: -> { true }
  23. 1 renders_many :results
  24. 1 renders_one :new_item
  25. 1 def classes
  26. join_classes(
  27. "lui-association-overlay__results",
  28. then: 0 else: 0 show_results ? "overflow-scroll" : "overflow-hidden",
  29. )
  30. end
  31. end
  32. 1 class Header < LoopComponent
  33. 1 include FaviconAware
  34. 1 option :title, optional: true
  35. 1 option :icon, optional: true
  36. 1 renders_many :action_buttons, LooposUi::Button
  37. 1 def initialize(...)
  38. super(...)
  39. @icon = faviconize(icon)
  40. end
  41. end
  42. end
  43. end

app/components/loopos_ui/association_overlay/association_overlay.html.erb

0.0% lines covered

0.0% branches covered

9 relevant lines. 0 lines covered and 9 lines missed.
8 total branches, 0 branches covered and 8 branches missed.
    
  1. <%= tag.div(id: id, class: "lui-association-overlay", **tag_options) do %>
  2. then: 0 else: 0 <% if header? %>
  3. <%= header %>
  4. <div class="lui-association-overlay__divider"></div>
  5. <% end %>
  6. then: 0 else: 0 <%= selected_container if show_selected %>
  7. then: 0 else: 0 <%= tag.div(class: "lui-association-overlay__divider") if show_selected %>
  8. then: 0 else: 0 <% if can_search %>
  9. <div class="lui-association-overlay__search">
  10. <i class="fa-regular fa-search text-xxs font-bold"></i>
  11. <input type="text" placeholder="<%= t('.search_or_create') %>" data-model-association-overlay-target="input" autocomplete="off">
  12. </div>
  13. <div class="lui-association-overlay__divider"></div>
  14. <% end %>
  15. <%= results_container %>
  16. <% end %>

app/components/loopos_ui/association_overlay/header.html.erb

0.0% lines covered

0.0% branches covered

3 relevant lines. 0 lines covered and 3 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <div class="lui-association-overlay__header">
  2. then: 0 else: 0 <%= tag.i(class: icon) if icon.present?%>
  3. then: 0 else: 0 <%= title if title.present? %>
  4. </div>

app/components/loopos_ui/association_overlay/results_container.html.erb

0.0% lines covered

0.0% branches covered

7 relevant lines. 0 lines covered and 7 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. <%= tag.div( class: classes, id: id,
  2. data: { "model-association-overlay-target": "resultsContainer"} ) do %>
  3. then: 0 else: 0 <% results.each do |result| %>
  4. <%= result %>
  5. <% end if show_results %>
  6. then: 0 else: 0 <%= new_item if new_item? %>
  7. then: 0 else: 0 <%= tag.div(
  8. t('.no_results_found'),
  9. class: "hidden lui-association-overlay__empty-search",
  10. data: { model_association_overlay_target: "emptySearch" }
  11. ) if show_results %>
  12. <% end %>

app/components/loopos_ui/association_overlay/selected_container.html.erb

0.0% lines covered

100.0% branches covered

5 relevant lines. 0 lines covered and 5 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div(class: "lui-association-overlay__selected_list") do %>
  2. <div class="flex items-center gap-2 flex-wrap" id="<%= id %>" data-controller="drag">
  3. <% selected_items.each do |item| %>
  4. <%= item %>
  5. <% end %>
  6. </div>
  7. <% end %>

app/components/loopos_ui/async_select/async_select_component.html.erb

0.0% lines covered

0.0% branches covered

9 relevant lines. 0 lines covered and 9 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%= tag.div data: data_attributes do %>
  2. <%= header %>
  3. <div data-async-select-component-target='dropdown' data-dropdown-toggle-target="<%= dropdown_id %>" class="loopui-async-select__tag">
  4. <%= toggler %>
  5. </div>
  6. <div id="<%= dropdown_id %>" class="hidden loopui-async-select__dropdown">
  7. <div class="p-3">
  8. <label for="input-group-search" class="loopui-async-select__label">Search</label>
  9. <div class="relative">
  10. <div class="loopui-async-select__search-icon">
  11. <i class="fa-regular fa-magnifying-glass"></i>
  12. </div>
  13. <input
  14. id="input-group-search"
  15. type="text"
  16. data-action='keydown->async-select-component#filterKeyDown input->async-select-component#filterInput'
  17. class="loopui-async-select__search-input"
  18. then: 0 else: 0 placeholder="<%= @creatable ? "Search or Create..." : "Search..." %>">
  19. </div>
  20. </div>
  21. <div class="tags-filter-container loopui-async-select__filter-container" aria-labelledby="dropdownSearchButton">
  22. <%= turbo_frame_tag filter_wrapper_id, data:{ "async-select-component-target": "dropdownTurbo"} do %>
  23. <% end %>
  24. </div>
  25. </div>
  26. <% end %>

app/components/loopos_ui/async_select/async_select_component.rb

55.0% lines covered

100.0% branches covered

20 relevant lines. 11 lines covered and 9 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 module AsyncSelect
  4. 1 class AsyncSelectComponent < ViewComponent::Base
  5. 1 include Turbo::FramesHelper
  6. 1 renders_one :header
  7. 1 renders_one :toggler
  8. 1 renders_many :options, AsyncSelectOptionComponent
  9. 1 attr_accessor :object, :filter_wrapper_id, :param_key
  10. 1 def initialize(filter_path:,
  11. assign_path:,
  12. data_attributes: {},
  13. options: [],
  14. object: nil,
  15. filter_wrapper_id: nil,
  16. param_key: "name",
  17. creatable: false)
  18. @filter_path = filter_path
  19. @assign_path = assign_path
  20. @data_attributes = data_attributes
  21. @object = object
  22. @filter_wrapper_id = filter_wrapper_id
  23. @param_key = param_key
  24. @creatable = creatable
  25. end
  26. 1 def dropdown_id
  27. # Maybe we can unique property for the select component, random for now
  28. @dropdown_id ||= SecureRandom.hex(10)
  29. end
  30. 1 def data_attributes
  31. {
  32. controller: "async-select-component",
  33. "async-select-component-filter-path-value": @filter_path,
  34. "async-select-component-assign-path-value": @assign_path,
  35. "async-select-component-param-key-value": @param_key,
  36. }.merge!(@data_attributes)
  37. end
  38. end
  39. end
  40. end

app/components/loopos_ui/async_select/async_select_option_component.html.erb

0.0% lines covered

0.0% branches covered

12 relevant lines. 0 lines covered and 12 lines missed.
10 total branches, 0 branches covered and 10 branches missed.
    
  1. <%= turbo_frame_tag "filter-#{object_identifier}" do %>
  2. <%= tag.div( class:"w-full", data: { action: "click->async-select-component#labelClick", payload: data_payload, value: submit_value }.merge(label_data)) do %>
  3. then: 0 else: 0 <div <%= data_attributes.map { |key, value| "data-#{key}=#{value}" }.join(' ') if data_attributes.present? %>
  4. class="filter-options-container async-select__options-wrapper">
  5. then: 0 <% if label_component? %>
  6. <span class="loopui-async-select__options-text"><%= label_component %></span>
  7. else: 0 <% else %>
  8. then: 0 else: 0 <%= tag.label(input_id, class: "tags-label cursor-pointer tags-label-#{object_identifier} loopui-async-select__options-text", style: color.present? ? "color: #{color}" : '') do %>
  9. <%= label %>
  10. <% end %>
  11. <% end %>
  12. then: 0 <% if persisted? %>
  13. then: 0 else: 0 <% if edit_component.present? %>
  14. <%= edit_component %>
  15. <% end %>
  16. <% else %>
  17. else: 0 <%# TODO: translations %>
  18. <span class='loopui-async-select__create-new'>create new</span>
  19. <% end %>
  20. </div>
  21. <% end %>
  22. <% end %>

app/components/loopos_ui/async_select/async_select_option_component.rb

52.0% lines covered

0.0% branches covered

25 relevant lines. 13 lines covered and 12 lines missed.
14 total branches, 0 branches covered and 14 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 module AsyncSelect
  4. 1 class AsyncSelectOptionComponent < ViewComponent::Base
  5. 1 include Turbo::FramesHelper
  6. 1 renders_one :label_component
  7. 1 renders_one :edit_component
  8. 1 attr_accessor :label, :object, :data_payload, :data_attributes, :label_data, :submit_value
  9. 1 def initialize(label:, object: nil, data_payload: {}, data_attributes: nil, label_data: {}, submit_value: nil)
  10. @label = label
  11. @object = object
  12. @data_payload = data_payload
  13. then: 0 else: 0 @data_attributes = data_attributes.respond_to?(:dig) ? data_attributes.compact_blank : {}
  14. then: 0 else: 0 @label_data = label_data.respond_to?(:dig) ? label_data.compact_blank : {}
  15. @submit_value = submit_value || label
  16. end
  17. 1 def input_id
  18. "input_label_#{object_identifier}"
  19. end
  20. 1 def object_identifier
  21. then: 0 else: 0 object.respond_to?(:dom_id) ? dom_id(object) : object.to_s
  22. end
  23. 1 def theme
  24. then: 0 else: 0 @object.respond_to?(:theme) ? @object.theme : nil
  25. end
  26. 1 def color
  27. then: 0 else: 0 return theme.text_color if theme.present? && object.persisted?
  28. then: 0 else: 0 @color || (@object.respond_to?(:color) ? @object.color : "black")
  29. end
  30. 1 def persisted?
  31. then: 0 else: 0 @object.respond_to?(:persisted?) ? @object.persisted? : true
  32. end
  33. end
  34. end
  35. end

app/components/loopos_ui/async_select/filter_component.rb

70.0% lines covered

100.0% branches covered

10 relevant lines. 7 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 module AsyncSelect
  4. 1 class FilterComponent < ViewComponent::Base
  5. 1 include Turbo::FramesHelper
  6. 1 include Turbo::StreamsHelper
  7. 1 attr_accessor :resource, :filtered_resource, :filter_wrapper_prefix
  8. 1 def initialize(resource:, filtered_resource:, filter_wrapper_prefix:)
  9. @resource = resource
  10. @filtered_resource = filtered_resource
  11. @filter_wrapper_prefix = filter_wrapper_prefix
  12. end
  13. end
  14. end
  15. end

app/components/loopos_ui/async_select/filter_component.turbo_stream.erb

0.0% lines covered

0.0% branches covered

5 relevant lines. 0 lines covered and 5 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%= turbo_stream.update "#{@filter_wrapper_prefix}#{dom_id(@resource)}" do %>
  2. then: 0 <% if @filtered_resource.present? %>
  3. <% @filtered_resource.each do |bd| %>
  4. <%= render LooposUi::AsyncSelect::AsyncSelectOptionComponent.new(label: bd.name, object: bd, submit_value: bd.id) %>
  5. <% end %>
  6. else: 0 <% else %>
  7. No results found
  8. <% end %>
  9. <% end %>

app/components/loopos_ui/async_select/update_list_component.rb

77.78% lines covered

100.0% branches covered

9 relevant lines. 7 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 module AsyncSelect
  4. 1 class UpdateListComponent < ViewComponent::Base
  5. 1 include Turbo::FramesHelper
  6. 1 include Turbo::StreamsHelper
  7. 1 attr_accessor :update_wrapper, :partial_content
  8. 1 def initialize(update_wrapper:, partial_content:)
  9. @update_wrapper = update_wrapper
  10. @partial_content = partial_content
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/async_select/update_list_component.turbo_stream.erb

0.0% lines covered

100.0% branches covered

2 relevant lines. 0 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= turbo_stream.update @update_wrapper do %>
  2. <%= raw @partial_content %>
  3. <% end %>

app/components/loopos_ui/avatar.rb

71.43% lines covered

0.0% branches covered

21 relevant lines. 15 lines covered and 6 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. # frozen_string_literal: true
  2. # TODO: update this for UI 2.0 Foundations
  3. 1 module LooposUi
  4. 1 class Avatar < LoopComponent
  5. 1 SIZES = [:xs, :small, :medium, :large, :xl]
  6. 1 TYPES = [:circle, :square]
  7. 1 option :type, Types::Symbol.enum(*TYPES), default: -> { :circle }
  8. 1 mod :type
  9. 1 option :size, Types::Symbol.enum(*SIZES), default: -> { :medium }
  10. 1 mod :size
  11. 1 option :image_url, Types::String | Types::Symbol.enum(:default)
  12. 1 class << self
  13. # Some factory methods to make it easier to create avatars
  14. # For now they only set the correct type, but later they will be able to extract the image_url from the user or partner object
  15. 1 def user(user: nil, **options)
  16. new(type: :circle, **options)
  17. end
  18. 1 def partner(partner: nil, **options)
  19. new(type: :square, **options)
  20. end
  21. end
  22. 1 def before_render
  23. then: 0 else: 0 @image_url = default_image if image_url == :default
  24. end
  25. 1 private
  26. # TODO: set this to the correct path
  27. 1 def default_image
  28. case type
  29. when: 0 when :circle
  30. helpers.image_url("loopos_ui/avatar/user-avatar.png")
  31. else: 0 else # :square
  32. helpers.image_url("loopos_ui/avatar/partner-avatar.png")
  33. end
  34. end
  35. end
  36. end

app/components/loopos_ui/avatar/avatar.html.erb

0.0% lines covered

100.0% branches covered

3 relevant lines. 0 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div class: classes do %>
  2. <%= image_tag image_url, alt: "Avatar", class: "lui-avatar__image" %>
  3. <% end %>

app/components/loopos_ui/basic_radio_button.rb

92.31% lines covered

100.0% branches covered

13 relevant lines. 12 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class BasicRadioButton < LoopComponent
  3. 1 option :value, Types::Coercible::String
  4. 1 option :name, Types::Coercible::String
  5. 1 option :text, Types::String
  6. 1 option :checked, Types::Bool, default: -> { false }
  7. 1 option :disabled, Types::Bool, default: -> { false }
  8. 1 option :description, Types::String, default: -> { nil }
  9. 1 option :form, Types::Coercible::String, optional: true
  10. 1 use_extra_options
  11. 1 private
  12. 1 def option_id
  13. "#{name}-#{value}-#{text.parameterize}"
  14. end
  15. end
  16. end

app/components/loopos_ui/basic_radio_button/basic_radio_button.html.erb

0.0% lines covered

0.0% branches covered

7 relevant lines. 0 lines covered and 7 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%= tag.div class: classes, **extra_options do %>
  2. <%= tag.span class: "lui-basic_radio_button__option" do %>
  3. <%= tag.input type: "radio", id: option_id, name: name, value: value, checked: checked, disabled: disabled, form: form %>
  4. <%= tag.label text, for: option_id, class: "copy-14 text-content"%>
  5. <% end %>
  6. then: 0 else: 0 <% if description.present? %>
  7. <div class="lui-basic_radio_button__description">
  8. <%= tag.span description, class: "copy-14 text-content-secondary" %>
  9. </div>
  10. <% end %>
  11. <% end %>

app/components/loopos_ui/bottom_bar.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class BottomBar < LoopComponent
  3. 1 renders_many :mini_apps, LooposUi::MiniApp
  4. 1 renders_one :action_buttons, LooposUi::ActionButtons
  5. end
  6. end

app/components/loopos_ui/bottom_bar/bottom_bar.html.erb

0.0% lines covered

100.0% branches covered

6 relevant lines. 0 lines covered and 6 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <div>
  2. <% mini_apps.each do |mini_app| %>
  3. <div class="draggable w-fit h-fit absolute" data-controller="miniapp-drag">
  4. <%= mini_app %>
  5. </div>
  6. <% end %>
  7. </div>
  8. <div class="lui-bottom_bar">
  9. <%= action_buttons %>
  10. </div>

app/components/loopos_ui/breadcrumb_list.rb

53.33% lines covered

0.0% branches covered

15 relevant lines. 8 lines covered and 7 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class BreadcrumbList < ViewComponent::Base
  4. 1 renders_many :breadcrumbs, "Breadcrumb"
  5. 1 def before_render
  6. breadcrumbs.last && breadcrumbs.last.last = true
  7. end
  8. 1 class Breadcrumb < ViewComponent::Base
  9. 1 attr_writer :last
  10. 1 def initialize(href: nil, last: false, data: {})
  11. @href = href
  12. @last = last
  13. @data = data
  14. end
  15. 1 def call
  16. then: 0 if @last
  17. tag.span(content, class: "lui-breadcrumb_list__breadcrumb")
  18. else: 0 else
  19. link_to(
  20. content,
  21. @href,
  22. class: "lui-breadcrumb_list__breadcrumb",
  23. data: { turbo_action: "advance" }.merge(@data),
  24. )
  25. end
  26. end
  27. end
  28. end
  29. end

app/components/loopos_ui/breadcrumb_list/breadcrumb_list.html.erb

0.0% lines covered

0.0% branches covered

8 relevant lines. 0 lines covered and 8 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <div class="lui-breadcrumb_list">
  2. <% breadcrumbs.each_with_index do |b, index| %>
  3. <div class="lui-breadcrumb_list__breadcrumb-container">
  4. <%= b %>
  5. then: 0 else: 0 <% if index < breadcrumbs.length - 1 %>
  6. <span class="lui-breadcrumb_list__breadcrumb-separator">/</span>
  7. <% end %>
  8. </div>
  9. <% end %>
  10. </div>

app/components/loopos_ui/button.rb

32.26% lines covered

0.0% branches covered

124 relevant lines. 40 lines covered and 84 lines missed.
75 total branches, 0 branches covered and 75 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class Button < LoopComponent
  4. 1 include Presets
  5. 1 include LooposUi::FaviconAware
  6. 1 SIZES = [
  7. :default, :small, :tiny,
  8. ]
  9. 1 NEW_SIZES = [
  10. :large, :medium, :small, # We could make this compatible with the old sizes, but there's a collision :(
  11. ]
  12. 1 TYPES = [
  13. :primary,
  14. :secondary,
  15. :tertiary,
  16. ]
  17. # Deprecated, will be removed
  18. 1 KINDS = [
  19. :app,
  20. :neutral,
  21. :success,
  22. :danger,
  23. ]
  24. 1 APP_ICONS = [
  25. "core",
  26. "manager",
  27. "submission",
  28. "hubs",
  29. "validation",
  30. "handling",
  31. "exits",
  32. "impact",
  33. "submission_extra",
  34. ]
  35. 1 renders_one :status_dot, ->(kind:, **kwargs) {
  36. LooposUi::StatusDot.new(kind: kind, **kwargs)
  37. }
  38. 1 renders_one :counter, ->(count:, **kwargs) {
  39. LooposUi::Counter.new(count: count, **kwargs)
  40. }
  41. 1 renders_one :leading_icon, ->(icon:, **_kwargs) {
  42. then: 0 if APP_ICONS.include?(icon.to_s)
  43. else: 0 icon
  44. then: 0 elsif MIcon.icon?(icon)
  45. LooposUi::MIcon.new(icon, tag: :i, size: icon_size)
  46. else
  47. else: 0 # To temprarily support fa- icons but keep the new sizes
  48. content_tag(:div, class: "flex items-center justify-center", style: "width: #{icon_size}px; height: #{icon_size}px;") do
  49. content_tag(:i, "", class: "lui-button__icon lui-button__icon--#{@size} #{icon}", data: { "lui--button-target": "leadingIcon" })
  50. end
  51. end
  52. }
  53. 1 renders_one :trailing_icon, ->(icon:, **_kwargs) {
  54. then: 0 if APP_ICONS.include?(icon.to_s)
  55. else: 0 icon
  56. then: 0 elsif MIcon.icon?(icon)
  57. LooposUi::MIcon.new(icon, tag: :i, size: icon_size)
  58. else
  59. else: 0 # To temprarily support fa- icons but keep the new sizes
  60. content_tag(:div, class: "flex items-center justify-center", style: "width: #{icon_size}px; height: #{icon_size}px;") do
  61. content_tag(:i, "", class: "lui-button__icon lui-button__icon--#{@size} #{icon}")
  62. end
  63. end
  64. }
  65. 1 def icon_size
  66. case @size
  67. when: 0 when :default
  68. 16
  69. when: 0 when :small
  70. 14
  71. else: 0 else
  72. 12
  73. end
  74. end
  75. 1 option :primary_color, Types::Coercible::String, optional: true
  76. 1 option :hover_color, Types::Coercible::String, optional: true
  77. 1 option :text_color, Types::Coercible::String, default: -> { LooposUi::Colors.find("general-global-white") }
  78. 1 option :load_on_click, Types::Bool, default: -> { true }
  79. 1 attr_reader :text, :tag, :kind
  80. 1 def initialize(
  81. kind: :app,
  82. type: :primary,
  83. app: nil,
  84. size: :default,
  85. href: nil,
  86. tag: nil,
  87. tag_options: {},
  88. text: nil,
  89. app_icon_size: :small,
  90. leading_icon: nil,
  91. icon: nil,
  92. trailing_icon: nil,
  93. disabled: false,
  94. tooltip_text: nil,
  95. active: true,
  96. full: false,
  97. status_dot: nil,
  98. load_on_click: true
  99. )
  100. else: 0 then: 0 raise ArgumentError, "Invalid size: #{size}" unless SIZES.include?(size)
  101. else: 0 then: 0 raise ArgumentError, "Invalid type: #{type}" unless TYPES.include?(type)
  102. else: 0 then: 0 raise ArgumentError, "Invalid kind: #{kind}" unless KINDS.include?(kind)
  103. # Deprecated, will be removed
  104. then: 0 else: 0 @app = kind == :app ? (app || LooposUi.config.app_type) : nil
  105. @kind = kind
  106. @text = text
  107. @size = size
  108. @type = type
  109. @tooltip_text = initial_tooltip_text(tag_options: tag_options, tooltip_text: tooltip_text)
  110. @app_icon_size = app_icon_size
  111. @disabled = disabled
  112. @active = active
  113. @full = full
  114. @href = href
  115. then: 0 else: 0 @tag = tag || (href.present? && !disabled? ? :a : :button)
  116. @tag_options = tag_options
  117. @load_on_click = load_on_click
  118. then: 0 @leading_icon = if (leading_icon && APP_ICONS.include?(leading_icon.to_s)) || MIcon.icon?(leading_icon)
  119. leading_icon
  120. else: 0 else
  121. faviconize(leading_icon || icon)
  122. end
  123. then: 0 @trailing_icon = if (trailing_icon && APP_ICONS.include?(trailing_icon.to_s)) || MIcon.icon?(trailing_icon)
  124. trailing_icon
  125. else: 0 else
  126. faviconize(trailing_icon)
  127. end
  128. # Validate that text is present unless there's at least one icon
  129. else: 0 then: 0 unless @text.present? || @leading_icon.present? || @trailing_icon.present?
  130. raise ArgumentError, "Text must be provided unless leading_icon or trailing_icon is present"
  131. end
  132. @status_dot = status_dot
  133. end
  134. 1 def before_render
  135. then: 0 else: 0 with_trailing_icon(icon: @trailing_icon) if @trailing_icon
  136. then: 0 else: 0 with_leading_icon(icon: @leading_icon) if @leading_icon
  137. then: 0 else: 0 with_status_dot(kind: @status_dot) if @status_dot
  138. end
  139. 1 def classes
  140. [
  141. then: 0 else: 0 icon_only? ? "lui-button--icon-only" : nil,
  142. theme_class,
  143. app_classes,
  144. size_classes,
  145. disabled_classes,
  146. specific_app_classes,
  147. active_classes,
  148. full_classes,
  149. ].compact.join(" ")
  150. end
  151. 1 def tag_options
  152. then: 0 options = if @tag == :a && !disabled?
  153. deep_merge_args(
  154. {
  155. href: @href,
  156. then: 0 else: 0 data: {}.merge(@load_on_click ? { action: "lui--button#startLoading" } : {}),
  157. },
  158. @tag_options,
  159. )
  160. else: 0 else
  161. @tag_options
  162. end
  163. then: 0 else: 0 if load_on_click_for_button?(options)
  164. options = deep_merge_args(options, { data: { action: "lui--button#startLoading" } })
  165. end
  166. then: 0 else: 0 options.merge!(disabled: true) if disabled?
  167. deep_merge_args(
  168. {
  169. data: {
  170. controller: "lui--button",
  171. },
  172. },
  173. options,
  174. )
  175. end
  176. # TODO: document
  177. 1 def disabled?
  178. @disabled
  179. end
  180. 1 def active?
  181. @active
  182. end
  183. 1 def full?
  184. @full
  185. end
  186. # Disabled buttons do not receive pointer events; attach the tooltip to a wrapper instead.
  187. 1 def tooltip_on_wrapper?
  188. disabled? && @tooltip_text.present?
  189. end
  190. 1 def button_classes
  191. [
  192. "lui-button",
  193. classes,
  194. then: 0 else: 0 tooltip_on_wrapper? && full? ? "w-full" : full_classes,
  195. "relative",
  196. ].compact.join(" ")
  197. end
  198. 1 def tooltip_wrapper_classes
  199. ["lui-button__tooltip-wrapper", "inline-flex", full_classes].compact.join(" ")
  200. end
  201. 1 private
  202. 1 def load_on_click_for_button?(options)
  203. else: 0 then: 0 return false unless @load_on_click
  204. else: 0 then: 0 return false unless @tag == :button
  205. then: 0 else: 0 options[:form].present? || options[:type]&.to_sym == :submit
  206. end
  207. 1 def initial_tooltip_text(tag_options:, tooltip_text:)
  208. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 else: 0 then: 0 return tooltip_text unless tag_options[:data]&.dig(:controller)&.to_s&.include?("lui--button-tooltip-toggle")
  209. toggled = tag_options[:data][:"lui--button-tooltip-toggle-toggled-value"]
  210. then: 0 if toggled
  211. tag_options[:data][:"lui--button-tooltip-toggle-toggled-text-value"]
  212. else: 0 else
  213. tag_options[:data][:"lui--button-tooltip-toggle-untoggled-text-value"]
  214. end
  215. end
  216. 1 def icon_only?
  217. only_one_icon = [@leading_icon.present?, @trailing_icon.present?].count(true) == 1
  218. no_text = @text.blank?
  219. no_status_dot = @status_dot.blank?
  220. only_one_icon && no_text && no_status_dot
  221. end
  222. 1 def specific_app_classes
  223. then: 0 else: 0 "lui-button--neutral--#{@type}" if @app.present?
  224. end
  225. 1 def app_classes
  226. else: 0 then: 0 "lui-button--neutral--#{@type}" unless @app.present?
  227. end
  228. 1 def theme_class
  229. else: 0 then: 0 return unless @type == :primary
  230. else: 0 then: 0 return unless LooposUi::Current.theme_context.present?
  231. "lui-button--themed"
  232. end
  233. 1 def size_classes
  234. "lui-button--size-#{@size}"
  235. end
  236. 1 def disabled_classes
  237. then: 0 else: 0 "lui-button--disabled" if disabled?
  238. end
  239. 1 def active_classes
  240. else: 0 then: 0 "lui-button--inactive" unless active?
  241. end
  242. 1 def full_classes
  243. then: 0 else: 0 full? ? "w-full" : "w-fit"
  244. end
  245. end
  246. end

app/components/loopos_ui/button/button.html.erb

0.0% lines covered

0.0% branches covered

27 relevant lines. 0 lines covered and 27 lines missed.
22 total branches, 0 branches covered and 22 branches missed.
    
  1. <span class="<%= tooltip_wrapper_classes %>">
  2. then: 0 else: 0 <%= render LooposUi::Tooltip.new(title: @tooltip_text) if @tooltip_text.present? %>
  3. <%= content_tag(tag, { class: button_classes, **tag_options }) do %>
  4. <% case leading_icon.to_s %>
  5. when: 0 <% when "core", "submission", "hubs", "validation", "handling", "exits", "impact", "submission_extra" %>
  6. <div class="flex items-center justify-center opacity-100" data-lui--button-target="leadingIcon" style="width: <%= icon_size %>px; height: <%= icon_size %>px;">
  7. then: 0 else: 0 <%= helpers.app_svg(app: leading_icon.to_s, size: defined?(@app_icon_size) ? @app_icon_size : :small) %>
  8. </div>
  9. else: 0 <% else %>
  10. then: 0 else: 0 <%= content_tag(:div, class: "opacity-100 inline-flex", data: { "lui--button-target": "leadingIcon" }) do %>
  11. <%= leading_icon %>
  12. <% end if leading_icon? %>
  13. <% end %>
  14. then: 0 else: 0 <%= content_tag(:span, class: "lui-button__text opacity-100 inline-flex", data: { "lui--button-target": :text }) do %>
  15. <%= text %>
  16. <% end if text.present? %>
  17. <% case trailing_icon.to_s %>
  18. when: 0 <% when "core", "submission", "hubs", "validation", "handling", "exits", "impact" %>
  19. <%= content_tag(:div, class: "opacity-100 inline-flex", data: { "lui--button-target": "trailingIcon" }) do %>
  20. then: 0 else: 0 <%= helpers.app_svg(app: trailing_icon.to_s, size: defined?(@app_icon_size) ? @app_icon_size : :small) %>
  21. <% end %>
  22. else: 0 <% else %>
  23. then: 0 else: 0 <%= content_tag(:div, class: "opacity-100 inline-flex", data: { "lui--button-target": "trailingIcon" }) do %>
  24. <%= trailing_icon %>
  25. <% end if trailing_icon? %>
  26. <% end %>
  27. then: 0 else: 0 <%= content_tag(:div, class: "opacity-100 inline-flex", data: { "lui--button-target": "statusDot" }) do %>
  28. <%= status_dot %>
  29. <% end if status_dot? %>
  30. then: 0 else: 0 <%= content_tag(:div, class: "opacity-100 inline-flex", data: { "lui--button-target": "counter" }) do %>
  31. <%= counter %>
  32. <% end if counter? %>
  33. <div class="absolute w-full flex items-center justify-center opacity-0" data-lui--button-target="loadingIcon">
  34. then: 0 else: 0 <%= render LooposUi::Spinner.new(theme: @type == :primary ? :dark : :light, size: @size) %>
  35. </div>
  36. <% end %>
  37. </span>

app/components/loopos_ui/card/card_component.html.erb

0.0% lines covered

0.0% branches covered

14 relevant lines. 0 lines covered and 14 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. then: 0 else: 0 <div class="loopos-card <%= @full_width ? 'loopos-card__full-width' : '' %>">
  2. <div class="loopos-card__title-wrapper">
  3. <div class="loopos-card__title">
  4. <%= title %>
  5. </div>
  6. <div class="loopos-card__actions">
  7. <%= action %>
  8. </div>
  9. </div>
  10. then: 0 else: 0 <% if top_contents.present? %>
  11. <div class="loopos-card__top-contents">
  12. <% top_contents.each do |element| %>
  13. <%= element %>
  14. <% end %>
  15. </div>
  16. <% end %>
  17. <div class="loopos-card__content">
  18. <%= card_content %>
  19. </div>
  20. then: 0 else: 0 <% if footer.present? %>
  21. <div class="loopos-card__footer">
  22. <%= footer %>
  23. </div>
  24. <% end %>
  25. </div>

app/components/loopos_ui/card/card_component.rb

90.91% lines covered

100.0% branches covered

11 relevant lines. 10 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Card
  3. 1 class CardComponent < ViewComponent::Base
  4. 1 include Turbo::FramesHelper
  5. 1 renders_one :title
  6. 1 renders_one :action
  7. 1 renders_many :top_contents
  8. 1 renders_one :card_content
  9. 1 renders_one :footer
  10. 1 def initialize(full_width: false)
  11. @full_width = full_width
  12. end
  13. end
  14. end
  15. end

app/components/loopos_ui/carousel.rb

77.78% lines covered

100.0% branches covered

18 relevant lines. 14 lines covered and 4 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class Carousel < LoopComponent
  3. 1 option :images, Types::Array.of(Types::Coercible::String)
  4. 1 option :height, Types::TSize, optional: true
  5. 1 option :width, Types::TSize, optional: true
  6. 1 option :dots, Types::Bool, default: -> { true }
  7. 1 mod :with_dots, condition: -> { dots && images.count > 1 }
  8. 1 option :preview, Types::Bool, default: -> { false }
  9. 1 mod :with_preview, condition: -> { preview && images.count > 1 }
  10. 1 option :data, Types::Hash, default: -> { {} }
  11. 1 def size_styles
  12. <<~CSS
  13. height: #{height};
  14. width: #{width};
  15. CSS
  16. end
  17. 1 def width_styles
  18. <<~CSS
  19. width: #{width};
  20. CSS
  21. end
  22. 1 def own_class
  23. "#{super}_container"
  24. end
  25. 1 def data
  26. deep_merge_args(
  27. { controller: "lui--carousel-container" },
  28. @data,
  29. )
  30. end
  31. end
  32. end

app/components/loopos_ui/carousel/carousel.html.erb

0.0% lines covered

100.0% branches covered

14 relevant lines. 0 lines covered and 14 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div(class: classes, data: data) do %>
  2. <section
  3. id="<%= random_id %>"
  4. class="lui-carousel"
  5. data-controller="carousel"
  6. data-carousel-loop-value="true"
  7. data-carousel-dots-value="true">
  8. <%= tag.div(class: "lui-carousel-viewport", data: { carousel_target: :viewport }, style: size_styles) do %>
  9. <%= tag.div(class: "flex aspect-square", style: size_styles ) do %>
  10. <% images.each do |image| %>
  11. <%= image_tag image, class: "lui-carousel-image" %>
  12. <% end %>
  13. <% end %>
  14. <div class="lui-carousel-dots" data-lui--carousel-container-target="dots">
  15. <div class="lui-carousel-dots-container" data-carousel-target="dotsContainer"></div>
  16. </div>
  17. <% end %>
  18. </section>
  19. <section
  20. class="lui-carousel-preview"
  21. data-lui--carousel-container-target="preview"
  22. data-controller="carousel"
  23. data-carousel-loop-value="false"
  24. data-carousel-drag-free-value="false"
  25. data-carousel-dots-value="false"
  26. data-carousel-buttons-value="false"
  27. data-carousel-thumbnails-value="true"
  28. data-carousel-main-carousel-value="<%= random_id %>"
  29. >
  30. <div
  31. class="overflow-hidden outline-hidden cursor-grab active:cursor-grabbing"
  32. data-carousel-target="viewport"
  33. style="<%= width_styles %>"
  34. >
  35. <div class="lui-carousel-thumbnails">
  36. <% images.each do |image| %>
  37. <button class="shrink-0 grow-0 basis-1/4 aspect-square relative select-none outline-hidden" data-carousel-target="thumbnailButton">
  38. <div class="lui-carousel-thumbnails__image">
  39. <%= image_tag image, class: "aspect-square object-cover" %>
  40. </div>
  41. </button>
  42. <% end %>
  43. </div>
  44. </div>
  45. </section>
  46. <% end %>

app/components/loopos_ui/charts/chart_component.html.erb

0.0% lines covered

0.0% branches covered

21 relevant lines. 0 lines covered and 21 lines missed.
10 total branches, 0 branches covered and 10 branches missed.
    
  1. then: 0 else: 0 <% chart_opts = @options.transform_keys { |k| k.is_a?(String) ? k.to_sym : k } %>
  2. then: 0 <% if @options["legend"].present? %>
  3. <% side = case @options["legend"]
  4. when: 0 when "top"
  5. "gap-6 flex flex-col"
  6. when: 0 when "left"
  7. "gap-6 flex-wrap: wrap"
  8. when: 0 when "right"
  9. "gap-6 flex-wrap: wrap reverse_row"
  10. else: 0 else
  11. "gap-6 flex flex-col reverse_col"
  12. end
  13. %>
  14. <div class="<%= side %> flex flex-row flex-wrap max-w-lg">
  15. <div class="loopui-chart-component__legend-container__<%= @options["legend"] %>">
  16. <% @data.each_with_index do |data, index| %>
  17. <div class="loopui-chart-component__wrapper" >
  18. <p class="loopui-chart-component__box" style="background-color:<%= @options["colors"][index] %>"></p>
  19. <p class="text-sm"><%= data[0] %></p>
  20. </div>
  21. <% end %>
  22. </div>
  23. <div id="impact-chart">
  24. <%= send(@chart, @data, **chart_opts, id: @options["id"], legend: false, library: { backgroundColor: @options["backgroundColor"] }.merge(chart_opts[:library] || {})) %>
  25. </div>
  26. </div>
  27. else: 0 <% else %>
  28. then: 0 <% library_options = if @chart == "bar_chart"
  29. {
  30. height: @data.length * 20,
  31. chartArea: {
  32. top: 5,
  33. bottom: 5
  34. },
  35. hAxis: {
  36. textStyle: {
  37. fontSize: 9
  38. }
  39. },
  40. vAxis: {
  41. textStyle: {
  42. fontSize: 9
  43. },
  44. showTextEvery: 1
  45. }
  46. }
  47. else: 0 else
  48. {}
  49. end %>
  50. <%= send(@chart, @data, **chart_opts, id: @options["id"], legend: false,
  51. library: {
  52. backgroundColor: @options["backgroundColor"],
  53. }.merge(library_options).merge(chart_opts[:library] || {})
  54. ) %>
  55. <%end%>

app/components/loopos_ui/charts/chart_component.rb

47.37% lines covered

0.0% branches covered

19 relevant lines. 9 lines covered and 10 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. # Chart Types
  2. # [pie, line, column, bar, area, scatter]
  3. # Props Dictionary
  4. # [donut, legend, precision, loading, empty, xtitle, ytitle, curve, points, colors,stacked,
  5. # discrete, label, prefix, suffix, library, min, max, id, width, height]
  6. 1 module LooposUi
  7. 1 module Charts
  8. 1 class ChartComponent < ViewComponent::Base
  9. 1 include Turbo::FramesHelper
  10. 1 attr_reader :chart, :data, :options
  11. CHARTKIQ_CHART_TYPES = {
  12. 1 pie: "pie_chart",
  13. line: "line_chart",
  14. column: "column_chart",
  15. bar: "bar_chart",
  16. area: "area_chart",
  17. scatter: "scatter_chart",
  18. }
  19. CHARTKIQ_DICTIONARY = {
  20. 1 donut: "donut",
  21. legend: "legend",
  22. precision: "precision",
  23. loading: "loading",
  24. empty: "empty",
  25. xtitle: "xtitle",
  26. ytitle: "ytitle",
  27. curve: "curve",
  28. points: "points",
  29. colors: "colors",
  30. stacked: "stacked",
  31. discrete: "discrete",
  32. label: "label",
  33. prefix: "prefix",
  34. suffix: "suffix",
  35. library: "library",
  36. min: "min",
  37. max: "max",
  38. id: "id",
  39. width: "width",
  40. height: "height",
  41. backgroundColor: "backgroundColor",
  42. }
  43. 1 def initialize(chart: nil, data: nil, options: {})
  44. @chart = translator(chart)
  45. @data = data
  46. @options = translator(options)
  47. end
  48. 1 def translator(data)
  49. # This method is to plug and play with other chart gem if necessary.
  50. # To change the logic is just configure the chart types, and the dictionary for the new gem and change here.
  51. then: 0 if data.is_a?(String)
  52. CHARTKIQ_CHART_TYPES[data.to_sym]
  53. else: 0 else
  54. translated_data = {}
  55. data.each do |key, value|
  56. translated_key = CHARTKIQ_DICTIONARY[key.to_sym] || key
  57. translated_data[translated_key] = value
  58. end
  59. translated_data
  60. end
  61. end
  62. end
  63. end
  64. end

app/components/loopos_ui/chat.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class Chat < LoopComponent
  3. 1 option :item_token
  4. 1 option :user_id
  5. 1 option :app_id
  6. 1 option :api_key
  7. 1 option :type
  8. 1 option :user_name
  9. 1 option :locale
  10. 1 option :app_name, default: -> { "core" }
  11. 1 option :chatbox_height, default: -> { 611 }
  12. 1 option :style, default: -> {""}
  13. end
  14. end

app/components/loopos_ui/chat/chat.html.erb

0.0% lines covered

100.0% branches covered

2 relevant lines. 0 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= react_component(
  2. "ItemChat", {
  3. itemToken: item_token,
  4. userId: user_id,
  5. appId: app_id,
  6. apiKey: api_key,
  7. type: type,
  8. userName: user_name,
  9. locale: locale,
  10. appName: app_name,
  11. chatboxHeight: chatbox_height
  12. }, {style: style}
  13. ) %>

app/components/loopos_ui/chip.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class Chip < LoopComponent
  3. 1 option :text, default: -> { "" }, type: Types::String
  4. 1 use_extra_options
  5. end
  6. end

app/components/loopos_ui/chip/chip.html.erb

0.0% lines covered

100.0% branches covered

2 relevant lines. 0 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div(class: "lui-chip", **extra_options) do %>
  2. <%= text %>
  3. <% end %>

app/components/loopos_ui/chip_list.rb

83.33% lines covered

100.0% branches covered

6 relevant lines. 5 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class ChipList < LoopComponent
  3. 1 renders_many :chips, LooposUi::Chip
  4. 1 use_extra_options
  5. 1 def extra_options
  6. deep_merge_args(
  7. { data: { controller: "lui--blurred-scroll" } },
  8. @extra_options,
  9. )
  10. end
  11. end
  12. end

app/components/loopos_ui/chip_list/chip_list.html.erb

0.0% lines covered

100.0% branches covered

3 relevant lines. 0 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div(class: "lui-chip-list", **extra_options) do %>
  2. <% chips.each do |chip| %>
  3. <%= chip %>
  4. <% end %>
  5. <% end %>

app/components/loopos_ui/code_editor.rb

52.27% lines covered

0.0% branches covered

44 relevant lines. 23 lines covered and 21 lines missed.
15 total branches, 0 branches covered and 15 branches missed.
    
  1. 1 require "uri"
  2. 1 module LooposUi
  3. 1 class CodeEditor < LoopComponent
  4. # Internal placeholder representing intentionally-empty code in inline editing.
  5. 1 EMPTY_CODE_PLACEHOLDER = "-"
  6. 1 option :type, type: Types::String
  7. 1 option :readonly, type: Types::Bool.default(true)
  8. 1 option :code, type: Types::String.constructor(
  9. ->(value) do
  10. case value
  11. when: 0 when String
  12. value
  13. when: 0 when Hash
  14. value.to_json
  15. else: 0 else
  16. ""
  17. end
  18. end,
  19. )
  20. 1 option :element, optional: true
  21. 1 option :attribute, type: Types::String.optional, optional: true
  22. 1 option :show_path, type: Types::String.optional, optional: true
  23. 1 option :edit_path, type: Types::String.optional, optional: true
  24. 1 option :form_url, type: Types::String.optional, optional: true
  25. 1 option :input_name, type: Types::String.optional, optional: true
  26. 1 option :show_edit_button, type: Types::Bool, default: -> { true }
  27. 1 option :turbo_id, type: Types::String.optional, optional: true
  28. 1 option :with_turbo_wrapper, type: Types::Bool, default: -> { true }
  29. 1 option :hidden_fields, type: Types::Hash, default: -> { {} }
  30. 1 option :title, type: Types::String.optional, optional: true
  31. 1 use_extra_options
  32. 1 def inline_locals
  33. else: 0 then: 0 return {} unless element.present?
  34. inferred_attribute = attribute.presence || "code"
  35. inferred_show_path = show_path.presence || current_path
  36. locals = {
  37. element: element,
  38. attribute: inferred_attribute,
  39. show_path: inferred_show_path,
  40. edit_path: edit_path.presence || path_with_keep_inline_edit(inferred_show_path),
  41. form_url: form_url.presence || inferred_show_path,
  42. show_edit_button: show_edit_button,
  43. turbo_id: turbo_id,
  44. with_turbo_wrapper: with_turbo_wrapper,
  45. hidden_fields: hidden_fields || {},
  46. title: title,
  47. has_title: title.present?,
  48. extra_options: extra_options,
  49. editor_input_name: input_name.presence || "#{element.model_name.to_s.underscore}[#{inferred_attribute}]",
  50. then: 0 else: 0 edit_default_value: code == EMPTY_CODE_PLACEHOLDER ? "" : code,
  51. }
  52. then: 0 else: 0 locals[:input_name] = input_name if input_name.present?
  53. locals
  54. end
  55. 1 private
  56. 1 def current_path
  57. then: 0 else: 0 view_context.request&.fullpath
  58. rescue StandardError
  59. nil
  60. end
  61. 1 def path_with_keep_inline_edit(path)
  62. then: 0 else: 0 return if path.blank?
  63. uri = URI.parse(path)
  64. query = Rack::Utils.parse_nested_query(uri.query.to_s)
  65. query["keep_inline_edit"] = "true"
  66. uri.query = query.to_query
  67. uri.to_s
  68. rescue URI::InvalidURIError
  69. then: 0 else: 0 separator = path.include?("?") ? "&" : "?"
  70. "#{path}#{separator}keep_inline_edit=true"
  71. end
  72. end
  73. end

app/components/loopos_ui/code_editor/code_editor.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render "loopos_ui/inline/code_editor",
  2. **{ type: type, code: code, readonly: readonly, extra_options: extra_options }.merge(inline_locals)
  3. %>

app/components/loopos_ui/context_menu.rb

53.33% lines covered

0.0% branches covered

30 relevant lines. 16 lines covered and 14 lines missed.
12 total branches, 0 branches covered and 12 branches missed.
    
  1. 1 module LooposUi
  2. 1 class ContextMenu < LoopComponent
  3. 1 renders_many :items, "LooposUi::ContextMenu::Item"
  4. 1 renders_one :trigger, types: {
  5. button: { renders: Button, as: :trigger_button },
  6. slot: { renders: ->(&block) { capture(&block) }, as: :trigger },
  7. }
  8. 1 class Item < LoopComponent
  9. 1 option :text, type: Types::String
  10. 1 option :icon, type: Types::String, default: nil, optional: true
  11. 1 option :disabled, type: Types::Bool, default: false, optional: true
  12. 1 option :data_attributes, type: Types::Hash, default: -> { {} }, optional: true
  13. 1 option :selected, type: Types::Bool, default: false, optional: true
  14. 1 renders_many :sub_items, Item
  15. 1 def data_attributes
  16. deep_merge_args(
  17. {
  18. lui__context_menu_target: "item",
  19. },
  20. @data_attributes,
  21. )
  22. end
  23. 1 private
  24. 1 def icon_class
  25. then: 0 else: 0 return "" if disabled
  26. then: 0 else: 0 return "fa-regular fa-chevron-right" if submenu?
  27. then: 0 if icon.present?
  28. else: 0 icon
  29. then: 0 elsif selected
  30. "fa-regular fa-check"
  31. else: 0 else
  32. ""
  33. end
  34. end
  35. # FIXME: use mod helper instead
  36. 1 def item_class
  37. base_classes = "lui-context-menu__item"
  38. then: 0 else: 0 disabled_classes = disabled ? "lui-context-menu__item--disabled" : ""
  39. then: 0 else: 0 submenu_classes = submenu? ? "lui-context-menu__item--submenu" : ""
  40. join_classes([
  41. base_classes,
  42. disabled_classes,
  43. submenu_classes,
  44. ])
  45. end
  46. 1 def submenu?
  47. sub_items.present? && !disabled
  48. end
  49. end
  50. end
  51. end

app/components/loopos_ui/context_menu/context_menu.html.erb

0.0% lines covered

100.0% branches covered

5 relevant lines. 0 lines covered and 5 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <div class="inline-flex lui-context-menu" data-controller="context-menu lui--context-menu" data-context-menu-auto-close-value="false">
  2. <div class="cursor-context-menu">
  3. <%= trigger %>
  4. </div>
  5. <dialog
  6. class="lui-context-menu__dialog"
  7. style="margin: 0;"
  8. data-context-menu-target="menu"
  9. data-controller="menu"
  10. data-action="click@document->dropdown-popover#closeOnClickOutside
  11. keydown.up->menu#prev keydown.down->menu#next
  12. keydown.up->menu#preventScroll keydown.down->menu#preventScroll"
  13. >
  14. <div class="flex flex-col" role="menu">
  15. <% items.each do |item| %>
  16. <%= item %>
  17. <% end %>
  18. </div>
  19. </dialog>
  20. </div>

app/components/loopos_ui/context_menu/item.html.erb

0.0% lines covered

0.0% branches covered

19 relevant lines. 0 lines covered and 19 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. then: 0 <% if sub_items.present? && !disabled %>
  2. <div class="flex flex-col" role="menu">
  3. <div class="relative w-full focus-visible:outline-none"
  4. data-controller="dropdown-popover"
  5. data-dropdown-popover-nested-value="true"
  6. data-dropdown-popover-auto-close-value="false"
  7. data-dropdown-popover-hover-value="true"
  8. data-dropdown-popover-flip-class="translate-y-full">
  9. <div
  10. class="focus-visible:outline-none"
  11. data-dropdown-popover-target="button"
  12. data-action="dropdown-popover#toggle"
  13. data-menu-target="item"
  14. role="menuitem">
  15. <%= tag.div(class: item_class, data: data_attributes) do %>
  16. <span class="lui-context-menu__item-text">
  17. <%= text %>
  18. </span>
  19. <%= tag.span(class: "lui-context-menu__icon") do %>
  20. <%= tag.i(class: icon_class) %>
  21. <% end %>
  22. <% end %>
  23. </div>
  24. <dialog
  25. class="lui-context-menu__sub-dialog"
  26. data-controller="menu"
  27. data-dropdown-popover-target="menu"
  28. data-action="click@document->dropdown-popover#closeOnClickOutside
  29. keydown.up->menu#prev keydown.down->menu#next
  30. keydown.up->menu#preventScroll keydown.down->menu#preventScroll"
  31. >
  32. <div class="flex flex-col p-2" role="menu">
  33. <% sub_items.each do |sub_item| %>
  34. <%= tag.div(data: sub_item.data_attributes) do %>
  35. <%= sub_item %>
  36. <% end %>
  37. <% end %>
  38. </div>
  39. </dialog>
  40. </div>
  41. </div>
  42. else: 0 <% else %>
  43. <div>
  44. <%= tag.div(class: item_class, data: data_attributes) do %>
  45. <span class="lui-context-menu__item-text">
  46. <%= text %>
  47. </span>
  48. <%= tag.span(class: "lui-context-menu__icon") do %>
  49. <%= tag.i(class: icon_class) %>
  50. <% end %>
  51. <%= tag.i(class: "lui-context-menu__checkmark fa-regular fa-check") %>
  52. <% end %>
  53. </div>
  54. <% end %>

app/components/loopos_ui/core/product_show_header.rb

100.0% lines covered

100.0% branches covered

3 relevant lines. 3 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Core
  3. 1 class ProductShowHeader < ViewComponent::Base
  4. end
  5. end
  6. end

app/components/loopos_ui/counter.rb

31.25% lines covered

0.0% branches covered

16 relevant lines. 5 lines covered and 11 lines missed.
8 total branches, 0 branches covered and 8 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class Counter < LoopComponent
  4. COLORS = {
  5. # text, background
  6. # TODO: use Colors.find(...)
  7. 1 success: [find_color("general-global-white"), find_color("general-success-800")],
  8. danger: [find_color("general-global-white"), find_color("general-danger-800")],
  9. warning: [find_color("general-global-white"), find_color("general-notice-800")],
  10. neutral: [find_color("general-global-black"), find_color("general-gray-100")],
  11. informative: [find_color("general-global-white"), find_color("general-informative-800")],
  12. }
  13. # TODO: refactor to use LoopComponent
  14. # size: tinny and small, but small is the default
  15. 1 def initialize(count: nil, kind: :success, size: :tinny, increment: false, kind_styles: false)
  16. @kind = kind
  17. @count = count
  18. @size = size
  19. @text_color, @bg_color = COLORS[kind] || COLORS[:neutral]
  20. @increment = increment
  21. @kind_styles = kind_styles
  22. then: 0 else: 0 raise "No color defined for: #{kind}" if !@increment && (@text_color.nil? || @bg_color.nil?)
  23. end
  24. 1 def styles
  25. then: 0 else: 0 then: 0 else: 0 return if @increment ? !@kind_styles : (@text_color.nil? || @bg_color.nil?)
  26. then: 0 if @kind == :neutral
  27. <<~CSS.squish
  28. color: #{@text_color};
  29. background-color: #{@bg_color};
  30. border: var(--Spacings-0, 1px) solid var(--General-Gray-500, #C4CAD0);
  31. CSS
  32. else: 0 else
  33. <<~CSS.squish
  34. color: #{@text_color};
  35. background-color: #{@bg_color};
  36. CSS
  37. end
  38. end
  39. end
  40. end

app/components/loopos_ui/counter/counter.html.erb

0.0% lines covered

0.0% branches covered

7 relevant lines. 0 lines covered and 7 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. then: 0 <% if @increment %>
  2. then: 0 else: 0 <%= tag.span(class: "lui-counter #{!@kind_styles ? "lui-counter--increment" : "lui-counter--#{@size}"}", style: styles) do %>
  3. then: 0 else: 0 <%= tag.span("+#{@count}", class: "lui-counter__text #{!@kind_styles ? "lui-counter__text--increment" : ""}") %>
  4. <% end %>
  5. else: 0 <% else %>
  6. <%= tag.span(class: "lui-counter lui-counter--#{@size}", style: styles) do %>
  7. <%= tag.span(@count, class: "lui-counter__text") %>
  8. <% end %>
  9. <% end %>

app/components/loopos_ui/counter_label.rb

50.0% lines covered

0.0% branches covered

12 relevant lines. 6 lines covered and 6 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class CounterLabel < LooposUi::Label
  4. # TODO: this should be stored in a shared colors file
  5. COLORS = {
  6. # text, background
  7. 1 manager: [find_color("apps-manager-800-primary"), find_color("apps-manager-200")],
  8. core: [find_color("apps-core-800-primary"), find_color("apps-core-200")],
  9. hubs: [find_color("apps-hubs-800-primary"), find_color("apps-hubs-200")],
  10. submission: [find_color("apps-submission-800-primary"), find_color("apps-submission-200")],
  11. validation: [find_color("apps-validation-800-primary"), find_color("apps-validation-200")],
  12. handling: [find_color("apps-handling-800-primary"), find_color("apps-handling-200")],
  13. default: [find_color("general-gray-900"), find_color("general-gray-200")],
  14. danger: [find_color("general-danger-800"), find_color("general-danger-200")],
  15. white: [find_color("general-global-black"), find_color("general-global-white")],
  16. warning: [find_color("general-notice-800"), find_color("general-notice-200")],
  17. informative: [find_color("general-informative-800"), find_color("general-informative-200")],
  18. }
  19. 1 attr_accessor :text, :icon
  20. # TODO: document system_args
  21. 1 def initialize(text: nil, icon: nil, color: nil, **system_args)
  22. super(text: text, icon: icon, color: color, system_args: system_args)
  23. @text_color, @bg_color = COLORS[color] || COLORS[LooposUi.config.app_type.to_sym]
  24. then: 0 else: 0 raise "No color defined for: #{LooposUi.config.app_type}" if @text_color.nil? || @bg_color.nil?
  25. end
  26. 1 def styles
  27. else: 0 then: 0 return super unless @color == :white
  28. then: 0 else: 0 if @color == :white
  29. <<~CSS.squish
  30. #{super}
  31. border: 1px solid #{find_color("general-gray-200")};
  32. CSS
  33. end
  34. end
  35. end
  36. end

app/components/loopos_ui/dashboard/dashboard_component.html.erb

0.0% lines covered

100.0% branches covered

5 relevant lines. 0 lines covered and 5 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <style>
  2. /* temporary solution for gridstack problems */
  3. .w-dashboard-3{
  4. width: 100%;
  5. max-width: 23%;
  6. min-width: fit-content;
  7. }
  8. .w-dashboard-4{
  9. width: 100%;
  10. max-width: 31%;
  11. min-width: fit-content;
  12. }
  13. .w-dashboard-6{
  14. width: 100%;
  15. max-width: 47%;
  16. min-width: 47%;
  17. .loopos-card {
  18. width: 100%;
  19. }
  20. }
  21. .w-dashboard-8{
  22. width: 100%;
  23. max-width: 64%;
  24. min-width: fit-content;
  25. }
  26. .w-dashboard-12{
  27. width:100%;
  28. max-width: 98%;
  29. min-width: fit-content;
  30. }
  31. </style>
  32. <div class="grid-stack flex flex-wrap gap-8 w-full h-fit">
  33. <% dashboard_cards.each do |dashboard_card| %>
  34. <div class="w-dashboard-<%= dashboard_card.size %> h-fit" gs-w="<%= dashboard_card.size %>">
  35. <div class="grid-stack-item-content h-fit">
  36. <%= dashboard_card %>
  37. </div>
  38. </div>
  39. <% end %>
  40. </div>

app/components/loopos_ui/dashboard/dashboard_component.rb

88.89% lines covered

100.0% branches covered

9 relevant lines. 8 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Dashboard
  3. 1 class DashboardComponent < LoopComponent
  4. 1 include Turbo::FramesHelper
  5. 1 renders_many :dashboard_cards, "LooposUi::Dashboard::DashboardCardComponent"
  6. end
  7. 1 class DashboardCardComponent < LoopComponent
  8. 1 option :size, default: proc { 6 }
  9. 1 def call
  10. content
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/data_card.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class DataCard < LoopComponent
  3. 1 option :title, optional: true
  4. 1 option :icon, optional: true
  5. 1 option :icon_rails, default: -> { true }
  6. 1 option :balance_value, optional: true
  7. 1 option :reuse_cost, optional: true
  8. 1 option :saved_value, optional: true
  9. 1 option :metric_unit, optional: true
  10. end
  11. end

app/components/loopos_ui/data_card/data_card.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= react_component("ImpactCard",
  2. { title: title,
  3. icon: icon,
  4. iconRails: icon_rails,
  5. balanceValue: balance_value,
  6. reuseCost: reuse_cost,
  7. savedValue: saved_value,
  8. metricUnit: metric_unit
  9. }, { class: "flex flex-1 min-w-auto" })
  10. %>

app/components/loopos_ui/date_picker.rb

37.5% lines covered

0.0% branches covered

104 relevant lines. 39 lines covered and 65 lines missed.
63 total branches, 0 branches covered and 63 branches missed.
    
  1. 1 module LooposUi
  2. 1 class DatePicker < LoopComponent
  3. # @see https://railsblocks.com/docs/date-picker
  4. 1 GRANULARITIES = ["date", "month_year", "year", "date_time", "time"].freeze
  5. GRANULARITY_CONFIG = {
  6. 1 "date" => {
  7. date_format: "dd/MM/yyyy",
  8. },
  9. "month_year" => {
  10. date_format: "MM/yyyy",
  11. start_view: "months",
  12. min_view: "months",
  13. show_this_month_button: true,
  14. },
  15. "year" => {
  16. date_format: "yyyy",
  17. start_view: "years",
  18. min_view: "years",
  19. show_this_year_button: true,
  20. },
  21. "date_time" => {
  22. date_format: "dd/MM/yyyy",
  23. time_format: "HH:mm",
  24. timepicker: true,
  25. },
  26. "time" => {
  27. time_format: "HH:mm",
  28. time_only: true,
  29. },
  30. }.freeze
  31. 1 option :inline, Types::Bool, default: -> { false }
  32. 1 option :id, Types::String, optional: true
  33. 1 option :name, Types::String, optional: true
  34. 1 option :submit_on_select, Types::Bool, default: -> { false }
  35. 1 option :range, Types::Bool, default: -> { false }
  36. 1 option :presets, Types::Bool, default: -> { false }
  37. 1 option :granularity, Types::String, default: -> { "date" }
  38. 1 option :locale, Types::String, default: -> { I18n.locale.to_s }
  39. 1 option :initial_date, optional: true
  40. # Display/submit value for the readonly input (e.g. "25/06/2025" or "01/01/2025 ~ 31/12/2025").
  41. 1 option :value, optional: true
  42. 1 option :min_date, Types::Date, optional: true
  43. 1 option :max_date, Types::Date, optional: true
  44. 1 option :blocked_days, optional: true
  45. 1 option :help, optional: true
  46. 1 option :error, optional: true
  47. 1 def dom_id
  48. id || "datepicker-#{SecureRandom.hex(4)}"
  49. end
  50. 1 def variant
  51. then: 0 else: 0 return :presets if presets
  52. then: 0 else: 0 return :range if range
  53. when: 0 case normalized_granularity
  54. when: 0 when "month_year" then :month
  55. else: 0 when "year" then :year
  56. else :single_date
  57. end
  58. end
  59. 1 def field_wrapper_class
  60. "lui-date_picker__field"
  61. end
  62. 1 def wrapper_classes
  63. [
  64. "lui-date_picker-wrapper",
  65. then: 0 else: 0 ("lui-date_picker-wrapper--with-error" if error.present?),
  66. ].compact.join(" ")
  67. end
  68. 1 def input_name
  69. then: 0 else: 0 return name || "date_range" if variant.in?([:range, :presets])
  70. then: 0 else: 0 return name || "month" if variant == :month
  71. then: 0 else: 0 return name || "year" if variant == :year
  72. then: 0 else: 0 return name || "time" if normalized_granularity == "time"
  73. name || "date"
  74. end
  75. 1 def input_class
  76. then: 0 else: 0 variant == :range ? "lui-date_picker-ranged-picker" : "air-datepicker-input"
  77. end
  78. 1 def input_value
  79. then: 0 else: 0 value.presence&.to_s
  80. end
  81. 1 def placeholder
  82. key =
  83. when: 0 case normalized_granularity
  84. when: 0 when "month_year" then "loopos_ui.date_picker.placeholder.select_month"
  85. when: 0 when "year" then "loopos_ui.date_picker.placeholder.select_year"
  86. when: 0 when "time" then "loopos_ui.date_picker.placeholder.select_time"
  87. when "date_time" then "loopos_ui.date_picker.placeholder.select_date_time"
  88. else: 0 else
  89. then: 0 else: 0 variant == :range || variant == :presets ? "loopos_ui.date_picker.placeholder.select_date_range" : "loopos_ui.date_picker.placeholder.select_date"
  90. end
  91. default =
  92. when: 0 case normalized_granularity
  93. when: 0 when "month_year" then "Select month..."
  94. when: 0 when "year" then "Select year..."
  95. when: 0 when "time" then "Select time..."
  96. when "date_time" then "Select date and time..."
  97. else: 0 else
  98. then: 0 else: 0 variant == :range || variant == :presets ? "Select date range..." : "Select date..."
  99. end
  100. I18n.t(key, default: default, locale: locale)
  101. end
  102. 1 def label(key, default:)
  103. I18n.t("loopos_ui.date_picker.#{key}", default: default, locale: locale)
  104. end
  105. 1 def base_date_picker_data
  106. {
  107. controller: "date-picker",
  108. "date-picker-locale-value": locale,
  109. "date-picker-submit-on-select-value": submit_on_select,
  110. }
  111. end
  112. 1 def field_date_picker_data
  113. variant_field_data.merge(granularity_stimulus_data).merge(date_constraint_data)
  114. end
  115. 1 def presets_wrapper_data
  116. {
  117. controller: "dropdown-popover date-picker",
  118. "dropdown-popover-placement-value": "bottom-start",
  119. "dropdown-popover-auto-position-value": true,
  120. "date-picker-range-value": true,
  121. "date-picker-locale-value": locale,
  122. "date-picker-today-label-value": label("today", default: "Today"),
  123. "date-picker-now-label-value": label("now", default: "Now"),
  124. "date-picker-this-week-label-value": label("this_week", default: "This week"),
  125. "date-picker-this-month-label-value": label("this_month", default: "This month"),
  126. "date-picker-this-year-label-value": label("this_year", default: "This year"),
  127. "date-picker-clear-label-value": label("clear", default: "Clear"),
  128. "date-picker-submit-on-select-value": submit_on_select,
  129. }.merge(granularity_stimulus_data).merge(date_constraint_data)
  130. end
  131. 1 def presets_inline_calendar_data
  132. base_date_picker_data.merge(
  133. "date-picker-inline-value": true,
  134. "date-picker-range-value": true,
  135. "date-picker-show-clear-button-value": true,
  136. "date-picker-clear-label-value": label("clear", default: "Clear"),
  137. ).merge(granularity_stimulus_data).merge(date_constraint_data)
  138. end
  139. 1 private
  140. 1 def normalized_granularity
  141. value = granularity.to_s
  142. then: 0 else: 0 return value if GRANULARITY_CONFIG.key?(value)
  143. "date"
  144. end
  145. 1 def granularity_config
  146. GRANULARITY_CONFIG.fetch(normalized_granularity)
  147. end
  148. 1 def granularity_stimulus_data
  149. config = granularity_config
  150. data = {
  151. "date-picker-granularity-value": normalized_granularity,
  152. "date-picker-range-separator-value": "~",
  153. }
  154. then: 0 else: 0 data["date-picker-date-format-value"] = config[:date_format] if config[:date_format]
  155. then: 0 else: 0 data["date-picker-time-format-value"] = config[:time_format] if config[:time_format]
  156. then: 0 else: 0 data["date-picker-timepicker-value"] = true if config[:timepicker]
  157. then: 0 else: 0 data["date-picker-time-only-value"] = true if config[:time_only]
  158. then: 0 else: 0 data["date-picker-start-view-value"] = config[:start_view] if config[:start_view]
  159. then: 0 else: 0 data["date-picker-min-view-value"] = config[:min_view] if config[:min_view]
  160. then: 0 else: 0 if config[:show_this_month_button]
  161. data["date-picker-show-this-month-button-value"] = true
  162. data["date-picker-this-month-label-value"] = label("this_month", default: "This month")
  163. end
  164. then: 0 else: 0 if config[:show_this_year_button]
  165. data["date-picker-show-this-year-button-value"] = true
  166. data["date-picker-this-year-label-value"] = label("this_year", default: "This year")
  167. end
  168. data
  169. end
  170. 1 def date_constraint_data
  171. then: 0 else: 0 return {} if normalized_granularity == "time"
  172. {
  173. "date-picker-min-date-value": iso8601_date(min_date),
  174. "date-picker-max-date-value": iso8601_date(max_date),
  175. "date-picker-blocked-days-value": blocked_days.to_s.presence,
  176. }.compact
  177. end
  178. 1 def iso8601_date(value)
  179. then: 0 else: 0 return if value.nil?
  180. # Strings (datetime "YYYY-MM-DD HH:MM" or time "HH:MM") pass through as-is.
  181. then: 0 else: 0 return value if value.is_a?(String)
  182. value.iso8601
  183. end
  184. 1 def variant_field_data
  185. now_label_data =
  186. then: 0 if normalized_granularity.in?(["time", "date_time"])
  187. { "date-picker-now-label-value": label("now", default: "Now") }
  188. else: 0 else
  189. {}
  190. end
  191. case variant
  192. when: 0 when :range
  193. base_date_picker_data.merge(
  194. "date-picker-range-value": true,
  195. "date-picker-initial-date-value": iso8601_date(initial_date),
  196. "date-picker-show-today-button-value": true,
  197. "date-picker-today-label-value": label("today", default: "Today"),
  198. "date-picker-show-clear-button-value": true,
  199. "date-picker-clear-label-value": label("clear", default: "Clear"),
  200. ).merge(now_label_data)
  201. else: 0 else
  202. base_date_picker_data.merge(
  203. "date-picker-initial-date-value": iso8601_date(initial_date),
  204. "date-picker-show-today-button-value": normalized_granularity.in?(["date", "date_time", "time"]),
  205. "date-picker-today-label-value": label("today", default: "Today"),
  206. "date-picker-show-clear-button-value": true,
  207. "date-picker-clear-label-value": label("clear", default: "Clear"),
  208. ).merge(now_label_data)
  209. end
  210. end
  211. end
  212. end

app/components/loopos_ui/date_picker/date_picker.html.erb

0.0% lines covered

0.0% branches covered

15 relevant lines. 0 lines covered and 15 lines missed.
9 total branches, 0 branches covered and 9 branches missed.
    
  1. <%# TODO: remove lui-input classes, used temporary to match old styles %>
  2. <%= tag.div(class: wrapper_classes) do %>
  3. <div class="lui-date_picker h-[32px]!">
  4. <% case variant %>
  5. when: 0 <% when :month %>
  6. <%= render LooposUi::DatePicker::Month.new(
  7. field_wrapper_class: field_wrapper_class,
  8. field_date_picker_data: field_date_picker_data,
  9. input_name: input_name,
  10. input_class: input_class,
  11. input_value: input_value,
  12. placeholder: placeholder
  13. ) %>
  14. when: 0 <% when :year %>
  15. <%= render LooposUi::DatePicker::Year.new(
  16. field_wrapper_class: field_wrapper_class,
  17. field_date_picker_data: field_date_picker_data,
  18. input_name: input_name,
  19. input_class: input_class,
  20. input_value: input_value,
  21. placeholder: placeholder
  22. ) %>
  23. when: 0 <% when :range %>
  24. <%= render LooposUi::DatePicker::Range.new(
  25. field_date_picker_data: field_date_picker_data,
  26. input_name: input_name,
  27. input_class: input_class,
  28. input_value: input_value,
  29. placeholder: placeholder
  30. ) %>
  31. when: 0 <% when :presets %>
  32. <%= render LooposUi::DatePicker::Presets.new(
  33. presets_wrapper_data: presets_wrapper_data,
  34. input_name: input_name,
  35. placeholder: placeholder,
  36. locale: locale,
  37. label_proc: method(:label),
  38. presets_inline_calendar_data: presets_inline_calendar_data
  39. ) %>
  40. else: 0 <% else %>
  41. <%= render LooposUi::DatePicker::Single.new(
  42. field_wrapper_class: field_wrapper_class,
  43. field_date_picker_data: field_date_picker_data,
  44. input_name: input_name,
  45. input_class: input_class,
  46. input_value: input_value,
  47. placeholder: placeholder
  48. ) %>
  49. <% end %>
  50. </div>
  51. then: 0 else: 0 <%= content_tag(:span, help, class: "lui-input__help") if help.present? && error.blank? %>
  52. then: 0 else: 0 <%= content_tag(:span, error, class: "lui-input__error") if error.present? %>
  53. <% end %>

app/components/loopos_ui/date_picker/field.rb

85.71% lines covered

100.0% branches covered

14 relevant lines. 12 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class DatePicker < LoopComponent
  3. 1 class Field < LoopComponent
  4. 1 option :wrapper_class, optional: true
  5. 1 option :wrapper_data, optional: true
  6. 1 option :input_name
  7. 1 option :input_class
  8. 1 option :input_data, default: -> { {} }
  9. 1 option :input_value, optional: true
  10. 1 option :placeholder
  11. 1 def normalized_wrapper_data
  12. (wrapper_data || {}).compact
  13. end
  14. 1 def normalized_input_data
  15. (input_data || {}).compact.merge("date-picker-target": "input")
  16. end
  17. end
  18. end
  19. end

app/components/loopos_ui/date_picker/field/field.html.erb

0.0% lines covered

100.0% branches covered

6 relevant lines. 0 lines covered and 6 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div(class: wrapper_class, data: normalized_wrapper_data) do %>
  2. <%= tag.input(
  3. name: input_name,
  4. class: input_class,
  5. readonly: true,
  6. value: input_value,
  7. data: normalized_input_data,
  8. placeholder: placeholder
  9. ) %>
  10. <div class="lui-date_picker__icon">
  11. <%= render LooposUi::Icon.new(icon: :calendar, size: 16, color: "currentColor") %>
  12. </div>
  13. <%= content %>
  14. <% end %>

app/components/loopos_ui/date_picker/month.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class DatePicker < LoopComponent
  3. 1 class Month < LoopComponent
  4. 1 option :field_wrapper_class
  5. 1 option :field_date_picker_data
  6. 1 option :input_name
  7. 1 option :input_class
  8. 1 option :input_value, optional: true
  9. 1 option :placeholder
  10. end
  11. end
  12. end

app/components/loopos_ui/date_picker/month/month.html.erb

0.0% lines covered

100.0% branches covered

2 relevant lines. 0 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::DatePicker::Field.new(
  2. wrapper_class: field_wrapper_class,
  3. wrapper_data: field_date_picker_data,
  4. input_name: input_name,
  5. input_class: input_class,
  6. input_value: input_value,
  7. placeholder: placeholder
  8. ) %>

app/components/loopos_ui/date_picker/presets.rb

90.91% lines covered

100.0% branches covered

11 relevant lines. 10 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class DatePicker < LoopComponent
  3. 1 class Presets < LoopComponent
  4. 1 option :presets_wrapper_data
  5. 1 option :input_name
  6. 1 option :placeholder
  7. 1 option :locale, Types::String
  8. 1 option :label_proc
  9. 1 option :presets_inline_calendar_data
  10. 1 def label(key, default:)
  11. label_proc.call(key, default: default)
  12. end
  13. end
  14. end
  15. end

app/components/loopos_ui/date_picker/presets/presets.html.erb

0.0% lines covered

100.0% branches covered

6 relevant lines. 0 lines covered and 6 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div(class: "lui-date_picker__presets", data: presets_wrapper_data) do %>
  2. <%= render LooposUi::DatePicker::Field.new(
  3. wrapper_class: nil,
  4. wrapper_data: {},
  5. input_name: input_name,
  6. input_class: "air-datepicker-input",
  7. input_data: {
  8. "dropdown-popover-target": "button",
  9. action: "click->dropdown-popover#toggle"
  10. },
  11. placeholder: placeholder
  12. ) do %>
  13. <dialog data-dropdown-popover-target="menu"
  14. data-controller="menu"
  15. data-action="click@document->dropdown-popover#closeOnClickOutside keydown.up->menu#prev keydown.down->menu#next keydown.up->menu#preventScroll keydown.down->menu#preventScroll"
  16. class="outline-hidden absolute z-50 rounded-lg border border-neutral-200 bg-white text-neutral-800 shadow-md transition-opacity ease-out duration-150 opacity-0 [[open]]:scale-100 [[open]]:opacity-100"
  17. data-menu-index-value="-1">
  18. <div class="flex flex-col sm:flex-row">
  19. <%= render LooposUi::DatePicker::PresetsMenu.new(locale: locale, label_proc: label_proc) %>
  20. <%= render LooposUi::DatePicker::PresetsInlineCalendar.new(presets_inline_calendar_data: presets_inline_calendar_data) %>
  21. </div>
  22. </dialog>
  23. <% end %>
  24. <% end %>

app/components/loopos_ui/date_picker/presets_inline_calendar.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class DatePicker < LoopComponent
  3. 1 class PresetsInlineCalendar < LoopComponent
  4. 1 option :presets_inline_calendar_data
  5. end
  6. end
  7. end

app/components/loopos_ui/date_picker/presets_inline_calendar/presets_inline_calendar.html.erb

0.0% lines covered

100.0% branches covered

4 relevant lines. 0 lines covered and 4 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <!-- Right side: Inline calendar -->
  2. <%= tag.div(class: "inline-calendar", data: presets_inline_calendar_data) do %>
  3. <%= tag.input(class: "hidden", data: { "date-picker-target": "input" }) %>
  4. <% end %>

app/components/loopos_ui/date_picker/presets_menu.rb

85.71% lines covered

100.0% branches covered

7 relevant lines. 6 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class DatePicker < LoopComponent
  3. 1 class PresetsMenu < LoopComponent
  4. 1 option :locale, Types::String
  5. 1 option :label_proc
  6. 1 def label(key, default:)
  7. label_proc.call(key, default: default)
  8. end
  9. end
  10. end
  11. end

app/components/loopos_ui/date_picker/presets_menu/presets_menu.html.erb

0.0% lines covered

100.0% branches covered

12 relevant lines. 0 lines covered and 12 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <!-- Left side: Time range presets -->
  2. <div class="air-datepicker-presets-left-container">
  3. <div class="air-datepicker-presets-left-container-buttons">
  4. <button type="button"
  5. class="air-datepicker-presets--button"
  6. data-menu-target="item"
  7. data-action="click->date-picker#setToday">
  8. <%= label("today", default: "Today") %>
  9. </button>
  10. <button type="button"
  11. class="air-datepicker-presets--button"
  12. data-menu-target="item"
  13. data-action="click->date-picker#setYesterday">
  14. <%= label("yesterday", default: "Yesterday") %>
  15. </button>
  16. <button type="button"
  17. class="air-datepicker-presets--button"
  18. data-menu-target="item"
  19. data-action="click->date-picker#setLastDays"
  20. data-days="7">
  21. <%= I18n.t("loopos_ui.date_picker.last_n_days", count: 7, default: "Last 7 days", locale: locale) %>
  22. </button>
  23. <button type="button"
  24. class="air-datepicker-presets--button"
  25. data-menu-target="item"
  26. data-action="click->date-picker#setLastDays"
  27. data-days="30">
  28. <%= I18n.t("loopos_ui.date_picker.last_n_days", count: 30, default: "Last 30 days", locale: locale) %>
  29. </button>
  30. <button type="button"
  31. class="air-datepicker-presets--button"
  32. data-menu-target="item"
  33. data-action="click->date-picker#setLastDays"
  34. data-days="90">
  35. <%= I18n.t("loopos_ui.date_picker.last_n_days", count: 90, default: "Last 90 days", locale: locale) %>
  36. </button>
  37. <button type="button"
  38. class="air-datepicker-presets--button"
  39. data-menu-target="item"
  40. data-action="click->date-picker#setLastDays"
  41. data-days="180">
  42. <%= I18n.t("loopos_ui.date_picker.last_n_days", count: 180, default: "Last 180 days", locale: locale) %>
  43. </button>
  44. <button type="button"
  45. class="air-datepicker-presets--button"
  46. data-menu-target="item"
  47. data-action="click->date-picker#setLastDays"
  48. data-days="365">
  49. <%= I18n.t("loopos_ui.date_picker.last_n_days", count: 365, default: "Last 365 days", locale: locale) %>
  50. </button>
  51. </div>
  52. <div class="air-datepicker-presets-left-divider-line"></div>
  53. <div class="air-datepicker-presets-left-container-buttons">
  54. <button type="button"
  55. class="air-datepicker-presets--button"
  56. data-menu-target="item"
  57. data-action="click->date-picker#setPreset"
  58. data-preset-type="this-month">
  59. <%= label("this_month", default: "This month") %>
  60. </button>
  61. <button type="button"
  62. class="air-datepicker-presets--button"
  63. data-menu-target="item"
  64. data-action="click->date-picker#setPreset"
  65. data-preset-type="last-month">
  66. <%= label("last_month", default: "Last month") %>
  67. </button>
  68. <button type="button"
  69. class="air-datepicker-presets--button"
  70. data-menu-target="item"
  71. data-action="click->date-picker#setPreset"
  72. data-preset-type="this-year">
  73. <%= label("this_year", default: "This year") %>
  74. </button>
  75. </div>
  76. <div class="air-datepicker-presets-left-divider-line"></div>
  77. <div class="air-datepicker-presets-left-container-buttons">
  78. <button type="button"
  79. class="air-datepicker-presets--button-close"
  80. data-menu-target="item"
  81. data-action="click->date-picker#clearSelection">
  82. <%= label("clear_and_close", default: "Clear & Close") %>
  83. </button>
  84. </div>
  85. </div>

app/components/loopos_ui/date_picker/range.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class DatePicker < LoopComponent
  3. 1 class Range < LoopComponent
  4. 1 option :field_date_picker_data
  5. 1 option :input_name
  6. 1 option :input_class
  7. 1 option :input_value, optional: true
  8. 1 option :placeholder
  9. end
  10. end
  11. end

app/components/loopos_ui/date_picker/range/range.html.erb

0.0% lines covered

100.0% branches covered

2 relevant lines. 0 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::DatePicker::Field.new(
  2. wrapper_class: "lui-date_picker__field",
  3. wrapper_data: field_date_picker_data,
  4. input_name: input_name,
  5. input_class: input_class,
  6. input_value: input_value,
  7. placeholder: placeholder
  8. ) %>

app/components/loopos_ui/date_picker/single.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class DatePicker < LoopComponent
  3. 1 class Single < LoopComponent
  4. 1 option :field_wrapper_class
  5. 1 option :field_date_picker_data
  6. 1 option :input_name
  7. 1 option :input_class
  8. 1 option :input_value, optional: true
  9. 1 option :placeholder
  10. end
  11. end
  12. end

app/components/loopos_ui/date_picker/single/single.html.erb

0.0% lines covered

100.0% branches covered

2 relevant lines. 0 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::DatePicker::Field.new(
  2. wrapper_class: field_wrapper_class,
  3. wrapper_data: field_date_picker_data,
  4. input_name: input_name,
  5. input_class: input_class,
  6. input_value: input_value,
  7. placeholder: placeholder
  8. ) %>

app/components/loopos_ui/date_picker/year.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class DatePicker < LoopComponent
  3. 1 class Year < LoopComponent
  4. 1 option :field_wrapper_class
  5. 1 option :field_date_picker_data
  6. 1 option :input_name
  7. 1 option :input_class
  8. 1 option :input_value, optional: true
  9. 1 option :placeholder
  10. end
  11. end
  12. end

app/components/loopos_ui/date_picker/year/year.html.erb

0.0% lines covered

100.0% branches covered

2 relevant lines. 0 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::DatePicker::Field.new(
  2. wrapper_class: field_wrapper_class,
  3. wrapper_data: field_date_picker_data,
  4. input_name: input_name,
  5. input_class: input_class,
  6. input_value: input_value,
  7. placeholder: placeholder
  8. ) %>

app/components/loopos_ui/date_show.rb

55.56% lines covered

0.0% branches covered

9 relevant lines. 5 lines covered and 4 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. 1 module LooposUi
  2. 1 class DateShow < LoopComponent
  3. 1 option :date, type: Types::TDate, optinal: true, default: -> { nil }
  4. 1 option :format, default: -> { :long }, type: Types::Coercible::Symbol.enum(:short, :long)
  5. 1 def formatted_date
  6. then: 0 else: 0 return "-" if date.blank?
  7. case format
  8. when: 0 when :long
  9. date.strftime("%d-%m-%Y %Hh%M")
  10. when: 0 when :short
  11. date.strftime("%d-%m-%Y")
  12. else
  13. skipped # :nocov:
  14. skipped end
  15. skipped end
  16. skipped end
  17. skipped end

app/components/loopos_ui/date_show/date_show.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <span class="lui-date-show"><%= formatted_date %></span>

app/components/loopos_ui/dots.rb

81.82% lines covered

100.0% branches covered

11 relevant lines. 9 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class Dots < LoopComponent
  3. 1 option :amount, default: -> { 1 }, type: Types::Integer
  4. 1 option :active, default: -> { 1 }, type: Types::Integer
  5. 1 option :data, default: -> { {} }, type: Types::Hash
  6. 1 option :dot_data, default: -> { {} }, type: Types::Hash
  7. 1 private
  8. 1 def data_attributes
  9. deep_merge_args({ controller: "dots", dots_index_value: active - 1 }, data)
  10. end
  11. 1 def dot_data_attributes
  12. deep_merge_args({ dots_target: "dot", action: "click->dots#selectDot" }, dot_data)
  13. end
  14. end
  15. end

app/components/loopos_ui/dots/dots.html.erb

0.0% lines covered

0.0% branches covered

3 relevant lines. 0 lines covered and 3 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%= tag.div class: "lui-dots", data: data_attributes do %>
  2. <% amount.times do |i| %>
  3. then: 0 else: 0 <%= tag.div class: "lui-dot #{i + 1 == active ? "lui-dot--active" : ""}", data: dot_data_attributes %>
  4. <% end %>
  5. <% end %>

app/components/loopos_ui/double_state_label.html.erb

0.0% lines covered

100.0% branches covered

5 relevant lines. 0 lines covered and 5 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <div class="lui-double-state-label">
  2. <%= render LooposUi::StateLabel.new(
  3. text: leading_text,
  4. color: leading_color || :default,
  5. icon: leading_icon,
  6. condensed: leading_condensed,
  7. light: light) %>
  8. <%= render LooposUi::StateLabel.new(
  9. text: trailing_text,
  10. color: trailing_color || :white,
  11. icon: trailing_icon,
  12. condensed: trailing_condensed,
  13. light: light) %>
  14. </div>

app/components/loopos_ui/double_state_label.rb

86.67% lines covered

100.0% branches covered

15 relevant lines. 13 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class DoubleStateLabel < LoopComponent
  4. 1 option :leading_text, Types::Coercible::String
  5. 1 option :leading_color, Types::Coercible::String
  6. 1 option :leading_icon, Types::Coercible::String, optional: true
  7. 1 option :leading_condensed, Types::Bool, default: -> { false }
  8. 1 option :trailing_text, Types::Coercible::String
  9. 1 option :trailing_color, Types::Coercible::String
  10. 1 option :trailing_icon, Types::Coercible::String, optional: true
  11. 1 option :trailing_condensed, Types::Bool, default: -> { false }
  12. 1 option :icon, Types::Coercible::String, optional: true
  13. 1 option :light, Types::Bool, default: -> { false }
  14. 1 def initialize(...)
  15. super
  16. @trailing_icon ||= icon
  17. end
  18. end
  19. end

app/components/loopos_ui/drawer_bar.rb

56.86% lines covered

0.0% branches covered

51 relevant lines. 29 lines covered and 22 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. 1 module LooposUi
  2. 1 class DrawerBar < LoopComponent
  3. 1 renders_many :headers
  4. 1 renders_one :footer
  5. 1 option :close_on_outside_click, default: -> { true }
  6. 1 option :default_open, default: -> { false }
  7. 1 private
  8. 1 def state
  9. then: 0 else: 0 default_open ? "open" : "closed"
  10. end
  11. 1 def classes
  12. "lui-drawer_bar lui-drawer_bar--#{state}"
  13. end
  14. 1 class Header < LoopComponent
  15. 1 erb_template <<~HTML
  16. <div class="flex justify-between">
  17. <div class="flex flex-col p-4 gap-3 self-stretch">
  18. <%= content %>
  19. </div>
  20. <div class="p-3">
  21. <%= corner_actions %>
  22. </div>
  23. </div>
  24. HTML
  25. 1 renders_one :corner_actions
  26. end
  27. 1 class IconDisplay < LoopComponent
  28. 1 option :icon, optional: true
  29. 1 option :image, optional: true
  30. 1 option :svg, optional: true
  31. 1 erb_template <<~HTML
  32. <div class="flex w-[40px] h-[40px] justify-center items-center rounded-md border border-solid border-tertiary bg-secondary overflow-hidden">
  33. then: 0 <% if icon.present? %>
  34. else: 0 <%= tag.icon class: icon + " flex" %>
  35. then: 0 <% elsif image.present? %>
  36. <%= image_tag image, class: "w-full" %>
  37. else: 0 <% else %>
  38. <%= svg %>
  39. <% end %>
  40. </div>
  41. HTML
  42. end
  43. 1 class Entity < LoopComponent
  44. 1 erb_template <<~HTML
  45. <div class="flex gap-2 flex-start">
  46. <%= render LooposUi::DrawerBar::IconDisplay.new(icon: icon, svg: svg, image: image) %>
  47. <%= render LooposUi::TitleDescription.new( title: title, description: description, size: "small") %>
  48. </div>
  49. HTML
  50. 1 option :icon, optional: true
  51. 1 option :image, optional: true
  52. 1 option :svg, optional: true
  53. 1 option :title
  54. 1 option :description
  55. # def initialize(...)
  56. # super
  57. # if icon.nil? && image.nil?
  58. # raise ArgumentError, "Either icon or image must be provided"
  59. # end
  60. # end
  61. end
  62. 1 class ContentSection < LoopComponent
  63. 1 erb_template <<~HTML
  64. <div class="flex justify-between border-b border-t border-solid border-gray-300 py-[16px] px-[24px]">
  65. <%= render LooposUi::TitleDescription.new( title: title, description: description, size: "small") %>
  66. <%= content %>
  67. </div>
  68. HTML
  69. 1 option :title
  70. 1 option :description
  71. end
  72. 1 class Footer < LoopComponent
  73. end
  74. end
  75. end

app/components/loopos_ui/drawer_bar/drawer_bar.html.erb

0.0% lines covered

100.0% branches covered

9 relevant lines. 0 lines covered and 9 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%# TODO: prop for open, for preview %>
  2. <div
  3. class="<%= classes %>"
  4. data-drawer-bar-target="component"
  5. >
  6. <div class="lui-drawer_bar__close_button" data-drawer-bar-target="closeButton" >
  7. <%= render LooposUi::Button.new(
  8. icon: "close",
  9. type: :secondary,
  10. kind: :neutral,
  11. size: :tiny,
  12. tag_options: {
  13. data: { action: "click->drawer-bar#handleCloseButtonClick" }
  14. }
  15. )%>
  16. </div>
  17. <div class="lui-drawer_bar__drawer" data-drawer-bar-target="drawer" data-close-on-outside-click="<%= close_on_outside_click %>">
  18. <div class="lui-drawer_bar__drawer__top">
  19. <% headers.each do |header| %>
  20. <%= header %>
  21. <% end %>
  22. </div>
  23. <div class="lui-drawer_bar__drawer__body">
  24. <%= content %>
  25. </div>
  26. </div>
  27. </div>

app/components/loopos_ui/dummy_slot.rb

75.0% lines covered

0.0% branches covered

12 relevant lines. 9 lines covered and 3 lines missed.
12 total branches, 0 branches covered and 12 branches missed.
    
  1. 1 module LooposUi
  2. 1 class DummySlot < LoopComponent
  3. 1 option :text, default: -> { "SLOT" }, type: Types::Coercible::String
  4. 1 option :classes, optional: true, type: Types::String
  5. 1 private
  6. 1 def all_classes
  7. [
  8. default_width_class,
  9. default_height_class,
  10. classes,
  11. "lui-dummy_slot",
  12. "text-[#78818a]", # should not be hardcoded, but this is dummy slot
  13. "copy-12 font-bold",
  14. "leading-none",
  15. ].compact.join(" ")
  16. end
  17. 1 def default_width_class
  18. then: 0 else: 0 then: 0 else: 0 else: 0 then: 0 "w-full" unless classes&.split&.find { |c| c.starts_with?("w-") }
  19. end
  20. 1 def default_height_class
  21. then: 0 else: 0 then: 0 else: 0 else: 0 then: 0 "w-full" unless classes&.split&.find { |c| c.starts_with?("h-") }
  22. end
  23. end
  24. 1 Dummy = DummySlot # Alias, I don't like "Slot" in the name
  25. end

app/components/loopos_ui/dummy_slot/dummy_slot.html.erb

0.0% lines covered

100.0% branches covered

2 relevant lines. 0 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div(class: all_classes) do %>
  2. <%= content || text %>
  3. <% end %>

app/components/loopos_ui/email_preview.rb

100.0% lines covered

100.0% branches covered

3 relevant lines. 3 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class EmailPreview < LoopComponent
  3. 1 option :email_message, default: -> { "" }
  4. end
  5. end

app/components/loopos_ui/email_preview/email_preview.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <iframe id="emailIframe" width="100%" scrolling="no" style="border: none;" onload="setHeight()" srcdoc="<%= email_message || "" %>" ></iframe>
  2. <script>
  3. function setHeight()
  4. {
  5. var child = document.getElementById('emailIframe');
  6. child.style.height = child.contentWindow.document.body.scrollHeight + "px";
  7. }
  8. </script>

app/components/loopos_ui/entities/app_instance.rb

57.14% lines covered

100.0% branches covered

14 relevant lines. 8 lines covered and 6 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class AppInstance < LooposUi::Entity
  4. 1 with_color :general
  5. 1 initialize_with name: :text
  6. 1 class << self
  7. 1 def from_hash(hash)
  8. new(app_instance: OpenStruct.new(hash))
  9. end
  10. end
  11. # FIXME: app instance needs more data to create new
  12. 1 def initialize(app_instance:, **kwargs)
  13. super(**kwargs)
  14. @text = app_instance.name
  15. # When it's a new app instance, it doesn't have an url nor kind
  16. @url = app_instance.try(:url) || nil
  17. @href_options = app_instance.try(:href_options) || {}
  18. @icon = app_instance.try(:kind) || app_instance.name
  19. end
  20. end
  21. end
  22. end

app/components/loopos_ui/entities/brand.rb

70.0% lines covered

100.0% branches covered

10 relevant lines. 7 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class Brand < LooposUi::Entity
  4. 1 with_icon :seal
  5. 1 with_color :general
  6. 1 initialize_with name: :text
  7. 1 def initialize(brand:, **kwargs)
  8. super(**kwargs)
  9. @brand = brand
  10. @text = @brand.name
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/entities/catalog_node.rb

66.67% lines covered

100.0% branches covered

9 relevant lines. 6 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class CatalogNode < LooposUi::Entity
  4. 1 attr_reader :category
  5. 1 attr_reader :product
  6. 1 def initialize(category:, product:, **kwargs)
  7. super(**kwargs)
  8. @category = category
  9. @product = product
  10. end
  11. end
  12. end
  13. end

app/components/loopos_ui/entities/catalog_node/catalog_node.html.erb

0.0% lines covered

100.0% branches covered

5 relevant lines. 0 lines covered and 5 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::TokenList.new(max_tokens: 2) do |token_list| %>
  2. <% token_list.with_token_manual do %>
  3. <%= render LooposUi::Entities::Category.new(category: @category, url: @url) %>
  4. <% end %>
  5. <% token_list.with_token_manual do %>
  6. <%= render LooposUi::Entities::Product.new(product: @product, url: @url) %>
  7. <% end %>
  8. <% end %>

app/components/loopos_ui/entities/catalog_section_inherited_protocol.rb

70.0% lines covered

100.0% branches covered

10 relevant lines. 7 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class CatalogSectionInheritedProtocol < LooposUi::Entity
  4. 1 with_icon :box
  5. 1 with_color :general
  6. 1 initialize_with name: :text
  7. 1 def initialize(catalog_section_inherited_protocol:, **kwargs)
  8. super(**kwargs)
  9. @catalog_section_inherited_protocol = catalog_section_inherited_protocol
  10. @text = @catalog_section_inherited_protocol.name
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/entities/catalog_section_protocol.rb

70.0% lines covered

100.0% branches covered

10 relevant lines. 7 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class CatalogSectionProtocol < LooposUi::Entity
  4. 1 with_icon :box
  5. 1 with_color :general
  6. 1 initialize_with name: :text
  7. 1 def initialize(catalog_section_protocol:, **kwargs)
  8. super(**kwargs)
  9. @catalog_section_protocol = catalog_section_protocol
  10. @text = @catalog_section_protocol.name
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/entities/category.rb

70.0% lines covered

100.0% branches covered

10 relevant lines. 7 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class Category < LooposUi::Entity
  4. 1 with_icon :grid
  5. 1 with_color :general
  6. 1 initialize_with name: :text
  7. 1 def initialize(category:, **kwargs)
  8. super(**kwargs)
  9. @category = category
  10. @text = @category.name
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/entities/email.rb

70.0% lines covered

100.0% branches covered

10 relevant lines. 7 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class Email < LooposUi::Entity
  4. 1 with_icon :seal
  5. 1 with_color :general
  6. 1 initialize_with name: :text
  7. 1 def initialize(email:, **kwargs)
  8. super(**kwargs)
  9. @email = email
  10. @text = @email.name
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/entities/flow.rb

70.0% lines covered

100.0% branches covered

10 relevant lines. 7 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class Flow < LooposUi::Entity
  4. 1 with_icon :timeline_arrow
  5. 1 with_color :general
  6. 1 initialize_with name: :text
  7. 1 def initialize(flow:, **kwargs)
  8. super(**kwargs)
  9. @flow = flow
  10. @text = @flow.name
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/entities/identifier.rb

36.36% lines covered

0.0% branches covered

22 relevant lines. 8 lines covered and 14 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class Identifier < LooposUi::Entity
  4. 1 with_color :general
  5. 1 initialize_with text: :text, key_text: :key_text
  6. 1 renders_many :actions
  7. 1 def initialize(identifier:, **kwargs)
  8. super(**kwargs)
  9. @identifier = identifier
  10. @text = @identifier.text
  11. @key_text = @identifier.key_text
  12. @key_value_actions = kwargs.delete(:key_value_actions) || {
  13. data: {
  14. turbo_action: :advance,
  15. },
  16. }
  17. @kind = :key_value
  18. end
  19. # Sobrescreve o template para passar as ações diretamente para o EntityToken
  20. 1 erb_template <<~ERB
  21. <% token = entity %>
  22. then: 0 else: 0 <% if actions.any? %>
  23. <% actions.each do |action| %>
  24. <% token.with_action do %>
  25. <%= action %>
  26. <% end %>
  27. <% end %>
  28. <% end %>
  29. <%= render token %>
  30. ERB
  31. end
  32. end
  33. end

app/components/loopos_ui/entities/incoming_payment.rb

70.0% lines covered

100.0% branches covered

10 relevant lines. 7 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class IncomingPayment < LooposUi::Entity
  4. 1 with_icon :seal
  5. 1 with_color :general
  6. 1 initialize_with name: :text
  7. 1 def initialize(incoming_payment:, **kwargs)
  8. super(**kwargs)
  9. @incoming_payment = incoming_payment
  10. @text = @incoming_payment.name
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/entities/inherited_protocol.rb

70.0% lines covered

100.0% branches covered

10 relevant lines. 7 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class InheritedProtocol < LooposUi::Entity
  4. 1 with_icon :seal
  5. 1 with_color :general
  6. 1 initialize_with name: :text
  7. 1 def initialize(inherited_protocol:, **kwargs)
  8. super(**kwargs)
  9. @inherited_protocol = inherited_protocol
  10. @text = @inherited_protocol.name
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/entities/invoice.rb

70.0% lines covered

100.0% branches covered

10 relevant lines. 7 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class Invoice < LooposUi::Entity
  4. 1 with_icon :seal
  5. 1 with_color :general
  6. 1 initialize_with name: :text
  7. 1 def initialize(invoice:, **kwargs)
  8. super(**kwargs)
  9. @invoice = invoice
  10. @text = @invoice.name
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/entities/item.rb

70.0% lines covered

100.0% branches covered

10 relevant lines. 7 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class Item < LooposUi::Entity
  4. 1 with_icon :box
  5. 1 with_color :general
  6. 1 initialize_with full_id: :text
  7. 1 def initialize(item:, **kwargs)
  8. super(**kwargs)
  9. @item = item
  10. @text = @item.full_id
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/entities/partnable.rb

53.85% lines covered

0.0% branches covered

13 relevant lines. 7 lines covered and 6 lines missed.
5 total branches, 0 branches covered and 5 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class Partnable < LooposUi::Entity
  4. 1 with_icon :bars_staggered_tag
  5. 1 with_color :general
  6. 1 initialize_with name: :text
  7. 1 def initialize(partnable:, **kwargs)
  8. super(**kwargs)
  9. @partnable = partnable
  10. @text = @partnable.name
  11. else: 0 @icon = case @partnable.icon
  12. when ActiveStorage::Attached::One
  13. when: 0 # Partner or PartnerGroup
  14. then: 0 else: 0 @partnable.icon&.url
  15. when String
  16. when: 0 # PartnableResource::Partnable
  17. @partnable.icon
  18. end
  19. end
  20. end
  21. end
  22. end

app/components/loopos_ui/entities/partner_group.rb

70.0% lines covered

100.0% branches covered

10 relevant lines. 7 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class PartnerGroup < LooposUi::Entity
  4. 1 with_icon :box
  5. 1 with_color :general
  6. 1 initialize_with name: :text
  7. 1 def initialize(partner_group:, **kwargs)
  8. super(**kwargs)
  9. @partner_group = partner_group
  10. @text = @partner_group.name
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/entities/payment.rb

70.0% lines covered

100.0% branches covered

10 relevant lines. 7 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class Payment < LooposUi::Entity
  4. 1 with_icon :fa_seal
  5. 1 with_color :general
  6. 1 initialize_with name: :text
  7. 1 def initialize(payment:, **kwargs)
  8. super(**kwargs)
  9. @payment = payment
  10. @text = @payment.name
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/entities/pricing_rule.rb

70.0% lines covered

100.0% branches covered

10 relevant lines. 7 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class PricingRule < LooposUi::Entity
  4. 1 with_icon :euro_sign
  5. 1 with_color :general
  6. 1 initialize_with name: :text
  7. 1 def initialize(pricing_rule:, **kwargs)
  8. super(**kwargs)
  9. @pricing_rule = pricing_rule
  10. @text = @pricing_rule.name
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/entities/pricing_rule_inheritance.rb

72.73% lines covered

100.0% branches covered

11 relevant lines. 8 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class PricingRuleInheritance < LooposUi::Entity
  4. 1 with_icon :euro_sign
  5. 1 with_trailing_icon :diagram_nested
  6. 1 with_color :general
  7. 1 initialize_with name: :text
  8. 1 def initialize(pricing_rule:, **kwargs)
  9. super(**kwargs)
  10. @pricing_rule = pricing_rule
  11. @text = @pricing_rule.name
  12. end
  13. end
  14. end
  15. end

app/components/loopos_ui/entities/product.rb

70.0% lines covered

100.0% branches covered

10 relevant lines. 7 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class Product < LooposUi::Entity
  4. 1 with_icon :box_open
  5. 1 with_color :general
  6. 1 initialize_with name: :text
  7. 1 def initialize(product:, **kwargs)
  8. super(**kwargs)
  9. @product = product
  10. @text = @product.name
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/entities/protocol.rb

70.0% lines covered

100.0% branches covered

10 relevant lines. 7 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class Protocol < LooposUi::Entity
  4. 1 with_icon :bars_staggered
  5. 1 with_color :app
  6. 1 initialize_with name: :text
  7. 1 def initialize(protocol:, **kwargs)
  8. super(**kwargs)
  9. @protocol = protocol
  10. @text = @protocol.name
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/entities/provider.rb

70.0% lines covered

100.0% branches covered

10 relevant lines. 7 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class Provider < LooposUi::Entity
  4. 1 with_icon :seal
  5. 1 with_color :general
  6. 1 initialize_with name: :text
  7. 1 def initialize(provider:, **kwargs)
  8. super(**kwargs)
  9. @provider = provider
  10. @text = @provider.name
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/entities/script.rb

70.0% lines covered

100.0% branches covered

10 relevant lines. 7 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class Script < LooposUi::Entity
  4. 1 with_icon :code
  5. 1 with_color :general
  6. 1 initialize_with name: :text
  7. 1 def initialize(script:, **kwargs)
  8. super(**kwargs)
  9. @script = script
  10. @text = @script.name
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/entities/script_kind.rb

66.67% lines covered

100.0% branches covered

9 relevant lines. 6 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class ScriptKind < LooposUi::Entity
  4. 1 with_color :general
  5. 1 initialize_with name: :text
  6. 1 def initialize(script_kind:, **kwargs)
  7. super(**kwargs)
  8. @script_kind = script_kind
  9. @text = @script_kind.name
  10. end
  11. end
  12. end
  13. end

app/components/loopos_ui/entities/shipping.rb

70.0% lines covered

100.0% branches covered

10 relevant lines. 7 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class Shipping < LooposUi::Entity
  4. 1 with_icon :truck
  5. 1 with_color :general
  6. 1 initialize_with name: :text
  7. 1 def initialize(shipping:, **kwargs)
  8. super(**kwargs)
  9. @shipping = shipping
  10. @text = @shipping.name
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/entities/sms.rb

70.0% lines covered

100.0% branches covered

10 relevant lines. 7 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class Sms < LooposUi::Entity
  4. 1 with_icon :seal
  5. 1 with_color :general
  6. 1 initialize_with name: :text
  7. 1 def initialize(sms:, **kwargs)
  8. super(**kwargs)
  9. @sms = sms
  10. @text = @sms.name
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/entities/template.rb

70.0% lines covered

100.0% branches covered

10 relevant lines. 7 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class Template < LooposUi::Entity
  4. 1 with_icon :file_pen
  5. 1 with_color :general
  6. 1 initialize_with name: :text
  7. 1 def initialize(template:, **kwargs)
  8. super(**kwargs)
  9. @template = template
  10. @text = @template.name
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/entities/user_group.rb

70.0% lines covered

100.0% branches covered

10 relevant lines. 7 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Entities
  3. 1 class UserGroup < LooposUi::Entity
  4. 1 with_icon :users
  5. 1 with_color :general
  6. 1 initialize_with name: :text
  7. 1 def initialize(user_group:, **kwargs)
  8. super(**kwargs)
  9. @user_group = user_group
  10. @text = @user_group.name
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/entity.rb

59.52% lines covered

0.0% branches covered

42 relevant lines. 25 lines covered and 17 lines missed.
14 total branches, 0 branches covered and 14 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class Entity < LoopComponent
  4. 1 extend FaviconAware
  5. 25 then: 0 else: 0 class_attribute :icon
  6. 3 then: 0 else: 0 class_attribute :trailing_icon
  7. 28 then: 0 else: 0 class_attribute :color
  8. 28 then: 0 else: 0 class_attribute :struct_mapping
  9. 1 class << self
  10. # This is a generic implementation of the without_model method
  11. # It can only be used if you define a struct_mapping class attribute on your Entity
  12. # An alternative to defining a struct_mapping is to override this method in your Entity class
  13. # Example:
  14. # UserGroupStruct = ::Struct.new(:name, keyword_init: true)
  15. # def without_model(text:, **kwargs)
  16. # new(user_group: UserGroupStruct.new(name: text), **kwargs)
  17. # end
  18. 1 def without_model(text:, **kwargs)
  19. then: 0 else: 0 if struct_mapping.nil? || struct_mapping.empty? || !struct_mapping.values.include?(:text)
  20. raise NotImplementedError, "
  21. You need to define struct_mapping in the #{self} class, or override #{self}.without_model
  22. struct_mapping should be a hash, and it should have at least one key named :text
  23. "
  24. end
  25. # Get the first argument of the initialize method. Usually it's the symbolized name of the model
  26. # eg: :user_group, :protocol, etc
  27. arg = instance_method(:initialize).parameters.first.second
  28. # Create a struct to hold the date. It's just an object that has accessors for the keys
  29. struct = "::Struct::#{name.demodulize}Struct".safe_constantize
  30. else: 0 if struct.nil?
  31. then: 0
  32. ::Struct.new("#{name.demodulize}Struct", *struct_mapping.keys, keyword_init: true)
  33. struct = "::Struct::#{name.demodulize}Struct".constantize
  34. end
  35. # Instanciate the Entity with the struct, using the passed args
  36. args = struct_mapping.map { |k, v| [k, binding.local_variable_get(v)] }.to_h
  37. new(arg => struct.new(**args), **kwargs)
  38. end
  39. 1 private
  40. 1 def with_icon(icon)
  41. 23 self.icon = faviconize(icon)
  42. end
  43. 1 def with_trailing_icon(icon)
  44. 1 self.trailing_icon = faviconize(icon)
  45. end
  46. 1 def with_color(color)
  47. # TODO: check valid values
  48. 26 self.color = color
  49. end
  50. 1 def initialize_with(struct_mapping)
  51. 26 self.struct_mapping = struct_mapping
  52. end
  53. end
  54. 1 attr_reader :url
  55. 1 attr_reader :href_options
  56. 1 attr_accessor :text
  57. 1 def initialize(url: nil, href_options: {}, **kwargs)
  58. @url = url
  59. @href_options = href_options
  60. @tooltip = kwargs.delete(:tooltip)
  61. @kwargs = kwargs
  62. end
  63. 1 def entity
  64. @entity ||= LooposUi::EntityToken.new(
  65. color: self.class.color,
  66. icon: @icon || self.class.icon,
  67. trailing_icon: self.class.trailing_icon,
  68. text: text,
  69. url: url,
  70. tooltip: @tooltip,
  71. href_options: href_options,
  72. kind: @kind,
  73. key_text: @key_text,
  74. key_value_actions: @key_value_actions,
  75. **@kwargs,
  76. )
  77. end
  78. 1 then: 0 else: 0 delegate :with_action, to: :entity
  79. 1 erb_template <<~ERB
  80. <%= render entity %>
  81. ERB
  82. end
  83. end

app/components/loopos_ui/entity_token.rb

29.73% lines covered

0.0% branches covered

37 relevant lines. 11 lines covered and 26 lines missed.
20 total branches, 0 branches covered and 20 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class EntityToken < LooposUi::Token
  4. COLORS = {
  5. # text, bg
  6. 1 general: [find_color("general-global-black"), nil],
  7. }
  8. APP_COLORS = {
  9. # text, bg
  10. 1 manager: [find_color("apps-800-primary", LooposUi.config.app_type), find_color("apps-300", LooposUi.config.app_type)], # Apps/Manager/800-Primary, Apps/Manager/300
  11. core: [find_color("apps-800-primary", LooposUi.config.app_type), find_color("apps-300", LooposUi.config.app_type)], # Apps/Core/800-Primary, Apps/Core/300
  12. hubs: [find_color("apps-800-primary", LooposUi.config.app_type), find_color("apps-300", LooposUi.config.app_type)], # Apps/Hubs/800-Primary, Apps/Hubs/300
  13. }
  14. 1 def initialize(**kwargs)
  15. @draft = kwargs.delete(:draft)
  16. @url = kwargs.delete(:url)
  17. @href_options = kwargs.delete(:href_options) || {}
  18. @key_text = kwargs.delete(:key_text)
  19. @key_value_actions = kwargs.delete(:key_value_actions)
  20. @kind = kwargs.delete(:kind)
  21. super(kind: @kind, key_text: @key_text, key_value_actions: @key_value_actions, **kwargs)
  22. @color ||= :general
  23. set_type
  24. end
  25. 1 def classes
  26. [
  27. "lui-entity-token",
  28. "lui-entity-token-#{@type}",
  29. then: 0 else: 0 draft? ? "lui-entity-token--draft" : "",
  30. then: 0 else: 0 @disabled ? "lui-token--disabled" : "",
  31. ].compact.join(" ")
  32. end
  33. 1 def styles
  34. then: 0 else: 0 cursor_style = @draggable ? "cursor: grab;" : ""
  35. then: 0 else: 0 bg_style = @bg_color.present? ? "background-color: #{@bg_color};" : ""
  36. <<~CSS.squish
  37. color: #{@text_color};
  38. #{bg_style}
  39. #{cursor_style}
  40. CSS
  41. end
  42. 1 private
  43. 1 def set_color(color)
  44. # TODO: Restrecture with LoopOS UI Colros component
  45. then: 0 @text_color, @bg_color = if color.present?
  46. then: 0 if COLORS.key?(color.to_sym)
  47. else: 0 COLORS[color.to_sym]
  48. then: 0 elsif color.to_sym == :app
  49. APP_COLORS[LooposUi.config.app_type.to_sym] || COLORS[:general]
  50. else: 0 else
  51. raise ArgumentError, "Invalid color: #{color}, available colors: #{COLORS.keys.join(", ")}, app."
  52. end
  53. else: 0 else
  54. COLORS.excluding(LooposUi.config.app_type.to_sym).values.sample
  55. end
  56. end
  57. 1 def set_type
  58. then: 0 else: 0 @type = case @color&.to_sym
  59. when: 0 when :app
  60. then: 0 else: 0 APP_COLORS.key?(LooposUi.config.app_type.to_sym) ? LooposUi.config.app_type : :general
  61. else: 0 else
  62. @color
  63. end
  64. end
  65. 1 def draft?
  66. @draft
  67. end
  68. end
  69. end

app/components/loopos_ui/error_page.rb

83.33% lines covered

100.0% branches covered

6 relevant lines. 5 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class ErrorPage < LoopComponent
  3. 1 option :error, type: Types::Integer
  4. 1 option :app, default: proc { LooposUi.config.app_type }, type: Types::Coercible::String
  5. 1 def error_text
  6. {
  7. 403 =>
  8. {
  9. title: "The access to this page is denied",
  10. description: "You do not have access to this page or resource.
  11. Please check the URL for any typos or visit our <a class=\"lui-error-page__description__link\" href=\"/\">Home</a>.",
  12. },
  13. 404 =>
  14. {
  15. title: "This page cannot be found",
  16. description: "The URL is incorrect or the page has been moved. Please check the URL for any typos.<br>
  17. Visit our <a class=\"lui-error-page__description__link\" href=\"/\">Home</a> or select another page from the menu.",
  18. },
  19. 500 =>
  20. {
  21. title: "Something went wrong on our end",
  22. description: "An issue is blocking your request. Please refresh the page.<br>
  23. If it persists, try again later or <a class=\"lui-error-page__description__link\" href=\"mailto:support@loop-os.com\">contact us</a>.",
  24. },
  25. }
  26. end
  27. end
  28. end

app/components/loopos_ui/error_page/error_page.html.erb

0.0% lines covered

0.0% branches covered

10 relevant lines. 0 lines covered and 10 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <div class="lui-error-page">
  2. <%= helpers.app_svg(app: app, width: 120) %>
  3. <div class="lui-error-page__content">
  4. <div class="lui-error-page__header">
  5. <p class="lui-error-page__header__error">
  6. Error <%= error %>
  7. </p>
  8. <p class="lui-error-page__header__title">
  9. <%= error_text.dig(error, :title) %>
  10. </p>
  11. </div>
  12. <p class="lui-error-page__description">
  13. <%= sanitize error_text.dig(error, :description) %>
  14. </p>
  15. then: 0 else: 0 <% if error == 500 %>
  16. <div class="pt-4">
  17. <%= render LooposUi::Button.new(
  18. text: "Refresh",
  19. size: :default,
  20. type: :secondary,
  21. tag_options: {
  22. "onClick" => "location.href=location.href"
  23. }) %>
  24. </div>
  25. <% end %>
  26. </div>
  27. </div>

app/components/loopos_ui/extra_data_viewer.rb

76.0% lines covered

0.0% branches covered

25 relevant lines. 19 lines covered and 6 lines missed.
5 total branches, 0 branches covered and 5 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class ExtraDataViewer < LoopComponent
  4. 1 VIEW_MODES = ["human", "json"].freeze
  5. 1 option :title, type: Types::String.optional, optional: true
  6. 1 option :data, type: Types::Hash | Types::Array | Types::String
  7. 1 option :readonly, type: Types::Bool, default: -> { false }
  8. 1 option :default_view, type: Types::String, default: -> { "human" }
  9. 1 option :inline_element, optional: true
  10. 1 option :inline_attribute, type: Types::String.optional, optional: true
  11. 1 option :inline_show_path, type: Types::String.optional, optional: true
  12. 1 option :inline_edit_path, type: Types::String.optional, optional: true
  13. 1 option :inline_form_url, type: Types::String.optional, optional: true
  14. 1 option :inline_input_name, type: Types::String.optional, optional: true
  15. 1 option :inline_show_edit_button, type: Types::Bool, default: -> { true }
  16. 1 option :inline_turbo_id, type: Types::String.optional, optional: true
  17. 1 option :inline_hidden_fields, type: Types::Hash, default: -> { {} }
  18. 1 def current_view
  19. then: 0 else: 0 VIEW_MODES.include?(default_view) ? default_view : "human"
  20. end
  21. 1 def editor_readonly?
  22. readonly
  23. end
  24. 1 def json_code
  25. when: 0 case data
  26. when: 0 when String then data
  27. when Hash, Array then data.to_json
  28. else: 0 else
  29. data.to_s
  30. end
  31. end
  32. end
  33. end

app/components/loopos_ui/extra_data_viewer/extra_data_viewer.html.erb

0.0% lines covered

0.0% branches covered

20 relevant lines. 0 lines covered and 20 lines missed.
10 total branches, 0 branches covered and 10 branches missed.
    
  1. <%
  2. then: 0 else: 0 turbo_wrapper_id = inline_turbo_id if inline_element.present? && inline_turbo_id.present?
  3. persisted_view = params[:extra_data_viewer_default_view].presence
  4. then: 0 else: 0 selected_view = LooposUi::ExtraDataViewer::VIEW_MODES.include?(persisted_view) ? persisted_view : current_view
  5. editor_hidden_fields = inline_hidden_fields.merge("extra_data_viewer_default_view" => selected_view)
  6. %>
  7. <% inner_content = capture do %>
  8. <%= tag.div(
  9. id: random_id,
  10. class: "lui-extra_data_viewer",
  11. data: {
  12. controller: "extra-data-viewer",
  13. action: "change->extra-data-viewer#toggleView",
  14. extra_data_viewer_default_view_value: selected_view,
  15. extra_data_viewer_persist_field_value: "extra_data_viewer_default_view",
  16. extra_data_viewer_storage_key_value: turbo_wrapper_id
  17. }
  18. ) do %>
  19. <div class="lui-extra_data_viewer__toggle">
  20. <%= render LooposUi::Toggle.new(
  21. label: t("loopos_ui.extra_data_viewer.json_view"),
  22. value: selected_view == "json",
  23. attrs: {
  24. data: {
  25. view_toggle: "json",
  26. "extra-data-viewer-target": "jsonToggle"
  27. }
  28. }
  29. ) %>
  30. </div>
  31. <%= tag.div(
  32. data: { extra_data_viewer_target: "humanPanel" },
  33. then: 0 else: 0 class: "lui-extra_data_viewer__panel lui-extra_data_viewer__panel--human #{selected_view == "human" ? "" : "lui-extra_data_viewer__panel--hidden"}"
  34. ) do %>
  35. <%= render LooposUi::HumanView.new(
  36. title: title,
  37. data: data
  38. ) %>
  39. <% end %>
  40. <%= tag.div(
  41. data: { extra_data_viewer_target: "jsonPanel" },
  42. then: 0 else: 0 class: "lui-extra_data_viewer__panel lui-extra_data_viewer__panel--json #{selected_view == "json" ? "" : "lui-extra_data_viewer__panel--hidden"}"
  43. ) do %>
  44. <%= render LooposUi::CodeEditor.new(
  45. type: "json",
  46. code: json_code,
  47. readonly: editor_readonly?,
  48. element: inline_element,
  49. attribute: inline_attribute,
  50. show_path: inline_show_path,
  51. input_name: inline_input_name,
  52. show_edit_button: inline_show_edit_button,
  53. turbo_id: inline_turbo_id,
  54. with_turbo_wrapper: false,
  55. hidden_fields: editor_hidden_fields,
  56. title: title
  57. ) %>
  58. <% end %>
  59. <% end %>
  60. <% end %>
  61. then: 0 else: 0 <%= turbo_wrapper_id.present? ? helpers.turbo_frame_tag(turbo_wrapper_id) { inner_content } : inner_content %>

app/components/loopos_ui/features/list.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Features
  3. 1 class List < LoopComponent
  4. 1 ORDER = [:flows, :protocols, :pricing_rules, :forward_trade_in, :acceptable, :acceptable_online, :saleable_as_new]
  5. ICONS = {
  6. 1 flows: "fa-regular fa-timeline-arrow",
  7. protocols: "fa-regular fa-bars-staggered",
  8. pricing_rules: "fa-regular fa-euro-sign",
  9. forward_trade_in: "",
  10. acceptable: "",
  11. acceptable_online: "",
  12. saleable_as_new: "",
  13. }
  14. 1 option :list
  15. end
  16. end
  17. end

app/components/loopos_ui/features/list/list.html.erb

0.0% lines covered

0.0% branches covered

8 relevant lines. 0 lines covered and 8 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%= render LooposUi::TokenList.new(max_tokens: 3) do |token_list| %>
  2. <% ORDER.each do |feature| %>
  3. <% details = list[feature] %>
  4. then: 0 else: 0 <% if details.present? %>
  5. <% token_list.with_token_manual do %>
  6. <%= render(LooposUi::EntityToken.new(
  7. text: details[:label] || feature.to_s.titleize,
  8. icon: ICONS[feature],
  9. url: details[:url],
  10. tooltip: details[:tooltip],
  11. color: :general
  12. )) %>
  13. <% end %>
  14. <% end %>
  15. <% end %>
  16. <% end %>

app/components/loopos_ui/filter/filters_dropdown_component.html.erb

0.0% lines covered

0.0% branches covered

19 relevant lines. 0 lines covered and 19 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <button id="dropdownBgHoverButton" data-dropdown-toggle="dropdownBgHover" class="loopui-filters-toggle border-opacity-10 bg-background border-primary-dark" type="button">
  2. <i class="fa-sharp fa-regular fa-filter"></i>
  3. <div class="loopui-filters-badge hidden">20</div>
  4. </button>
  5. then: 0 else: 0 <% if @has_active_filters %>
  6. <div class='loopui-filters-clear'>
  7. <%= link_to 'Clear all filters', URI.parse(@clear_path.call(**@clear_parameters.merge(turbo_frame: index_turbo_frame))).to_s,
  8. data: {
  9. turbo_action: "replace",
  10. turbo_method: @method,
  11. } %>
  12. </div>
  13. <% end %>
  14. <div id="dropdownBgHover" data-controller='filters-dropdown' class="hidden loopui-filters-dropdown">
  15. <div class="loopui-filters-dropdown__back hidden" data-filters-dropdown-target='back' data-action='click->filters-dropdown#backButtonClick'>
  16. <i class="fa-regular fa-arrow-left mr-2 text-sm"></i>
  17. <span class="copy-14-medium text-black">Back</span>
  18. </div>
  19. <ul class="loopui-filters-dropdown__wrapper" data-filter-state='main-filter-wrapper' aria-labelledby="dropdownSearchButton">
  20. <% @filters_titles.each do |data| %>
  21. <li class="cursor-pointer">
  22. <div class="loopui-filters-dropdown__options-wrapper group">
  23. <label class="loopui-filters-dropdown__label" data-child='<%= data[:value] %>_children' data-action='click->filters-dropdown#onLabelClick'><%= data[:label] %></label>
  24. <i class="fa-regular fa-chevron-right mr-2 group-hover:text-app-800-primary" data-child='<%= data[:value] %>_children' data-action='click->filters-dropdown#onLabelClick'></i>
  25. </div>
  26. </li>
  27. <% end %>
  28. </ul>
  29. <% @filters_data.each do |micro_key, states| %>
  30. <ul class="loopui-filters-dropdown__wrapper hidden" data-filter-state='<%= micro_key %>_children' aria-labelledby="dropdownSearchButton">
  31. <% states.each do |data| %>
  32. <li class="cursor-pointer">
  33. <div class="loopui-filters-dropdown__options-wrapper group cursor-pointer" data-action="click->filters-dropdown#inputCheck">
  34. then: 0 else: 0 <%= check_box_tag "q[#{data[:filter_param]}][]", data[:value], params.dig(:q, data[:filter_param])&.include?(data[:value].to_s), class: 'w-4 h-4 text-blue-600 bg-gray-100 border-gray-base rounded focus:ring-trasparent focus:ring-0 cursor-pointer', data: { 'filters-dropdown-target' => 'input', 'action' => 'change->filters-dropdown#checkboxChanged' } %>
  35. <label class="loopui-filters-dropdown__label max-w-[120px]" data-action="click->filters-dropdown#inputCheck"><%= data[:label] %></label>
  36. </div>
  37. </li>
  38. <% end %>
  39. </ul>
  40. <% end %>
  41. <div class='flex'>
  42. <input type='submit' value='Apply' data-filters-dropdown-target='submit' class="loopui-filters-dropdown__submit" />
  43. </div>
  44. </div>

app/components/loopos_ui/filter/filters_dropdown_component.rb

46.15% lines covered

100.0% branches covered

13 relevant lines. 6 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Filter
  3. 1 class FiltersDropdownComponent < ViewComponent::Base
  4. 1 include Turbo::FramesHelper
  5. 1 attr_reader :filters_data,
  6. :filters_titles,
  7. :index_turbo_frame,
  8. :has_active_filters,
  9. :clear_path,
  10. :clear_parameters
  11. 1 def initialize(filters_data:, filters_titles:, index_turbo_frame:, has_active_filters: false, clear_path: "",
  12. method: :get, clear_parameters: {})
  13. @filters_data = filters_data
  14. @index_turbo_frame = index_turbo_frame
  15. @has_active_filters = has_active_filters
  16. @clear_path = clear_path
  17. @filters_titles = filters_titles
  18. @clear_parameters = clear_parameters
  19. @method = method
  20. end
  21. end
  22. end
  23. end

app/components/loopos_ui/filter/search_component.html.erb

0.0% lines covered

100.0% branches covered

2 relevant lines. 0 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <div class='loopui-search-input'>
  2. <div class="loopui-search-input__container">
  3. <label for="default-search" class="loopui-search-input__label">Search</label>
  4. <div class="loopui-search-input__wrapper">
  5. <div class="loopui-search-input__icon">
  6. <svg aria-hidden="true" class="loopui-search-input__icon-svg" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
  7. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
  8. </svg>
  9. </div>
  10. <%= search_field_tag @param, @search_query, class: 'loopui-search-input__field', placeholder: @placeholder %>
  11. </div>
  12. </div>
  13. </div>

app/components/loopos_ui/filter/search_component.rb

66.67% lines covered

100.0% branches covered

9 relevant lines. 6 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Filter
  3. 1 class SearchComponent < ViewComponent::Base
  4. 1 include Turbo::FramesHelper
  5. 1 attr_reader :param, :search_query, :placeholder
  6. 1 def initialize(param:, search_query:, placeholder: "")
  7. @param = param
  8. @search_query = search_query
  9. @placeholder = placeholder
  10. end
  11. end
  12. end
  13. end

app/components/loopos_ui/filter_bar.rb

45.45% lines covered

0.0% branches covered

55 relevant lines. 25 lines covered and 30 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. 1 module LooposUi
  2. 1 class FilterBar < LoopComponent
  3. 1 renders_many :filter_pills, LooposUi::FilterPill
  4. 1 option :buttons, Types::Array.of(Types::Hash), default: -> { [] }
  5. 1 option :show_filter_buttons, Types::Bool, default: -> { false }
  6. 1 option :show_toggle_switch, Types::Bool, default: -> { false }
  7. 1 option :search_options, Types::Hash, default: -> { {} }
  8. 1 option :toggle_options, Types::Hash, default: -> { {} }
  9. 1 option :mode, Types::Symbol, default: -> { :both }
  10. 1 option :table_id, Types::Coercible::String, optional: true
  11. 1 option :align_actions, Types::Symbol, default: -> { :start }
  12. 1 def search_options_for_render
  13. search_options.compact
  14. end
  15. 1 def filter_pills_exists?
  16. filter_pills.present? && table_id.present?
  17. end
  18. 1 def has_filters_pill?
  19. pills_enabled? && table_id.present?
  20. end
  21. 1 def has_filter_header?
  22. header_enabled? && (search_options.present? || show_buttons? || show_toggle?)
  23. end
  24. 1 def filter_key_for_pill(pill)
  25. pill_extra = pill.extra_options || {}
  26. pill_data = pill_extra[:data] || pill_extra["data"] || {}
  27. filter_key = pill_data[:filter_key] || pill_data["filter_key"] || pill_data[:filterKey] || pill_data["filterKey"]
  28. filter_key || pill_extra[:filter_key] || pill_extra["filter_key"]
  29. end
  30. 1 def toggle_options_table
  31. toggle_opts = toggle_options.dup
  32. toggle_opts[:position] = :left
  33. toggle_opts[:size] = :normal
  34. else: 0 if table_id.present?
  35. then: 0
  36. existing_action = toggle_opts.dig(:data_action) || ""
  37. toggle_opts[:data_action] = [existing_action, "change->lui--filter-toggle#handleToggleChange"].reject(&:blank?).join(" ")
  38. end
  39. toggle_opts
  40. end
  41. 1 def button_options_for_render(button_options)
  42. button_opts = button_options.dup
  43. button_opts[:tag_options] ||= {}
  44. button_opts[:tag_options][:data] ||= {}
  45. existing_action = button_opts.dig(:tag_options, :data, :action) || ""
  46. button_opts[:tag_options][:data][:action] = [existing_action, "click->lui--filter-buttons#handleButtonClick"].reject(&:blank?).join(" ")
  47. button_opts[:tag_options][:type] ||= "button"
  48. then: 0 else: 0 if button_opts.dig(:tag_options, :name).present?
  49. button_opts[:tag_options][:data][:filter_name] = button_opts.dig(:tag_options, :name)
  50. end
  51. button_opts
  52. end
  53. 1 private
  54. 1 def header_enabled?
  55. [:both, :header].include?(mode)
  56. end
  57. 1 def pills_enabled?
  58. [:both, :pills].include?(mode)
  59. end
  60. 1 def show_search?
  61. show_search
  62. end
  63. 1 def show_actions?
  64. show_buttons? || show_toggle?
  65. end
  66. 1 def show_buttons?
  67. show_filter_buttons
  68. end
  69. 1 def show_toggle?
  70. show_toggle_switch
  71. end
  72. end
  73. end

app/components/loopos_ui/filter_bar/filter_bar.html.erb

0.0% lines covered

0.0% branches covered

30 relevant lines. 0 lines covered and 30 lines missed.
18 total branches, 0 branches covered and 18 branches missed.
    
  1. <%= tag.div(
  2. class: classes,
  3. data: {
  4. controller: "lui--filter-buttons lui--filter-toggle lui--filter-pills",
  5. "lui--filter-pills-table-id-value": table_id,
  6. "lui--filter-buttons-table-id-value": table_id,
  7. "lui--filter-toggle-table-id-value": table_id
  8. }
  9. ) do %>
  10. then: 0 else: 0 <% if has_filter_header? %>
  11. <div class="lui-filter_bar__top">
  12. then: 0 else: 0 <% if search_options.present? %>
  13. <div class="lui-filter_bar__search">
  14. <%= render LooposUi::Inputs::Search.new(**search_options_for_render) %>
  15. </div>
  16. <% end %>
  17. then: 0 else: 0 <% if show_actions? %>
  18. then: 0 else: 0 <div class="lui-filter_bar__actions<%= ' ml-auto' if align_actions == :end %>">
  19. then: 0 else: 0 <% if show_buttons? %>
  20. <div class="lui-filter_bar__button">
  21. <% buttons.each_with_index do |button_options, index| %>
  22. <%= render LooposUi::FilterButton.new(**button_options_for_render(button_options)) %>
  23. <% end %>
  24. </div>
  25. <% end %>
  26. then: 0 else: 0 <% if show_toggle? %>
  27. <div class="lui-filter_bar__toggle">
  28. <%= render LooposUi::Toggle.new(**toggle_options_table) %>
  29. </div>
  30. <% end %>
  31. </div>
  32. <% end %>
  33. </div>
  34. <% end %>
  35. then: 0 else: 0 <% if has_filters_pill? %>
  36. <%= tag.div(
  37. then: 0 else: 0 class: "lui-filter_bar__pills #{filter_pills_exists? ? "" : "lui-filter_bar__pills--empty"}",
  38. data: {
  39. controller: "lui--filter-pills",
  40. "lui--filter-pills-table-id-value": table_id,
  41. "lui--filter-pills-target": "div"
  42. }
  43. ) do %>
  44. <div class="lui-filter_bar__pills-list" data-lui--filter-pills-target="container">
  45. <% filter_pills.each do |pill| %>
  46. <%= tag.div(
  47. data: {
  48. "lui--filter-pills-target": "pill"
  49. then: 0 else: 0 }.tap { |h| h["filter-key"] = filter_key_for_pill(pill) if filter_key_for_pill(pill).present? }
  50. ) do %>
  51. <%= pill %>
  52. <% end %>
  53. <% end %>
  54. </div>
  55. <div class="lui-filter_bar__clear_all" data-lui--filter-pills-target="clearAllButton" style="display: none;">
  56. <%= render LooposUi::Button.new(
  57. text: t(".clear_all_filters"),
  58. type: :tertiary,
  59. kind: :neutral,
  60. size: :small,
  61. tag_options: {
  62. type: "button",
  63. data: {
  64. action: "click->lui--filter-pills#clearAll"
  65. }
  66. }
  67. ) %>
  68. </div>
  69. <% end %>
  70. <% end %>
  71. <% end %>

app/components/loopos_ui/filter_button.rb

46.51% lines covered

0.0% branches covered

86 relevant lines. 40 lines covered and 46 lines missed.
26 total branches, 0 branches covered and 26 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. # rubocop:disable Naming/MethodName
  4. 1 class FilterButton < LoopComponent
  5. 1 include LooposUi::FaviconAware
  6. 1 SIZES = [
  7. :large, :medium, :small,
  8. ].freeze
  9. 1 STATES = [
  10. :enabled,
  11. :hover,
  12. :active,
  13. :loading,
  14. :focus,
  15. :disabled,
  16. ].freeze
  17. 1 APP_ICONS = [
  18. "core",
  19. "manager",
  20. "submission",
  21. "hubs",
  22. "validation",
  23. "handling",
  24. "exits",
  25. "impact",
  26. "submission_extra",
  27. ].freeze
  28. 1 renders_one :left_icon, ->(icon:, **kwargs) {
  29. then: 0 if APP_ICONS.include?(icon.to_s)
  30. else: 0 icon
  31. then: 0 elsif MIcon.icon?(icon)
  32. MIcon.new(icon, **kwargs)
  33. else: 0 else
  34. content_tag(:i, "", class: "lui-filter_button__icon lui-filter_button__icon--#{@size} #{icon}")
  35. end
  36. }
  37. 1 renders_one :right_icon, ->(icon:, **kwargs) {
  38. then: 0 if APP_ICONS.include?(icon.to_s)
  39. else: 0 icon
  40. then: 0 elsif MIcon.icon?(icon)
  41. MIcon.new(icon, **kwargs)
  42. else: 0 else
  43. content_tag(:i, "", class: "lui-filter_button__icon lui-filter_button__icon--#{@size} #{icon}")
  44. end
  45. }
  46. 1 option :text, Types::Coercible::String, optional: true
  47. 1 option :size, Types::Symbol.enum(*SIZES), optional: true
  48. 1 option :state, Types::Symbol.enum(*STATES), optional: true
  49. # TODO: Rename this to snake_case
  50. 1 option :isSelected, Types::Bool, optional: true
  51. 1 option :showNotification, Types::Bool, optional: true
  52. 1 option :showRightIcon, Types::Bool, optional: true
  53. 1 option :showLeftIcon, Types::Bool, optional: true
  54. 1 option :showCounter, Types::Bool, optional: true
  55. # Counter appearance (delegates to LooposUi::Counter)
  56. # kind: :success, :danger, :warning, :neutral, :informative
  57. 1 option :counter_kind, Types::Symbol.optional, default: -> { :neutral }
  58. 1 option :left_icon, Types::Coercible::String, optional: true
  59. 1 option :right_icon, Types::Coercible::String, optional: true
  60. 1 option :count, Types::Coercible::Integer, optional: true
  61. 1 option :href, Types::Coercible::String, optional: true
  62. 1 option :tag, Types::Symbol, optional: true
  63. 1 option :tag_options, Types::Hash, default: -> { {} }
  64. 1 option :name, Types::Coercible::String, optional: true
  65. 1 mod :size
  66. 1 mod :state
  67. 1 mod :isSelected, condition: -> { isSelected? }
  68. 1 attr_reader :tag
  69. # rubocop:disable Naming/VariableName
  70. 1 def initialize(
  71. text: nil,
  72. size: nil,
  73. state: nil,
  74. isSelected: nil,
  75. showNotification: nil,
  76. showRightIcon: nil,
  77. showLeftIcon: nil,
  78. showCounter: nil,
  79. left_icon: nil,
  80. right_icon: nil,
  81. count: nil,
  82. href: nil,
  83. tag: nil,
  84. tag_options: {},
  85. name: nil,
  86. **kwargs
  87. )
  88. @text = text
  89. @size = size || :medium
  90. @state = state || :enabled
  91. @isSelected = isSelected || false
  92. @showNotification = showNotification || false
  93. @showRightIcon = showRightIcon || false
  94. @showLeftIcon = showLeftIcon || false
  95. @showCounter = showCounter || false
  96. @left_icon = left_icon
  97. @right_icon = right_icon
  98. @count = count
  99. @href = href
  100. then: 0 else: 0 @tag = tag || (href.present? && @state != :disabled ? :a : :button)
  101. @tag_options = tag_options
  102. @name = name
  103. else: 0 then: 0 raise ArgumentError, "Invalid size: #{@size}" unless SIZES.include?(@size)
  104. else: 0 then: 0 raise ArgumentError, "Invalid state: #{@state}" unless STATES.include?(@state)
  105. super(**kwargs)
  106. end
  107. # rubocop:enable Naming/VariableName
  108. 1 def before_render
  109. then: 0 else: 0 with_left_icon(icon: @left_icon) if @left_icon.present? && showLeftIcon?
  110. then: 0 else: 0 with_right_icon(icon: @right_icon) if @right_icon.present? && showRightIcon?
  111. end
  112. 1 def classes
  113. [
  114. own_class,
  115. mod(:size),
  116. mod(:state),
  117. mod(:isSelected),
  118. additional_classes,
  119. ].compact.join(" ")
  120. end
  121. 1 def tag_options
  122. then: 0 options = if @tag == :a && @state != :disabled
  123. { href: @href }.merge!(@tag_options)
  124. else: 0 else
  125. @tag_options || {}
  126. end
  127. then: 0 else: 0 options.merge!(disabled: true) if @state == :disabled
  128. # deep_merge_args preserves top-level keys from options (like name, type)
  129. # but merges nested hashes (like data)
  130. # Ensure type is set to "button" if not specified (prevents form submission)
  131. base_options = {
  132. class: classes,
  133. data: {
  134. controller: "lui--filter-button",
  135. },
  136. }
  137. # Only set default type if tag is button and type is not already specified
  138. then: 0 else: 0 if @tag == :button && !options.key?(:type)
  139. base_options[:type] = "button"
  140. end
  141. then: 0 else: 0 options[:name] ||= @name if @name.present?
  142. deep_merge_args(base_options, options)
  143. end
  144. 1 def disabled?
  145. @state == :disabled
  146. end
  147. 1 def isSelected?
  148. @isSelected
  149. end
  150. 1 def showNotification?
  151. @showNotification
  152. end
  153. 1 def showRightIcon?
  154. @showRightIcon
  155. end
  156. 1 def showLeftIcon?
  157. @showLeftIcon
  158. end
  159. 1 def showCounter?
  160. @showCounter && @count.present? && @count > 0
  161. end
  162. 1 attr_reader :state
  163. 1 attr_reader :size
  164. end
  165. # rubocop:enable Naming/MethodName
  166. end

app/components/loopos_ui/filter_button/filter_button.html.erb

0.0% lines covered

0.0% branches covered

18 relevant lines. 0 lines covered and 18 lines missed.
14 total branches, 0 branches covered and 14 branches missed.
    
  1. <%= content_tag(tag, tag_options) do %>
  2. then: 0 else: 0 <% if showNotification? %>
  3. <span class="lui-filter_button__notification"></span>
  4. <% end %>
  5. then: 0 else: 0 <% if @state == :loading %>
  6. <svg aria-hidden="true" class="lui-filter_button__spinner" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
  7. <path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
  8. <path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
  9. </svg>
  10. <% end %>
  11. then: 0 else: 0 <% if showLeftIcon? && @left_icon.present? && @state != :loading %>
  12. <%= render LooposUi::MIcon.new(@left_icon.to_sym, tag: :i) %>
  13. <% end %>
  14. then: 0 else: 0 <% if text.present? && @state != :loading %>
  15. <span class="lui-filter_button__text">
  16. <%= text %>
  17. </span>
  18. <% end %>
  19. then: 0 else: 0 <% if showCounter? && @state != :loading && count.present? %>
  20. <%= render LooposUi::Counter.new(
  21. count: count,
  22. kind: counter_kind || :neutral,
  23. size: :tinny
  24. ) %>
  25. <% end %>
  26. then: 0 else: 0 <% if showRightIcon? && @right_icon.present? && @state != :loading %>
  27. <%= render LooposUi::MIcon.new(@right_icon.to_sym, tag: :i) %>
  28. <% end %>
  29. then: 0 else: 0 <% if @state == :loading && text.present? %>
  30. <span class="lui-filter_button__text lui-filter_button__text--loading">
  31. <%= text %>
  32. </span>
  33. <% end %>
  34. <% end %>

app/components/loopos_ui/filter_pill.rb

81.82% lines covered

100.0% branches covered

22 relevant lines. 18 lines covered and 4 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class FilterPill < LoopComponent
  3. 1 use_extra_options
  4. 1 SIZES = [
  5. :small,
  6. :medium,
  7. ].freeze
  8. 1 STATES = [
  9. :enabled,
  10. :active,
  11. :state2,
  12. ].freeze
  13. 1 option :text, Types::Coercible::String
  14. 1 option :size, Types::Symbol.enum(*SIZES), default: -> { :medium }
  15. 1 option :state, Types::Symbol.enum(*STATES), default: -> { :enabled }
  16. 1 option :selected, Types::Bool, default: -> { false }
  17. 1 option :hasCloseButton, Types::Bool, default: -> { true }
  18. 1 option :count, Types::Coercible::Integer.optional, optional: true
  19. 1 mod :size
  20. 1 mod :state
  21. 1 mod :selected, condition: -> { selected? }
  22. 1 def selected?
  23. selected
  24. end
  25. 1 def show_count?
  26. count.present? && count.positive?
  27. end
  28. 1 def has_close_button?
  29. :hasCloseButton
  30. end
  31. 1 def classes
  32. [
  33. own_class,
  34. mod(:size),
  35. mod(:state),
  36. mod(:selected),
  37. additional_classes,
  38. ].compact.join(" ")
  39. end
  40. end
  41. end

app/components/loopos_ui/filter_pill/filter_pill.html.erb

0.0% lines covered

0.0% branches covered

8 relevant lines. 0 lines covered and 8 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <%= tag.button type: "button", class: classes, **extra_options do %>
  2. <span class="lui-filter_pill__label">
  3. <%= text %>
  4. </span>
  5. then: 0 else: 0 <% if show_count? %>
  6. <span class="lui-filter_pill__count">
  7. <%= count %>
  8. </span>
  9. <% end %>
  10. then: 0 else: 0 <% if has_close_button? %>
  11. <%= render LooposUi::MIcon.new(
  12. :close,
  13. class: "lui-filter_pill__close",
  14. data: { action: "click->lui--filter-pills#removePill" },
  15. onclick: "event.preventDefault(); event.stopPropagation();"
  16. ) %>
  17. <% end %>
  18. <% end %>

app/components/loopos_ui/flex_layout.rb

83.33% lines covered

0.0% branches covered

12 relevant lines. 10 lines covered and 2 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 class FlexLayout < LoopComponent
  3. 1 renders_many :sections, ->(**kwargs) do
  4. LooposUi::FlexLayout::Section.new(size: size, grow: grow, **kwargs)
  5. end
  6. 1 option :size, default: -> { :full }, type: MediaQuerySizesConcern::Size
  7. 1 option :grow, Types::Bool, default: -> { true }
  8. 1 class Section < LoopComponent
  9. 1 include MediaQuerySizesConcern
  10. 1 option :size, MediaQuerySizesConcern::Size, optional: true
  11. 1 option :grow, Types::Bool, default: -> { true }
  12. 1 def classes
  13. [
  14. then: 0 else: 0 grow ? "grow" : nil,
  15. size_class(size || :full),
  16. media_query_classes,
  17. ].compact.uniq.join(" ")
  18. end
  19. end
  20. end
  21. end

app/components/loopos_ui/flex_layout/flex_layout.html.erb

0.0% lines covered

100.0% branches covered

4 relevant lines. 0 lines covered and 4 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <div class="lui-flex_layout">
  2. <% sections.each do |section| %>
  3. <%= section %>
  4. <% end %>
  5. </div>

app/components/loopos_ui/flex_layout/media_query_sizes_concern.rb

70.0% lines covered

0.0% branches covered

20 relevant lines. 14 lines covered and 6 lines missed.
10 total branches, 0 branches covered and 10 branches missed.
    
  1. 1 module LooposUi
  2. 1 class FlexLayout
  3. 1 module MediaQuerySizesConcern
  4. 1 extend ActiveSupport::Concern
  5. 1 Size = Types::Symbol.enum(:full, :half) | Types::Integer.constrained(gteq: 1, lteq: 12)
  6. 1 included do
  7. 1 option :sm, optional: true, type: Size
  8. 1 option :md, optional: true, type: Size
  9. 1 option :lg, optional: true, type: Size
  10. 1 option :xl, optional: true, type: Size
  11. 1 option :xxl, optional: true, type: Size
  12. 1 def media_query_classes
  13. [
  14. size_class(sm, :sm),
  15. size_class(md, :md),
  16. size_class(lg, :lg),
  17. size_class(xl, :xl),
  18. size_class(xxl, :"2xl"),
  19. ].compact.join(" ")
  20. end
  21. end
  22. 1 private
  23. 1 def size_class(size, query = nil)
  24. then: 0 else: 0 return if size.nil?
  25. then: 0 else: 0 size = 6 if size == :half
  26. then: 0 if query.nil?
  27. then: 0 else: 0 size == :full ? "basis-full" : "basis-#{size}/12"
  28. else: 0 else
  29. then: 0 else: 0 size == :full ? "#{query}:basis-full" : "#{query}:basis-#{size}/12"
  30. end
  31. end
  32. end
  33. end
  34. end

app/components/loopos_ui/flex_layout/section.html.erb

0.0% lines covered

100.0% branches covered

2 relevant lines. 0 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div(class: classes) do %>
  2. <%= content %>
  3. <% end %>

app/components/loopos_ui/float_bar.rb

36.36% lines covered

20.0% branches covered

55 relevant lines. 20 lines covered and 35 lines missed.
10 total branches, 2 branches covered and 8 branches missed.
    
  1. 1 module LooposUi
  2. 1 class FloatBar < LoopComponent
  3. 1 option :count, default: -> { 0 }
  4. 1 option :input_name, Types::Coercible::String, optional: true # JS default is "id[]"
  5. 1 renders_many :buttons, types: LooposUi::Button.presets
  6. # Generate wrapper methods for each button preset
  7. # When using the presets, the button will be rendered with the default arguments for a FloatBar
  8. 1 LooposUi::Button.presets.keys.each do |preset|
  9. 23 then: 1 else: 22 next if preset == :manual # Manual does not accept arguments
  10. 22 define_method("with_action_#{preset}") do |*args, **kwargs, &block|
  11. send("with_button_#{preset}", *args, **with_default_args(**kwargs), &block)
  12. end
  13. end
  14. # Manual slots do not accept arguments, simple wrapper
  15. 1 def with_action_manual(...)
  16. with_button_manual(...)
  17. end
  18. 1 def with_action(*args, **kwargs, &block)
  19. with_button(*args, **with_default_args(**kwargs), &block)
  20. end
  21. 1 def with_modal_action(**kwargs, &block)
  22. modal = LooposUi::Modal.new(**kwargs)
  23. # Curious what this does :3 ?
  24. modal.define_singleton_method(:with_trigger_button) do |**kwargs, &block|
  25. super(**FloatBar.with_default_args(**kwargs), &block)
  26. end
  27. modal.define_singleton_method(:with_primary_action) do |**kwargs, &block|
  28. super(
  29. **deep_merge_args({ tag_options: FloatBar.action_link_options }, kwargs),
  30. &block
  31. )
  32. end
  33. modal.define_singleton_method(:with_secondary_action) do |**kwargs, &block|
  34. super(
  35. **deep_merge_args({ tag_options: FloatBar.action_link_options }, kwargs),
  36. &block
  37. )
  38. end
  39. with_action_manual do
  40. render(modal, &block)
  41. end
  42. end
  43. 1 def with_popover_action(**kwargs, &block)
  44. popover = LooposUi::Popover.new(**kwargs)
  45. popover.define_singleton_method(:with_toggle_button) do |**kwargs, &block|
  46. super(**FloatBar.with_default_args(**kwargs), &block)
  47. end
  48. with_action_manual do
  49. render(popover, &block)
  50. end
  51. end
  52. 1 def render?
  53. buttons.any?
  54. end
  55. 1 def before_render
  56. check_button_order
  57. end
  58. 1 class << self
  59. 1 def with_default_args(**args)
  60. {
  61. size: :tiny,
  62. type: :tertiary,
  63. kind: :neutral,
  64. tag_options: action_link_options,
  65. }.deep_merge(args)
  66. end
  67. 1 def action_link_options
  68. {
  69. data: { "float-bar-target": "actionLink" },
  70. }
  71. end
  72. end
  73. 1 private
  74. 1 delegate :with_default_args, to: :class
  75. # detect if buttons kind is always in the following order:
  76. # :app, :neutral
  77. # TODO: apply this to ActionButtons list
  78. 1 def check_button_order
  79. order = [:app, :neutral]
  80. index = 0
  81. then: 0 else: 0 button_kinds = buttons.map { |b| b.respond_to?(:kind) ? b.kind : :manual }
  82. button_index = 0
  83. loop do
  84. then: 0 else: 0 break if index >= order.size || button_index >= button_kinds.size
  85. kind = button_kinds[button_index]
  86. # Manual buttons are ignored and can be placed anywhere
  87. then: 0 else: 0 if kind == :manual || kind == order[index]
  88. button_index += 1
  89. next
  90. end
  91. index += 1
  92. end
  93. then: 0 else: 0 if button_index != button_kinds.size
  94. raise <<~ERROR
  95. FloatBar buttons are not in the expected order according to the design rules for this component.
  96. Invalid element at index #{button_index}, valid values: #{order[order.index(button_kinds[button_index-1])..]}.:
  97. Expected:
  98. #{order}
  99. Got (without kind: :manual, which is always valid):
  100. #{button_kinds.reject { |k| k == :manual }}
  101. Real (including kind: :manual):
  102. #{button_kinds}
  103. Check the kind of the buttons in the FloatBar component.
  104. ERROR
  105. end
  106. end
  107. end
  108. end

app/components/loopos_ui/float_bar/float_bar.html.erb

0.0% lines covered

100.0% branches covered

13 relevant lines. 0 lines covered and 13 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%# Turbo prefetch is disabled because the apps have actions that make GET requests, which is not the way %>
  2. <%= tag.div(
  3. class: "lui-float_bar",
  4. data: {
  5. controller: "float-bar",
  6. turbo_prefetch: false,
  7. float_bar_select_all_text_value: t('.select_all'),
  8. float_bar_deselect_all_text_value: t('.deselect_all'),
  9. float_bar_ids_input_name_value: input_name.presence,
  10. }.compact,
  11. ) do %>
  12. <span class="lui-float_bar__count">
  13. <%= tag.span(count, data: { float_bar_target: "counter" }) %>
  14. <%= tag.span(t('.selected')) %>
  15. </span>
  16. <span>
  17. <%= render LooposUi::ActionButtons.new do |ab| %>
  18. <% ab.with_button_group do |bg| %>
  19. <% bg.with_button(**with_default_args(text: t('.select_all'), size: :tiny, tag_options: {
  20. data: { action: "click->float-bar#selectAll", "float-bar-target": "actionLink selectAllButton" }
  21. })) %>
  22. <% end %>
  23. <% ab.with_button_group do |bg| %>
  24. <% buttons.each do |button| %>
  25. <% bg.with_button_manual do %>
  26. <span data-float-bar-target="action">
  27. <%= button %>
  28. </span>
  29. <% end %>
  30. <% end %>
  31. <% end %>
  32. <% end %>
  33. </span>
  34. <% end %>

app/components/loopos_ui/flows/drawer_bar.rb

57.89% lines covered

0.0% branches covered

38 relevant lines. 22 lines covered and 16 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Flows
  3. 1 class DrawerBar < LoopComponent
  4. 1 include Pagy::Backend
  5. 1 option :block
  6. 1 option :dropkiq_scope
  7. 1 option :description
  8. 1 option :view_items, default: -> { true }
  9. 1 option :view_app_settings, default: -> { false }
  10. 1 option :view_block_settings, default: -> { false }
  11. 1 option :app_url, default: -> { nil }
  12. 1 private
  13. 1 def app_block?
  14. @app_block ||= [
  15. "core",
  16. "submission",
  17. "hubs",
  18. "handling",
  19. "validation",
  20. ].include?(block.block_type)
  21. end
  22. 1 def ai_decision_block?
  23. block.type == "Flows::Blocks::V2::Decision::Ai"
  24. end
  25. 1 def exits_block?
  26. block.type == "Flows::Blocks::V1::Shopify" || block.type == "Flows::Blocks::V1::Mirakl"
  27. end
  28. 1 def header_image_or_icon_args
  29. then: 0 if app_block? || ai_decision_block? || exits_block?
  30. {
  31. svg: helpers.app_svg(app: block.block_type, width: 20),
  32. }
  33. else: 0 else
  34. {
  35. icon: block.kind_class.gsub("fa-solid", "fa-regular"),
  36. }
  37. end
  38. end
  39. 1 def app_instance_button
  40. LooposUi::Button.new(
  41. icon: "arrow-up-right-from-square",
  42. type: :tertiary,
  43. kind: :neutral,
  44. size: :tiny,
  45. href: app_url,
  46. tag_options: { target: "_blank" },
  47. )
  48. end
  49. 1 def states
  50. ::Item.unscoped { block.items.select(:current_flow_token).distinct }.pluck(:current_flow_token)
  51. end
  52. 1 def table_args(state)
  53. {
  54. id: "table-#{dom_id(block)}_#{state}",
  55. fetch_url: helpers.main_app.admin_v2_flows_block_url(block, state: state),
  56. columns: columns_for_state(state),
  57. show_pagination_size_changer: false,
  58. show_header: false,
  59. }
  60. end
  61. 1 def items_and_pagy_for_state(state)
  62. items = block.items.includes(:logs).where(current_flow_token: state)
  63. then: 0 else: 0 items = items.search_by_full_id(params[:q][:search]) if params.dig(:q, :search).present?
  64. _pagy, items = pagy(items, items: 5)
  65. [items, _pagy]
  66. rescue Pagy::OverflowError => _e
  67. # Assume this is not the wanted table. We cant know for now, because the table
  68. # doesn't send _which_ table made the request
  69. [[], nil]
  70. end
  71. 1 def search_request?
  72. params.dig(:q, :search).present? ||
  73. params.dig(:page).present? ||
  74. params.dig(:per_page).present?
  75. end
  76. 1 def columns_for_state(state)
  77. [
  78. {
  79. title: state.to_s.humanize,
  80. dataIndex: :item_slug,
  81. key: :item_slug,
  82. },
  83. {
  84. title: "Updated at",
  85. dataIndex: :updated_at,
  86. key: :updated_at,
  87. },
  88. ]
  89. end
  90. end
  91. end
  92. end

app/components/loopos_ui/flows/drawer_bar/drawer_bar.html.erb

0.0% lines covered

0.0% branches covered

29 relevant lines. 0 lines covered and 29 lines missed.
10 total branches, 0 branches covered and 10 branches missed.
    
  1. <%= render LooposUi::DrawerBar.new do |drawer| %>
  2. <% drawer.with_header do %>
  3. <%= render LooposUi::DrawerBar::Header.new do |header|%>
  4. <% header.with_corner_actions do %>
  5. <%= render "shared/translations_for_model", model: block %>
  6. <% end %>
  7. <% entity_title = capture do %>
  8. <div class="flex items-center gap-2">
  9. <span><%= block.translated_block_type %></span>
  10. then: 0 else: 0 <% if app_block? && app_url.present? %>
  11. <%= render app_instance_button %>
  12. <% end %>
  13. </div>
  14. <% end %>
  15. <%= render LooposUi::DrawerBar::Entity.new(
  16. title: entity_title,
  17. description: block.translated_category,
  18. **header_image_or_icon_args
  19. )%>
  20. <% end %>
  21. <% end %>
  22. <% drawer.with_header do %>
  23. <% title = capture do %>
  24. <div class="-ml-2 w-full">
  25. <%= tag.turbo_frame id: "#{dom_id(block)}_drawer_name_form" do %>
  26. <%= form_with(id: "#{dom_id(block)}_name_form", model: [:admin_v2, block.becomes(::Flows::Block)], class: "w-full") do |form| %>
  27. <%= render LooposUi::Inputs::Text.new(
  28. name: "block[name]",
  29. value: block.name,
  30. )%>
  31. <% end %>
  32. <% end %>
  33. </div>
  34. <% end %>
  35. <div class="px-4 pb-4">
  36. <%= render LooposUi::TitleDescription.new(
  37. title: title,
  38. description: description
  39. ) %>
  40. </div>
  41. <% end %>
  42. <%= render LooposUi::TabsLayout.new(keep_tab_in_url: false, active_tab_key: "block_settings") do |layout| %>
  43. then: 0 else: 0 <% layout.with_tab(
  44. title: t(".items"),
  45. key: "items",
  46. url: helpers.main_app.admin_v2_flows_block_path(block, tab: "items"),
  47. lazy_load: true,
  48. ) if view_items %>
  49. then: 0 else: 0 <% layout.with_tab(
  50. title: t(".block_settings"),
  51. key: "block_settings",
  52. url: helpers.main_app.admin_v2_flows_block_path(block, tab: "block_settings"),
  53. lazy_load: true,
  54. ) if view_block_settings %>
  55. then: 0 else: 0 <% layout.with_tab(
  56. title: "Test",
  57. key: "test",
  58. url: helpers.main_app.admin_v2_flows_block_path(block, tab: "test"),
  59. lazy_load: true,
  60. load_only_on_click: true,
  61. ) if ai_decision_block? %>
  62. then: 0 else: 0 <% layout.with_tab(
  63. title: t(".app_settings"),
  64. key: "app_settings",
  65. url: helpers.main_app.admin_v2_flows_block_path(block, tab: "app_settings"),
  66. lazy_load: true,
  67. load_only_on_click: true,
  68. ) if app_block? && view_app_settings %>
  69. <% end %>
  70. <% end %>

app/components/loopos_ui/form_entry.rb

73.08% lines covered

0.0% branches covered

26 relevant lines. 19 lines covered and 7 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 class FormEntry < LoopComponent
  3. 1 renders_one :input
  4. 1 renders_one :icon_tooltip
  5. 1 option :label
  6. 1 option :required, default: -> { false }
  7. 1 option :orientation,
  8. Types::Coercible::Symbol.enum(
  9. :vertical, :horizontal, :horizontal_reverse
  10. ),
  11. default: -> { :vertical }
  12. 1 option :tooltip, optional: true
  13. 1 option :tooltip_icon, optional: true
  14. 1 option :tooltip_link_href, optional: true
  15. 1 option :tooltip_link_name, optional: true
  16. 1 option :label_width, optional: true
  17. 1 option :error, optional: true
  18. 1 def show_label?
  19. label.to_s.strip.present?
  20. end
  21. 1 def show_tooltip?
  22. tooltip.present? || tooltip_link_href.present?
  23. end
  24. 1 def resolved_tooltip_icon
  25. tooltip_icon.presence || "fa-regular fa-circle-info"
  26. end
  27. 1 def resolved_tooltip_link_name
  28. tooltip_link_name.presence || tooltip_link_href.to_s
  29. end
  30. 1 def horizontal_orientation?
  31. orientation == :horizontal || orientation == :horizontal_reverse
  32. end
  33. 1 def label_wrapper_class
  34. else: 0 then: 0 return "pl-2" unless horizontal_orientation?
  35. nil
  36. end
  37. end
  38. end

app/components/loopos_ui/form_entry/form_entry.html.erb

0.0% lines covered

0.0% branches covered

20 relevant lines. 0 lines covered and 20 lines missed.
14 total branches, 0 branches covered and 14 branches missed.
    
  1. <div class="lui-form_entry lui-form_entry--<%= orientation %>">
  2. <div class="lui-form_entry__row">
  3. <div class="lui-form_entry__input">
  4. <%= input %>
  5. </div>
  6. then: 0 else: 0 <% if show_label? %>
  7. then: 0 else: 0 <div class="lui-form_entry__label-wrapper <%= label_wrapper_class %>" style="<%= label_width ? "width: #{label_width}px; min-width: #{label_width}px; word-break: break-word;" : "" %>">
  8. <span class="lui-form_entry__label"><%= label %></span>
  9. then: 0 else: 0 <span class="lui-form_entry__required"><%= required ? "*" : "" %></span>
  10. then: 0 else: 0 <% if show_tooltip? || icon_tooltip.present? %>
  11. <div class="lui-form_entry__icon">
  12. then: 0 <% if show_tooltip? %>
  13. <%= render LooposUi::IconTooltip.new(icon: resolved_tooltip_icon, size: "14", icon_color: "--text-content") do |icon_tooltip_component| %>
  14. <% icon_tooltip_component.with_custom_tooltip do %>
  15. <%= render LooposUi::Tooltip.new(
  16. title: tooltip,
  17. interactive: tooltip_link_href.present?,
  18. position: "top",
  19. ) do |tooltip_component| %>
  20. then: 0 else: 0 <% if tooltip_link_href.present? %>
  21. <% tooltip_component.with_link(name: resolved_tooltip_link_name, href: tooltip_link_href) %>
  22. <% end %>
  23. <% end %>
  24. <% end %>
  25. <% end %>
  26. else: 0 <% else %>
  27. <%= icon_tooltip %>
  28. <% end %>
  29. </div>
  30. <% end %>
  31. </div>
  32. <% end %>
  33. </div>
  34. then: 0 else: 0 <%= content_tag(:span, error, class: "lui-form_entry__error") if error.present? %>
  35. </div>

app/components/loopos_ui/form_entry_ai.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class FormEntryAi < LoopComponent
  3. 1 option :title, Types::Coercible::String, default: -> { "LoopOS AI" }
  4. 1 option :description, Types::Coercible::String, optional: true
  5. 1 option :placeholder, Types::Coercible::String, optional: true
  6. 1 option :spinner, Types::Bool, optional: true
  7. 1 option :max_rows, Types::Coercible::Integer, optional: true
  8. 1 option :disabled, Types::Bool, default: -> { false }
  9. 1 option :tooltip, Types::Coercible::String, optional: true
  10. end
  11. end

app/components/loopos_ui/form_entry_ai/form_entry_ai.html.erb

0.0% lines covered

0.0% branches covered

11 relevant lines. 0 lines covered and 11 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <div class="lui-ai-prompt">
  2. <div class="lui-ai-prompt__header">
  3. <div class="lui-ai-prompt__title-desc">
  4. <div class="lui-ai-prompt__title-row">
  5. <div class="lui-ai-prompt__icon-title-container">
  6. <div class="lui-ai-prompt__icon">
  7. <%= image_tag "app_logos/LoopOS_AI-Symbol.png", alt: "LoopOS AI", class: "w-full h-full object-contain" %>
  8. </div>
  9. <p class="lui-ai-prompt__title">
  10. <%= title %>
  11. </p>
  12. </div>
  13. <div class="lui-ai-prompt__state-label">
  14. <%= render LooposUi::Token.new(
  15. text: "BETA",
  16. ) %>
  17. </div>
  18. </div>
  19. then: 0 else: 0 <% if description %>
  20. <p class="lui-ai-prompt__description">
  21. <%= description %>
  22. </p>
  23. <% end %>
  24. </div>
  25. </div>
  26. <div class="lui-ai-prompt__input-row">
  27. <%= render LooposUi::Inputs::Ai.new(
  28. **{
  29. placeholder: placeholder,
  30. spinner: spinner,
  31. max_rows: max_rows,
  32. disabled: disabled,
  33. tooltip: tooltip
  34. }.compact
  35. ) %>
  36. </div>
  37. </div>

app/components/loopos_ui/grid_layout.rb

77.78% lines covered

100.0% branches covered

9 relevant lines. 7 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class GridLayout < LoopComponent
  3. 1 renders_many :sections, ->(**_kwargs) do
  4. LooposUi::GridLayout::Section.new
  5. end
  6. 1 option :cols, Types::Integer, default: -> { 2 }
  7. 1 option :gap, Types::Integer, default: -> { 0 }
  8. # # Responsive options
  9. # option :responsive_cols, Types::Hash, default: -> { { sm: 2, md: 3, lg: 4, xl: 5, xxl: 6 } }
  10. 1 def grid_style
  11. <<~CSS
  12. --grid-cols: #{cols};
  13. --grid-gap: #{gap}px;
  14. CSS
  15. # --grid-cols-sm: #{responsive_cols[:sm]};
  16. # --grid-cols-md: #{responsive_cols[:md]};
  17. # --grid-cols-lg: #{responsive_cols[:lg]};
  18. # --grid-cols-xl: #{responsive_cols[:xl]};
  19. # --grid-cols-xxl: #{responsive_cols[:xxl]};
  20. end
  21. 1 class Section < LoopComponent
  22. end
  23. end
  24. end

app/components/loopos_ui/grid_layout/grid_layout.html.erb

0.0% lines covered

100.0% branches covered

4 relevant lines. 0 lines covered and 4 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <div class="lui-grid_layout" style="<%= grid_style %>">
  2. <% sections.each do |section| %>
  3. <%= section %>
  4. <% end %>
  5. </div>

app/components/loopos_ui/grid_layout/section.html.erb

0.0% lines covered

100.0% branches covered

2 relevant lines. 0 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div(class: classes) do %>
  2. <%= content %>
  3. <% end %>

app/components/loopos_ui/group_avatar.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class GroupAvatar < LoopComponent
  3. 1 option :main_avatar, Types::String | Types::Symbol.enum(:default)
  4. 1 option :secondary_avatar, Types::String | Types::Symbol.enum(:default)
  5. end
  6. end

app/components/loopos_ui/group_avatar/group_avatar.html.erb

0.0% lines covered

100.0% branches covered

4 relevant lines. 0 lines covered and 4 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div(class: "lui-group_avatar") do %>
  2. <%= render LooposUi::Avatar.new(image_url: main_avatar, size: :xl, type: :square) %>
  3. <%= tag.div(class: "lui-group_avatar__secondary") do %>
  4. <%= render LooposUi::Avatar.new(image_url: secondary_avatar, size: :medium, type: :circle) %>
  5. <% end %>
  6. <% end %>

app/components/loopos_ui/header.rb

83.33% lines covered

0.0% branches covered

30 relevant lines. 25 lines covered and 5 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. 1 module LooposUi
  2. 1 class Header < LoopComponent
  3. 1 option :icon, optional: true
  4. 1 option :icon_args, Types::Hash, default: -> { {} }, optional: true
  5. 1 option :icon_kind, Types::Coercible::Symbol.enum(:informative, :success, :warning, :error), default: -> { :informative }
  6. 1 option :title, optional: true
  7. 1 option :description, optional: true
  8. 1 option :description_icon, optional: true
  9. 1 option :size, Types::Coercible::Symbol.enum(:page, :tiny, :small, :medium, :large), default: -> { :page }
  10. 1 mod :size
  11. 1 option :tooltip, optional: true
  12. 1 option :count, optional: true
  13. 1 option :app_instance, optional: true
  14. 1 option :gapless, Types::Bool, default: -> { false }
  15. 1 mod :gapless
  16. 1 renders_one :custom_description
  17. 1 renders_one :info
  18. 1 renders_one :token_zone
  19. 1 renders_one :title_token_zone
  20. 1 renders_one :input_title
  21. 1 renders_one :semantic_icon, ->(*args, **kwargs) {
  22. LooposUi::MIcon.new(*args, **kwargs, size: icon_size)
  23. }
  24. 1 def semantic_icon_semantic
  25. else: 0 then: 0 return unless semantic_icon?
  26. semantic_icon.semantic
  27. end
  28. 1 private
  29. 1 def icon_size
  30. then: 0 else: 0 [:small, :tiny].include?(size) ? 14 : 16
  31. end
  32. 1 def icon_class
  33. "lui-header__icon lui-header__icon__#{semantic_icon_semantic} lui-header__icon--#{size}"
  34. end
  35. end
  36. end

app/components/loopos_ui/header/header.html.erb

0.0% lines covered

0.0% branches covered

30 relevant lines. 0 lines covered and 30 lines missed.
22 total branches, 0 branches covered and 22 branches missed.
    
  1. <%= tag.div class: classes do %>
  2. then: 0 else: 0 <%= tag.div class: "lui-header__info" do %>
  3. <%= info %>
  4. <% end if info? %>
  5. <%= tag.div class: "lui-header__title_container" do %>
  6. then: 0 else: 0 <%= render LooposUi::AppLogo.new(app: app_instance) if app_instance.present? %>
  7. then: 0 else: 0 <%= render LooposUi::MIcon.new(icon, size: icon_size, kind: icon_kind, tag: :a, href: "https://www.google.com") if icon.present? %>
  8. then: 0 else: 0 <%= tag.div class: icon_class do %>
  9. <%= semantic_icon %>
  10. <% end if semantic_icon? %>
  11. <span class="lui-header__title_container__title">
  12. then: 0 <% if input_title? %>
  13. <%= input_title %>
  14. else: 0 <% else %>
  15. <%= title %>
  16. <% end %>
  17. </span>
  18. then: 0 else: 0 <%= tag.span class: "flex w-fit" do %>
  19. <%= render LooposUi::Tooltip.new(title: tooltip) %>
  20. <%= render LooposUi::MIcon.new(:info) %>
  21. <% end if tooltip.present? %>
  22. then: 0 else: 0 <%= render LooposUi::Counter.new(count: count, kind: :neutral, size: :small) if count.present? %>
  23. then: 0 else: 0 <%= tag.div class: "lui-header__title_container__token_zone" do %>
  24. <%= title_token_zone %>
  25. <% end if title_token_zone? %>
  26. <% end %>
  27. then: 0 else: 0 <%= tag.div class: "lui-header__token_zone" do %>
  28. <%= token_zone %>
  29. <% end if token_zone? %>
  30. then: 0 else: 0 <% if description.present? %>
  31. <span class="lui-header__description">
  32. <%= description %>
  33. then: 0 else: 0 <%= render LooposUi::MIcon.new(description_icon) if description_icon.present? %>
  34. </span>
  35. <% end %>
  36. <% end %>

app/components/loopos_ui/header_component.rb

39.58% lines covered

0.0% branches covered

48 relevant lines. 19 lines covered and 29 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class HeaderComponent < ViewComponent::Base
  4. 1 attr_reader :app_instance,
  5. :current_user,
  6. :layout,
  7. :environment_name,
  8. :additional_list_items,
  9. :partnable_grouped_applications,
  10. :selected_partnable,
  11. :the_loop_main_logo_file,
  12. :loopos_manager_avatar_file,
  13. :current_manager_version,
  14. :me_admin_user_url
  15. 1 def initialize
  16. @app_instance = OpenStruct.new
  17. @current_user = OpenStruct.new(avatar_url: "loopos-icon.png")
  18. @layout = OpenStruct.new
  19. @environment_name = OpenStruct.new
  20. @additional_list_items = []
  21. @partnable_grouped_applications = []
  22. @selected_partnable = FakePartnable.new
  23. @the_loop_main_logo_file = "loopos-icon.png"
  24. @loopos_manager_avatar_file = "loopos-icon.png"
  25. @current_manager_version = "0.0.0"
  26. @me_admin_user_url = "https://loopos.com"
  27. end
  28. 1 def before_render
  29. then: 0 else: 0 @app_kind = if params[:app].present?
  30. @app_kind = if [
  31. "core",
  32. "validation",
  33. "handling",
  34. "hubs",
  35. "submission",
  36. then: 0 ].include?(params[:app])
  37. params[:app]
  38. else: 0 else
  39. then: 0 else: 0 app_instance&.kind
  40. end
  41. end || "default"
  42. end
  43. 1 def app_image_file
  44. # @app_kind.present? ? "loopos_ui/#{@app_kind}_logo.png" : "loopos-icon.png"
  45. "loopos-icon.png"
  46. end
  47. 1 def loopos_logo_file
  48. "loopos-icon.png"
  49. end
  50. 1 def app_logo_url
  51. # app_instance&.app&.logo&.url || "loopos-logo.svg"
  52. "loopos-icon.png"
  53. end
  54. 1 class PartnableApplicationsComponent < ViewComponent::Base
  55. 1 with_collection_parameter :partnable_applications
  56. 1 attr_reader :partnable, :applications
  57. 1 def initialize(partnable_applications:)
  58. @partnable, @applications = partnable_applications
  59. end
  60. end
  61. 1 class UserMenuComponent < ViewComponent::Base
  62. 1 attr_reader :current_user,
  63. :current_manager_version,
  64. :layout,
  65. :additional_list_items,
  66. :app_instance,
  67. :me_admin_user_url,
  68. :powered_by_logo
  69. 1 def initialize(current_user:, current_manager_version:, layout:, additional_list_items:, app_instance:,
  70. me_admin_user_url:)
  71. @current_user = current_user
  72. @current_manager_version = current_manager_version
  73. @layout = layout
  74. @additional_list_items = additional_list_items
  75. @app_instance = app_instance
  76. @me_admin_user_url = me_admin_user_url
  77. @powered_by_logo = "loopos-icon.png"
  78. end
  79. end
  80. end
  81. 1 class FakePartnable
  82. 1 def id
  83. 1
  84. end
  85. 1 def name
  86. "Fake Partnable"
  87. end
  88. 1 def icon_url
  89. "loopos-icon.png"
  90. end
  91. end
  92. end

app/components/loopos_ui/header_component/header_component.html.erb

0.0% lines covered

0.0% branches covered

62 relevant lines. 0 lines covered and 62 lines missed.
56 total branches, 0 branches covered and 56 branches missed.
    
  1. then: 0 else: 0 then: 0 else: 0 <% app_kind = ["core", "validation", "handling", "hubs", "submission"].include?(params[:app]) ? params[:app] : app_instance&.kind %>
  2. <% app_kind ||= "default" %>
  3. <div class="los-manager-header">
  4. <div class="los-manager-header__relative-wrapper" id="los-manager-relative-wrapper">
  5. <div class="los-manager-header__application-menu" id="los-manager-app-menu-open-button">
  6. <div class="los-manager-header__application-menu-wrapper">
  7. <%= image_tag(image_url('loopOSMenu.svg')) %>
  8. </div>
  9. </div>
  10. <!-- Application Menu -->
  11. <div class="los-manager-app-menu" id="los-manager-app-menu">
  12. <div class="los-manager-app-menu__header">
  13. <div class="los-manager-app-menu__application-menu-header" id="los-manager-app-menu-close-button">
  14. <div class="los-manager-header__application-menu-wrapper los-manager-header__application-menu-wrapper--active">
  15. <%= image_tag(image_url(app_image_file)) %>
  16. </div>
  17. </div>
  18. <div class="los-manager-app-menu__main-logo">
  19. <%= image_tag(image_url(loopos_logo_file), class: 'los-manager-app-menu__image') %>
  20. </div>
  21. </div>
  22. <!-- partners slider -->
  23. <div class="los-manager-app-menu_simple-tabs-carousel-container">
  24. then: 0 <% if partnable_grouped_applications.length > 1 %>
  25. <button class="los-manager-app-menu_tab-btn los-manager-app-menu_tab-previous"><i class="fa-regular fa-chevron-left"></i></button>
  26. else: 0 <% else %>
  27. <div class="los-manager-app-menu_tab-btn--empty"></div>
  28. <% end %>
  29. <div class="los-manager-app-menu_tabs-container">
  30. <li class="los-manager-app-menu_tab active">
  31. then: 0 else: 0 <div class="los-manager-avatar los-manager-avatar--large" data-partner-apps="partner_apps_<%= selected_partnable&.id%>">
  32. then: 0 else: 0 <%= image_tag(image_url(selected_partnable&.icon_url || "default-icon.png")) %>
  33. </div>
  34. then: 0 else: 0 <p><%= selected_partnable&.name %></p>
  35. </li>
  36. <div class="los-manager-app-menu_simple-tabs-wrapper">
  37. <ul class="los-manager-app-menu_simple-tabs">
  38. <% partnable_grouped_applications.each_with_index do |partnable_applications, index| %>
  39. then: 0 else: 0 <% if partnable_grouped_applications.length > 1 %>
  40. <% partnable, applications = partnable_applications %>
  41. then: 0 else: 0 <% if partnable != selected_partnable %>
  42. <li class="los-manager-app-menu_tab">
  43. then: 0 else: 0 <div class="los-manager-avatar los-manager-avatar--large " data-partner-apps="partner_apps_<%= partnable&.id%>">
  44. then: 0 else: 0 <%= image_tag(image_url(partnable&.icon_url || "default-icon.png")) %>
  45. </div>
  46. then: 0 else: 0 <p><%= partnable&.name %></p>
  47. </li>
  48. <% end %>
  49. <% end %>
  50. <% end %>
  51. </ul>
  52. </div>
  53. </div>
  54. then: 0 <% if partnable_grouped_applications.length > 1 %>
  55. <button class="los-manager-app-menu_tab-btn los-manager-app-menu_tab-next"><i class="fa-regular fa-chevron-right"></i></button>
  56. else: 0 <% else %>
  57. <div class="los-manager-app-menu_tab-btn--empty"></div>
  58. <% end %>
  59. </div>
  60. <div class="scrollable-content" style= "max-height: 350px; overflow-y: auto; overscroll-behavior-y: contain">
  61. <%= render PartnableApplicationsComponent.with_collection(partnable_grouped_applications, layout: layout) %>
  62. </div>
  63. <div class="los-manager-app-menu__footer los-manager-menu-footer">
  64. <div class="los-manager-menu-footer__main-logo">
  65. <%= image_tag(image_url(the_loop_main_logo_file), class: 'los-manager-menu-footer__image') %>
  66. </div>
  67. <div class="los-manager-menu-footer__app-version">
  68. <div class="los-manager-avatar los-manager-avatar--large">
  69. then: 0 else: 0 then: 0 else: 0 then: 0 <% if app_instance&.app&.icon.present? %>
  70. <%= image_tag(image_url(app_instance.app.icon.url)) %>
  71. else: 0 <% else %>
  72. then: 0 <% if app_instance.nil?%> <%# Manager rendering this for itself %>
  73. <%= image_tag(image_url("loopos-icon.png")) %>
  74. <% else %>
  75. else: 0 <%= image_tag(image_url(loopos_manager_avatar_file)) %>
  76. <% end %>
  77. <% end %>
  78. </div>
  79. <% if app_instance.present? %>
  80. then: 0 <div class="los-manager-menu-content__info">
  81. <p class="los-manager-menu-footer__id">id_<%= app_instance.id %></p>
  82. <span class="los-manager-menu-footer__version">v_<%= app_instance.current_deployed_version %></span>
  83. </div>
  84. <% else %>
  85. else: 0 <div class="los-manager-menu-content__info">
  86. <span class="los-manager-menu-footer__version">v_<%= current_manager_version %></span>
  87. </div>
  88. <% end %>
  89. </div>
  90. </div>
  91. </div>
  92. </div>
  93. <div class="los-manager-header__topbar los-manager-topbar">
  94. <div class="los-manager-topbar__brand los-manager-brand">
  95. <% if layout == "zeroo" %>
  96. then: 0 <div class="los-manager-topbar__brand-wrapper">
  97. <%= image_tag(image_url('logo-zero.svg'), class: 'los-manager-brand__image') %>
  98. <div class="los-manager-topbar__brand-spliter"></div>
  99. <%= image_tag(image_url(partner.icon_url), class: 'los-manager-brand__image' ) %>
  100. </div>
  101. <% else %>
  102. else: 0 <%= link_to app_instance&.app_url do %>
  103. then: 0 else: 0 <%= image_tag(image_url(app_logo_url), class:"los-manager-brand__image") %>
  104. <% end %>
  105. <div class="los-manager-brand__info">
  106. <div class="los-manager-topbar-app-instance-wrapper">
  107. <% if app_instance&.partner.present? %>
  108. then: 0 else: 0 then: 0 else: 0 <div class="los-manager-app-instance">
  109. <%= app_instance&.partner.name %>
  110. then: 0 else: 0 </div>
  111. <% end %>
  112. <% if app_instance&.name.present? %>
  113. then: 0 else: 0 then: 0 else: 0 <div class="los-manager-app-instance">
  114. <%= app_instance&.name %>
  115. then: 0 else: 0 </div>
  116. <% end %>
  117. <% if environment_name&.present? %>
  118. then: 0 else: 0 then: 0 else: 0 <div class="los-manager-app-instance">
  119. <%= environment_name %>
  120. </div>
  121. <% end %>
  122. </div>
  123. </div>
  124. <% end %>
  125. </div>
  126. <%= render UserMenuComponent.new(
  127. current_user: current_user,
  128. app_instance: app_instance,
  129. current_manager_version: current_manager_version,
  130. layout: layout,
  131. additional_list_items: additional_list_items,
  132. me_admin_user_url: me_admin_user_url,
  133. ) %>
  134. </div>
  135. </div>
  136. <%= render "loopos_ui/header_component/header_styles", app: app_instance&.app %>

app/components/loopos_ui/header_component/partnable_applications_component.html.erb

0.0% lines covered

0.0% branches covered

2 relevant lines. 0 lines covered and 2 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. then: 0 else: 0 <div class="los-manager-app-menu__content los-manager-menu-content los-manager-content-applications--hidden" id="partner_apps_<%= partnable&.id%>">
  2. <div class="los-manager-menu-content__applications los-manager-content-applications">
  3. <%= render partial: "api/v1/general/application_button", locals: { layout: layout }, collection: applications, as: :application %>
  4. </div>
  5. </div>

app/components/loopos_ui/header_component/user_menu_component.html.erb

0.0% lines covered

0.0% branches covered

38 relevant lines. 0 lines covered and 38 lines missed.
26 total branches, 0 branches covered and 26 branches missed.
    
  1. <div class="los-manager-topbar__user-wrapper">
  2. <div class="los-manager-topbar__user los-manager-user" id="los-user-open-menu">
  3. <div class="los-manager-avatar los-manager-avatar--normal">
  4. <%= image_tag(image_url(current_user.avatar_url), class: 'los-manager-user__avatar-image' ) %>
  5. </div>
  6. <div class="los-manager-user__info">
  7. <p class="los-manager-user__name"><%= current_user.full_name %></p>
  8. then: 0 else: 0 <% if current_user.partner.present? %>
  9. <div class="los-manager-user__partner">
  10. <div class="los-manager-user__partner-image-wrapper">
  11. then: 0 else: 0 <%= image_tag(image_url(current_user.partner&.icon_url || "default-icon.png") , class: 'los-manager-user__partner-image-avatar' ) %>
  12. </div>
  13. then: 0 else: 0 <span class="los-manager-user__partner-name"><%= current_user.partner&.name %></span>
  14. </div>
  15. <% end %>
  16. </div>
  17. </div>
  18. <!-- Start of user Menu -->
  19. <div class="los-manager-user-menu-wrapper">
  20. <div class="mt-4"></div>
  21. <div class="los-manager-user__menu los-manager-user-menu" id="los-manager-user-menu">
  22. <div class="los-manager-user-menu__header">
  23. <div class="los-manager-avatar los-manager-avatar--xxl">
  24. <%= image_tag(image_url(current_user.avatar_url)) %>
  25. </div>
  26. <div class="los-manager-user__header-info">
  27. <p class="los-manager-user__header-name"> <%= I18n.t("header.user.menu.hello")%>, <%= current_user.full_name %></p>
  28. <p class="los-manager-user__header-email"><%= current_user.login %></p>
  29. then: 0 else: 0 <% if current_user.partner.present? %>
  30. <div class="los-manager-user__partner">
  31. <div class="los-manager-avatar los-manager-avatar--small">
  32. then: 0 else: 0 <%= image_tag(image_url(current_user.partner&.icon_url || "default-icon.png") , class: 'los-manager-user__avatar-image' ) %>
  33. </div>
  34. then: 0 else: 0 <p class="los-manager-user__partner-name"><%= current_user.partner&.name %></p>
  35. </div>
  36. <% end %>
  37. </div>
  38. </div>
  39. <div class="los-manager-user-menu__content">
  40. <div class="los-manager-user-menu__navigation-list los-manager-navigation-list">
  41. <h4 class="los-manager-navigation-list__title"> <%= I18n.t("header.user.menu.account")%> </h4>
  42. <ul class="los-manager-navigation-list__list">
  43. <li style="width:100%">
  44. <%# Submission has it's own profile page %>
  45. then: 0 else: 0 then: 0 else: 0 <% if app_instance&.kind != "submission" %>
  46. <%= link_to I18n.t("header.user.menu.profile"),
  47. me_admin_user_url,
  48. class: "los-manager-navigation-list__link los-manager-navigation-list__link"
  49. %>
  50. <% end %>
  51. </li>
  52. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 <% if current_user&.partner&.id.present? && current_user&.can_view_partner? %>
  53. <li style="width:100%"><a href="<%= admin_partner_me_url %>" target="_blank" class="los-manager-navigation-list__link los-manager-navigation-list__link"> <%= I18n.t("header.user.menu.partner_profile") %> </a></li>
  54. <% end %>
  55. <% additional_list_items.each do |item| %>
  56. <% label, link = item.values_at(:label, :link) %>
  57. <li style="width:100%">
  58. <a href="<%= link %>" target="_blank" class="los-manager-navigation-list__link los-manager-navigation-list__link"> <%= label %> </a>
  59. </li>
  60. <% end %>
  61. <!--<li style="width:100%">
  62. <a class="los-manager-navigation-list__link los-manager-navigation-list__link--disabled">
  63. <%#= I18n.t("header.user.menu.preferences") %>
  64. </a>
  65. </li> -->
  66. </ul>
  67. </div>
  68. <div class="los-manager-user-menu__navigation-list los-manager-navigation-list">
  69. <h4 class="los-manager-navigation-list__title"> <%= I18n.t("header.user.menu.about_us")%> </h4>
  70. <ul class="los-manager-navigation-list__list">
  71. <li style="width:100%">
  72. then: 0 <% if layout == "zeroo" %>
  73. <a href="" target="_blank" class="los-manager-navigation-list__link los-manager-navigation-list__link">Zeroo</a>
  74. else: 0 <% else %>
  75. <a href="https://loop-os.pt/" target="_blank" class="los-manager-navigation-list__link los-manager-navigation-list__link">LoopOS</a>
  76. <% end %>
  77. </li>
  78. <li style="width:100%"><a href="https://www.theloop.pt/" target="_blank" class="los-manager-navigation-list__link los-manager-navigation-list__link">The Loop Co.</a></li>
  79. </ul>
  80. </div>
  81. </div>
  82. <div class="los-manager-user-menu__footer los-manager-user-footer">
  83. <div class="los-manager-user-footer__los-brand">
  84. <div class="los-manager-user-footer__powered"><span> <%= I18n.t("header.user.menu.powered_by") %> </span> <%= image_tag(image_url(powered_by_logo)) %> </div>
  85. <div class="los-manager-user-footer__version"> <%= I18n.t("header.user.menu.version") %> <%= current_manager_version %> </div>
  86. </div>
  87. <div class="los-manager-user-footer__logout">
  88. <div class="los-manager-button los-manager-button--danger" id="logout-button"><i class="fa-regular fa-arrow-right-from-bracket"></i> <%= I18n.t("header.logout") %> </div>
  89. </div>
  90. </div>
  91. </div>
  92. </div>
  93. </div>

app/components/loopos_ui/history_card.rb

100.0% lines covered

100.0% branches covered

3 relevant lines. 3 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class HistoryCard < LoopComponent
  3. 1 option :proposals_data
  4. end
  5. end

app/components/loopos_ui/history_card/history_card.html.erb

0.0% lines covered

100.0% branches covered

2 relevant lines. 0 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= react_component(
  2. "HistoryCard", { proposalsData: proposals_data, locale: I18n.locale }
  3. )%>

app/components/loopos_ui/human_view.rb

55.56% lines covered

0.0% branches covered

27 relevant lines. 15 lines covered and 12 lines missed.
11 total branches, 0 branches covered and 11 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "json"
  3. 1 module LooposUi
  4. 1 class HumanView < LoopComponent
  5. 1 ICON_BEHAVIORS = ["rotate", "swap"].freeze
  6. 1 option :title, type: Types::String.optional, optional: true
  7. 1 option :data, type: Types::Hash | Types::Array | Types::String
  8. 1 option :tree_icon_behavior, type: Types::String, default: -> { "rotate" }
  9. 1 option :tree_icon_closed, type: Types::String, default: -> { "fa-chevron-right" }
  10. 1 option :tree_icon_open, type: Types::String, default: -> { "fa-chevron-down" }
  11. 1 def parsed_data
  12. @parsed_data ||= parse_data(data)
  13. end
  14. 1 def tree_children(value = parsed_data)
  15. case value
  16. when: 0 when Hash
  17. value
  18. when: 0 when Array
  19. value.each_with_index.map { |child, index| ["[#{index}]", child] }
  20. else: 0 else
  21. []
  22. end
  23. end
  24. 1 def formatted_value(value)
  25. then: 0 else: 0 return "null" if value.nil?
  26. value.to_s
  27. end
  28. 1 def resolved_tree_icon_behavior
  29. then: 0 else: 0 ICON_BEHAVIORS.include?(tree_icon_behavior) ? tree_icon_behavior : "rotate"
  30. end
  31. 1 private
  32. 1 def parse_data(value)
  33. then: 0 else: 0 return value if value.is_a?(Hash) || value.is_a?(Array)
  34. then: 0 else: 0 return {} if value.blank?
  35. JSON.parse(value.to_s)
  36. rescue JSON::ParserError, TypeError
  37. value
  38. end
  39. end
  40. end

app/components/loopos_ui/human_view/human_view.html.erb

0.0% lines covered

0.0% branches covered

12 relevant lines. 0 lines covered and 12 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. <%= tag.div(
  2. class: "lui-human_view",
  3. data: {
  4. controller: "human-view",
  5. human_view_icon_behavior_value: resolved_tree_icon_behavior
  6. }
  7. ) do %>
  8. <div class="lui-human_view__content">
  9. then: 0 <% if parsed_data.blank? %>
  10. else: 0 <span class="lui-human_view__empty"><%= t("loopos_ui.human_view.empty") %></span>
  11. then: 0 <% elsif parsed_data.is_a?(Hash) || parsed_data.is_a?(Array) %>
  12. <% tree_children.each do |child_key, child_value| %>
  13. <%= render(LooposUi::HumanView::TreeNode.new(
  14. node_key: child_key,
  15. node_value: child_value,
  16. tree_icon_closed: tree_icon_closed,
  17. tree_icon_open: tree_icon_open
  18. )) %>
  19. <% end %>
  20. else: 0 <% else %>
  21. then: 0 else: 0 <% if title.present? %>
  22. <span class="lui-human_view__title"><%= title %>:</span>
  23. <% end %>
  24. <span class="lui-human_view__primitive_value"><%= formatted_value(parsed_data) %></span>
  25. <% end %>
  26. </div>
  27. <% end %>

app/components/loopos_ui/human_view/tree_node.rb

47.83% lines covered

0.0% branches covered

23 relevant lines. 11 lines covered and 12 lines missed.
9 total branches, 0 branches covered and 9 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class HumanView
  4. 1 class TreeNode < LoopComponent
  5. 1 option :node_key
  6. 1 option :node_value
  7. 1 option :tree_icon_closed, type: Types::String, default: -> { "fa-chevron-right" }
  8. 1 option :tree_icon_open, type: Types::String, default: -> { "fa-chevron-down" }
  9. 1 def node_key_label
  10. node_key.to_s
  11. end
  12. 1 def children
  13. case node_value
  14. when: 0 when Hash
  15. node_value.sort_by { |key, _| key.to_s }
  16. when: 0 when Array
  17. node_value.each_with_index.map { |child, index| [(index + 1).to_s, child] }
  18. else: 0 else
  19. []
  20. end
  21. end
  22. 1 def value_color_class
  23. when: 0 case node_value
  24. when: 0 when String then "lui-human_view-tree_node__value--string"
  25. when: 0 when Numeric then "lui-human_view-tree_node__value--number"
  26. when NilClass then "lui-human_view-tree_node__value--null"
  27. else: 0 else
  28. "lui-human_view-tree_node__value--default"
  29. end
  30. end
  31. 1 def formatted_value
  32. then: 0 else: 0 return "null" if node_value.nil?
  33. node_value.to_s
  34. end
  35. end
  36. end
  37. end

app/components/loopos_ui/human_view/tree_node/tree_node.html.erb

0.0% lines covered

0.0% branches covered

19 relevant lines. 0 lines covered and 19 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <div class="lui-human_view-tree_node">
  2. then: 0 <% if children.any? %>
  3. <% content_id = "extra-data-tree-#{SecureRandom.hex(6)}" %>
  4. <div class="lui-human_view-tree_node__branch">
  5. <button
  6. type="button"
  7. class="lui-human_view-tree_node__trigger"
  8. data-action="click->human-view#toggleTree"
  9. aria-expanded="false"
  10. aria-controls="<%= content_id %>">
  11. <i
  12. data-tree-icon
  13. data-tree-icon-closed="<%= tree_icon_closed %>"
  14. data-tree-icon-open="<%= tree_icon_open %>"
  15. class="fa-solid <%= tree_icon_closed %> lui-human_view-tree_node__icon"></i>
  16. <span class="lui-human_view-tree_node__key"><%= node_key_label %></span>
  17. </button>
  18. <div id="<%= content_id %>" data-tree-content data-state="closed" class="lui-human_view-tree_node__children" hidden>
  19. <% children.each do |child_key, child_value| %>
  20. <%= render(LooposUi::HumanView::TreeNode.new(
  21. node_key: child_key,
  22. node_value: child_value,
  23. tree_icon_closed: tree_icon_closed,
  24. tree_icon_open: tree_icon_open
  25. )) %>
  26. <% end %>
  27. </div>
  28. </div>
  29. else: 0 <% else %>
  30. <div class="lui-human_view-tree_node__leaf">
  31. <i class="fa-solid <%= tree_icon_closed %> lui-human_view-tree_node__icon lui-human_view-tree_node__icon--placeholder"></i>
  32. <span class="lui-human_view-tree_node__key"><%= node_key_label %>:</span>
  33. <span class="lui-human_view-tree_node__value <%= value_color_class %>"><%= formatted_value %></span>
  34. </div>
  35. <% end %>
  36. </div>

app/components/loopos_ui/icon.rb

75.0% lines covered

100.0% branches covered

12 relevant lines. 9 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class Icon < LoopComponent
  4. 1 include FaviconAware
  5. 1 option :icon, type: Types::Coercible::String
  6. 1 option :count, optional: true
  7. 1 option :size, default: -> { "16" }, type: Types::Coercible::Integer
  8. 1 option :color, default: -> { :black }, type: Types::Coercible::String
  9. 1 def initialize(...)
  10. super
  11. @icon = faviconize(icon)
  12. end
  13. 1 def styles
  14. <<~CSS.squish
  15. font-size: #{size}px;
  16. color: #{color};
  17. CSS
  18. end
  19. end
  20. end

app/components/loopos_ui/icon/icon.html.erb

0.0% lines covered

0.0% branches covered

3 relevant lines. 0 lines covered and 3 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <%= tag.div(class: "lui-icon h-[#{size}px] w-[#{size}px]") do %>
  2. then: 0 else: 0 <%= tag.i(class: "#{icon} lui-icon__icon" , style: styles) if icon.present? %>
  3. then: 0 else: 0 <%= render LooposUi::Counter.new(count: count, kind: :neutral) if count%>
  4. <% end %>

app/components/loopos_ui/icon_tooltip.rb

40.0% lines covered

0.0% branches covered

10 relevant lines. 4 lines covered and 6 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 class IconTooltip < LoopComponent
  3. 1 renders_one :custom_tooltip
  4. 1 def initialize(app: nil, icon: nil, count: nil, size: nil, text: nil, icon_color: nil)
  5. @app = app
  6. @icon = icon
  7. @count = count
  8. @text = text
  9. then: 0 else: 0 @size = size.presence || (@app.present? ? "small" : "16")
  10. @icon_color = icon_color
  11. end
  12. end
  13. end

app/components/loopos_ui/icon_tooltip/icon_tooltip.html.erb

0.0% lines covered

0.0% branches covered

12 relevant lines. 0 lines covered and 12 lines missed.
8 total branches, 0 branches covered and 8 branches missed.
    
  1. then: 0 <% if @app.present? %>
  2. <span class="lui-icon_tooltip">
  3. then: 0 <% if custom_tooltip %>
  4. <%= custom_tooltip %>
  5. else: 0 <% else %>
  6. <%= render LooposUi::Tooltip.new(title: @text, position: :top) %>
  7. <% end %>
  8. <%= render LooposUi::Logo.new(app: @app, count: @count, size: @size, icon: true ) %>
  9. else: 0 </span>
  10. then: 0 else: 0 <% elsif @icon.present? %>
  11. <span class="lui-icon_tooltip">
  12. then: 0 <% if custom_tooltip %>
  13. <%= custom_tooltip %>
  14. else: 0 <% else %>
  15. <%= render LooposUi::Tooltip.new(title: @text, position: :top) %>
  16. <% end %>
  17. <%= render LooposUi::Icon.new(icon: @icon, count: @count, size: @size, color: @icon_color) %>
  18. </span>
  19. <% end %>

app/components/loopos_ui/identity_token.rb

43.59% lines covered

0.0% branches covered

39 relevant lines. 17 lines covered and 22 lines missed.
14 total branches, 0 branches covered and 14 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class IdentityToken < LooposUi::Token
  4. COLORS = {
  5. # text, bg
  6. 1 general: [find_color("general-gray-900"), find_color("general-gray-200")], # General/Gray/900, General/Gray/200
  7. }
  8. APP_COLORS = {
  9. # text, bg
  10. 1 manager: [find_color("apps-manager-800-primary"), find_color("apps-manager-300")], # Apps/Manager/800-Primary, Apps/Manager/300
  11. core: [find_color("apps-core-800-primary"), find_color("apps-core-300")], # Apps/Core/800-Primary, Apps/Core/300
  12. hubs: [find_color("apps-hubs-800-primary"), find_color("apps-hubs-300")], # Apps/Hubs/800-Primary, Apps/Hubs/300
  13. }
  14. 1 class << self
  15. 1 def for_brand(brand)
  16. new(
  17. text: brand.name,
  18. icon: "fa-regular fa-seal",
  19. color: :general,
  20. )
  21. end
  22. 1 def for_category(category)
  23. new(
  24. text: category.name,
  25. icon: "fa-regular fa-grid",
  26. color: :general,
  27. )
  28. end
  29. 1 def for_protocol(protocol)
  30. new(
  31. text: protocol.name,
  32. icon: "fa-regular fa-bars-staggered",
  33. color: :app,
  34. )
  35. end
  36. 1 def for_inherited_protocol(protocol)
  37. new(
  38. text: protocol.name,
  39. leading_icon: "fa-kit fa-regular-bars-staggered-tag",
  40. trailing_icon: "fa-regular fa-diagram-nested",
  41. color: :app,
  42. )
  43. end
  44. 1 def for_partnable(partnable)
  45. new(
  46. text: partnable.name,
  47. icon: "fa-regular fa-box",
  48. color: :general,
  49. )
  50. end
  51. end
  52. 1 def initialize(**kwargs)
  53. @draft = kwargs.delete(:draft)
  54. super(**kwargs)
  55. @color ||= :general
  56. set_type
  57. end
  58. 1 def classes
  59. then: 0 else: 0 "#{super} lui-identity-token #{"lui-identity-token-#{@type}"} #{"lui-identity-token--draft" if draft?}".chomp
  60. end
  61. 1 def styles
  62. <<~CSS.squish
  63. color: #{@text_color};
  64. background-color: #{@bg_color};
  65. CSS
  66. end
  67. 1 private
  68. 1 def set_color(color)
  69. then: 0 @text_color, @bg_color = if color.present?
  70. then: 0 if COLORS.key?(color.to_sym)
  71. else: 0 COLORS[color.to_sym]
  72. then: 0 elsif color.to_sym == :app
  73. APP_COLORS[LooposUi.config.app_type.to_sym] || COLORS[:general]
  74. else: 0 else
  75. raise ArgumentError, "Invalid color: #{color}, available colors: #{COLORS.keys.join(", ")}, app."
  76. end
  77. else: 0 else
  78. COLORS.excluding(LooposUi.config.app_type.to_sym).values.sample
  79. end
  80. end
  81. 1 def set_type
  82. then: 0 else: 0 @type = case @color&.to_sym
  83. when: 0 when :app
  84. then: 0 else: 0 APP_COLORS.key?(LooposUi.config.app_type.to_sym) ? LooposUi.config.app_type : :general
  85. else: 0 else
  86. @color
  87. end
  88. end
  89. 1 def draft?
  90. @draft
  91. end
  92. end
  93. end

app/components/loopos_ui/image.rb

46.15% lines covered

0.0% branches covered

13 relevant lines. 6 lines covered and 7 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class Image < ViewComponent::Base
  4. 1 attr_reader :resource, :editable, :image_url, :size, :rounded
  5. 1 def initialize(resource: nil, editable: false, image_url: nil, size: "full", rounded: false)
  6. @resource = resource
  7. @editable = editable
  8. @image_url = image_url
  9. @size = size
  10. @rounded = rounded
  11. end
  12. 1 def default_svg
  13. <<-SVG.html_safe
  14. <svg class="loopui-image__image" width="162" height="162" viewBox="0 0 162 162" fill="none" xmlns="http://www.w3.org/2000/svg">
  15. <rect width="162" height="162" rx="8" fill="#DEE2E6"/>
  16. <path d="M81 73.5001C81 71.1989 82.8655 69.3334 85.1667 69.3334C87.4679 69.3334 89.3334 71.1989 89.3334 73.5001C89.3334 75.8013 87.4679 77.6667 85.1667 77.6667C82.8655 77.6667 81 75.8013 81 73.5001Z" fill="white"/>
  17. <path fill-rule="evenodd" clip-rule="evenodd" d="M77.5938 62.6667C75.322 62.6667 73.5153 62.6667 72.0577 62.7858C70.565 62.9078 69.2922 63.1629 68.1268 63.7567C66.2452 64.7154 64.7154 66.2452 63.7566 68.1268C63.1628 69.2923 62.9077 70.565 62.7858 72.0577C62.6667 73.5154 62.6667 75.3221 62.6667 77.5938V84.4063C62.6667 86.6781 62.6667 88.4848 62.7858 89.9425C62.9077 91.4351 63.1628 92.7079 63.7566 93.8733C64.7154 95.7549 66.2452 97.2847 68.1268 98.2435C69.2922 98.8373 70.565 99.0924 72.0577 99.2143C73.5153 99.3334 75.3219 99.3334 77.5936 99.3334H84.4063C86.678 99.3334 88.4848 99.3334 89.9424 99.2143C91.4351 99.0924 92.7078 98.8373 93.8733 98.2435C95.7549 97.2847 97.2847 95.7549 98.2434 93.8733C98.8372 92.7079 99.0923 91.4351 99.2143 89.9425C99.3334 88.4848 99.3334 86.6782 99.3334 84.4065V77.5938C99.3334 75.3221 99.3334 73.5153 99.2143 72.0577C99.0923 70.565 98.8372 69.2923 98.2434 68.1268C97.2847 66.2452 95.7549 64.7154 93.8733 63.7567C92.7078 63.1629 91.4351 62.9078 89.9424 62.7858C88.4847 62.6667 86.678 62.6667 84.4063 62.6667H77.5938ZM69.6401 66.7267C70.2573 66.4122 71.0426 66.2132 72.3291 66.1081C73.6351 66.0014 75.3056 66.0001 77.6667 66.0001H84.3334C86.6944 66.0001 88.365 66.0014 89.671 66.1081C90.9575 66.2132 91.7428 66.4122 92.36 66.7267C93.6144 67.3659 94.6342 68.3857 95.2734 69.6401C95.5879 70.2573 95.7869 71.0426 95.892 72.3292C95.9987 73.6351 96 75.3057 96 77.6667V84.3334C96 84.9308 95.9999 85.4839 95.9981 85.9981L93.357 83.3571C92.0553 82.0554 89.9447 82.0554 88.643 83.3571L84.9226 87.0775C84.5972 87.4029 84.0695 87.4029 83.7441 87.0775L73.357 76.6904C72.0553 75.3887 69.9447 75.3887 68.643 76.6904L66 79.3334V77.6667C66 75.3057 66.0013 73.6351 66.108 72.3292C66.2131 71.0426 66.4122 70.2573 66.7266 69.6401C67.3658 68.3857 68.3857 67.3659 69.6401 66.7267Z" fill="white"/>
  18. <path d="M81 73.5001C81 71.1989 82.8655 69.3334 85.1667 69.3334C87.4679 69.3334 89.3334 71.1989 89.3334 73.5001C89.3334 75.8013 87.4679 77.6667 85.1667 77.6667C82.8655 77.6667 81 75.8013 81 73.5001Z" stroke="white" stroke-width="1.67"/>
  19. <path fill-rule="evenodd" clip-rule="evenodd" d="M77.5938 62.6667C75.322 62.6667 73.5153 62.6667 72.0577 62.7858C70.565 62.9078 69.2922 63.1629 68.1268 63.7567C66.2452 64.7154 64.7154 66.2452 63.7566 68.1268C63.1628 69.2923 62.9077 70.565 62.7858 72.0577C62.6667 73.5154 62.6667 75.3221 62.6667 77.5938V84.4063C62.6667 86.6781 62.6667 88.4848 62.7858 89.9425C62.9077 91.4351 63.1628 92.7079 63.7566 93.8733C64.7154 95.7549 66.2452 97.2847 68.1268 98.2435C69.2922 98.8373 70.565 99.0924 72.0577 99.2143C73.5153 99.3334 75.3219 99.3334 77.5936 99.3334H84.4063C86.678 99.3334 88.4848 99.3334 89.9424 99.2143C91.4351 99.0924 92.7078 98.8373 93.8733 98.2435C95.7549 97.2847 97.2847 95.7549 98.2434 93.8733C98.8372 92.7079 99.0923 91.4351 99.2143 89.9425C99.3334 88.4848 99.3334 86.6782 99.3334 84.4065V77.5938C99.3334 75.3221 99.3334 73.5153 99.2143 72.0577C99.0923 70.565 98.8372 69.2923 98.2434 68.1268C97.2847 66.2452 95.7549 64.7154 93.8733 63.7567C92.7078 63.1629 91.4351 62.9078 89.9424 62.7858C88.4847 62.6667 86.678 62.6667 84.4063 62.6667H77.5938ZM69.6401 66.7267C70.2573 66.4122 71.0426 66.2132 72.3291 66.1081C73.6351 66.0014 75.3056 66.0001 77.6667 66.0001H84.3334C86.6944 66.0001 88.365 66.0014 89.671 66.1081C90.9575 66.2132 91.7428 66.4122 92.36 66.7267C93.6144 67.3659 94.6342 68.3857 95.2734 69.6401C95.5879 70.2573 95.7869 71.0426 95.892 72.3292C95.9987 73.6351 96 75.3057 96 77.6667V84.3334C96 84.9308 95.9999 85.4839 95.9981 85.9981L93.357 83.3571C92.0553 82.0554 89.9447 82.0554 88.643 83.3571L84.9226 87.0775C84.5972 87.4029 84.0695 87.4029 83.7441 87.0775L73.357 76.6904C72.0553 75.3887 69.9447 75.3887 68.643 76.6904L66 79.3334V77.6667C66 75.3057 66.0013 73.6351 66.108 72.3292C66.2131 71.0426 66.4122 70.2573 66.7266 69.6401C67.3658 68.3857 68.3857 67.3659 69.6401 66.7267Z" stroke="white" stroke-width="1.67"/>
  20. </svg>
  21. SVG
  22. end
  23. 1 def classes
  24. then: 0 else: 0 "loopui-image--#{@size} loopui-image--#{@rounded ? "round" : "square"}"
  25. end
  26. end
  27. end

app/components/loopos_ui/image/image.html.erb

0.0% lines covered

0.0% branches covered

16 relevant lines. 0 lines covered and 16 lines missed.
8 total branches, 0 branches covered and 8 branches missed.
    
  1. <div class="loopui-image group <%= classes %>" data-controller="upload">
  2. then: 0 else: 0 then: 0 <%= if @resource&.image_attached?
  3. image_tag @resource.image,
  4. class: "loopui-image__image",
  5. else: 0 data: { upload_target: "image" }
  6. then: 0 elsif @image_url.present?
  7. image_tag @image_url,
  8. class: "loopui-image__image",
  9. data: { upload_target: "image" }
  10. else: 0 else
  11. default_svg
  12. end %>
  13. then: 0 else: 0 <% if @editable %>
  14. <div class="loopui-image__image-edit" data-action="click->upload#openFilePicker">
  15. <%= form_for @resource.object, url: @resource.attach_image_path, html: { data: { turbo_frame: "single_image_uploader", upload_target: "form" } } do |f| %>
  16. <%= f.label :image, title: 'Upload image',
  17. class:'loopui-image__image-edit-label group' do %>
  18. <i class="loopui-image__image-edit-icon fa-light fa-upload"></i>
  19. <span class="loopui-image__image-edit-text">Upload Image</span>
  20. <% end %>
  21. <%= f.file_field :image, direct_upload: true, class: "hidden", data: { upload_target: "file", action: "change->upload#previewAndSubmit", direct_upload_url: @resource.direct_upload_url } %>
  22. <%= f.submit "Save", class: "hidden" %>
  23. <% end %>
  24. </div>
  25. <% end %>
  26. </div>

app/components/loopos_ui/index_header.rb

75.0% lines covered

0.0% branches covered

12 relevant lines. 9 lines covered and 3 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. 1 module LooposUi
  2. 1 class IndexHeader < LoopComponent
  3. 1 include LooposUi::ResourceAware
  4. 1 renders_many :title_labels, types: {
  5. counter: LooposUi::CounterLabel,
  6. state: LooposUi::StateLabel,
  7. double_state: LooposUi::DoubleStateLabel,
  8. }
  9. 1 then: 0 else: 0 option :title, optional: true, default: proc { resource&.model_class_plural_name }
  10. 1 option :with_counter, default: proc { true }
  11. 1 def before_render
  12. then: 0 else: 0 if resource.present? && with_counter?
  13. with_title_label_counter(text: resource.label_counter(resource.model_class.count))
  14. end
  15. end
  16. 1 private
  17. 1 def with_counter?
  18. @with_counter
  19. end
  20. end
  21. end

app/components/loopos_ui/index_header/index_header.html.erb

0.0% lines covered

100.0% branches covered

5 relevant lines. 0 lines covered and 5 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <turbo-frame id="lui-index-header" class="lui-index_header">
  2. <h1 class="lui-index_header__title">
  3. <%= title %>
  4. </h1>
  5. <div>
  6. <% title_labels.each do |label| %>
  7. <%= label %>
  8. <% end %>
  9. </div>
  10. </turbo-frame>

app/components/loopos_ui/index_layout.rb

66.67% lines covered

0.0% branches covered

12 relevant lines. 8 lines covered and 4 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. # frozen_string_literal: true
  2. # TODO: document LookBook
  3. 1 module LooposUi
  4. 1 class IndexLayout < LoopComponent
  5. 1 renders_one :action_bar, ->(**args, &block) { LooposUi::ActionBar.new(**args, current_action: :index, &block) }
  6. 1 renders_one :header, ->(**args) {
  7. case header_type
  8. when: 0 when :index
  9. LooposUi::IndexHeader.new(**args, model_class: model_class)
  10. else: 0 else
  11. LooposUi::PageHeader.new(**args, model_class: model_class)
  12. end
  13. }
  14. 1 option :model_class, optional: true
  15. 1 option :header_type, Types::Coercible::Symbol.enum(:page, :index), default: -> { :index }
  16. 1 renders_one :highlight
  17. 1 def before_render
  18. then: 0 else: 0 with_header(with_counter: true) if model_class.present? && !header?
  19. end
  20. end
  21. end

app/components/loopos_ui/index_layout/index_layout.html.erb

0.0% lines covered

0.0% branches covered

9 relevant lines. 0 lines covered and 9 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <div class="lui-index-layout">
  2. <%= action_bar %>
  3. <%= header %>
  4. then: 0 else: 0 <% if highlight? %>
  5. <div class="w-full">
  6. <%= highlight %>
  7. </div>
  8. <% end %>
  9. <div class="w-full grow">
  10. <%= content %>
  11. </div>
  12. <%# TODO: extract this class to drawer_bar.scss %>
  13. <turbo-frame class="lui-show-layout__drawer_wrapper" id="lui-main-layout-drawer_bar" class="fixed top-[106px] right-0" data-controller="drawer-bar">
  14. </turbo-frame>
  15. </div>

app/components/loopos_ui/inline_edit_component.rb

39.02% lines covered

0.0% branches covered

41 relevant lines. 16 lines covered and 25 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 module InlineEditComponent
  4. 1 def element_edit(element, show_path, edit_path, attribute, type = :text_field, **kwargs, &block)
  5. render(
  6. LooposUi::InlineEditComponent::Base.new(
  7. model: element,
  8. attribute: attribute,
  9. type: type,
  10. show_path: show_path,
  11. edit_path: edit_path,
  12. **kwargs,
  13. ),
  14. &block
  15. )
  16. end
  17. 1 class Base < ViewComponent::Base
  18. 1 include Turbo::FramesHelper
  19. 1 renders_one :show
  20. 1 renders_one :edit
  21. 1 attr_reader :model,
  22. :attribute,
  23. :type,
  24. :show_path,
  25. :edit_path,
  26. :with_turbo_wrapper,
  27. :custom_class,
  28. :size,
  29. :has_margin,
  30. :custom_turbo_frame_id
  31. 1 def initialize(model:, attribute:, type:, show_path:, edit_path:, with_turbo_wrapper: true, custom_class: "",
  32. size: "base", has_margin: false, show_edit_button: true, custom_turbo_frame_id: nil)
  33. super
  34. @model = model
  35. @attribute = attribute
  36. @type = type
  37. @show_path = show_path
  38. @edit_path = edit_path
  39. @with_turbo_wrapper = with_turbo_wrapper
  40. @custom_class = custom_class
  41. @size = size
  42. @has_margin = has_margin
  43. @show_edit_button = show_edit_button
  44. @custom_turbo_frame_id = custom_turbo_frame_id
  45. end
  46. 1 def show_action?
  47. !(edit_action? || keep_inline_edit?)
  48. end
  49. 1 def edit_action?
  50. view_context.action_name == "edit" || (Rails.env.development? && params[:edit_mode])
  51. end
  52. 1 def keep_inline_edit?
  53. params[:keep_inline_edit] == "true"
  54. end
  55. 1 def turbo_frame_id
  56. custom_turbo_frame_id.presence || "inline_edit_#{dom_id(model)}_#{attribute}"
  57. end
  58. 1 def form_attributes(form_attrs)
  59. then: 0 if form_attrs.is_a?(Array)
  60. else: 0 { model: form_attrs}
  61. then: 0 elsif form_attrs.is_a?(String)
  62. { url: form_attrs, method: :patch }
  63. else: 0 else
  64. raise ArgumentError, "form_attributes must be an Array or a String"
  65. end
  66. end
  67. 1 def render_edit_button
  68. else: 0 then: 0 return unless @show_edit_button
  69. render(
  70. partial: "loopos_ui/edit_button",
  71. locals: {
  72. path: edit_path,
  73. frame_name: turbo_frame_id,
  74. extra_classes: "text-xxs! mt-[-6px]",
  75. has_margin: has_margin,
  76. size: size,
  77. },
  78. )
  79. end
  80. 1 def render_submit_buttons(form:)
  81. render(
  82. partial: "loopos_ui/edit_submit",
  83. locals: {
  84. show_path: show_path,
  85. frame_name: turbo_frame_id,
  86. form: form,
  87. extra_classes: "text-xxs! mt-[-6px]",
  88. extra_wrapper_classes: "gap-2xs!",
  89. has_margin: has_margin,
  90. size: size,
  91. },
  92. )
  93. end
  94. end
  95. end
  96. end

app/components/loopos_ui/inline_edit_component/base.html.erb

0.0% lines covered

0.0% branches covered

13 relevant lines. 0 lines covered and 13 lines missed.
10 total branches, 0 branches covered and 10 branches missed.
    
  1. <%# FIXME: looks ugly but it's fast rn %>
  2. then: 0 else: 0 <%= raw("<turbo-frame id=\"#{turbo_frame_id}\" class=\"#{custom_class}\">") if with_turbo_wrapper %>
  3. <div class="flex flex-row gap-2xs">
  4. then: 0 <% if show_action? %>
  5. then: 0 <% if show? %>
  6. <%= show %>
  7. <% else %>
  8. else: 0 <%# TODO: default inline render sytle %>
  9. <%= model.send(attribute) %>
  10. <%= render_edit_button %>
  11. <% end %>
  12. else: 0 <% else %>
  13. then: 0 <% if edit?%>
  14. <%= edit %>
  15. else: 0 <% else %>
  16. <%= form_with(model: [:admin, model], class: "w-full") do |form| %>
  17. <%= form.send(type, attribute) %>
  18. <%= render_submit_buttons(form: form) %>
  19. <% end %>
  20. <% end %>
  21. <% end %>
  22. </div>
  23. then: 0 else: 0 <%= raw("</turbo-frame>") if with_turbo_wrapper %>

app/components/loopos_ui/inline_text_edit.rb

84.62% lines covered

100.0% branches covered

13 relevant lines. 11 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class InlineTextEdit < LoopComponent
  4. 1 include Turbo::FramesHelper
  5. 1 option :model
  6. 1 option :attribute
  7. 1 option :form_attrs
  8. 1 option :editable, default: -> { false }
  9. 1 option :open_actions, default: -> { false }
  10. 1 private
  11. 1 def turbo_frame_id
  12. "inline_text_edit_#{dom_id(model)}_#{attribute}"
  13. end
  14. 1 def value
  15. model.send(attribute)
  16. end
  17. end
  18. end

app/components/loopos_ui/inline_text_edit/inline_text_edit.html.erb

0.0% lines covered

0.0% branches covered

15 relevant lines. 0 lines covered and 15 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%
  2. =begin%>
  3. <%# FIXME: this should be the final version %>
  4. <% if editable %>
  5. <%= turbo_frame_tag turbo_frame_id do %>
  6. <%= form_with(**form_attrs, class: "-ml-4") do |form| %>
  7. <%= render LooposUi::Inputs::Text.new(
  8. model: model,
  9. attribute: attribute,
  10. readonly: !editable
  11. )%>
  12. <% end %>
  13. <% end %>
  14. <% else %>
  15. <div class="lui-show-header__title">
  16. <%= model.send(attribute) %>
  17. </div>
  18. <% end %>
  19. <%
  20. =end %>
  21. then: 0 <% if editable %>
  22. <%= turbo_frame_tag turbo_frame_id do %>
  23. <%= form_with(**form_attrs) do |form| %>
  24. <div class="flex flex-start gap-2" data-controller="inline-edit" data-inline-edit-open-actions-value="<%= open_actions %>">
  25. <div class="lui-show-header__title flex max-h-[36px] items-center">
  26. <%= form.hidden_field attribute, value: value, data: { "inline-edit-target": "input" } %>
  27. <%= hidden_field_tag :inline_edit, true %>
  28. <%= content_tag(:span, value,
  29. class: "lui-input-text",
  30. contenteditable: true,
  31. data: { action: "click->inline-edit#openActions input->inline-edit#onInput", "inline-edit-target": "fakeInput" })%>
  32. </div>
  33. <span class="hidden items-center gap-1 h-fit" data-inline-edit-target="actions">
  34. <%= render LooposUi::Button.new(
  35. leading_icon: "fa-regular fa-xmark",
  36. kind: :neutral,
  37. type: :secondary,
  38. size: :tiny,
  39. # href: "#", #admin_v2_catalog_product_path(@product),
  40. tag_options: {
  41. data: {
  42. "inline-edit-target": "cancel",
  43. action: "click->inline-edit#closeActions click->inline-edit#restore" },
  44. type: :button
  45. }) %>
  46. <%= render LooposUi::Button.new(
  47. leading_icon: "fa-regular fa-check",
  48. kind: :success,
  49. type: :secondary,
  50. size: :tiny,
  51. tag_options: { type: "submit", data: { "inline-edit-target": "submit" } }) %>
  52. </span>
  53. </div>
  54. <% end %>
  55. <% end %>
  56. else: 0 <% else %>
  57. <div class="lui-show-header__title">
  58. <%= model.send(attribute) %>
  59. </div>
  60. <% end %>

app/components/loopos_ui/input.rb

54.55% lines covered

0.0% branches covered

22 relevant lines. 12 lines covered and 10 lines missed.
8 total branches, 0 branches covered and 8 branches missed.
    
  1. 1 module LooposUi
  2. 1 class Input < LoopComponent
  3. 1 include Inputs::BaseInputConcern
  4. 1 option :type, default: -> { "text" }
  5. 1 option :input_attributes, default: -> { {} }
  6. 1 renders_one :left_addon
  7. 1 renders_one :right_addon
  8. 1 def initialize(**kwargs)
  9. then: 0 if kwargs[:model].present? && kwargs[:attribute].present?
  10. kwargs[:name] = model_name(kwargs[:model], kwargs[:attribute])
  11. kwargs[:value] = kwargs[:model].send(kwargs[:attribute])
  12. then: 0 else: 0 kwargs[:error] =
  13. else: 0 kwargs[:model].errors[kwargs[:attribute]].first if kwargs[:model].errors[kwargs[:attribute]].present?
  14. then: 0 else: 0 elsif kwargs[:name].blank?
  15. raise ArgumentError, "You must provide a name attribute, or a model"
  16. end
  17. super(**kwargs)
  18. end
  19. 1 def classes
  20. then: 0 else: 0 "lui-input #{error.present? ? "lui-input--with-error" : ""}"
  21. end
  22. 1 private
  23. 1 def model_name(model, attribute)
  24. "#{model.model_name.param_key}[#{attribute}]"
  25. end
  26. 1 def merged_input_attributes
  27. {
  28. name: name,
  29. type: type,
  30. value: value,
  31. placeholder: placeholder,
  32. class: "lui-input__input",
  33. mode: mode,
  34. form: form,
  35. contentEditable: true,
  36. data: {
  37. "input-target": "input",
  38. action: "input->input#onChange change->input#onChange",
  39. },
  40. }.deep_merge(input_attributes).compact
  41. end
  42. end
  43. end

app/components/loopos_ui/input/input.html.erb

0.0% lines covered

0.0% branches covered

27 relevant lines. 0 lines covered and 27 lines missed.
14 total branches, 0 branches covered and 14 branches missed.
    
  1. <%#
  2. TODO: refactor css classes
  3. we have
  4. - lui-inner-input (top level), controller attached
  5. - lui-input (input wrapper), with addons
  6. - lui-input__input (actual input tag)
  7. Carefull: other wrapper components have css selectors that overide these styles, just changing it will break other
  8. components, as well as it's controllers.
  9. %>
  10. then: 0 <% if !readonly %>
  11. <div data-controller="input" data-input-open-actions-value="<%= open_actions %>" class="lui-inner-input relative flex gap-2"
  12. data-input-original-input-value="<%= value %>"
  13. data-input-mode-value="<%= mode %>"
  14. data-input-form-value="<%= form %>">
  15. <div class="w-full flex flex-col">
  16. <div class="lui-input-chrome relative w-full">
  17. then: 0 <% if content.present? %>
  18. <%= content %>
  19. else: 0 <% else %>
  20. <%= content_tag(:span, class: "#{classes}") do %>
  21. then: 0 else: 0 <% if left_addon %>
  22. <span class="lui-input__addon-left">
  23. <%= left_addon %>
  24. </span>
  25. <% end %>
  26. <%= tag.input(**merged_input_attributes) %>
  27. then: 0 else: 0 <% if right_addon %>
  28. <span class="lui-input__addon-right">
  29. <%= right_addon %>
  30. </span>
  31. <% end %>
  32. <span class="lui-input__spinner">
  33. <%= tag.i(class: "fa-regular fa-spinner") %>
  34. </span>
  35. <% end %>
  36. <% end %>
  37. </div>
  38. then: 0 else: 0 <%= content_tag(:span, help, class: "lui-input__help") if !error.present? && help.present? %>
  39. then: 0 else: 0 <%= content_tag(:span, error, class: "lui-input__error") if error.present? %>
  40. </div>
  41. <span class="lui-inner-input__actions opacity-0 flex items-center gap-1 h-fit" data-input-target="actions">
  42. <%= render LooposUi::Button.new(
  43. leading_icon: "fa-regular fa-xmark",
  44. kind: :neutral,
  45. type: :secondary,
  46. size: :tiny,
  47. # href: "#", #admin_v2_catalog_product_path(@product),
  48. tag_options: {
  49. data: {
  50. "input-target": "cancel",
  51. action: "click->input#handleClose" },
  52. type: :button,
  53. disabled: true
  54. }) %>
  55. <%= render LooposUi::Button.new(
  56. leading_icon: "fa-regular fa-check",
  57. kind: :success,
  58. type: :secondary,
  59. size: :tiny,
  60. tag_options: {
  61. form: form,
  62. type: "submit",
  63. data: {
  64. "input-target": "submit",
  65. "action": "click->input#setLoading"
  66. },
  67. disabled: true
  68. }) %>
  69. </span>
  70. else: 0 </div>
  71. <% elsif custom_readonly %>
  72. then: 0 <%# Support for custom readonly inputs, like RichText, it will receive all the props %>
  73. <%= content %>
  74. else: 0 <% else %>
  75. <%= content_tag(:span, value.presence || "-", class: "lui-input__value") %>
  76. <% end %>

app/components/loopos_ui/inputs/ai.rb

80.0% lines covered

0.0% branches covered

10 relevant lines. 8 lines covered and 2 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Inputs
  3. 1 class Ai < LoopComponent
  4. 1 option :placeholder, Types::Coercible::String, optional: true
  5. 1 option :spinner, Types::Bool, default: -> { false }
  6. 1 option :disabled, Types::Bool, default: -> { false }
  7. 1 option :tooltip, Types::Coercible::String, optional: true
  8. 1 option :max_rows,
  9. Types::Coercible::Integer.constructor { |value|
  10. then: 0 else: 0 value.to_i <= 1 ? 1 : value.to_i + 1
  11. },
  12. default: -> { 7 }
  13. end
  14. end
  15. end

app/components/loopos_ui/inputs/ai/ai.html.erb

0.0% lines covered

0.0% branches covered

14 relevant lines. 0 lines covered and 14 lines missed.
12 total branches, 0 branches covered and 12 branches missed.
    
  1. then: 0 else: 0 <div class="w-full <%= 'cursor-not-allowed' if disabled %>">
  2. then: 0 else: 0 <%= render LooposUi::Tooltip.new(title: tooltip) if tooltip.present? && disabled %>
  3. <div
  4. then: 0 else: 0 class="lui-ai-input__wrapper lui-ai-animated-border <%= 'pointer-events-none' if disabled %>"
  5. else: 0 then: 0 data-controller="<%= 'input-ai' unless disabled %>"
  6. data-input-ai-spinner-value="<%= spinner %>"
  7. data-input-ai-max-rows-value="<%= max_rows %>"
  8. >
  9. <textarea
  10. data-input-ai-target="input"
  11. rows="1"
  12. placeholder="<%= placeholder || t(".placeholder") %>"
  13. class="lui-ai-input__input lui-ai-input__textarea"
  14. style="--lui-ai-max-rows: <%= max_rows %>;"
  15. ></textarea>
  16. <div data-input-ai-target="buttonWrapper" class="lui-ai-input__button-wrapper lui-ai-input__button-wrapper--center">
  17. <%= render LooposUi::Button.new(
  18. leading_icon: "fa-solid fa-paper-plane-top",
  19. kind: :neutral,
  20. type: :secondary,
  21. size: :default,
  22. disabled: disabled,
  23. else: 0 then: 0 tooltip_text: (tooltip unless disabled),
  24. tag_options: {
  25. class: "lui-ai-input__button",
  26. data: {
  27. "input-ai-target": "button",
  28. action: "click->input-ai#handleButtonClick"
  29. },
  30. type: :button
  31. }
  32. ) %>
  33. then: 0 else: 0 <% if spinner %>
  34. <i
  35. data-input-ai-target="spinner"
  36. class="fa-solid fa-spinner fa-spin lui-ai-input__spinner"
  37. ></i>
  38. <% end %>
  39. </div>
  40. </div>
  41. </div>

app/components/loopos_ui/inputs/base_input_concern.rb

77.78% lines covered

0.0% branches covered

36 relevant lines. 28 lines covered and 8 lines missed.
8 total branches, 0 branches covered and 8 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Inputs
  3. 1 module BaseInputConcern
  4. 1 extend ActiveSupport::Concern
  5. 1 class FormTagHelper
  6. 1 include ActionView::Helpers::FormTagHelper
  7. end
  8. 1 def form_tag_helper
  9. @form_tag_helper ||= FormTagHelper.new
  10. end
  11. # TODO: All inputs should use this
  12. # If you saw this, please refactor the input to use this
  13. 1 def field_name(model, attribute, multiple: false)
  14. form_tag_helper.field_name(model.model_name.param_key, attribute, multiple: multiple)
  15. end
  16. 1 included do
  17. # Model usage
  18. 15 option :model, optional: true
  19. 15 option :attribute, optional: true
  20. # Manual usage
  21. 15 option :name, optional: true
  22. 15 option :value, Types::Coercible::String, optional: true
  23. 15 option :placeholder, optional: true
  24. 15 option :error, optional: true
  25. 15 option :help, optional: true
  26. 15 option :mode, default: -> { :inline }, type: Types::Coercible::Symbol.enum(
  27. :inline, :form, :autosubmit
  28. )
  29. # Visual modifier (CSS `--form`, etc.). Defaults to `mode` when omitted.
  30. 15 option :appearance, optional: true, type: Types::Coercible::Symbol.enum(
  31. :inline, :form, :autosubmit
  32. )
  33. 15 option :form, optional: true
  34. 15 option :readonly, default: -> { false }, type: Types::Bool
  35. 15 option :custom_readonly, default: -> { false }, type: Types::Bool # This will make the input render the content instead of the value
  36. 15 option :open_actions, default: -> { false }
  37. 15 option :extra_input_attributes, default: -> { {} }, type: Types::Hash
  38. end
  39. 1 def unique_id
  40. @unique_id ||= [
  41. self.class.name.downcase.parameterize,
  42. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 model.present? ? dom_id(model) : name&.to_s&.gsub(/\[|\]/, "_"),
  43. attribute.presence,
  44. rand(10**10),
  45. ].compact.join("_")
  46. end
  47. 1 def base_input_attributes
  48. [
  49. :model,
  50. :attribute,
  51. :name,
  52. :value,
  53. :placeholder,
  54. :error,
  55. :help,
  56. :mode,
  57. :appearance,
  58. :form,
  59. :readonly,
  60. :open_actions,
  61. ].to_h do |o|
  62. [o, send(o)]
  63. end
  64. end
  65. # CSS modifier class suffix; follows `mode` unless `appearance` is set explicitly.
  66. 1 def resolved_appearance
  67. then: 0 else: 0 appearance.nil? ? mode : appearance
  68. end
  69. 1 [:inline, :form, :autosubmit].each do |mode|
  70. 3 define_method("#{mode}?") do
  71. self.mode == mode
  72. end
  73. end
  74. end
  75. end
  76. end

app/components/loopos_ui/inputs/checkbox.rb

75.0% lines covered

0.0% branches covered

24 relevant lines. 18 lines covered and 6 lines missed.
8 total branches, 0 branches covered and 8 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Inputs
  3. 1 class Checkbox < LoopComponent
  4. 1 option :label, optional: true
  5. 1 option :state, optional: true
  6. 1 option :value, optional: true
  7. 1 option :name, optional: true
  8. 1 option :required, default: -> { false }, type: Types::Bool
  9. 1 option :readonly, default: -> { false }, type: Types::Bool
  10. 1 option :form, optional: true
  11. 1 option :data, optional: true
  12. 1 option :error, optional: true
  13. 1 option :help, optional: true
  14. 1 option :submit_via_hidden, default: -> { false }, type: Types::Bool
  15. 1 option :submitted_value, optional: true
  16. 1 def checked?
  17. then: 0 else: 0 return state == :checked if state.present?
  18. then: 0 else: 0 val = (value == "-" ? false : value)
  19. ActiveModel::Type::Boolean.new.cast(val)
  20. end
  21. 1 def indeterminate?
  22. state == :indeterminate
  23. end
  24. 1 def state_css_class
  25. then: 0 else: 0 return "lui-checkbox--#{state}" if state.present?
  26. then: 0 else: 0 checked? ? "lui-checkbox--checked" : nil
  27. end
  28. end
  29. end
  30. end

app/components/loopos_ui/inputs/checkbox/checkbox.html.erb

0.0% lines covered

0.0% branches covered

18 relevant lines. 0 lines covered and 18 lines missed.
18 total branches, 0 branches covered and 18 branches missed.
    
  1. <%= tag.div(
  2. class: [
  3. "lui-checkbox",
  4. state_css_class,
  5. then: 0 else: 0 ("lui-checkbox--with-error" if error.present?),
  6. ].compact.join(" "),
  7. data: { controller: "checkbox" }
  8. ) do %>
  9. then: 0 else: 0 <% if submit_via_hidden %>
  10. <%= hidden_field_tag(
  11. name,
  12. then: 0 else: 0 submitted_value.nil? ? "false" : submitted_value.to_s,
  13. form: form,
  14. data: { checkbox_target: "hiddenInput" },
  15. ) %>
  16. <% end %>
  17. <label class="lui-checkbox__wrapper">
  18. <span class="lui-checkbox__input">
  19. <%= tag.input(
  20. type: "checkbox",
  21. data: {
  22. checkbox_target: "input",
  23. action: "change->checkbox#toggle"
  24. }.merge(data || {}),
  25. checked: checked?,
  26. indeterminate: indeterminate?,
  27. class: "lui-checkbox__original",
  28. value: value,
  29. then: 0 else: 0 name: submit_via_hidden ? nil : name,
  30. disabled: readonly,
  31. then: 0 else: 0 form: submit_via_hidden ? nil : form
  32. ) %>
  33. <span class="lui-checkbox__inner"></span>
  34. </span>
  35. then: 0 else: 0 <%= tag.span(class: "lui-checkbox__label") do %>
  36. then: 0 else: 0 <%= label %><%= content %><%= tag.span("*", class: "lui-checkbox__required") if required %>
  37. <% end if label.present? || content.present? %>
  38. </label>
  39. then: 0 else: 0 <%= content_tag(:span, help, class: "lui-input__help") if help.present? && error.blank? %>
  40. then: 0 else: 0 <%= content_tag(:span, error, class: "lui-input__error") if error.present? %>
  41. <% end %>

app/components/loopos_ui/inputs/combobox.rb

45.09% lines covered

0.0% branches covered

173 relevant lines. 78 lines covered and 95 lines missed.
81 total branches, 0 branches covered and 81 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 module Inputs
  4. # Searchable select powered by Tom Select (Rails Blocks–compatible Stimulus controller).
  5. # @see https://railsblocks.com/docs/combobox
  6. 1 class Combobox < LoopComponent
  7. 1 include BaseInputConcern
  8. 1 option :options, type: [], default: -> { [] } do
  9. 1 option :value, Types::Coercible::String
  10. 1 option :text, Types::Coercible::String
  11. 1 option :disabled, Types::Bool, optional: true, default: -> { false }
  12. 1 option :data, Types::Hash, optional: true, default: -> { {} }
  13. end
  14. 1 option :include_blank, Types::Bool | Types::String, optional: true, default: -> { false }
  15. 1 option :multiple, Types::Bool, default: -> { false }
  16. 1 option :disabled, Types::Bool, default: -> { false }
  17. 1 option :required, Types::Bool, default: -> { false }
  18. 1 option :label, optional: true
  19. 1 option :description, optional: true
  20. 1 option :html_id, optional: true
  21. 1 option :selected, optional: true
  22. 1 option :url, optional: true
  23. 1 option :value_field, default: -> { "value" }
  24. 1 option :label_field, default: -> { "label" }
  25. 1 option :search_param, default: -> { "query" }
  26. 1 option :per_page, type: Types::Coercible::Integer, default: -> { 60 }
  27. 1 option :virtual_scroll, Types::Bool, default: -> { false }
  28. 1 option :response_data_field, default: -> { "data" }
  29. 1 option :optgroup_columns, Types::Bool, default: -> { false }
  30. 1 option :searchable, Types::Bool, default: -> { true }
  31. 1 option :clear_button, Types::Bool, default: -> { true }
  32. 1 option :open_on_mouse_down, Types::Bool, default: -> { true }
  33. 1 option :lock_scroll, Types::Bool, default: -> { false }
  34. 1 option :allow_create, Types::Bool, default: -> { false }
  35. 1 option :submit_on_change, Types::Bool, default: -> { false }
  36. 1 option :disable_typing, Types::Bool, default: -> { false }
  37. 1 option :scroll_buttons, Types::Bool, default: -> { false }
  38. 1 option :update_field, Types::Bool, default: -> { false }
  39. 1 option :update_field_target, optional: true
  40. 1 option :update_field_source, default: -> { "name" }
  41. 1 option :show_count, Types::Bool, default: -> { false }
  42. 1 option :count_text, default: -> { "selected" }
  43. 1 option :count_text_singular, optional: true
  44. 1 option :tags_position, default: -> { "inline" }
  45. 1 option :enable_flag_toggle, Types::Bool, default: -> { false }
  46. 1 option :image_field, optional: true
  47. 1 option :subtitle_field, optional: true
  48. 1 option :meta_fields, optional: true
  49. 1 option :badge_field, optional: true
  50. 1 option :render_template, optional: true
  51. 1 option :dropdown_placeholder, default: -> { "Search..." }
  52. 1 option :no_more_results_text, optional: true
  53. 1 option :no_search_results_text, optional: true
  54. 1 option :loading_text, optional: true
  55. 1 option :create_text, optional: true
  56. 1 option :extra_data, Types::Hash, default: -> { {} }
  57. 1 def initialize(**kwargs)
  58. multiple = kwargs.fetch(:multiple, false)
  59. helper = LooposUi::Inputs::BaseInputConcern::FormTagHelper.new
  60. prime_name_and_model_fields!(kwargs, multiple, helper)
  61. normalize_multiple!(kwargs, multiple)
  62. super(**kwargs)
  63. end
  64. 1 def combobox_field_id
  65. html_id.presence || unique_id
  66. end
  67. 1 def description_dom_id
  68. "#{combobox_field_id}_description"
  69. end
  70. 1 def pairs_for_options_helper
  71. rows = []
  72. then: 0 else: 0 if include_blank
  73. then: 0 else: 0 label = include_blank.is_a?(String) ? include_blank : ""
  74. rows << [label, ""]
  75. end
  76. options.each do |o|
  77. attrs = {}
  78. then: 0 else: 0 attrs[:disabled] = true if o.disabled
  79. then: 0 else: 0 attrs[:data] = o.data if o.data.present?
  80. then: 0 else: 0 rows << (attrs.empty? ? [o.text, o.value] : [o.text, o.value, attrs])
  81. end
  82. rows
  83. end
  84. 1 def selected_for_options_helper
  85. then: 0 if multiple
  86. selected_values
  87. else: 0 else
  88. (selected.presence || value).presence
  89. end
  90. end
  91. 1 def selected_values
  92. Array.wrap(selected).map(&:to_s).reject(&:blank?)
  93. end
  94. 1 def readonly_display_text
  95. then: 0 if multiple
  96. texts = options.select { |o| selected_values.include?(o.value.to_s) }.map(&:text)
  97. then: 0 else: 0 texts.presence&.join(", ") || "-"
  98. else: 0 else
  99. current = (selected.presence || value).to_s
  100. then: 0 else: 0 options.find { |o| o.value.to_s == current }&.text.presence || "-"
  101. end
  102. end
  103. 1 def inline_side_actions?
  104. inline? && !multiple && !readonly
  105. end
  106. 1 def inline_original_input_value
  107. (selected.presence || value).to_s
  108. end
  109. 1 def combobox_data_attributes
  110. base = {
  111. controller: "combobox",
  112. combobox_submit_on_change_value: effective_submit_on_change,
  113. combobox_dropdown_input_value: searchable,
  114. combobox_dropdown_input_placeholder_value: dropdown_placeholder,
  115. combobox_clear_button_value: effective_clear_button,
  116. combobox_open_on_mouse_down_value: open_on_mouse_down,
  117. combobox_lock_scroll_value: lock_scroll,
  118. combobox_disable_typing_value: disable_typing,
  119. combobox_allow_new_value: allow_create,
  120. combobox_scroll_buttons_value: scroll_buttons,
  121. combobox_update_field_value: update_field,
  122. combobox_update_field_source_value: update_field_source,
  123. combobox_per_page_value: per_page,
  124. combobox_virtual_scroll_value: virtual_scroll,
  125. combobox_optgroup_columns_value: optgroup_columns,
  126. combobox_response_data_field_value: response_data_field,
  127. combobox_search_param_value: search_param,
  128. combobox_show_count_value: show_count,
  129. combobox_count_text_value: count_text,
  130. combobox_tags_position_value: tags_position,
  131. combobox_enable_flag_toggle_value: enable_flag_toggle,
  132. combobox_value_field_value: value_field,
  133. combobox_label_field_value: label_field,
  134. }
  135. then: 0 else: 0 base[:combobox_url_value] = url if url.present?
  136. then: 0 else: 0 base[:combobox_update_field_target_value] = update_field_target if update_field_target.present?
  137. then: 0 else: 0 base[:combobox_count_text_singular_value] = count_text_singular if count_text_singular.present?
  138. then: 0 else: 0 base[:combobox_image_field_value] = image_field if image_field.present?
  139. then: 0 else: 0 base[:combobox_subtitle_field_value] = subtitle_field if subtitle_field.present?
  140. then: 0 else: 0 base[:combobox_meta_fields_value] = meta_fields if meta_fields.present?
  141. then: 0 else: 0 base[:combobox_badge_field_value] = badge_field if badge_field.present?
  142. then: 0 else: 0 base[:combobox_render_template_value] = render_template if render_template.present?
  143. then: 0 else: 0 base[:combobox_no_more_results_text_value] = no_more_results_text if no_more_results_text.present?
  144. then: 0 else: 0 base[:combobox_no_search_results_text_value] = no_search_results_text if no_search_results_text.present?
  145. then: 0 else: 0 base[:combobox_loading_text_value] = loading_text if loading_text.present?
  146. then: 0 else: 0 base[:combobox_create_text_value] = create_text if create_text.present?
  147. then: 0 else: 0 base[:combobox_inline_actions_value] = true if inline_side_actions?
  148. base.merge(extra_data.symbolize_keys)
  149. end
  150. 1 def merged_select_attributes
  151. extra = extra_input_attributes.deep_dup
  152. extra_class = extra.delete(:class)
  153. extra_data = (extra.delete(:data) || {}).symbolize_keys
  154. merged_data = combobox_data_attributes.merge(extra_data)
  155. then: 0 else: 0 if inline_side_actions?
  156. merged_data = merged_data.merge(
  157. action: "change->input#onChange",
  158. input_target: "input",
  159. )
  160. end
  161. {
  162. id: combobox_field_id,
  163. class: class_names("lui-combobox__select", "w-full", extra_class),
  164. multiple: multiple,
  165. disabled: disabled,
  166. required: required,
  167. form: form,
  168. style: "visibility: hidden;",
  169. data: merged_data,
  170. aria: aria_attributes,
  171. }.deep_merge(extra.compact)
  172. end
  173. 1 def aria_attributes
  174. then: 0 else: 0 return {} if description.blank?
  175. { describedby: description_dom_id }
  176. end
  177. 1 def wrapper_classes
  178. class_names(
  179. "lui-combobox w-full flex flex-col gap-1",
  180. combobox_mode_modifier_class,
  181. then: 0 else: 0 error.present? ? "lui-combobox--error" : nil,
  182. *Array.wrap(@additional_classes).compact,
  183. )
  184. end
  185. 1 def effective_submit_on_change
  186. submit_on_change || autosubmit?
  187. end
  188. 1 def effective_clear_button
  189. then: 0 else: 0 return false if form? || autosubmit?
  190. then: 0 else: 0 return false if required
  191. clear_button
  192. end
  193. 1 def combobox_mode_modifier_class
  194. case resolved_appearance
  195. when: 0 when :autosubmit
  196. "lui-combobox--autosubmit"
  197. when: 0 when :form
  198. "lui-combobox--form"
  199. else: 0 else
  200. "lui-combobox--inline"
  201. end
  202. end
  203. 1 private
  204. 1 def prime_name_and_model_fields!(kwargs, multiple, helper)
  205. model = kwargs[:model]
  206. attribute = kwargs[:attribute]
  207. then: 0 if model.present? && attribute.present?
  208. else: 0 prime_from_model!(kwargs, model, attribute, multiple, helper)
  209. then: 0 else: 0 elsif kwargs[:name].blank?
  210. raise ArgumentError, "You must provide a name attribute, or a model and attribute"
  211. end
  212. end
  213. 1 def prime_from_model!(kwargs, model, attribute, multiple, helper)
  214. kwargs[:name] ||= helper.field_name(model.model_name.param_key, attribute, multiple: multiple)
  215. apply_raw_attribute_to_selection!(kwargs, model, attribute, multiple)
  216. copy_model_error!(kwargs, model, attribute)
  217. end
  218. 1 def apply_raw_attribute_to_selection!(kwargs, model, attribute, multiple)
  219. then: 0 else: 0 return if kwargs.key?(:selected) || kwargs.key?(:value)
  220. raw = model.public_send(attribute)
  221. assign_raw_to_selection!(kwargs, multiple, raw)
  222. end
  223. 1 def assign_raw_to_selection!(kwargs, multiple, raw)
  224. then: 0 if multiple
  225. kwargs[:selected] = Array.wrap(raw).map(&:to_s).reject(&:blank?)
  226. else: 0 else
  227. then: 0 else: 0 kwargs[:value] = raw&.to_s
  228. end
  229. end
  230. 1 def copy_model_error!(kwargs, model, attribute)
  231. errs = model.errors[attribute]
  232. then: 0 else: 0 then: 0 else: 0 kwargs[:error] ||= errs.first&.to_s if errs.present?
  233. end
  234. 1 def normalize_multiple!(kwargs, multiple)
  235. else: 0 then: 0 return unless multiple
  236. ensure_multiple_name!(kwargs)
  237. normalize_selected_vs_value!(kwargs)
  238. end
  239. 1 def ensure_multiple_name!(kwargs)
  240. n = kwargs[:name].to_s
  241. else: 0 then: 0 kwargs[:name] = "#{n}[]" unless n.end_with?("[]")
  242. end
  243. 1 def normalize_selected_vs_value!(kwargs)
  244. then: 0 if kwargs[:selected].nil?
  245. normalize_nil_selected!(kwargs)
  246. else: 0 else
  247. kwargs[:selected] = Array.wrap(kwargs[:selected]).map(&:to_s).reject(&:blank?)
  248. kwargs[:value] = nil
  249. end
  250. end
  251. 1 def normalize_nil_selected!(kwargs)
  252. then: 0 if kwargs[:value].is_a?(Array)
  253. kwargs[:selected] = kwargs[:value].map(&:to_s).reject(&:blank?)
  254. else: 0 kwargs[:value] = nil
  255. then: 0 elsif kwargs[:multiple] && !kwargs[:value].is_a?(Array)
  256. kwargs[:selected] = Array.wrap(kwargs[:value]).map(&:to_s).reject(&:blank?)
  257. kwargs[:value] = nil
  258. else: 0 else
  259. kwargs[:selected] = []
  260. end
  261. end
  262. end
  263. end
  264. end

app/components/loopos_ui/inputs/combobox/combobox.html.erb

0.0% lines covered

0.0% branches covered

27 relevant lines. 0 lines covered and 27 lines missed.
14 total branches, 0 branches covered and 14 branches missed.
    
  1. then: 0 <% if readonly %>
  2. <%= content_tag(:span, readonly_display_text, class: "lui-input__value") %>
  3. else: 0 <% else %>
  4. <div class="<%= wrapper_classes %>">
  5. then: 0 else: 0 <% if label.present? %>
  6. <%= label_tag combobox_field_id, class: "text-sm font-medium text-content" do %>
  7. <%= label %>
  8. then: 0 else: 0 <% if required %>
  9. <span class="text-semantic-error-default" aria-hidden="true">*</span>
  10. <% end %>
  11. <% end %>
  12. <% end %>
  13. then: 0 else: 0 <% if description.present? %>
  14. <p id="<%= description_dom_id %>" class="copy-12 text-foreground">
  15. <%= description %>
  16. </p>
  17. <% end %>
  18. then: 0 <% if inline_side_actions? %>
  19. <%= tag.div(
  20. class: "lui-inner-input lui-combobox__inline-row relative flex w-full gap-2 items-center",
  21. data: {
  22. controller: "input",
  23. input_open_actions_value: open_actions,
  24. input_original_input_value: inline_original_input_value,
  25. input_mode_value: "inline",
  26. input_form_value: form,
  27. },
  28. ) do %>
  29. <div class="min-w-0 w-full">
  30. <%= select_tag(
  31. name,
  32. options_for_select(pairs_for_options_helper, selected_for_options_helper),
  33. merged_select_attributes,
  34. ) %>
  35. </div>
  36. <span class="lui-inner-input__actions opacity-0 flex items-center gap-1 h-fit shrink-0" data-input-target="actions">
  37. <%= render LooposUi::Button.new(
  38. leading_icon: "fa-regular fa-xmark",
  39. kind: :neutral,
  40. type: :secondary,
  41. size: :tiny,
  42. tag_options: {
  43. data: {
  44. "input-target": "cancel",
  45. action: "click->input#handleClose",
  46. },
  47. type: :button,
  48. disabled: true,
  49. },
  50. ) %>
  51. <%= render LooposUi::Button.new(
  52. leading_icon: "fa-regular fa-check",
  53. kind: :success,
  54. type: :secondary,
  55. size: :tiny,
  56. tag_options: {
  57. form: form,
  58. type: "submit",
  59. data: {
  60. "input-target": "submit",
  61. action: "click->input#setLoading",
  62. },
  63. disabled: true,
  64. },
  65. ) %>
  66. </span>
  67. <% end %>
  68. else: 0 <% else %>
  69. <%= select_tag(
  70. name,
  71. options_for_select(pairs_for_options_helper, selected_for_options_helper),
  72. merged_select_attributes,
  73. ) %>
  74. <% end %>
  75. then: 0 else: 0 <% if help.present? && error.blank? %>
  76. <%= content_tag(:span, help, class: "lui-input__help") %>
  77. <% end %>
  78. then: 0 else: 0 <% if error.present? %>
  79. <%= content_tag(:span, error, class: "lui-input__error") %>
  80. <% end %>
  81. </div>
  82. <% end %>

app/components/loopos_ui/inputs/date.rb

86.67% lines covered

0.0% branches covered

15 relevant lines. 13 lines covered and 2 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Inputs
  3. 1 class Date < LoopComponent
  4. 1 include BaseInputConcern
  5. 1 option :is_range, default: -> { false }
  6. 1 option :kind, default: -> { "DatePicker" }
  7. 1 option :exclude, default: -> { false }
  8. 1 option :limit_min, default: -> { nil }
  9. 1 option :lang, default: -> { "pt" }
  10. 1 option :variant, default: -> { "core" }
  11. GRANULARITY = {
  12. 1 DatePicker: "date",
  13. DateTimePicker: "date_time",
  14. YearPicker: "year",
  15. MonthYearPicker: "month_year",
  16. TimePicker: "time",
  17. }
  18. 1 def classes
  19. then: 0 else: 0 "lui-input lui-input--#{error.present? ? "with-error" : ""}"
  20. end
  21. 1 def input_attributes
  22. base_input_attributes.merge({
  23. input_attributes: {
  24. is_range: is_range,
  25. kind: kind,
  26. exclude: exclude,
  27. limit_min: limit_min,
  28. lang: lang,
  29. variant: variant,
  30. data: {
  31. "date-input-target": "input",
  32. },
  33. },
  34. })
  35. end
  36. end
  37. end
  38. end

app/components/loopos_ui/inputs/date/date.html.erb

0.0% lines covered

0.0% branches covered

11 relevant lines. 0 lines covered and 11 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <div
  2. id="<%= unique_id %>"
  3. data-controller="date-input"
  4. data-date-input-name-value="<%= name %>"
  5. data-date-input-mode-value="<%= mode %>"
  6. data-date-input-input-outlet="<%="##{unique_id} .lui-inner-input" %>"
  7. class="lui-date lui-date--<%= resolved_appearance %> relative"
  8. >
  9. <%= render LooposUi::Input.new(**input_attributes) do %>
  10. then: 0 else: 0 <% if !readonly %>
  11. <%= react_component("DatetimePicker", {
  12. name: name,
  13. selectedDate: value,
  14. kind: kind,
  15. isRange: is_range,
  16. placeholder: placeholder,
  17. inputClass: "#{classes}",
  18. variant: variant,
  19. granularity: GRANULARITY[kind.to_sym],
  20. lang: lang
  21. }) %>
  22. <% end %>
  23. <% end %>
  24. </div>

app/components/loopos_ui/inputs/file.rb

42.86% lines covered

0.0% branches covered

70 relevant lines. 30 lines covered and 40 lines missed.
36 total branches, 0 branches covered and 36 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Inputs
  3. 1 class File < LoopComponent
  4. 1 module FileInputConcern
  5. 1 extend ActiveSupport::Concern
  6. 1 include DirectUploadConcern
  7. 1 included do
  8. 4 option :name, optional: true
  9. 4 option :mode, Types::Symbol.enum(:inline, :form, :autosubmit), default: -> { :inline }
  10. 4 option :model, Types::Instance(ActiveRecord::Base), optional: true
  11. 4 option :attribute, Types::Coercible::Symbol, optional: true
  12. 4 option :multiple, default: -> { false }, type: Types::Bool
  13. 4 option :delete_path, Types::Coercible::String, optional: true
  14. 4 option :accept,
  15. Types::Array.of(Types::Coercible::String) | Types::Symbol.enum(:all),
  16. optional: true,
  17. default: -> { LooposUi.config.file_default_accept_formats }
  18. 4 option :form_id, Types::Coercible::String, optional: true
  19. # Update context can be used to render the component differently when streaming an update, for example
  20. 4 option :context, Types::Symbol.enum(:default, :action), default: -> { :default }
  21. end
  22. end
  23. 1 include FileInputConcern
  24. # Pre-fill value for display and hidden signed_id inputs.
  25. # Pass a single signed_id string or an array of signed_ids (e.g. from Core protocol answers).
  26. # URL strings are accepted for display-only (they are excluded from hidden inputs).
  27. 1 option :value, Types::Array | Types::Coercible::String, optional: true
  28. 1 option :readonly, default: -> { false }, type: Types::Bool
  29. 1 option :error, optional: true
  30. 1 option :help, optional: true
  31. 1 def initialize(*params, **data)
  32. super
  33. validate_input_identity!
  34. validate_delete_path_requirement!
  35. validate_direct_upload_requirements!
  36. end
  37. 1 def call
  38. mimefy_accept_types
  39. then: 0 if mode == :form
  40. else: 0 render(LooposUi::Inputs::File::FormFile.new(**args))
  41. then: 0 elsif multiple?
  42. render(LooposUi::Inputs::File::Multiple.new(**args))
  43. else: 0 else
  44. render(LooposUi::Inputs::File::Single.new(**args.except(:context)))
  45. end
  46. end
  47. 1 private
  48. 1 def validate_input_identity!
  49. then: 0 else: 0 return if mode == :form
  50. then: 0 else: 0 return if attribute.present? || name.present?
  51. raise ArgumentError, "attribute or name are required when mode is not :form"
  52. end
  53. 1 def validate_delete_path_requirement!
  54. then: 0 else: 0 return if mode == :form || readonly || direct_upload?
  55. else: 0 then: 0 return unless model.present? && attribute.present?
  56. then: 0 else: 0 return if delete_path.present?
  57. raise ArgumentError, "delete_path is required for editable model-backed file inputs"
  58. end
  59. 1 def validate_direct_upload_requirements!
  60. else: 0 then: 0 return unless direct_upload?
  61. then: 0 else: 0 return if name.present? || (model.present? && attribute.present?)
  62. raise ArgumentError, "name or model+attribute is required when direct_upload_url is set"
  63. end
  64. 1 def multiple?
  65. then: 0 else: 0 return multiple if name.present?
  66. model.class.reflect_on_attachment(attribute).macro == :has_many_attached
  67. end
  68. 1 def mimefy_accept_types
  69. then: 0 else: 0 if accept == :all
  70. @accept = nil
  71. return
  72. end
  73. @accept = accept.flat_map do |mime|
  74. text = mime.to_s.strip
  75. then: 0 else: 0 next if text.blank?
  76. then: 0 else: 0 next text if text.include?("/") || text.start_with?(".")
  77. extension = ".#{text}"
  78. [extension, *::MIME::Types.type_for(extension).map(&:content_type)]
  79. end.compact_blank.uniq
  80. end
  81. 1 def args
  82. self.class.dry_initializer.definitions.keys.filter_map do |key|
  83. val = send(key)
  84. then: 0 else: 0 next [key, val] if val.present?
  85. then: 0 else: 0 next [key, val] if key == :value && !val.nil?
  86. then: 0 else: 0 next [key, val] if key == :readonly && val
  87. # Boolean false values that must be forwarded explicitly to sub-components.
  88. then: 0 else: 0 next [key, val] if key == :submit_on_upload && val == false
  89. then: 0 else: 0 next [key, val] if key == :validate_on_upload && val
  90. nil
  91. end.to_h
  92. end
  93. end
  94. end
  95. end

app/components/loopos_ui/inputs/file/direct_upload_concern.rb

30.93% lines covered

0.0% branches covered

97 relevant lines. 30 lines covered and 67 lines missed.
54 total branches, 0 branches covered and 54 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "cgi"
  3. 1 module LooposUi
  4. 1 module Inputs
  5. 1 class File
  6. 1 module DirectUploadConcern
  7. 1 extend ActiveSupport::Concern
  8. 1 ACTIVE_STORAGE_SIGNED_ID_PATTERN = %r{
  9. eyJ[A-Za-z0-9+\/=_-]+
  10. --[a-f0-9]+
  11. }x
  12. 1 included do
  13. # Override Active Storage direct upload target (e.g. Core app URL for protocol forms).
  14. 4 option :direct_upload_url, Types::Coercible::String.optional, optional: true
  15. # Authorization header when uploading to a remote host (e.g. Core API bearer token).
  16. 4 option :direct_upload_authorization, Types::Coercible::String.optional, optional: true
  17. # When false, upload completion does NOT call requestSubmit() on the linked form.
  18. # Set to false in protocol forms where submission is driven externally once all answers
  19. # are filled. Defaults to true to preserve existing standalone upload-and-save behaviour.
  20. 4 option :submit_on_upload, default: -> { true }, type: Types::Bool
  21. # When true, standalone uploaders linked to an outer form call requestSubmit() after
  22. # upload/removal so protocol validation (e.g. min_files) runs immediately — mirroring
  23. # input:images list_view behaviour while keeping submit_on_upload false.
  24. 4 option :validate_on_upload, default: -> { false }, type: Types::Bool
  25. end
  26. 1 def direct_upload?
  27. direct_upload_url.present?
  28. end
  29. 1 def standalone_direct_upload?
  30. direct_upload? && model.blank?
  31. end
  32. 1 def effective_direct_upload_url
  33. direct_upload_url.presence || active_storage_direct_upload_path
  34. end
  35. 1 def active_storage_direct_upload_path
  36. Rails.application.routes.url_helpers.rails_direct_uploads_path
  37. rescue ActionController::UrlGenerationError, NoMethodError
  38. "#{ActiveStorage.routes_prefix.to_s.chomp("/")}/direct_uploads"
  39. end
  40. 1 def existing_signed_ids
  41. then: 0 else: 0 return normalize_signed_id_list(value) if value.present?
  42. else: 0 then: 0 return [] unless model.present? && attribute.present?
  43. attachment = model.send(attribute)
  44. then: 0 if attachment.respond_to?(:attachments)
  45. else: 0 attachment.attachments.map { |a| a.blob.signed_id }
  46. then: 0 elsif attachment.respond_to?(:signed_id)
  47. then: 0 else: 0 attachment.attached? ? [attachment.signed_id] : []
  48. else: 0 else
  49. []
  50. end
  51. end
  52. 1 def existing_file_names
  53. else: 0 then: 0 return [] unless model.present? && attribute.present?
  54. attachment = model.send(attribute)
  55. then: 0 if attachment.respond_to?(:attachments)
  56. attachment.attachments.map { |a| a.filename.to_s }
  57. else: 0 else
  58. []
  59. end
  60. end
  61. 1 def standalone_file_entries
  62. else: 0 then: 0 return [] unless standalone_direct_upload?
  63. then: 0 else: 0 entries = Array(value.is_a?(Array) ? value : [value]).filter_map do |entry|
  64. then: 0 if entry.respond_to?(:to_hash)
  65. h = entry.to_hash
  66. then: 0 else: 0 h = h.with_indifferent_access if h.respond_to?(:with_indifferent_access)
  67. candidate = (h[:signed_id].presence || h[:value].presence).to_s
  68. signed_ids =
  69. then: 0 if candidate.match?(ACTIVE_STORAGE_SIGNED_ID_PATTERN)
  70. [candidate]
  71. else: 0 else
  72. candidate.scan(ACTIVE_STORAGE_SIGNED_ID_PATTERN)
  73. end
  74. then: 0 else: 0 next if signed_ids.blank?
  75. signed_ids.map do |signed_id|
  76. filename = h[:filename].to_s.presence || blob_filename_from_url(h[:url].to_s)
  77. { signed_id: signed_id, filename: filename, url: h[:url].to_s.presence }
  78. end
  79. else: 0 else
  80. signed_ids = entry.to_s.scan(ACTIVE_STORAGE_SIGNED_ID_PATTERN)
  81. then: 0 else: 0 next if signed_ids.blank?
  82. signed_ids.map { |signed_id| { signed_id: signed_id, filename: nil } }
  83. end
  84. end.flatten
  85. entries.uniq { |e| e[:signed_id] }
  86. end
  87. 1 def upload_context_field_name?
  88. else: 0 then: 0 return false unless direct_upload?
  89. !(standalone_direct_upload? && form_id.present?)
  90. end
  91. 1 def upload_context_input_attributes
  92. {
  93. type: "hidden",
  94. autocomplete: "off",
  95. value: encrypted_upload_context,
  96. data: { "input-file-target": "uploadContext" },
  97. }.tap do |attrs|
  98. then: 0 else: 0 attrs[:name] = "context" if upload_context_field_name?
  99. then: 0 else: 0 attrs[:form] = form_id if form_id.present?
  100. end
  101. end
  102. 1 def encrypted_upload_context
  103. LooposUi::EncryptionService.encrypt(upload_context_payload)
  104. end
  105. 1 def upload_context_payload
  106. then: 0 if model.present? && attribute.present?
  107. {
  108. model_class: model.class.name,
  109. model_id: model.id,
  110. attribute: attribute,
  111. accept: accept_attribute,
  112. }.compact
  113. else: 0 else
  114. { accept: accept_attribute }.compact
  115. end
  116. end
  117. 1 def file_input_submit_action
  118. "change->input-file#upload"
  119. end
  120. 1 def input_file_stimulus_values
  121. {
  122. controller: "input-file",
  123. "input-file-form-value": form_id,
  124. "input-file-delete-path-value": delete_path,
  125. "input-file-direct-upload-value": direct_upload?,
  126. "input-file-direct-upload-authorization-value": direct_upload_authorization.to_s,
  127. "input-file-standalone-value": standalone_direct_upload?,
  128. "input-file-field-name-value": input_name,
  129. "input-file-submit-on-upload-value": submit_on_upload,
  130. "input-file-validate-on-upload-value": validate_on_upload,
  131. }.compact
  132. end
  133. 1 def file_input_data_attributes
  134. base = {
  135. "input-file-target": "file",
  136. action: file_input_submit_action,
  137. }
  138. else: 0 then: 0 return base unless direct_upload?
  139. base.merge(direct_upload_url: effective_direct_upload_url)
  140. end
  141. # File binary is not submitted; hidden inputs carry signed_id(s) for the outer form.
  142. 1 def file_input_name_attribute
  143. then: 0 else: 0 direct_upload? ? nil : input_name
  144. end
  145. 1 private
  146. 1 def blob_filename_from_url(url)
  147. then: 0 else: 0 return if url.blank?
  148. path = url.to_s.split(/[?#]/, 2).first.to_s
  149. basename = ::File.basename(CGI.unescape(path))
  150. then: 0 else: 0 return if basename.blank? || basename == "."
  151. basename
  152. end
  153. 1 def normalize_signed_id_list(raw)
  154. then: 0 else: 0 list = raw.is_a?(Array) ? raw.flatten : [raw]
  155. list.flat_map do |entry|
  156. then: 0 if entry.respond_to?(:to_hash)
  157. h = entry.to_hash
  158. then: 0 else: 0 h = h.with_indifferent_access if h.respond_to?(:with_indifferent_access)
  159. candidate = (h[:signed_id].presence || h[:value].presence).to_s
  160. then: 0 if candidate.match?(ACTIVE_STORAGE_SIGNED_ID_PATTERN)
  161. [candidate]
  162. else: 0 else
  163. candidate.scan(ACTIVE_STORAGE_SIGNED_ID_PATTERN)
  164. end
  165. else: 0 else
  166. text = entry.to_s
  167. then: 0 else: 0 next [] if text.match?(%r{\Ahttps?://}i)
  168. text.scan(ACTIVE_STORAGE_SIGNED_ID_PATTERN)
  169. end
  170. end.compact_blank.uniq
  171. end
  172. end
  173. end
  174. end
  175. end

app/components/loopos_ui/inputs/file/file_actions.html.erb

0.0% lines covered

100.0% branches covered

8 relevant lines. 0 lines covered and 8 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div(class: "lui-input-file__actions lui-action-menu__option") do %>
  2. <%= render LooposUi::Button.new(
  3. icon: :file_arrow_down,
  4. size: :small,
  5. type: :tertiary,
  6. kind: :neutral,
  7. tooltip_text: t('.download_file'),
  8. href: attachment.url,
  9. tag_options: {
  10. target: "_blank",
  11. rel: "noopener noreferrer",
  12. data: { turbo: false }
  13. })%>
  14. <%= render LooposUi::Modal.new(title: t('.delete_file')) do |modal| %>
  15. <% modal.with_trigger_button(
  16. icon: :trash,
  17. size: :small,
  18. type: :tertiary,
  19. kind: :neutral,
  20. tooltip_text: t('.delete_file'))%>
  21. <% modal.with_custom_content do %>
  22. <p><%= t('.delete_file_warning') %></p>
  23. <% end %>
  24. <% modal.with_primary_action(
  25. text: t('.delete'),
  26. kind: :danger,
  27. tag_options: {
  28. type: :button,
  29. data: delete_data
  30. })%>
  31. <% end %>
  32. <% end %>

app/components/loopos_ui/inputs/file/file_actions.rb

100.0% lines covered

0.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Inputs
  3. 1 class File
  4. 1 class FileActions < LoopComponent
  5. 1 param :attachment, Types.Instance(ActiveStorage::Attachment) | Types.Instance(ActiveStorage::Attached::One)
  6. 1 option :delete_data, Types::Coercible::Hash, default: -> { {} }
  7. 1 then: 0 else: 0 delegate :t, to: LooposUi::Inputs::File
  8. end
  9. end
  10. end
  11. end

app/components/loopos_ui/inputs/file/form_file.html.erb

0.0% lines covered

0.0% branches covered

32 relevant lines. 0 lines covered and 32 lines missed.
10 total branches, 0 branches covered and 10 branches missed.
    
  1. <%= tag.div(
  2. class: [
  3. "lui-input-file-wrapper",
  4. then: 0 else: 0 ("lui-input-file-wrapper--with-error" if error.present?),
  5. ].compact.join(" "),
  6. ) do %>
  7. <% file_attributes = form_file_input_attributes %>
  8. then: 0 <% if readonly %>
  9. <div class="lui-input-file-form lui-input-file-form--readonly" id="div_<%= form_file_input_id %>">
  10. <span
  11. class="lui-input-file-form__file-name"
  12. id="span_<%= form_file_input_id %>"
  13. >
  14. <%= initial_form_file_display_name %>
  15. </span>
  16. else: 0 </div>
  17. then: 0 <% elsif direct_upload? %>
  18. <%= tag.div(
  19. class: "lui-input-file-form",
  20. id: "div_#{form_file_input_id}",
  21. data: input_file_stimulus_values,
  22. ) do %>
  23. <%= tag.input(**upload_context_input_attributes) %>
  24. <%= signed_id_hidden_fields %>
  25. <%= tag.input(**file_attributes) %>
  26. <%= render LooposUi::Button.new(
  27. kind: :neutral,
  28. type: :secondary,
  29. size: :tiny,
  30. text: t('.select_file'),
  31. icon: "fa-regular fa-file-arrow-up",
  32. tag: :label,
  33. tag_options: {
  34. for: file_attributes[:id],
  35. id: "#{file_attributes[:id]}_label",
  36. data: {
  37. "input-file-target": "button",
  38. "select-text": t('.select_file'),
  39. "replace-text": t('.replace_file'),
  40. },
  41. },
  42. ) %>
  43. <span
  44. data-input-file-target="fileName"
  45. data-no-file-label="<%= t('.no_file_chosen') %>"
  46. data-file-chosen-label="<%= t('.file_chosen') %>"
  47. data-files-chosen-template="<%= t('.files_chosen', count: '__COUNT__') %>"
  48. class="lui-input-file-form__file-name"
  49. id="span_<%= file_attributes[:id] %>"
  50. >
  51. <%= initial_form_file_display_name %>
  52. </span>
  53. <% end %>
  54. else: 0 <% else %>
  55. <div
  56. class="lui-input-file-form"
  57. data-controller="form-file"
  58. data-form-file-form-id-value="<%= form_id.to_s %>"
  59. id="div_<%= file_attributes[:id] %>"
  60. >
  61. <%= tag.input(
  62. **file_attributes.merge(
  63. class: "lui-input-file-form__input sr-only"
  64. )
  65. ) %>
  66. <%= render LooposUi::Button.new(
  67. kind: :neutral,
  68. type: :secondary,
  69. size: :tiny,
  70. text: t('.select_file'),
  71. icon: "fa-regular fa-file-arrow-up",
  72. tag: :label,
  73. tag_options: {
  74. for: file_attributes[:id],
  75. id: "#{file_attributes[:id]}_label",
  76. data: {
  77. form_file_target: "button",
  78. select_text: t('.select_file'),
  79. replace_text: t('.replace_file')
  80. }
  81. }
  82. ) %>
  83. <span
  84. data-form-file-target="fileName"
  85. class="lui-input-file-form__file-name"
  86. id="span_<%= file_attributes[:id] %>"
  87. >
  88. <%= initial_form_file_display_name %>
  89. </span>
  90. </div>
  91. <% end %>
  92. then: 0 else: 0 <%= content_tag(:span, help, class: "lui-input__help") if help.present? && error.blank? %>
  93. then: 0 else: 0 <%= content_tag(:span, error, class: "lui-input__error") if error.present? %>
  94. <% end %>

app/components/loopos_ui/inputs/file/form_file.rb

38.89% lines covered

0.0% branches covered

36 relevant lines. 14 lines covered and 22 lines missed.
22 total branches, 0 branches covered and 22 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Inputs
  3. 1 class File
  4. 1 class FormFile < LoopComponent
  5. 1 include BaseInputConcern
  6. 1 include FileInputConcern
  7. 1 then: 0 else: 0 delegate :t, to: LooposUi::Inputs::File
  8. 1 def file
  9. then: 0 else: 0 model&.send(attribute)
  10. end
  11. 1 def form_file_input_id
  12. "#{unique_id}-file-input"
  13. end
  14. 1 def input_name
  15. then: 0 if model.present? && attribute.present?
  16. else: 0 field_name(model, attribute, multiple: false)
  17. then: 0 elsif name.present?
  18. name
  19. else: 0 else
  20. raise ArgumentError, "You must provide either model and attribute, or a name"
  21. end
  22. end
  23. 1 def form_file_input_attributes
  24. then: 0 if direct_upload?
  25. {
  26. type: "file",
  27. id: form_file_input_id,
  28. name: file_input_name_attribute,
  29. # multiple must be set so the JS controller routes to directUploadMultiple,
  30. # which appends signed_id fields rather than replacing them (preserving pre-fill).
  31. multiple: multiple || nil,
  32. data: file_input_data_attributes,
  33. class: "lui-input-file-form__input sr-only",
  34. form: form_id,
  35. accept: accept_attribute,
  36. }.compact
  37. else: 0 else
  38. {
  39. type: "file",
  40. id: form_file_input_id,
  41. name: input_name,
  42. "data-form-file-target": "input",
  43. "data-action": "change->form-file#updateFileName",
  44. class: "lui-input-file-form__input",
  45. form: form_id,
  46. accept: accept_attribute,
  47. }.compact
  48. end
  49. end
  50. 1 def accept_attribute
  51. then: 0 else: 0 return if accept.blank?
  52. then: 0 else: 0 accept.is_a?(Array) ? accept.join(",") : accept
  53. end
  54. 1 def signed_id_hidden_fields
  55. existing_signed_ids.map do |signed_id|
  56. tag.input(
  57. type: "hidden",
  58. name: input_name,
  59. value: signed_id,
  60. form: form_id,
  61. data: { "input-file-target": "signedIdField" },
  62. )
  63. end.join.html_safe
  64. end
  65. # Initial label in the UI (file inputs cannot set a value).
  66. # When value is an array of signed_ids (protocol form pre-fill), shows a file count.
  67. 1 def initial_form_file_display_name
  68. then: 0 else: 0 return t(".no_file_chosen") if value.blank?
  69. then: 0 else: 0 if value.is_a?(Array)
  70. count = value.size
  71. then: 0 else: 0 return t(".no_file_chosen") if count.zero?
  72. then: 0 else: 0 return t(".file_chosen") if count == 1
  73. return t(".files_chosen", count: count)
  74. end
  75. path = value.to_s.split(/[?#]/, 2).first.to_s
  76. ::File.basename(path)
  77. end
  78. end
  79. end
  80. end
  81. end

app/components/loopos_ui/inputs/file/multiple.html.erb

0.0% lines covered

0.0% branches covered

53 relevant lines. 0 lines covered and 53 lines missed.
26 total branches, 0 branches covered and 26 branches missed.
    
  1. <%= tag.div(
  2. class: [
  3. "lui-input-file-wrapper",
  4. then: 0 else: 0 ("lui-input-file-wrapper--with-error" if error.present?),
  5. ].compact.join(" "),
  6. ) do %>
  7. then: 0 <% if readonly %>
  8. <%= tag.div(
  9. class: "lui-input-file-container lui-input-file-container--readonly",
  10. id: unique_id,
  11. ) do %>
  12. <div class="lui-input-file lui-input-file--readonly">
  13. <div class="flex justify-end items-center gap-[2px] min-w-0">
  14. <%= tag.span(class: "lui-input-file__text") do %>
  15. then: 0 <% if has_uploaded_files? %>
  16. <%= t('.files_uploaded', file_count: uploaded_files_count) %>
  17. else: 0 <% else %>
  18. <%= t('.no_file_chosen') %>
  19. <% end %>
  20. <% end %>
  21. </div>
  22. </div>
  23. <% end %>
  24. else: 0 <% else %>
  25. <%= tag.div(
  26. class: "lui-input-file-container",
  27. id: unique_id,
  28. data: input_file_stimulus_values.merge(action: "click->input-file#toggleActions"),
  29. ) do %>
  30. then: 0 else: 0 <%= tag.input(**upload_context_input_attributes) if direct_upload? %>
  31. then: 0 else: 0 <%= signed_id_hidden_fields if direct_upload? %>
  32. then: 0 else: 0 <%= file_name_hidden_fields if direct_upload? %>
  33. <div class="lui-input-file">
  34. <%= render LooposUi::Button.new(
  35. kind: :neutral,
  36. type: :secondary,
  37. size: :tiny,
  38. text: t('.select_files'),
  39. icon: "fa-regular fa-file-arrow-up",
  40. tag: :label,
  41. tag_options: { for: "#{unique_id}-file-input" },
  42. ) %>
  43. <%= tag.input(**file_input_attributes) %>
  44. <div class="flex justify-end items-center gap-[2px] min-w-0">
  45. <%= tag.span(
  46. class: "lui-input-file__text",
  47. data: {
  48. "input-file-target": "uploadedLabel",
  49. no_file_label: t('.no_file_chosen'),
  50. files_uploaded_template: t('.files_uploaded', file_count: "__COUNT__"),
  51. },
  52. ) do %>
  53. then: 0 <% if has_uploaded_files? %>
  54. <%= t('.files_uploaded', file_count: uploaded_files_count) %>
  55. else: 0 <% else %>
  56. <%= t('.no_file_chosen') %>
  57. <% end %>
  58. <% end %>
  59. then: 0 else: 0 <% if can_manage_uploaded_files? %>
  60. <div
  61. class="lui-input-file__actions"
  62. data-input-file-target="fileActions"
  63. else: 0 then: 0 <%= "hidden" unless has_uploaded_files? %>
  64. >
  65. <%= render LooposUi::Popover.new(
  66. open: context == :action,
  67. position: :bottom_right,
  68. anchor: :top_right,
  69. anchor_selector: "##{unique_id}",
  70. rotate_toggle: true,
  71. system_arguments: {
  72. data: {
  73. "input-file-target": "popover",
  74. },
  75. },
  76. ) do |popover| %>
  77. <% popover.with_custom_toggle do %>
  78. <i class="fa-regular fa-chevron-down text-[10px]"></i>
  79. <% end %>
  80. <% popover.with_target do %>
  81. <div class="lui-input-files lui-action-menu" data-input-file-target="fileList">
  82. then: 0 <% if standalone_direct_upload? %>
  83. <% standalone_file_entries.each_with_index do |entry, index| %>
  84. <div class="lui-input-files__item lui-action-menu__option" data-signed-id="<%= entry[:signed_id] %>">
  85. <%= tag.span(standalone_entry_label(entry, index), class: "lui-input-files__item-name") %>
  86. <div class="lui-input-files__item__icons">
  87. then: 0 else: 0 <% if entry[:url].present? %>
  88. <%= render LooposUi::Button.new(
  89. icon: "fa-regular fa-file-arrow-down",
  90. size: :small,
  91. type: :tertiary,
  92. kind: :neutral,
  93. tooltip_text: t('.download_file'),
  94. href: entry[:url],
  95. tag_options: {
  96. target: "_blank",
  97. rel: "noopener noreferrer",
  98. data: { turbo: false },
  99. },
  100. ) %>
  101. <% end %>
  102. <button
  103. type="button"
  104. class="lui-button lui-button--neutral--tertiary lui-button--small"
  105. data-action="click->input-file#removeSignedId"
  106. data-input-file-signed-id-param="<%= entry[:signed_id] %>"
  107. title="<%= t('.delete_file') %>"
  108. >
  109. <i class="fa-regular fa-trash"></i>
  110. </button>
  111. </div>
  112. </div>
  113. <% end %>
  114. else: 0 <% else %>
  115. <% files.each do |file| %>
  116. <%= render LooposUi::Inputs::File::Option.new(
  117. file,
  118. delete_data: {
  119. action: "click->input-file#destroy",
  120. "input-file-attachment-signed-id-param": file.signed_id,
  121. },
  122. ) %>
  123. <% end %>
  124. <% end %>
  125. </div>
  126. <% end %>
  127. <% end %>
  128. </div>
  129. <% end %>
  130. <span class="lui-input-file__spinner">
  131. <%= tag.i(class: "fa-regular fa-spinner") %>
  132. </span>
  133. </div>
  134. </div>
  135. <% end %>
  136. <% end %>
  137. then: 0 else: 0 <%= content_tag(:span, help, class: "lui-input__help") if help.present? && error.blank? %>
  138. then: 0 else: 0 <%= content_tag(:span, error, class: "lui-input__error") if error.present? %>
  139. <% end %>

app/components/loopos_ui/inputs/file/multiple.rb

37.5% lines covered

0.0% branches covered

56 relevant lines. 21 lines covered and 35 lines missed.
34 total branches, 0 branches covered and 34 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Inputs
  3. 1 class File
  4. 1 class Multiple < LoopComponent
  5. 1 include BaseInputConcern
  6. 1 include FileInputConcern
  7. # BaseInputConcern defines value as Coercible::String, which turns [] into "[]".
  8. 1 option :value, Types::Array | Types::Coercible::String, optional: true
  9. 1 then: 0 else: 0 delegate :t, to: LooposUi::Inputs::File
  10. 1 def files
  11. then: 0 else: 0 model&.send(attribute) || []
  12. end
  13. 1 def input_name
  14. then: 0 if model.present? && attribute.present?
  15. else: 0 field_name(model, attribute, multiple: true)
  16. then: 0 elsif name.present?
  17. name
  18. else: 0 else
  19. raise ArgumentError, "You must provide either model and attribute, or a name"
  20. end
  21. end
  22. 1 def uploaded_files_count
  23. then: 0 else: 0 return files.count if files.present?
  24. then: 0 else: 0 return standalone_file_entries.count if standalone_direct_upload?
  25. normalized = normalized_file_value
  26. then: 0 else: 0 return 0 if normalized.blank?
  27. then: 0 else: 0 return normalized.size if normalized.is_a?(Array)
  28. 1
  29. end
  30. 1 def has_uploaded_files?
  31. uploaded_files_count.positive?
  32. end
  33. 1 def can_manage_uploaded_files?
  34. # Standalone direct upload (protocol) needs actions/list shell rendered even
  35. # when starting empty, so JS can update it immediately after upload.
  36. then: 0 else: 0 return true if standalone_direct_upload?
  37. files.present? && delete_path.present?
  38. end
  39. 1 def standalone_entry_label(entry, index)
  40. entry[:filename].presence || "File #{index + 1}"
  41. end
  42. 1 def accept_attribute
  43. then: 0 else: 0 return if accept.blank?
  44. then: 0 else: 0 accept.is_a?(Array) ? accept.join(",") : accept
  45. end
  46. 1 def file_input_attributes
  47. {
  48. multiple: true,
  49. name: file_input_name_attribute,
  50. data: file_input_data_attributes,
  51. id: "#{unique_id}-file-input",
  52. class: "hidden",
  53. type: "file",
  54. form: form_id,
  55. accept: accept_attribute,
  56. }.compact
  57. end
  58. 1 def signed_id_hidden_fields
  59. then: 0 if standalone_direct_upload?
  60. standalone_file_entries.map do |entry|
  61. tag.input(
  62. type: "hidden",
  63. name: input_name,
  64. value: entry[:signed_id],
  65. form: form_id,
  66. data: { "input-file-target": "signedIdField" },
  67. )
  68. end.join.html_safe
  69. else: 0 else
  70. existing_signed_ids.map do |signed_id|
  71. tag.input(
  72. type: "hidden",
  73. name: input_name,
  74. value: signed_id,
  75. form: form_id,
  76. data: { "input-file-target": "signedIdField" },
  77. )
  78. end.join.html_safe
  79. end
  80. end
  81. 1 def file_name_hidden_fields
  82. then: 0 else: 0 file_name_field_name = input_name&.gsub("[values][]", "[file_names][]")
  83. then: 0 if standalone_direct_upload?
  84. standalone_file_entries.map do |entry|
  85. tag.input(
  86. type: "hidden",
  87. name: file_name_field_name,
  88. value: entry[:filename],
  89. )
  90. end.join.html_safe
  91. else: 0 else
  92. existing_file_names.map do |file_name|
  93. tag.input(
  94. type: "hidden",
  95. name: file_name_field_name,
  96. value: file_name,
  97. )
  98. end.join.html_safe
  99. end
  100. end
  101. 1 def input_file_stimulus_values
  102. super.merge("input-file-multiple-value": true)
  103. end
  104. 1 private
  105. 1 def normalized_file_value
  106. then: 0 else: 0 return [] if value.nil?
  107. then: 0 else: 0 return value if value.is_a?(Array)
  108. then: 0 else: 0 return [] if value.to_s == "[]"
  109. value
  110. end
  111. end
  112. end
  113. end
  114. end

app/components/loopos_ui/inputs/file/option.html.erb

0.0% lines covered

100.0% branches covered

3 relevant lines. 0 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%# FIXME: LOOPOS-32453 using styles from action menu, because it's not yet migrated to the popover API we need here %>
  2. <div class="lui-input-files__item lui-action-menu__option">
  3. <%= tag.span(attachment.filename, class: "lui-input-files__item-name") %>
  4. <div class="lui-input-files__item__icons">
  5. <%= render LooposUi::Inputs::File::FileActions.new(attachment, delete_data: delete_data) %>
  6. </div>
  7. </div>

app/components/loopos_ui/inputs/file/option.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Inputs
  3. 1 class File
  4. 1 class Option < LoopComponent
  5. 1 param :attachment, Types.Instance(ActiveStorage::Attachment)
  6. 1 option :delete_data, Types::Coercible::Hash, default: -> { {} }
  7. end
  8. end
  9. end
  10. end

app/components/loopos_ui/inputs/file/single.html.erb

0.0% lines covered

0.0% branches covered

30 relevant lines. 0 lines covered and 30 lines missed.
24 total branches, 0 branches covered and 24 branches missed.
    
  1. <%= tag.div(
  2. class: [
  3. "lui-input-file-wrapper",
  4. then: 0 else: 0 ("lui-input-file-wrapper--with-error" if error.present?),
  5. ].compact.join(" "),
  6. ) do %>
  7. then: 0 <% if readonly %>
  8. <%= tag.div(
  9. class: "lui-input-file-container lui-input-file-container--readonly",
  10. id: unique_id,
  11. ) do %>
  12. <div class="lui-input-file lui-input-file--readonly">
  13. <div class="flex flex-1 min-w-0 justify-end items-center gap-[2px]">
  14. then: 0 else: 0 <%= tag.span(class: ["lui-input-file__text", has_display_file? ? "lui-input-file__text--with_file" : ""]) do %>
  15. then: 0 <% if has_display_file? %>
  16. <%= display_file_name %>
  17. else: 0 <% else %>
  18. <%= t('.no_file_chosen') %>
  19. <% end %>
  20. <% end %>
  21. </div>
  22. </div>
  23. <% end %>
  24. else: 0 <% else %>
  25. <%= tag.div(
  26. class: "lui-input-file-container",
  27. id: unique_id,
  28. data: input_file_stimulus_values,
  29. ) do %>
  30. then: 0 else: 0 <%= tag.input(**upload_context_input_attributes) if direct_upload? %>
  31. then: 0 else: 0 <%= signed_id_hidden_fields if direct_upload? %>
  32. <div class="lui-input-file">
  33. <%= render LooposUi::Button.new(
  34. kind: :neutral,
  35. type: :secondary,
  36. size: :tiny,
  37. then: 0 else: 0 text: has_display_file? ? t('.replace_file') : t('.select_file'),
  38. icon: "fa-regular fa-file-arrow-up",
  39. tag: :label,
  40. tag_options: {
  41. for: "#{unique_id}-file-input",
  42. data: {
  43. "input-file-target": "button",
  44. "select-text": t(".select_file"),
  45. "replace-text": t(".replace_file"),
  46. },
  47. },
  48. ) %>
  49. <%= tag.input(**file_input_attributes) %>
  50. <div class="flex flex-1 min-w-0 justify-end items-center gap-[2px]">
  51. <%= tag.span(
  52. then: 0 else: 0 class: ["lui-input-file__text", has_display_file? ? "lui-input-file__text--with_file" : ""],
  53. data: { "input-file-target": "fileName" },
  54. ) do %>
  55. then: 0 <% if has_display_file? %>
  56. <%= display_file_name %>
  57. else: 0 <% else %>
  58. <%= t('.no_file_chosen') %>
  59. <% end %>
  60. <% end %>
  61. then: 0 else: 0 <%= render LooposUi::Inputs::File::FileActions.new(
  62. file,
  63. delete_data: {
  64. action: "click->input-file#destroy",
  65. "input-file-attachment-signed-id-param": file.signed_id,
  66. },
  67. ) if show_delete_actions? %>
  68. <span class="lui-input-file__spinner">
  69. <%= tag.i(class: "fa-regular fa-spinner") %>
  70. </span>
  71. </div>
  72. </div>
  73. <% end %>
  74. <% end %>
  75. then: 0 else: 0 <%= content_tag(:span, help, class: "lui-input__help") if help.present? && error.blank? %>
  76. then: 0 else: 0 <%= content_tag(:span, error, class: "lui-input__error") if error.present? %>
  77. <% end %>

app/components/loopos_ui/inputs/file/single.rb

46.88% lines covered

0.0% branches covered

32 relevant lines. 15 lines covered and 17 lines missed.
16 total branches, 0 branches covered and 16 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Inputs
  3. 1 class File
  4. 1 class Single < LoopComponent
  5. 1 include BaseInputConcern
  6. 1 include FileInputConcern
  7. 1 then: 0 else: 0 delegate :t, to: LooposUi::Inputs::File
  8. 1 def file
  9. then: 0 else: 0 model&.send(attribute)
  10. end
  11. 1 def input_name
  12. then: 0 if model.present? && attribute.present?
  13. else: 0 field_name(model, attribute, multiple: false)
  14. then: 0 elsif name.present?
  15. name
  16. else: 0 else
  17. raise ArgumentError, "You must provide either model and attribute, or a name"
  18. end
  19. end
  20. 1 def display_file_name
  21. then: 0 else: 0 return file.filename.to_s if file.present?
  22. then: 0 else: 0 return if value.blank?
  23. path = value.to_s.split(/[?#]/, 2).first.to_s
  24. ::File.basename(path)
  25. end
  26. 1 def has_display_file?
  27. display_file_name.present?
  28. end
  29. 1 def show_delete_actions?
  30. file.present? && delete_path.present?
  31. end
  32. 1 def accept_attribute
  33. then: 0 else: 0 return if accept.blank?
  34. then: 0 else: 0 accept.is_a?(Array) ? accept.join(",") : accept
  35. end
  36. 1 def file_input_attributes
  37. {
  38. name: file_input_name_attribute,
  39. data: file_input_data_attributes,
  40. id: "#{unique_id}-file-input",
  41. class: "hidden",
  42. type: "file",
  43. form: form_id,
  44. accept: accept_attribute,
  45. }.compact
  46. end
  47. 1 def signed_id_hidden_fields
  48. existing_signed_ids.map do |signed_id|
  49. tag.input(
  50. type: "hidden",
  51. name: input_name,
  52. value: signed_id,
  53. form: form_id,
  54. data: { "input-file-target": "signedIdField" },
  55. )
  56. end.join.html_safe
  57. end
  58. end
  59. end
  60. end
  61. end

app/components/loopos_ui/inputs/money.rb

66.67% lines covered

0.0% branches covered

18 relevant lines. 12 lines covered and 6 lines missed.
5 total branches, 0 branches covered and 5 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Inputs
  3. 1 class Money < LoopComponent
  4. 1 include BaseInputConcern
  5. 1 option :currency, Types::Coercible::String | Types::Instance(::Money::Currency)
  6. 1 option :min, default: -> { 0 }
  7. 1 option :max, default: -> { nil }
  8. 1 option :step, default: -> { 0.01 }
  9. 1 option :data_attributes, default: -> { {} }
  10. 1 option :autocomplete_type, optional: true
  11. 1 def input_attributes
  12. input_specific_attributes = {
  13. min: min,
  14. max: max,
  15. step: step,
  16. inputmode: "decimal",
  17. pattern: "[0-9]*[.,]?[0-9]*",
  18. data: data_attributes,
  19. }
  20. then: 0 else: 0 input_specific_attributes[:autocomplete] = autocomplete_type if autocomplete_type.present?
  21. base_input_attributes.merge({
  22. input_attributes: input_specific_attributes,
  23. type: :number,
  24. mode: mode,
  25. })
  26. end
  27. 1 def currency_symbol
  28. else: 0 case currency
  29. when: 0 when String
  30. ::Money::Currency.new(currency).symbol
  31. when: 0 when ::Money::Currency
  32. currency.symbol
  33. end
  34. end
  35. end
  36. end
  37. end

app/components/loopos_ui/inputs/money/money.html.erb

0.0% lines covered

100.0% branches covered

7 relevant lines. 0 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <div class="lui-money lui-money--<%= resolved_appearance %> relative" data-controller="money-input">
  2. <%= render LooposUi::Input.new(**input_attributes.merge(
  3. input_attributes: input_attributes[:input_attributes].merge(
  4. data: { "money-input-target": "input" }.merge(input_attributes.dig(:input_attributes, :data).presence || {})
  5. )
  6. )) do |input| %>
  7. <% input.with_left_addon do %>
  8. <span class="lui-money__currency text-gray-700">
  9. <%= currency_symbol %>
  10. </span>
  11. <% end %>
  12. <% end %>
  13. </div>

app/components/loopos_ui/inputs/number.rb

76.92% lines covered

0.0% branches covered

13 relevant lines. 10 lines covered and 3 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Inputs
  3. 1 class Number < LoopComponent
  4. 1 include BaseInputConcern
  5. 1 option :min, default: -> { nil }
  6. 1 option :max, default: -> { nil }
  7. 1 option :step, default: -> { 1 }
  8. 1 option :with_actions, default: -> { true }
  9. 1 option :autocomplete_type, optional: true
  10. 1 def input_attributes
  11. input_specific_attributes = {
  12. min: min,
  13. max: max,
  14. step: step,
  15. inputmode: "decimal",
  16. data: {
  17. "number-input-target": "input",
  18. },
  19. }
  20. then: 0 else: 0 input_specific_attributes[:autocomplete] = autocomplete_type if autocomplete_type.present?
  21. base_input_attributes.merge({
  22. input_attributes: input_specific_attributes,
  23. type: :number,
  24. mode: mode,
  25. })
  26. end
  27. end
  28. end
  29. end

app/components/loopos_ui/inputs/number/number.html.erb

0.0% lines covered

0.0% branches covered

7 relevant lines. 0 lines covered and 7 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <div data-controller="number-input" data-number-input-step-value="<%= step %>" data-number-input-mode-value="<%= mode %>">
  2. <div class="lui-number lui-number--<%= resolved_appearance %> relative">
  3. <%= render LooposUi::Input.new(**input_attributes) do |input| %>
  4. then: 0 else: 0 <% if with_actions %>
  5. <% input.with_right_addon do %>
  6. <div class="flex gap-2">
  7. <i class="fa-regular fa-minus cursor-pointer" data-action="click->number-input#decrease"></i>
  8. <i class="fa-regular fa-plus cursor-pointer" data-action="click->number-input#increase"></i>
  9. </div>
  10. <% end %>
  11. <% end %>
  12. <% end %>
  13. </div>
  14. </div>

app/components/loopos_ui/inputs/phone_number.rb

61.76% lines covered

0.0% branches covered

34 relevant lines. 21 lines covered and 13 lines missed.
14 total branches, 0 branches covered and 14 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Inputs
  3. 1 class PhoneNumber < LoopComponent
  4. 1 include BaseInputConcern
  5. 1 PLACEHOLDER_NUMBER_TYPES = ["MOBILE", "FIXED_LINE", "FIXED_LINE_OR_MOBILE"].freeze
  6. 1 option :autocomplete_type, default: -> { "off" }
  7. 1 option :initial_country, default: -> { "pt" }
  8. 1 option :separate_dial_code, default: -> { true }, type: Types::Bool
  9. 1 option :country_search, default: -> { true }, type: Types::Bool
  10. 1 option :show_flags, default: -> { true }, type: Types::Bool
  11. 1 option :format_as_you_type, default: -> { true }, type: Types::Bool
  12. 1 option :strict_mode, default: -> { true }, type: Types::Bool
  13. 1 option :allowed_number_types, default: -> { ["MOBILE", "FIXED_LINE"] }
  14. 1 option :placeholder_number_type,
  15. default: -> { "MOBILE" },
  16. type: Types::Coercible::String.enum(*PLACEHOLDER_NUMBER_TYPES)
  17. 1 option :locale, default: -> { I18n.locale.to_s }
  18. 1 option :submit_country_name, optional: true
  19. 1 def hidden_input_name
  20. then: 0 else: 0 return name if name.present?
  21. then: 0 else: 0 return field_name(model, attribute) if model.present? && attribute.present?
  22. nil
  23. end
  24. 1 def resolved_value
  25. then: 0 else: 0 return value if value.present?
  26. then: 0 else: 0 return model.send(attribute).to_s if model.present? && attribute.present?
  27. nil
  28. end
  29. 1 def resolved_error
  30. then: 0 else: 0 return error if error.present?
  31. then: 0 else: 0 if model.present? && attribute.present? && model.errors[attribute].present?
  32. model.errors[attribute].first
  33. end
  34. end
  35. # Attributes passed to the visible LooposUi::Input wrapper.
  36. # We always use a safe internal name so the visible field never gets
  37. # submitted alongside the hidden E.164 field.
  38. 1 def display_input_attributes
  39. visible_input_attrs = {
  40. type: :tel,
  41. data: { "phone-number-target": "visibleInput" },
  42. }
  43. visible_input_attrs[:autocomplete] = autocomplete_type
  44. then: 0 else: 0 visible_input_attrs.merge!(extra_input_attributes) if extra_input_attributes.present?
  45. {
  46. name: "_#{unique_id}",
  47. value: resolved_value,
  48. placeholder: placeholder,
  49. error: resolved_error,
  50. help: help,
  51. mode: mode,
  52. form: form,
  53. readonly: readonly,
  54. open_actions: open_actions,
  55. type: :tel,
  56. input_attributes: visible_input_attrs,
  57. }
  58. end
  59. # Data attributes placed on the outermost wrapper div to drive the
  60. # phone-number Stimulus controller.
  61. 1 def controller_data_values
  62. {
  63. controller: "phone-number",
  64. "phone-number-initial-country-value": initial_country,
  65. "phone-number-separate-dial-code-value": separate_dial_code,
  66. "phone-number-country-search-value": country_search,
  67. "phone-number-show-flags-value": show_flags,
  68. "phone-number-format-as-you-type-value": format_as_you_type,
  69. "phone-number-strict-mode-value": strict_mode,
  70. "phone-number-allowed-number-types-value": allowed_number_types.to_json,
  71. "phone-number-placeholder-number-type-value": placeholder_number_type,
  72. "phone-number-country-name-locale-value": locale,
  73. "phone-number-has-country-input-value": submit_country_name.present?,
  74. }
  75. end
  76. end
  77. end
  78. end

app/components/loopos_ui/inputs/phone_number/phone_number.html.erb

0.0% lines covered

0.0% branches covered

5 relevant lines. 0 lines covered and 5 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <%= tag.div(class: "lui-phone-number lui-phone-number--#{resolved_appearance} relative", data: controller_data_values) do %>
  2. <%= render LooposUi::Input.new(**display_input_attributes) %>
  3. then: 0 else: 0 <%= hidden_field_tag(hidden_input_name, resolved_value, data: { "phone-number-target": "hiddenInput" }) if hidden_input_name.present? %>
  4. then: 0 else: 0 <% if submit_country_name.present? %>
  5. <%= hidden_field_tag(submit_country_name, nil, data: { "phone-number-target": "countryInput" }) %>
  6. <% end %>
  7. <% end %>

app/components/loopos_ui/inputs/rich_text.rb

85.71% lines covered

100.0% branches covered

7 relevant lines. 6 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Inputs
  3. 1 class RichText < LoopComponent
  4. 1 include BaseInputConcern
  5. 1 option :upload_endpoint, Types::Coercible::String, optional: true
  6. 1 def input_attributes
  7. base_input_attributes.merge({
  8. type: :rich_text,
  9. mode: mode,
  10. input_attributes: {},
  11. custom_readonly: true,
  12. })
  13. end
  14. end
  15. end
  16. end

app/components/loopos_ui/inputs/rich_text/rich_text.html.erb

0.0% lines covered

100.0% branches covered

4 relevant lines. 0 lines covered and 4 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::Input.new(**input_attributes) do %>
  2. <div class="lui-rich_text">
  3. <%=
  4. react_component(
  5. "RichTextEditor",
  6. {
  7. value: value,
  8. placeholder: placeholder,
  9. name: name,
  10. language: I18n.locale,
  11. readOnly: readonly,
  12. uploadEndpoint: upload_endpoint,
  13. # TODO: Add all the props to the component
  14. }
  15. )
  16. %>
  17. </div>
  18. <% end %>

app/components/loopos_ui/inputs/search.rb

84.62% lines covered

100.0% branches covered

13 relevant lines. 11 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Inputs
  3. 1 class Search < LoopComponent
  4. 1 include BaseInputConcern
  5. 1 option :expanded, Types::Bool, default: -> { false } # DEPRECATED, will not be used anymore
  6. 1 option :data_attributes, default: -> { {} }
  7. 1 option :clearable, Types::Bool, default: -> { true }
  8. # Overrides from BaseInputConcern
  9. 1 option :mode, Types::Coercible::Symbol.enum(:autosubmit), default: -> { :autosubmit }
  10. 1 option :event_only, Types::Bool, default: -> { true }
  11. 1 def input_attributes
  12. base_input_attributes.deep_merge({
  13. type: "search",
  14. input_attributes: {
  15. data: {
  16. search_target: "input",
  17. action: "input->search#toggleClearButton input->input#setEditing input->search#onInput change->search#onInput",
  18. }.deep_merge(data_attributes),
  19. },
  20. })
  21. end
  22. 1 def classes
  23. "lui-search"
  24. end
  25. end
  26. end
  27. end

app/components/loopos_ui/inputs/search/search.html.erb

0.0% lines covered

0.0% branches covered

12 relevant lines. 0 lines covered and 12 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <div data-controller="search"
  2. id="<%= unique_id %>"
  3. class="<%= classes %>"
  4. data-search-input-outlet="<%="##{unique_id} .lui-inner-input" %>"
  5. data-search-event-only-value="<%= event_only %>"
  6. >
  7. <%= render LooposUi::Input.new(**input_attributes) do |input| %>
  8. <% input.with_left_addon do %>
  9. <div class="text-[12px] flex items-center text-center">
  10. <i class="fa-regular fa-magnifying-glass text-gray-400"></i>
  11. </div>
  12. <% end %>
  13. then: 0 else: 0 <% if clearable %>
  14. <% input.with_right_addon do %>
  15. <span class="flex">
  16. <i class="fa-regular fa-xmark cursor-pointer text-gray-400"
  17. data-search-target="clearButton"
  18. data-action="click->search#clear click->input#finishEditing">
  19. </i>
  20. </span>
  21. <% end %>
  22. <% end %>
  23. <% end %>
  24. </div>

app/components/loopos_ui/inputs/select.rb

64.71% lines covered

0.0% branches covered

34 relevant lines. 22 lines covered and 12 lines missed.
13 total branches, 0 branches covered and 13 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Inputs
  3. 1 class Select < LoopComponent
  4. 1 include BaseInputConcern
  5. 1 include LooposUi::FaviconAware
  6. 1 option :options, type: [], default: -> { [] } do
  7. 1 option :value, Types::Coercible::String
  8. 1 option :text, Types::Coercible::String
  9. 1 option :icon, Types::Coercible::String, optional: true
  10. 1 option :tooltip, Types::Coercible::String, optional: true
  11. end
  12. 1 option :placeholder, default: -> { nil }
  13. 1 option :portal_target, default: -> { nil }
  14. 1 option :include_blank, Types::Bool | Types::String, default: -> { false }
  15. 1 option :clearable, Types::Bool, default: -> { true }
  16. 1 option :variant, Types::Coercible::Symbol.enum(:list, :grid), default: -> { :list }
  17. 1 mod :blank_option_selected, base: "lui-input-select", condition: -> {
  18. then: 0 else: 0 include_blank && selected_option&.value == ""
  19. }
  20. 1 mod :variant, base: "lui-input-select"
  21. 1 def input_attributes
  22. base_input_attributes.merge({
  23. then: 0 else: 0 value: selected_option&.text,
  24. input_attributes: {
  25. readonly: true,
  26. placeholder: placeholder,
  27. then: 0 else: 0 value: selected_option&.text,
  28. data: {
  29. "input-select-target": "textInput",
  30. },
  31. }.merge(extra_input_attributes.presence || {}),
  32. })
  33. end
  34. 1 def selected_option
  35. then: 0 if include_blank
  36. options.find { |o| o.value == value } || options.first
  37. else: 0 else
  38. options.find { |o| o.value == value }
  39. end
  40. end
  41. 1 def options
  42. case include_blank
  43. when: 0 when String
  44. [Struct.new(:value, :text, :icon, :tooltip).new("", include_blank, nil, nil), *@options]
  45. when: 0 when true
  46. [Struct.new(:value, :text, :icon, :tooltip).new("", "", nil, nil), *@options]
  47. else: 0 else
  48. @options
  49. end
  50. end
  51. 1 def blank_option?(option)
  52. option.value.blank? && (option.text.blank? || option.text == include_blank)
  53. end
  54. 1 def grid_options
  55. else: 0 then: 0 return options unless include_blank
  56. options.reject { |option| blank_option?(option) }
  57. end
  58. end
  59. end
  60. end

app/components/loopos_ui/inputs/select/select.html.erb

0.0% lines covered

0.0% branches covered

33 relevant lines. 0 lines covered and 33 lines missed.
28 total branches, 0 branches covered and 28 branches missed.
    
  1. then: 0 <% if !readonly %>
  2. <%= tag.div(
  3. id: unique_id,
  4. class: "grow",
  5. data: {
  6. controller: "input-select",
  7. then: 0 else: 0 input_select_input_outlet: variant == :list ? "##{unique_id} .lui-inner-input" : nil,
  8. input_select_mode_value: mode,
  9. input_select_form_value: form,
  10. input_select_portal_target_value: portal_target,
  11. input_select_variant_value: variant,
  12. }.compact) do
  13. %>
  14. then: 0 <% if variant == :list %>
  15. <div class="lui-input-select lui-input-select--<%= resolved_appearance %> relative <%= classes %>">
  16. <%= render LooposUi::Input.new(**input_attributes) do |input| %>
  17. <% input.with_right_addon do %>
  18. <span class="flex flex-row items-center gap-2">
  19. then: 0 else: 0 then: 0 else: 0 <i class="<%= (selected_option.nil? || blank_option?(selected_option)) ? 'opacity-0' : '' %> fa-solid fa-xmark cursor-pointer text-gray-400 lui-input-select__clear <%= clearable ? '' : 'hidden!' %>"
  20. data-input-select-target="clearButton"
  21. data-action="click->input-select#clear">
  22. </i>
  23. <div data-input-select-target="icon">
  24. <%# FIXME: Color should not be hardcoded %>
  25. <%= render LooposUi::Icon.new(icon: "fa-regular fa-chevron-down", size: 12, color: "#212529") %>
  26. </div>
  27. </span>
  28. <% end %>
  29. <% end %>
  30. <div data-input-select-target="menu" class="lui-input-select__wrapper">
  31. <div class="lui-input-select__options" role="listbox">
  32. <% options.each do |option| %>
  33. then: 0 else: 0 <div class="lui-input-select__option <%= blank_option?(option) ? 'lui-input-select__option--blank' : '' %>"
  34. role="option"
  35. data-input-select-target="option"
  36. data-text="<%= option.text %>"
  37. data-value="<%= option.value %>">
  38. <div class="lui-input-select__option-wrapper">
  39. <div class="lui-input-select__option-text">
  40. then: 0 else: 0 <%= tag.i(class: faviconize(option.icon)) if option.icon %>
  41. then: 0 else: 0 <span role="text"><%= blank_option?(option) ? "—" : option.text %></span>
  42. then: 0 else: 0 <%= render LooposUi::IconTooltip.new(icon: "fa-regular fa-circle-info", size: "12", text: option.tooltip) if option.tooltip %>
  43. </div>
  44. then: 0 else: 0 then: 0 else: 0 <%= tag.i(class: "#{faviconize("fa-regular fa-check")} #{selected_option && selected_option.value&.eql?(option.value) ? '' : 'hidden!'}", data: { "input-select-target": "check" }) %>
  45. </div>
  46. </div>
  47. <% end %>
  48. </div>
  49. </div>
  50. </div>
  51. else: 0 <% else %>
  52. <div class="lui-input-select lui-input-select--grid <%= classes %>" role="listbox">
  53. <div class="lui-input-select__grid-options">
  54. <% grid_options.each do |option| %>
  55. <% is_selected = selected_option && selected_option.value.eql?(option.value) %>
  56. <%= render LooposUi::Button.new(
  57. text: option.text,
  58. type: :secondary,
  59. size: :small,
  60. full: true,
  61. tag_options: {
  62. type: "button",
  63. role: "option",
  64. "aria-selected": is_selected,
  65. data: {
  66. "input-select-target": "option",
  67. action: "click->input-select#selectGridOption",
  68. text: option.text,
  69. value: option.value,
  70. },
  71. }
  72. ) %>
  73. <% end %>
  74. </div>
  75. then: 0 else: 0 <%= content_tag(:span, help, class: "lui-input__help") if help.present? && error.blank? %>
  76. then: 0 else: 0 <%= content_tag(:span, error, class: "lui-input__error") if error.present? %>
  77. </div>
  78. <% end %>
  79. <%= hidden_field_tag(
  80. input_attributes[:name],
  81. value,
  82. form: form,
  83. data: {
  84. input_select_target: "hiddenInput"
  85. }) %>
  86. <% end %>
  87. <% else %>
  88. else: 0 <%
  89. selected_option = (options.presence || []).find { |o| o.value.to_s == value.to_s }
  90. %>
  91. then: 0 else: 0 <%= content_tag(:span, selected_option&.text.presence || "-", class: "lui-input__value") %>
  92. <% end %>

app/components/loopos_ui/inputs/select2.rb

48.96% lines covered

0.0% branches covered

96 relevant lines. 47 lines covered and 49 lines missed.
45 total branches, 0 branches covered and 45 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Inputs
  3. 1 class Select2 < LoopComponent
  4. 1 include BaseInputConcern
  5. 1 include LooposUi::FaviconAware
  6. # Override BaseInputConcern (String-only) so multiselect can pre-select via value: ["15", "16"].
  7. 1 option :value, (Types::Array.of(Types::Coercible::String) | Types::Coercible::String).optional, optional: true
  8. 1 class Option < Dry::Struct
  9. 1 attribute :value, Types::Coercible::String
  10. 1 attribute :text, Types::Coercible::String
  11. 1 attribute :icon, Types::Coercible::String.optional.default("".freeze)
  12. 1 attribute :tooltip, Types::Coercible::String.optional.default("".freeze)
  13. 1 attribute :child_options, Types::Array.of(Option).optional.default([].freeze)
  14. 1 attribute :attributes, Types::Hash.optional.default({}.freeze)
  15. end
  16. 1 option :options, type: [Option], default: -> { [] }
  17. 1 option :multiple, type: Types::Bool, default: -> { false }
  18. 1 option :placeholder, default: -> { nil }
  19. 1 option :include_blank, Types::Bool | Types::String, default: -> { false }
  20. 1 option :option_indent_size, type: Types::Coercible::Integer, default: -> { 20 }
  21. 1 option :parent_selects_children, default: -> { true }
  22. 1 option :show_actions_in_menu, default: -> { false }
  23. 1 option :portal_target, default: -> { nil }
  24. 1 option :clearable, Types::Bool, default: -> { true }
  25. 1 option :searchable, Types::Bool, default: -> { true }
  26. 1 option :submit_selected_value, Types::Bool, default: -> { false }
  27. 1 option :bind_hidden_input_to_form, Types::Bool, default: -> { false }
  28. 1 option :searchable, Types::Bool, default: -> { true }
  29. 1 option :show_loading_on_submit, Types::Bool, default: -> { true }
  30. 1 option :variant, Types::Coercible::Symbol.enum(:list, :grid), default: -> { :list }
  31. 1 def input_attributes
  32. base_input_attributes.merge({
  33. value: presented_selected_text,
  34. input_attributes: {
  35. readonly: true,
  36. placeholder: placeholder,
  37. value: presented_selected_text,
  38. data: {
  39. "input-select-target": "textInput",
  40. "action": "select2:updateController->input-select#updateValues",
  41. },
  42. },
  43. })
  44. end
  45. 1 def hidden_input_name
  46. attribute_name = input_attributes[:name].presence
  47. then: 0 else: 0 if attribute_name.blank? && model.present? && attribute.present?
  48. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 model_name = model.present? ? dom_id(model) : name&.to_s&.gsub(/\[|\]/, "_")
  49. "#{model_name}[#{attribute}]"
  50. end
  51. else: 0 then: 0 return attribute_name unless multiple
  52. attribute_name + "[]"
  53. end
  54. 1 def hidden_input_value
  55. then: 0 else: 0 then: 0 else: 0 return multiple ? Array(value).first : value.to_s if submit_selected_value
  56. then: 0 else: 0 return selected_value_list.first.to_s if effective_variant == :grid && selected_value_list.any?
  57. input_attributes[:value]
  58. end
  59. 1 def hidden_input_values
  60. selected_value_list
  61. end
  62. 1 def hidden_fields_html
  63. attrs = {
  64. form: hidden_input_form,
  65. data: { input_select2_target: "hiddenInput" },
  66. }.compact
  67. then: 0 else: 0 if multiple && submit_selected_value
  68. values_list = hidden_input_values
  69. then: 0 else: 0 if values_list.any?
  70. return values_list.map do |selected_value|
  71. tag.input(type: "hidden", name: hidden_input_name, value: selected_value, **attrs)
  72. end.join.html_safe
  73. end
  74. end
  75. tag.input(
  76. type: "hidden",
  77. name: hidden_input_name,
  78. value: hidden_input_value.to_s,
  79. **attrs,
  80. )
  81. end
  82. 1 def hidden_input_form
  83. then: 0 else: 0 return form if bind_hidden_input_to_form
  84. nil
  85. end
  86. 1 def selected_options
  87. then: 0 if multiple
  88. formatted_value = selected_value_list
  89. else: 0 all_options.filter { |o| formatted_value.include?(o.value.to_s) }
  90. then: 0 elsif include_blank
  91. [all_options.find { |o| o.value.to_s == value.to_s } || all_options.find { |o| blank_option?(o) }].compact
  92. else: 0 else
  93. [all_options.find { |o| o.value.to_s == value.to_s }].compact
  94. end
  95. end
  96. 1 def presented_selected_text
  97. then: 0 else: 0 if selected_options.present?
  98. selected_options.map(&:text).join(", ").gsub(/([\w\s]{40}).+/,'\1...')
  99. else
  100. nil
  101. end
  102. end
  103. 1 def options
  104. root_options = @options
  105. else: 0 then: 0 return root_options unless include_blank
  106. [blank_option_struct, *root_options]
  107. end
  108. 1 def all_options(root = options)
  109. root.map { |o| [o, all_options(o.child_options)] }.flatten
  110. end
  111. 1 def blank_option?(option)
  112. option.value.blank? && (option.text.blank? || option.text == include_blank)
  113. end
  114. 1 def effective_variant
  115. then: 0 else: 0 return :list if readonly
  116. else: 0 then: 0 return :list unless variant == :grid
  117. then: 0 else: 0 return :grid if grid_available?
  118. warn_grid_fallback
  119. :list
  120. end
  121. 1 def grid_available?
  122. @options.none? { |option| option.child_options.present? }
  123. end
  124. 1 def grid_options
  125. @options
  126. end
  127. 1 private
  128. 1 def selected_value_list
  129. then: 0 else: 0 return [] if value.nil?
  130. then: 0 else: 0 return value.map(&:to_s) if value.is_a?(Array)
  131. then: 0 else: 0 return value.values.map(&:to_s) if value.is_a?(Hash)
  132. [value.to_s]
  133. end
  134. 1 def warn_grid_fallback
  135. Rails.logger.warn(
  136. "[LooposUi::Inputs::Select2] variant :grid ignored: hierarchical options detected. " \
  137. "Falling back to :list. name=#{name.inspect}",
  138. )
  139. end
  140. 1 def blank_option_struct
  141. Option.new(
  142. value: "",
  143. text: blank_option_text,
  144. icon: nil,
  145. tooltip: nil,
  146. child_options: [],
  147. attributes: {},
  148. )
  149. end
  150. 1 def blank_option_text
  151. case include_blank
  152. when: 0 when String
  153. include_blank
  154. when: 0 when true
  155. ""
  156. else: 0 else
  157. ""
  158. end
  159. end
  160. end
  161. end
  162. end

app/components/loopos_ui/inputs/select2/select2.html.erb

0.0% lines covered

0.0% branches covered

57 relevant lines. 0 lines covered and 57 lines missed.
42 total branches, 0 branches covered and 42 branches missed.
    
  1. <%
  2. def render_function(option, level = 0, parent_id: nil)
  3. tag.div(
  4. then: 0 else: 0 class: "lui-input-select__option #{blank_option?(option) ? 'lui-input-select__option--blank' : ''}",
  5. role: "option",
  6. data: {
  7. "input-select2-target": :option,
  8. "select2-target": :option,
  9. then: 0 else: 0 action: !multiple ? "click->input-select2#select #{option[:attributes][:"data-action"]}" : "click->select2#select #{option[:attributes][:"data-action" ]}",
  10. text: option.text,
  11. value: option.value,
  12. },
  13. **option[:attributes]
  14. ) do
  15. tag.div(class: "lui-input-select__option-wrapper") do
  16. tag.div(class: "lui-input-select__option-text") do
  17. then: 0 (multiple ? render(LooposUi::Inputs::Checkbox.new(
  18. else: 0 then: 0 else: 0 state: option.value.in?(selected_options.map(&:value)) ? :checked : :unchecked
  19. )) : "".html_safe) +
  20. then: 0 else: 0 (tag.i(class: faviconize(option.icon)) if option.icon.present?) +
  21. tag.span(role: "text") do
  22. option.text.to_s
  23. end +
  24. then: 0 else: 0 (render LooposUi::IconTooltip.new(icon: "fa-regular fa-circle-info", size: "12", text: option.tooltip) if option.tooltip.present?)
  25. end +
  26. else: 0 then: 0 (tag.i(
  27. then: 0 else: 0 then: 0 else: 0 class: "#{faviconize("fa-regular fa-check")} #{selected_options && selected_options.find { |o| o.value&.eql?(option.value) } ? '' : 'hidden!'}",
  28. data: { "input-select2-target": "check" }
  29. ) unless multiple)
  30. end
  31. end +
  32. tag.div(class: "lui-input-select__option-children", style: "padding-left: #{option_indent_size}px", data: { "select2-parent": option.value }) do
  33. option.child_options.map { |sub_option| render_function(sub_option, level + 1) }.reduce(:+)
  34. end
  35. end
  36. %>
  37. then: 0 <% if readonly %>
  38. else: 0 <%= content_tag(:span, presented_selected_text.presence || "-", class: "lui-input__value") %>
  39. then: 0 <% elsif effective_variant == :grid %>
  40. <%= tag.div(
  41. id: unique_id,
  42. class: "grow",
  43. data: {
  44. controller: "input-select2",
  45. input_select2_mode_value: mode,
  46. input_select2_form_value: form,
  47. input_select2_multiple_value: multiple,
  48. input_select2_variant_value: :grid,
  49. input_select2_show_loading_on_submit_value: show_loading_on_submit,
  50. }) do
  51. %>
  52. then: 0 else: 0 <div class="lui-input-select lui-input-select--grid <%= 'lui-input-select--grid-error' if error.present? %>" role="listbox">
  53. <div class="lui-input-select__grid-options">
  54. <% grid_options.each do |option| %>
  55. <% is_selected = selected_options.any? { |o| o.value.eql?(option.value) } %>
  56. <%= render LooposUi::Button.new(
  57. text: option.text,
  58. type: :secondary,
  59. size: :small,
  60. full: true,
  61. tag_options: {
  62. type: "button",
  63. role: "option",
  64. "aria-selected": is_selected,
  65. data: {
  66. "input-select2-target": "option",
  67. action: "click->input-select2#selectGridOption",
  68. text: option.text,
  69. value: option.value,
  70. },
  71. }
  72. ) %>
  73. <% end %>
  74. </div>
  75. then: 0 else: 0 <%= content_tag(:span, help, class: "lui-input__help") if help.present? && error.blank? %>
  76. then: 0 else: 0 <%= content_tag(:span, error, class: "lui-input__error") if error.present? %>
  77. </div>
  78. <%= tag.div(data: { input_select2_target: "hiddenInputWrapper" }) do %>
  79. <%= hidden_field_tag(
  80. hidden_input_name,
  81. hidden_input_value,
  82. form: hidden_input_form,
  83. data: {
  84. input_select2_target: "hiddenInput"
  85. })
  86. %>
  87. <% end %>
  88. <% end %>
  89. else: 0 <% else %>
  90. <%= tag.div(
  91. id: unique_id,
  92. data: {
  93. controller: "input-select2",
  94. input_select2_input_outlet: "##{unique_id} .lui-inner-input",
  95. input_select2_mode_value: mode,
  96. input_select2_form_value: form,
  97. input_select2_multiple_value: multiple,
  98. input_select2_portal_target_value: portal_target,
  99. input_select2_show_loading_on_submit_value: show_loading_on_submit,
  100. input_select2_variant_value: :list,
  101. }) do
  102. %>
  103. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 <div class="lui-input-select lui-input-select--<%= resolved_appearance %> lui-select2--<%= multiple ? 'multiple' : 'single' %>--<%= show_actions_in_menu ? 'actions-menu' : 'no-actions-menu' %> relative <%= selected_options.any? { |option| blank_option?(option) } ? 'lui-input-select--blank-option-selected' : '' %>">
  104. <%# FIXME: The name is changed so not to interfere with the hidden field below %>
  105. <%= render LooposUi::Input.new(**input_attributes.merge(name: "_")) do |input| %>
  106. <% input.with_right_addon do %>
  107. <span class="flex flex-row items-center gap-2">
  108. then: 0 else: 0 then: 0 else: 0 <i class="<%= selected_options.blank? ? 'opacity-0' : '' %> fa-solid fa-xmark cursor-pointer text-gray-400 lui-input-select__clear <%= clearable ? '' : 'hidden!' %>"
  109. data-input-select2-target="clearButton"
  110. data-action="click->input-select2#clear">
  111. </i>
  112. <div data-input-select2-target="icon">
  113. <%# FIXME: Color should not be hardcoded %>
  114. <%= render LooposUi::Icon.new(icon: "fa-regular fa-chevron-down", size: 12, color: "#212529") %>
  115. </div>
  116. </span>
  117. <% end %>
  118. <% end %>
  119. <%= tag.div(
  120. class: "lui-input-select__wrapper",
  121. data: {
  122. controller: "select2",
  123. "input-select2-target": "menu",
  124. select2_parent_selects_children_value: parent_selects_children,
  125. select_target_id: unique_id,
  126. select2_id: unique_id,
  127. }
  128. ) do %>
  129. <div class="lui-input-select__options" role="listbox">
  130. then: 0 else: 0 <% if searchable %>
  131. <%= render LooposUi::Inputs::Search.new(
  132. name: "query",
  133. value: nil,
  134. placeholder: I18n.t('loopos_ui.inputs.select2.search_placeholder', default: 'Search...'),
  135. expanded: true,
  136. clearable: false,
  137. data_attributes: {
  138. action: "input->select2#handleSearch"
  139. }
  140. ) %>
  141. <div class="lui-association-overlay__divider"></div>
  142. <% end %>
  143. <div class="lui-association-overlay__results" data-select2-target="results">
  144. <% options.each do |option| %>
  145. <%= render_function(option, 0) %>
  146. <% end %>
  147. <div class="hidden lui-association-overlay__empty-search" data-select2-target="emptySearch">
  148. <%= I18n.t('loopos_ui.inputs.select2.no_results', default: 'No results found') %>
  149. </div>
  150. </div>
  151. then: 0 else: 0 <% if multiple && show_actions_in_menu %>
  152. <div class="lui-select2__actions" data-select2-target="actions">
  153. <%= render LooposUi::Button.new(
  154. text: I18n.t('loopos_ui.inputs.select2.reset', default: 'Clear'),
  155. kind: :neutral,
  156. type: :secondary,
  157. size: :tiny,
  158. tag_options: {
  159. data: {
  160. "action": "click->select2#clear"
  161. },
  162. type: :button
  163. }
  164. )
  165. %>
  166. <%= render LooposUi::Button.new(
  167. text: I18n.t('loopos_ui.inputs.select2.apply', default: 'Apply'),
  168. kind: :success,
  169. type: :primary,
  170. size: :tiny,
  171. tag_options: {
  172. type: :button,
  173. data: {
  174. "action": "click->select2#save"
  175. },
  176. }
  177. )
  178. %>
  179. </div>
  180. <% end %>
  181. </div>
  182. <% end %>
  183. </div>
  184. <%= tag.div(data: { input_select2_target: "hiddenInputWrapper" }) do %>
  185. <%= hidden_fields_html %>
  186. <% end %>
  187. <% end %>
  188. <% end %>

app/components/loopos_ui/inputs/text.rb

72.73% lines covered

0.0% branches covered

11 relevant lines. 8 lines covered and 3 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Inputs
  3. 1 class Text < LoopComponent
  4. 1 include BaseInputConcern
  5. 1 option :maxlength, default: -> { nil }
  6. 1 option :minlength, default: -> { nil }
  7. 1 option :autocomplete_type, optional: true
  8. 1 def input_attributes
  9. input_specific_attributes = {
  10. maxlength: maxlength,
  11. minlength: minlength,
  12. }
  13. then: 0 else: 0 input_specific_attributes[:autocomplete] = autocomplete_type if autocomplete_type.present?
  14. base_input_attributes.merge({
  15. type: :text,
  16. mode: mode,
  17. input_attributes: input_specific_attributes.merge(extra_input_attributes.presence || {}),
  18. })
  19. end
  20. end
  21. end
  22. end

app/components/loopos_ui/inputs/text/text.html.erb

0.0% lines covered

100.0% branches covered

2 relevant lines. 0 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <div class="lui-text lui-text--<%= resolved_appearance %> relative">
  2. <%= render LooposUi::Input.new(**input_attributes) %>
  3. </div>

app/components/loopos_ui/inputs/text_area.rb

72.22% lines covered

0.0% branches covered

18 relevant lines. 13 lines covered and 5 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Inputs
  3. 1 class TextArea < LoopComponent
  4. 1 include BaseInputConcern
  5. 1 option :rows, default: -> { nil }
  6. 1 option :cols, default: -> { nil }
  7. 1 option :maxlength, default: -> { nil }
  8. 1 option :minlength, default: -> { nil }
  9. 1 option :wrap, default: -> { "soft" }
  10. 1 option :resize, default: -> { "vertical" }
  11. 1 option :data_attributes, default: -> { {} }
  12. 1 option :autocomplete_type, optional: true
  13. 1 def input_attributes
  14. attrs = base_input_attributes
  15. then: 0 else: 0 attrs.delete(:readonly) if readonly
  16. input_specific_attributes = {
  17. rows: rows,
  18. cols: cols,
  19. maxlength: maxlength,
  20. minlength: minlength,
  21. wrap: wrap,
  22. style: "resize: #{resize}",
  23. data: { textarea_target: "input" }.merge(data_attributes),
  24. readonly: readonly,
  25. }
  26. then: 0 else: 0 input_specific_attributes[:autocomplete] = autocomplete_type if autocomplete_type.present?
  27. attrs.merge({
  28. input_attributes: input_specific_attributes,
  29. type: "textarea",
  30. })
  31. end
  32. end
  33. end
  34. end

app/components/loopos_ui/inputs/text_area/text_area.html.erb

0.0% lines covered

100.0% branches covered

2 relevant lines. 0 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <div data-controller="textarea" class="lui-text_area lui-text_area--<%= resolved_appearance %>">
  2. <%= render LooposUi::Input.new(**input_attributes) %>
  3. </div>

app/components/loopos_ui/item/state_label.rb

57.14% lines covered

0.0% branches covered

35 relevant lines. 20 lines covered and 15 lines missed.
8 total branches, 0 branches covered and 8 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Item
  3. 1 class StateLabel < LoopComponent
  4. 1 attr_reader :item
  5. 1 class ItemStruct < Dry::Struct
  6. 1 attribute? :macro_name, Types::Coercible::String.optional
  7. # FIXME: temporarily permit nil values
  8. 1 attribute? :macro_class, Types::Coercible::String.optional
  9. 1 attribute? :macro_kind, Types::Coercible::String.optional
  10. 1 attribute? :macro_icon, Types::Coercible::String.optional
  11. 1 attribute? :micro_name, Types::Coercible::String.optional
  12. # FIXME: temporarily permit nil values
  13. 1 attribute? :micro_class, Types::Coercible::String.optional
  14. 1 attribute? :micro_kind, Types::Coercible::String.optional
  15. 1 attribute? :micro_icon, Types::Coercible::String.optional
  16. # rubocop:disable Style/ClassMethodsDefinitions
  17. 1 def self.build_from_item(item)
  18. # Sometimes we construct the ItemStruct directly, in this case
  19. # the builder will already have the ItemStruct instance
  20. then: 0 else: 0 if item.is_a?(self)
  21. return item
  22. end
  23. args = schema.keys.each_with_object({}) do |key, args|
  24. value = item.send(key.name)
  25. # FIXME: temporarily remove nil check
  26. # raise TypeError.new("[LoopOS UI] Item::StateLabel: Item method #{key.name} cannot return nil") if value.nil?
  27. then: 0 else: 0 args[key.name] = value if value.present?
  28. end
  29. new(args)
  30. end
  31. 1 def self.from_core_client_item(item)
  32. new({
  33. micro_class: item.dig(:state_badge, :ui_micro_class),
  34. micro_name: item.dig(:state_badge, :micro_text),
  35. micro_kind: item.dig(:state_badge, :ui_micro_class),
  36. micro_icon: item.dig(:state_badge, :icon),
  37. macro_class: item.dig(:state_badge, :macro_class),
  38. macro_name: item.dig(:current_block, :name),
  39. macro_kind: item.dig(:state_badge, :macro_class),
  40. macro_icon: item.dig(:state_badge, :macro_icon),
  41. })
  42. end
  43. # rubocop:enable Style/ClassMethodsDefinitions
  44. end
  45. 1 option :item, Types.Constructor(ItemStruct, ItemStruct.method(:build_from_item))
  46. 1 option :macro_condensed, Types::Bool, default: -> { false }
  47. 1 option :micro_condensed, Types::Bool, default: -> { false }
  48. 1 def initialize(...)
  49. super
  50. @leading_text = item.macro_name
  51. then: 0 else: 0 @leading_color = item.macro_class || item&.macro_kind
  52. @trailing_text = item.micro_name
  53. then: 0 else: 0 @trailing_color = item.micro_class || item&.micro_kind
  54. @leading_icon = item.macro_icon
  55. @trailing_icon = item.micro_icon
  56. end
  57. 1 def unique_id
  58. @unique_id ||= SecureRandom.uuid
  59. end
  60. end
  61. end
  62. end

app/components/loopos_ui/item/state_label/state_label.html.erb

0.0% lines covered

0.0% branches covered

7 relevant lines. 0 lines covered and 7 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <div class="inline-flex">
  2. then: 0 else: 0 <%= render LooposUi::Tooltip.new do |tooltip| %>
  3. <%= render LooposUi::DoubleStateLabel.new(
  4. leading_text: @leading_text.to_s,
  5. leading_color: @leading_color,
  6. trailing_text: @trailing_text.to_s,
  7. trailing_color: @trailing_color,
  8. leading_icon: @leading_icon,
  9. trailing_icon: @trailing_icon,
  10. light: true,
  11. ) %>
  12. <% end if macro_condensed || micro_condensed %>
  13. <%= render LooposUi::DoubleStateLabel.new(
  14. leading_text: @leading_text.to_s,
  15. leading_color: @leading_color,
  16. leading_condensed: macro_condensed,
  17. trailing_text: @trailing_text.to_s,
  18. trailing_color: @trailing_color,
  19. leading_icon: @leading_icon,
  20. trailing_icon: @trailing_icon,
  21. trailing_condensed: micro_condensed,
  22. light: true,
  23. ) %>
  24. </div>

app/components/loopos_ui/item_value.rb

86.67% lines covered

100.0% branches covered

15 relevant lines. 13 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class ItemValue < LoopComponent
  3. 1 option :accept_value_btn, default: -> { true }
  4. 1 option :cancel_value_btn, default: -> { true }
  5. 1 option :proposal, default: -> { {} }
  6. 1 option :reason_edit_value, default: -> { "" }
  7. 1 option :proposal_kind, default: -> { "in" }
  8. 1 option :price_details
  9. 1 option :status
  10. 1 option :cancel_value_btn_text, default: -> { "acceptBtnText" }
  11. 1 private
  12. 1 def accept_value_btn?
  13. accept_value_btn || false
  14. end
  15. 1 def cancel_value_btn?
  16. cancel_value_btn || false
  17. end
  18. end
  19. end

app/components/loopos_ui/item_value/item_value.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= react_component("ItemValue",
  2. {
  3. proposal: proposal,
  4. priceEstimation: price_details,
  5. state: status,
  6. reasonEditValue: reason_edit_value,
  7. proposalKind: proposal_kind,
  8. locale: I18n.locale,
  9. }
  10. ) %>

app/components/loopos_ui/label.rb

42.11% lines covered

0.0% branches covered

19 relevant lines. 8 lines covered and 11 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class Label < LoopComponent
  4. 1 attr_accessor :text, :icon, :condensed
  5. 1 attr_reader :text_color, :bg_color
  6. 1 def initialize(text: nil, icon: nil, color: nil, count: nil, condensed: false, system_args: {})
  7. @text = text
  8. @icon = icon
  9. then: 0 else: 0 @color = color&.to_sym
  10. @count = count
  11. @condensed = condensed
  12. @system_args = system_args
  13. then: 0 else: 0 @text_color, @bg_color = color if color.is_a?(Array) && color.size == 2
  14. end
  15. 1 def system_args
  16. @system_args.slice(:data, :id)
  17. end
  18. 1 def text_too_long?
  19. text.present? && text.length >= 33
  20. end
  21. 1 def styles
  22. then: 0 else: 0 return if text_color.nil? || bg_color.nil?
  23. <<~CSS.squish
  24. color: #{text_color};
  25. background-color: #{bg_color};
  26. CSS
  27. end
  28. end
  29. end

app/components/loopos_ui/label/label.html.erb

0.0% lines covered

0.0% branches covered

8 relevant lines. 0 lines covered and 8 lines missed.
11 total branches, 0 branches covered and 11 branches missed.
    
  1. <%= tag.span(class: "lui-label", style: styles, **system_args) do %>
  2. then: 0 else: 0 <%= render LooposUi::Tooltip.new(title: text) if text_too_long? %>
  3. <% case icon %>
  4. when: 0 <% when "core", "submission", "hubs", "validation", "handling", "exits", "impact" %>
  5. <%= helpers.app_svg(app: icon, size: :tiny) %>
  6. when: 0 <% when "decision_ai" %>
  7. <%= helpers.app_svg(app: "ai_decision", size: :tiny) %>
  8. else: 0 <% else %>
  9. then: 0 else: 0 <%= tag.i(class: "#{icon} lui-label__icon") if icon.present? %>
  10. <% end %>
  11. <%# WARNING: Please do not remove the `lui--label` data attribute, it is used to update the text %>
  12. then: 0 else: 0 <%= tag.span(text, class: "lui-label__text", data: { "lui--label": "content" }) if !condensed %>
  13. then: 0 else: 0 <%= render LooposUi::Counter.new(count: @count, kind: @color) if @count %>
  14. <% end %>

app/components/loopos_ui/layout_component.html.erb

0.0% lines covered

0.0% branches covered

9 relevant lines. 0 lines covered and 9 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. <div class="app">
  2. <div class="los-manager-header" id="topbar">
  3. then: 0 else: 0 <%= @topbar if @user_signed_in %>
  4. </div>
  5. <div class="app__main-container">
  6. then: 0 else: 0 <%= @sidebar if @user_signed_in %>
  7. <div class="app__content-wrapper">
  8. <%= image_tag(@background, :class => "app__background") %>
  9. <div class="app__content">
  10. <%= @app_content %>
  11. </div>
  12. </div>
  13. </div>
  14. then: 0 else: 0 <% if @user_signed_in %>
  15. <%= turbo_stream_from("flash_container_#{@current_user.id}") %>
  16. <div id="notice_<%= @current_user.id %>"></div>
  17. <% end %>
  18. </div>

app/components/loopos_ui/layout_component.rb

36.36% lines covered

100.0% branches covered

11 relevant lines. 4 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class LayoutComponent < ViewComponent::Base
  4. 1 include Turbo::StreamsHelper
  5. 1 def initialize(topbar:, sidebar:, app_content:, user_signed_in:, current_user:, background: )
  6. LooposUi.logger.warn("DEPRECATION WARNING: LooposUi::LayoutComponent is deprecated. Please use LooposUi::AppLayoutComponent instead.")
  7. @topbar = topbar
  8. @sidebar = sidebar
  9. @app_content = app_content
  10. @user_signed_in = user_signed_in
  11. @current_user = current_user
  12. @background = background
  13. end
  14. end
  15. end

app/components/loopos_ui/layout_loading.rb

100.0% lines covered

100.0% branches covered

2 relevant lines. 2 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class LayoutLoading < LoopComponent
  3. # NOTE: This is a internal usage component, it's not intended to be used outside of the LooposUi gem.
  4. end
  5. end

app/components/loopos_ui/layout_loading/layout_loading.html.erb

0.0% lines covered

100.0% branches covered

4 relevant lines. 0 lines covered and 4 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <div class="lui-layout-loading">
  2. <div class="lui-layout-loading__content">
  3. <%= render LooposUi::Loadings::Skeleton.new(full: false) %>
  4. <%= render LooposUi::Loadings::Skeleton.new(full: true) %>
  5. <%= render LooposUi::Loadings::Skeleton.new(full: true) %>
  6. </div>
  7. </div>

app/components/loopos_ui/link.rb

90.0% lines covered

100.0% branches covered

10 relevant lines. 9 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class Link < LoopComponent
  3. 1 option :url, type: Types::String
  4. 1 option :text, optional: true, type: Types::String.optional
  5. 1 option :data, default: -> { {} }, type: Types::Hash
  6. 1 option :turbo_target, default: -> { "lui-main-layout" }, type: Types::String.optional
  7. 1 option :turbo_action, default: -> { "navigate" }, type: Types::String.optional
  8. 1 def merged_data_attributes
  9. { turbo_target: turbo_target, turbo_action: turbo_action }.merge(data)
  10. end
  11. 1 use_extra_options
  12. end
  13. end

app/components/loopos_ui/link/link.html.erb

0.0% lines covered

0.0% branches covered

8 relevant lines. 0 lines covered and 8 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <div class='lui-link'>
  2. <%= tag.a(href: url, data: merged_data_attributes, **extra_options) do %>
  3. then: 0 <%if content.present? %>
  4. else: 0 <%= content%>
  5. then: 0 <%elsif text%>
  6. <%= text%>
  7. else: 0 <%else %>
  8. <%= url%>
  9. <%end%>
  10. <%end%>
  11. </div>

app/components/loopos_ui/loading_ai.rb

100.0% lines covered

100.0% branches covered

2 relevant lines. 2 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class LoadingAi < LoopComponent
  3. end
  4. end

app/components/loopos_ui/loading_ai/loading_ai.html.erb

0.0% lines covered

100.0% branches covered

2 relevant lines. 0 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <div class="lui-loading_ai" >
  2. <%= image_tag('LoopOS_AI.svg') %>
  3. </div>

app/components/loopos_ui/loadings/skeleton.rb

80.0% lines covered

100.0% branches covered

10 relevant lines. 8 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Loadings
  3. 1 class Skeleton < LoopComponent
  4. 1 option :full, optional: true, default: -> { false }
  5. 1 option :n_rows, Types::Integer, default: -> { 4 }
  6. 1 class Bar < LoopComponent
  7. 1 option :width, Types::String, default: -> { "100%" }
  8. 1 erb_template <<-ERB.squish
  9. <div class="lui-skeleton__bar" style="width: <%= width %>"></div>
  10. ERB
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/loadings/skeleton/skeleton.html.erb

0.0% lines covered

0.0% branches covered

5 relevant lines. 0 lines covered and 5 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. then: 0 else: 0 <div class="lui-skeleton <%= full ? "lui-skeleton--full" : "" %>">
  2. <% n_rows.times.each do %>
  3. <div class="lui-skeleton__item">
  4. <%= render LooposUi::Loadings::Skeleton::Bar.new %>
  5. </div>
  6. <% end %>
  7. <div class="lui-skeleton__footer">
  8. <div class="lui-skeleton__bar--footer"></div>
  9. <div class="lui-skeleton__bar--footer lui-skeleton__bar--footer--invisible"></div>
  10. </div>
  11. </div>

app/components/loopos_ui/log.rb

29.2% lines covered

0.0% branches covered

113 relevant lines. 33 lines covered and 80 lines missed.
40 total branches, 0 branches covered and 40 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "cgi"
  3. 1 module LooposUi
  4. 1 class Log < LoopComponent
  5. 1 renders_one :source, types: {
  6. entity_token: { renders: EntityToken, as: :entity_token_source },
  7. entity: { renders: ->(entity:) { entity }, as: :source },
  8. }
  9. 1 renders_one :title_token, types: {
  10. entity_token: { renders: EntityToken, as: :title_entity_token },
  11. entity: { renders: ->(entity:) { entity }, as: :title_token },
  12. manual: { renders: ->(text:) { text }, as: :title_token_manual },
  13. }
  14. # Not meant to be used directly
  15. # We'd prefix this with _ but ViewComponent breaks if we do that
  16. # Again, not part of the public API, use with_expand instead and with_button instead
  17. 1 renders_one :private_expand
  18. 1 renders_many :private_buttons, types: {
  19. button: { renders: Button, as: :private_button },
  20. button_manual: { renders: ->(&block) { capture(&block) }, as: :private_button_manual },
  21. }
  22. 1 option :error, Types::Bool, default: -> { false }
  23. 1 mod :error
  24. 1 option :user, Types::String, optional: true
  25. 1 option :date, Types::TDate
  26. 2 option :log_class, Types::String, optional: true, default: -> { "item" }
  27. 1 option :block,
  28. Types::Hash.schema(
  29. name: Types::String,
  30. kind: Types::String,
  31. url: Types::String,
  32. ),
  33. optional: true
  34. 1 NilLog = new(error: true, user: nil, date: Time.current).with_content("Failed to show this log.")
  35. # Factory method to create a Log from any source (auto-detects format)
  36. 1 def self.from_any_source(source)
  37. log = LooposUi::Log::Factories.create_from_any_source(source)
  38. else: 0 then: 0 raise "Failed to build log from source: #{source}" unless log
  39. log
  40. end
  41. 1 def before_render
  42. then: 0 else: 0 @defer_render_blocks&.each do |block|
  43. block.call(self, @view_context)
  44. end
  45. @defer_render_blocks = nil
  46. end
  47. 1 def defer_render(&block)
  48. @defer_render_blocks ||= []
  49. @defer_render_blocks << block
  50. end
  51. 1 def initialize(...)
  52. super(...)
  53. rescue => e
  54. LooposUi.logger.error("Error parsing log: #{e.message}")
  55. LooposUi.logger.error(e.backtrace.join("\n"))
  56. raise e
  57. end
  58. 1 def parsed_content
  59. then: 0 else: 0 split_text_to_words(content&.to_s)
  60. end
  61. 1 def parsed_expand_content
  62. split_text_to_words(private_expand.to_s)
  63. end
  64. 1 def with_expand(&block)
  65. with_private_expand(&block)
  66. with_button(**expand_button_args)
  67. end
  68. 1 def with_expand_content(content)
  69. then: 0 if view_context.present?
  70. with_expand do
  71. view_context.capture { content }
  72. end
  73. else: 0 else
  74. defer_render do |log, view_context|
  75. log.with_expand do
  76. view_context.capture { content }
  77. end
  78. end
  79. end
  80. end
  81. 1 def with_button(*args, **kwargs, &block)
  82. with_private_button(*args, **default_button_args.merge(kwargs), &block)
  83. end
  84. 1 def with_button_manual(*args, **kwargs, &block)
  85. with_private_button_manual(&block)
  86. end
  87. 1 def expand_button_args
  88. {
  89. **default_button_args,
  90. text: t("loopos_ui.log.script_log.show_details"),
  91. trailing_icon: :chevron_down,
  92. tag_options: {
  93. data: {
  94. action: "click->log#showMore:prevent log:toggleExpand->lui--button#rotateTrailingIcon",
  95. log_target: "toggleExpand",
  96. },
  97. },
  98. }
  99. end
  100. 1 def default_button_args
  101. @default_button_args ||= {
  102. size: :tiny,
  103. kind: :neutral,
  104. type: :secondary,
  105. }
  106. end
  107. 1 def time
  108. date.strftime("%H:%Mh")
  109. end
  110. 1 def user_string
  111. then: 0 if user.present?
  112. user
  113. else: 0 else
  114. t("loopos_ui.log.script_log.system")
  115. end
  116. end
  117. 1 def block_icon(kind)
  118. normalized_kind = kind.to_s.sub(/-rails\z/, "")
  119. flow_block_icons = {
  120. # Tool blocks
  121. "tag" => "fa-solid fa-tag",
  122. "remove_tag" => "fa-regular fa-tag",
  123. "auto_validation" => "fa-solid fa-hand-holding-dollar",
  124. "auto_accept_proposal" => "fa-regular fa-circle-check",
  125. "script" => "fa-regular fa-code",
  126. "script_advanced" => "fa-regular fa-code",
  127. "item_value_agreed" => "fa-regular fa-circle-check",
  128. "impact" => "fa-solid fa-recycle",
  129. # Decision blocks
  130. "decision_custom" => "fa-solid fa-split",
  131. "decision_script" => "fa-solid fa-split",
  132. "decision_transport" => "fa-solid fa-split",
  133. "decision_multirule" => "fa-solid fa-split",
  134. "decision_ai" => "decision_ai",
  135. # Service/other blocks
  136. "marketplace" => "fa-regular fa-store",
  137. "transport" => "fa-regular fa-truck",
  138. "outgoing_payment" => "fa-regular fa-credit-card",
  139. "incoming_payment" => "fa-regular fa-envelope-open-dollar",
  140. "sms" => "fa-regular fa-message-sms",
  141. "invoice" => "fa-regular fa-file-invoice",
  142. "email" => "fa-regular fa-envelope",
  143. }
  144. then: 0 else: 0 return flow_block_icons[normalized_kind] if flow_block_icons.key?(normalized_kind)
  145. case normalized_kind
  146. when: 0 when "message"
  147. "fa-regular fa-message-sms"
  148. when: 0 when "payment"
  149. "fa-regular fa-credit-card"
  150. when: 0 when "shipping"
  151. "fa-regular fa-truck"
  152. when: 0 when "submission_extra"
  153. "submission"
  154. else: 0 else
  155. normalized_kind
  156. end
  157. end
  158. 1 def log_class_icon
  159. else: 0 case log_class
  160. when: 0 when "transition"
  161. :double_arrow
  162. when: 0 when "email"
  163. :mail
  164. when: 0 when "sms"
  165. :message
  166. when: 0 when "outgoing_payment"
  167. :payment_arrow_up
  168. when: 0 when "incoming_payment"
  169. :payment_arrow_down
  170. when: 0 when "shipping"
  171. :local_shipping
  172. when: 0 when "invoice"
  173. :receipt_long
  174. when: 0 when "item_value"
  175. :money_range
  176. when: 0 when "item"
  177. :package_2
  178. when: 0 when "privacy"
  179. :visibility_lock
  180. end
  181. end
  182. 1 private
  183. 1 def split_text_to_words(source, sanitize: false)
  184. html = if sanitize
  185. # Sanitization is removing data attrs, which breaks a lot
  186. then: 0 # of our components
  187. ActionController::Base.helpers.sanitize(
  188. source,
  189. tags: ["div", "strong", "em", "b", "i", "u", "span", "a"],
  190. attributes: ["href", "class", "style", "id", "title", "target", "rel"] + [/data\-.*/],
  191. )
  192. else: 0 else
  193. maybe_unescape_escaped_html(source)
  194. end
  195. fragment = Nokogiri::HTML::DocumentFragment.parse(html)
  196. text_nodes = fragment.xpath("./text()")
  197. text_nodes.each do |node|
  198. node_content = node.content.to_s
  199. words = node_content.split(/\s/).reject(&:blank?)
  200. then: 0 else: 0 next if words.blank?
  201. words = words.each_with_index.map do |word, i|
  202. is_last_word = i == words.length - 1
  203. should_append_space = !is_last_word || node_content.match?(/\s\z/)
  204. then: 0 else: 0 word = "#{word.strip} " if should_append_space
  205. new_node = Nokogiri::XML::Node.new("div", fragment).tap do |span|
  206. span["class"] = "lui-log__word"
  207. span.content = word
  208. end
  209. node.add_previous_sibling(new_node)
  210. end
  211. node.remove # remove the original text node
  212. end
  213. fragment.to_html.html_safe
  214. end
  215. # Some upstream paths end up storing HTML as escaped entities (e.g. "&lt;div ...&gt;").
  216. # Nokogiri would then treat it as plain text, and our "word split" would break markup.
  217. # If it looks like escaped tags, unescape (up to a couple times) before parsing.
  218. 1 def maybe_unescape_escaped_html(source)
  219. else: 0 then: 0 return source unless source.is_a?(String)
  220. # Quick heuristic: detect escaped HTML tags like "&lt;div" or "&lt;/span".
  221. else: 0 then: 0 return source unless source.match?(%r{&lt;/?\s*[a-zA-Z][a-zA-Z0-9:-]*})
  222. decoded = source
  223. 2.times do
  224. else: 0 then: 0 break unless decoded.match?(%r{&lt;/?\s*[a-zA-Z][a-zA-Z0-9:-]*})
  225. decoded = CGI.unescapeHTML(decoded)
  226. end
  227. decoded
  228. end
  229. end
  230. end

app/components/loopos_ui/log/factories.rb

63.16% lines covered

0.0% branches covered

19 relevant lines. 12 lines covered and 7 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class Log
  4. 1 module Factories
  5. # Registry for all available factories
  6. 1 class << self
  7. 1 def factories
  8. Base.subclasses
  9. end
  10. 1 def find_factory_for(source)
  11. else: 0 then: 0 return unless source
  12. then: 0 else: 0 factories&.find { |factory| factory.can_create?(source) }
  13. end
  14. 1 def create_from_any_source(source, factory_class: nil)
  15. klass = factory_class || find_factory_for(source)
  16. else: 0 then: 0 return unless klass
  17. factory = klass.new(source)
  18. factory.try_create
  19. end
  20. # Auto-load all factories in the factories directory
  21. 1 def load_all_factories
  22. 1 factories_dir = File.dirname(__FILE__)
  23. 1 Dir.glob(File.join(factories_dir, "factories", "*.rb")).each do |file|
  24. 3 require file
  25. end
  26. end
  27. end
  28. # Load all factories when this module is loaded
  29. 1 load_all_factories
  30. end
  31. end
  32. end

app/components/loopos_ui/log/factories/base.rb

44.44% lines covered

0.0% branches covered

18 relevant lines. 8 lines covered and 10 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 class Log
  3. 1 module Factories
  4. # Base factory class that all log factories should inherit from
  5. 1 class Base
  6. 1 def initialize(source, script: nil)
  7. @source = source
  8. @script = script
  9. end
  10. 1 def self.can_create?
  11. raise NotImplementedError, "#{self} must implement can_create?"
  12. end
  13. 1 def create
  14. raise NotImplementedError, "#{self} must implement create"
  15. end
  16. 1 def try_create
  17. create
  18. rescue => e
  19. LooposUi.logger.error("Error creating log: #{e.message}")
  20. LooposUi.logger.error(e.backtrace.join("\n"))
  21. then: 0 if Rails.env.development?
  22. raise e
  23. else: 0 else
  24. LooposUi::Log::NilLog
  25. end
  26. end
  27. end
  28. end
  29. end
  30. end

app/components/loopos_ui/log/factories/core_item_log_presenter_hash.rb

23.64% lines covered

0.0% branches covered

55 relevant lines. 13 lines covered and 42 lines missed.
22 total branches, 0 branches covered and 22 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class Log
  4. 1 module Factories
  5. # This builder expects the hash that the core item_logs_presenter returns
  6. # and adapts it to the log component
  7. # The expected schema of the hash is defined in the can_build? method
  8. 1 class CoreItemLogPresenterHash < Base
  9. 1 def self.valid_block?(block)
  10. else: 0 then: 0 return false unless block.is_a?(Hash)
  11. block = block.with_indifferent_access
  12. [:name, :kind, :url].all? { |key| block[key].present? }
  13. end
  14. 1 def self.can_create?(source)
  15. else: 0 then: 0 return false unless source.is_a?(::Hash)
  16. source = source.with_indifferent_access
  17. [
  18. :time, :author, :app_instance, :message, :error,
  19. ].all? { |key| source.key?(key) } &&
  20. source[:app_instance].is_a?(Hash) &&
  21. source[:app_instance][:name].present? &&
  22. source[:app_instance][:kind].present?
  23. rescue => e
  24. LooposUi.logger.error("Error creating CoreItemLogPresenterHash: #{e.message}")
  25. false
  26. end
  27. 1 def initialize(source)
  28. super(source)
  29. @source = source.with_indifferent_access
  30. end
  31. 1 def create
  32. log_options = {
  33. error: @source[:error],
  34. user: @source[:author],
  35. date: @source[:time],
  36. }
  37. then: 0 else: 0 log_options[:log_class] = @source[:log_class] if @source[:log_class].present?
  38. then: 0 else: 0 if valid_block?(@source[:block])
  39. log_options[:block] = {
  40. name: @source[:block][:name],
  41. kind: @source[:block][:kind],
  42. url: @source[:block][:url],
  43. }
  44. end
  45. log = LooposUi::Log.new(**log_options)
  46. log.with_content(@source[:message])
  47. then: 0 else: 0 log.with_title_entity_token(text: @source[:title]) if @source[:title].present?
  48. then: 0 else: 0 if @source[:button_info].present?
  49. log.with_button(
  50. text: @source[:button_info][:text],
  51. href: @source[:button_info][:url],
  52. tag_options: {
  53. turbo: false,
  54. onclick: "event.stopPropagation();",
  55. },
  56. )
  57. end
  58. then: 0 if modal_details?
  59. else: 0 add_modal_content(log)
  60. then: 0 else: 0 elsif @source[:extra_message].present?
  61. log.with_expand_content(@source[:extra_message])
  62. end
  63. add_source(log)
  64. log
  65. end
  66. 1 private
  67. 1 def valid_block?(block)
  68. self.class.valid_block?(block)
  69. end
  70. 1 def add_source(log)
  71. kind, name = @source[:app_instance].fetch_values(:kind, :name)
  72. then: 0 if kind == "neutral"
  73. log.with_entity_token_source(text: name, color: :general)
  74. else: 0 else
  75. log.with_source(entity: LooposUi::Entities::AppInstance.from_hash(@source[:app_instance]))
  76. end
  77. end
  78. 1 def modal_details?
  79. @source[:details_mode].to_s == "modal"
  80. end
  81. 1 def add_modal_content(log)
  82. modal_options = (@source[:modal_content] || {}).to_h.symbolize_keys
  83. modal_body = modal_options.delete(:content)
  84. log.defer_render do |_curr_log, view_context|
  85. log.with_button_manual do
  86. view_context.render(LooposUi::Modal.new(**modal_options)) do |modal|
  87. modal.with_trigger_button(
  88. text: LooposUi::Log.t(".show_details", scope: [:loopos_ui, :log, :script_log]),
  89. **log.default_button_args,
  90. )
  91. then: 0 else: 0 if modal_body.present?
  92. then: 0 if modal_body.respond_to?(:render_in)
  93. view_context.render(modal_body)
  94. else: 0 else
  95. modal_body.to_s
  96. end
  97. end
  98. end
  99. end
  100. end
  101. end
  102. end
  103. end
  104. end
  105. end

app/components/loopos_ui/log/factories/script_log.rb

34.69% lines covered

0.0% branches covered

49 relevant lines. 17 lines covered and 32 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class Log
  4. 1 module Factories
  5. 1 class ScriptLog < Base
  6. 1 def self.can_create?(source)
  7. defined?(LoopOs::ScriptLog) && source.is_a?(LoopOs::ScriptLog)
  8. end
  9. 1 def create
  10. @log = LooposUi::Log.new(
  11. date: @source.created_at,
  12. user: @source.user_identification,
  13. error: @source.level.to_s == "error",
  14. )
  15. @log.with_content(@source.name)
  16. then: 0 else: 0 add_download_button if @source.files.present?
  17. then: 0 else: 0 add_retry_button if should_add_retry_button?
  18. add_info_modal
  19. add_source
  20. @log
  21. end
  22. 1 def initialize(...)
  23. super(...)
  24. end
  25. 1 private
  26. 1 def should_add_retry_button?
  27. @script.present? && script_proxy? && has_input? && @script.has_retry_capability?
  28. end
  29. 1 def script_proxy?
  30. @script.is_a?(LoopOs::Scripts::ScriptProxy) && @script.script?
  31. end
  32. 1 def has_input?
  33. @source.input_data.present? || (@source.respond_to?(:input_files) && @source.input_files.attached?)
  34. end
  35. 1 def add_retry_button
  36. @log.defer_render do |log, view_context|
  37. log.with_button_manual do
  38. view_context.render(LooposUi::Modal.new(
  39. id: "retry_script_modal_#{@source.id}",
  40. title: t(".retry_script_modal.title"),
  41. description: t(".retry_script_modal.description"),
  42. )) do |modal|
  43. modal.with_trigger do
  44. view_context.render(LooposUi::Button.new(
  45. text: t(".retry_script"),
  46. icon: "fa-regular fa-rotate-right",
  47. **@log.default_button_args,
  48. ))
  49. end
  50. modal.with_primary_action(
  51. text: t(".retry_script_modal.confirm"),
  52. tag_options: {
  53. form: "retry_form_#{@source.id}",
  54. type: "submit",
  55. data: { type: "submit" },
  56. },
  57. )
  58. view_context.content_tag(:div, style: "display: none;") do
  59. view_context.form_with(url: @script.retry_path_for(@source), method: :post, id: "retry_form_#{@source.id}", data: { turbo_frame: "_top", turbo: true }) do |form|
  60. form.submit
  61. end
  62. end
  63. end
  64. end
  65. end
  66. end
  67. 1 def add_source
  68. @log.with_entity_token_source(text: t(".system"))
  69. end
  70. 1 def add_download_button
  71. @log.with_button(
  72. **download_button_args,
  73. )
  74. end
  75. 1 def t(key, **kwargs)
  76. LooposUi::Log.t(key, scope: [:loopos_ui, :log, :script_log], **kwargs)
  77. end
  78. 1 def add_info_modal
  79. @log.defer_render do |log, view_context|
  80. log.with_button_manual do
  81. view_context.render(LooposUi::Modal.new(title: t(".modal_title"), modal_width: 1080, show_footer: false)) do |modal|
  82. modal.with_trigger do
  83. view_context.render(LooposUi::Button.new(
  84. text: t(".show_details"),
  85. **@log.default_button_args,
  86. ))
  87. end
  88. then: 0 else: 0 modal.with_primary_action(**download_button_args) if @source.files.present?
  89. view_context.render(LooposUi::Log::Script::InfoModalBody.new(log: @source))
  90. end
  91. end
  92. end
  93. end
  94. 1 def download_button_args
  95. {
  96. text: t(".download_file"),
  97. icon: :download,
  98. href: @source.files.first.url,
  99. tag_options: { turbo: false },
  100. **@log.default_button_args,
  101. }
  102. end
  103. end
  104. end
  105. end
  106. end

app/components/loopos_ui/log/log.html.erb

0.0% lines covered

0.0% branches covered

29 relevant lines. 0 lines covered and 29 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. <%= tag.div(class: classes, data: {
  2. controller: "log",
  3. log_expanded_copy_text_value: t("loopos_ui.log.script_log.show_details"),
  4. log_collapsed_copy_text_value: t("loopos_ui.log.script_log.hide_details") }) do %>
  5. <%= tag.div(class: "lui-log__main") do %>
  6. <%= tag.div(class: "lui-log__content") do %>
  7. <%= tag.div(class: "lui-log__content__icon") do %>
  8. <%= render LooposUi::Tooltip.new(title: I18n.t("admin.items.logs.log_classes.#{log_class}", default: log_class.to_s.humanize)) %>
  9. <%= render LooposUi::MIcon.new(log_class_icon, size: 20) %>
  10. <% end %>
  11. <%= tag.div(class: "lui-log__content__header") do %>
  12. <%= tag.div(class: "lui-log__content__header__title", data: { log_target: "content" }) do %>
  13. <%= parsed_content %>
  14. <% end %>
  15. <%= tag.div(class: "lui-log__content__header__description") do %>
  16. <%= t("loopos_ui.log.script_log.by") %>
  17. <%= user_string %>
  18. <% end %>
  19. then: 0 else: 0 <%= tag.div(class: "lui-log__expand-content", data: { log_target: :expand }) do %>
  20. <%= tag.div(class: "lui-log__expand-inner") do %>
  21. <%= parsed_expand_content %>
  22. <% end %>
  23. <% end if private_expand? %>
  24. <%= tag.div(class: "lui-log__buttons") do %>
  25. <% private_buttons.each do |button| %>
  26. <%= button %>
  27. <% end %>
  28. <% end %>
  29. <% end %>
  30. <% end %>
  31. <%= tag.div(class: "lui-log__source_content") do %>
  32. <%= date.strftime("%d/%m/%Y %H:%M") %>
  33. then: 0 else: 0 <% if block.present? %>
  34. then: 0 <% if block.dig(:url).present? %>
  35. <%= tag.a(href: block[:url], class: "lui-log__source_content__link", target: "_blank", data: { turbo: "false" }) do %>
  36. <%= render LooposUi::StateLabel.new(
  37. text: block[:name],
  38. color: block[:kind],
  39. icon: block_icon(block[:kind])) %>
  40. <% end %>
  41. else: 0 <% else %>
  42. <%= render LooposUi::StateLabel.new(
  43. text: block[:name],
  44. color: block[:kind],
  45. icon: block_icon(block[:kind])) %>
  46. <% end %>
  47. <% end %>
  48. <% end %>
  49. <% end %>
  50. <% end %>

app/components/loopos_ui/log/script/info_modal_body.html.erb

0.0% lines covered

0.0% branches covered

39 relevant lines. 0 lines covered and 39 lines missed.
44 total branches, 0 branches covered and 44 branches missed.
    
  1. <%# TODO: this is copied from the script log modal, not yet fully migrated to UI %>
  2. <div class="flex flex-col w-full gap-2">
  3. <span class="text-[14px]"><%= I18n.t('loop_os_scripts.logs.parameters') %></span>
  4. <%
  5. then: 0 else: 0 then: 0 else: 0 has_input_data = log.input_data&.keys&.present?
  6. has_input_files = log.respond_to?(:input_files) && log.input_files.attached?
  7. %>
  8. <% if has_input_data || has_input_files %>
  9. then: 0 <%
  10. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 input_data_count = has_input_data ? log.input_data&.keys&.size : 0
  11. then: 0 else: 0 input_file_count = has_input_files ? log.input_files.length : 0
  12. total_count = input_data_count + input_file_count
  13. then: 0 else: 0 grid_class = total_count <= 6 ? "logs__modal__grid-layout logs__modal__grid-layout--one-row" : "logs__modal__grid-layout logs__modal__grid-layout--scrollable"
  14. %>
  15. <div class="<%= grid_class %>">
  16. then: 0 else: 0 <% if has_input_data %>
  17. <% log.input_data.keys.each do |input_field| %>
  18. <div class="logs__modal__grid-layout--item">
  19. <span><strong><%= input_field %></strong></span>
  20. <span><%= log.input_data.dig(input_field) %></span>
  21. </div>
  22. <% end %>
  23. <% end %>
  24. then: 0 else: 0 <% if has_input_files %>
  25. <% log.input_files.each do |file| %>
  26. <div class="logs__modal__grid-layout--item">
  27. <span><strong><%= file.blob.metadata['param_name'] || I18n.t('loop_os_scripts.logs.file') %></strong></span>
  28. <span>
  29. then: 0 else: 0 <% file_url = file.respond_to?(:url) ? file.url : url_for(file) %>
  30. <%= link_to file.blob.filename, file_url, target: "_blank", rel: "noopener noreferrer", class: "text-blue-600 hover:underline", download: file.blob.filename.to_s, data: { turbo: false } %>
  31. </span>
  32. </div>
  33. <% end %>
  34. <% end %>
  35. </div>
  36. else: 0 <% else %>
  37. -
  38. <% end %>
  39. </div>
  40. <div class="flex flex-col w-full gap-2">
  41. <span class="text-[14px]"><%= I18n.t('loop_os_scripts.logs.time_running') %></span>
  42. then: 0 else: 0 then: 0 else: 0 then: 0 <% if log.extra_data&.is_a?(Hash) && log.extra_data&.dig("time_running").present? %>
  43. else: 0 then: 0 else: 0 <span class="text-black text-[12px]"><%= distance_of_time_in_words(0, log.extra_data&.dig("time_running").to_i.seconds) %></span>
  44. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 then: 0 <% elsif log.extra_data&.is_a?(Array) && log.extra_data.last&.dig("extra")&.dig("time_running").present? %>
  45. then: 0 else: 0 then: 0 else: 0 <span class="text-black text-[12px]"><%= distance_of_time_in_words(0, log.extra_data.last&.dig("extra")&.dig("time_running").to_i.seconds) %></span>
  46. else: 0 <% else %>
  47. -
  48. <% end %>
  49. </div>
  50. <div class="flex flex-col w-full gap-2">
  51. <span class="text-[14px]"><%= I18n.t('loop_os_scripts.logs.results') %></span>
  52. <%= helpers.turbo_stream_from("script_log_data_#{log.id}") %>
  53. <div class="flex flex-col w-full h-full gap-2" style="max-height: 35vh;">
  54. then: 0 <% if log.extra_data.present? %>
  55. <pre id="extra_data_loop_os_script_log_<%= log.id %>" class="text-xs text-gray-800 bg-white p-4 rounded border overflow-x-auto"><%= JSON.pretty_generate(log.extra_data) %></pre>
  56. else: 0 <% else %>
  57. <div class="text-center text-gray-500 py-8">
  58. <p><%= I18n.t('loop_os_scripts.logs.empty_results') %></p>
  59. </div>
  60. <% end %>
  61. </div>
  62. </div>

app/components/loopos_ui/log/script/info_modal_body.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class Log
  3. 1 module Script
  4. 1 class InfoModalBody < LoopComponent
  5. 1 option :log
  6. end
  7. end
  8. end
  9. end

app/components/loopos_ui/log_list.rb

35.8% lines covered

3.33% branches covered

81 relevant lines. 29 lines covered and 52 lines missed.
30 total branches, 1 branches covered and 29 branches missed.
    
  1. 1 module LooposUi
  2. 1 class LogList < LoopComponent
  3. 1 class Config < Dry::Struct
  4. 1 MAX_ITEMS = 15
  5. 2 then: 0 else: 1 attribute :max_items, Types::Integer.constructor { |value| value.to_i <= 0 ? MAX_ITEMS : value.to_i }.default(MAX_ITEMS)
  6. 1 attribute :sort_direction, Types::Symbol.default(:desc).enum(:asc, :desc)
  7. 1 attribute :has_pagination, Types::Bool.default(true)
  8. end
  9. 1 option :id, Types::Coercible::String.constructor { |id|
  10. "lui-log-list-#{id}"
  11. }
  12. 1 option :path, Types::String, optional: true
  13. 1 option :config,
  14. Types::Hash.schema(
  15. 3 Config.schema.keys.to_h { |i| [i.name, i.type] },
  16. ) | Config,
  17. default: -> { Config.new.to_h }
  18. 1 option :logs, Types::Array.of(Types::Instance(LooposUi::Log)), default: -> { [] }
  19. 1 option :items, Types::Interface(:first), optional: true # enumerable
  20. 1 option :factory, optional: true # TODO: type checking
  21. 1 option :pagy, optional: true
  22. 1 option :pagination,
  23. Types::Hash.schema(
  24. page: Types::Coercible::Integer.default(1),
  25. items: Types::Coercible::Integer.default(Config.new.max_items),
  26. count: Types::Coercible::Integer,
  27. ),
  28. optional: true
  29. 1 def initialize(...)
  30. super(...)
  31. validate_factory!
  32. warn_if_missing_path
  33. create_pagy
  34. then: 0 else: 0 @logs = build_logs_from_items if @items.present?
  35. then: 0 else: 0 config_hash = @config.is_a?(Config) ? @config.to_h : @config
  36. then: 0 else: 0 if config_hash[:has_pagination] && @pagy.blank?
  37. raise "pagination parameters (pagy or pagination) must be present in the options if you want to use pagination"
  38. end
  39. @config = Config.new(
  40. **config_hash,
  41. has_pagination: config_hash[:has_pagination] && @pagy.present? && (@pagy.count > @logs.count),
  42. then: 0 else: 0 max_items: @pagy.present? && @pagy.count > 0 ? @pagy.count : config_hash[:max_items],
  43. )
  44. build_groups
  45. end
  46. 1 def build_logs_from_items
  47. @factory ||= LooposUi::Log::Factories.find_factory_for(@items.first)
  48. then: 0 else: 0 raise "No factory found for items: #{@items.first}" if @factory.nil?
  49. @items.map do |item|
  50. @factory.new(item).try_create
  51. end
  52. end
  53. 1 def self.from_any_source(source, id:, config: {}, **options)
  54. list_options = options.dup
  55. options = list_options.extract!(:page)
  56. then: 0 else: 0 if config[:has_pagination] && !options.key?(:page)
  57. raise "page must be present in the options if you want to use pagination"
  58. end
  59. config = Config.new(**config)
  60. config = Config.new(**config.to_h, has_pagination: config.has_pagination && options[:page].present?)
  61. log_list = LooposUi::LogList::Factories.create_from_any_source(
  62. source,
  63. options: options,
  64. list_options: {
  65. id: id,
  66. config: config,
  67. **list_options,
  68. },
  69. )
  70. else: 0 then: 0 raise "Failed to build log from source: #{source}" unless log_list
  71. log_list
  72. end
  73. 1 def render?
  74. true
  75. end
  76. 1 def groups
  77. @groups ||= [] # We could use a Set, but there won't be that many groups, so it's faster
  78. end
  79. 1 def add_log(log)
  80. log_date = log.date.to_date
  81. group = groups.find { |group| group.date == log_date }
  82. then: 0 if group.nil?
  83. group = LooposUi::LogList::Group.new(date: log_date, logs: [log])
  84. groups << group
  85. else: 0 else
  86. group.logs << log
  87. end
  88. group
  89. end
  90. 1 private
  91. 1 def build_groups
  92. then: 0 else: 0 if @logs.count > config.max_items
  93. LooposUi.logger.warn(<<~LOG.squish)
  94. LOOPOS_UI: Performance warning: LogList is being passed more than #{config.max_items} logs (#{@logs.count}).
  95. This will impact performance as it forces the component to sort the logs in memory.
  96. Consider passing the data already sorted and limited to #{config.max_items} items.
  97. Backtrace:
  98. #{caller.join("\n")}
  99. LOG
  100. @logs = @logs.sort_by(&:time)
  101. then: 0 else: 0 @logs.reverse! if config.sort_direction == :desc
  102. @logs = @logs.slice(0, config.max_items)
  103. end
  104. @logs.each { |log| add_log(log) }
  105. # This sort is no problem, at best we will have config.max_items groups
  106. groups.sort_by!(&:date)
  107. then: 0 else: 0 groups.reverse! if config.sort_direction == :desc
  108. # FIXME: This sort is not stable (does not account for milliseconds nor DB ordering)
  109. # Causes events with the same timestamp to appear out of order, some times.
  110. # You need to pass the logs already sorted!!
  111. # groups.each do |group|
  112. # group.logs.sort_by!(&:time)
  113. # group.logs.reverse! if config.sort_direction == :desc
  114. # end
  115. end
  116. 1 def pagination
  117. LooposUi::Pagination.new(pagy: pagy, path: path)
  118. end
  119. 1 def validate_factory!
  120. then: 0 else: 0 if @factory.present? && !(@factory <= LooposUi::Log::Factories::Base)
  121. raise "Factory must be a subclass of LooposUi::Log::Factories::Base"
  122. end
  123. end
  124. 1 def self.pagination_to_pagy(pagination)
  125. Pagy.new(
  126. page: 1,
  127. **pagination,
  128. )
  129. end
  130. 1 def create_pagy
  131. then: 0 else: 0 if @pagy.nil? && @pagination.present?
  132. @pagy = self.class.pagination_to_pagy(@pagination)
  133. end
  134. end
  135. 1 def warn_if_missing_path
  136. then: 0 else: 0 if @path.nil?
  137. LooposUi.logger.warn(<<~LOG.squish)
  138. LOOPOS_UI: LogList has missing path parameter, this will yield worse performance.
  139. This will cause the pagination component to use the current path, which probably renders more than just logs.
  140. Consider creating a controller action that renders just the logs component.
  141. Backtrace:
  142. #{caller.join("\n")}
  143. LOG
  144. end
  145. end
  146. end
  147. end

app/components/loopos_ui/log_list/factories.rb

66.67% lines covered

0.0% branches covered

18 relevant lines. 12 lines covered and 6 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class LogList
  4. 1 module Factories
  5. 1 class << self
  6. 1 def factories
  7. Base.subclasses
  8. end
  9. 1 def find_factory_for(source)
  10. else: 0 then: 0 return unless source
  11. then: 0 else: 0 factories&.find { |factory| factory.can_create?(source) }
  12. end
  13. 1 def create_from_any_source(source, **options)
  14. factory_class = find_factory_for(source)
  15. else: 0 then: 0 return unless factory_class
  16. factory_class.new(source, **options).try_create
  17. end
  18. # Auto-load all factories in the factories directory
  19. 1 def load_all_factories
  20. 1 factories_dir = File.dirname(__FILE__)
  21. 1 Dir.glob(File.join(factories_dir, "factories", "*.rb")).each do |file|
  22. 3 require file
  23. end
  24. end
  25. end
  26. # Load all factories when this module is loaded
  27. 1 load_all_factories
  28. end
  29. end
  30. end

app/components/loopos_ui/log_list/factories/base.rb

50.0% lines covered

0.0% branches covered

22 relevant lines. 11 lines covered and 11 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 class LogList
  3. 1 module Factories
  4. 1 class Base
  5. 1 include ::Pagy::Backend
  6. 1 class Options < Dry::Struct
  7. 1 attribute? :page, Types::Coercible::Integer
  8. end
  9. 1 def self.can_create?(source)
  10. raise NotImplementedError, "#{self} must implement can_build?"
  11. end
  12. 1 def initialize(source, options: {}, list_options: {})
  13. @source = source
  14. @options = Options.new(options) # this are additional options for the factory
  15. @list_options = list_options # this should be passed in to the LogList.new(...)
  16. end
  17. 1 def try_create
  18. create
  19. rescue => e
  20. LooposUi.logger.error("Error creating log: #{e.message}")
  21. LooposUi.logger.error(e.backtrace.join("\n"))
  22. then: 0 if Rails.env.development?
  23. raise e
  24. else: 0 else
  25. LooposUi::Log::NilLog
  26. end
  27. end
  28. 1 def create
  29. raise NotImplementedError, "#{self.class.name} must implement create"
  30. end
  31. end
  32. end
  33. end
  34. end

app/components/loopos_ui/log_list/factories/core_item_log_list_presenter_hash.rb

26.92% lines covered

0.0% branches covered

26 relevant lines. 7 lines covered and 19 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "pagy/extras/array"
  3. 1 module LooposUi
  4. 1 class LogList
  5. 1 module Factories
  6. # This builder expects the hash that the core item_logs_presenter returns
  7. # and adapts it to the log component
  8. # The expected schema of the hash is defined in the can_build? method
  9. 1 class CoreItemLogListPresenterHash < Base
  10. 1 def self.can_create?(source)
  11. else: 0 then: 0 return false unless source.is_a?(::Enumerable)
  12. # Check the schema for all the groups
  13. source.all? do |group|
  14. group = group.with_indifferent_access
  15. group.is_a?(::Hash) && # Each group is a hash
  16. [:logs, :date].all? { |key| group.key?(key) } && # Each group has a logs and date key
  17. # Each log can be built with the CoreItemLogPresenterHash
  18. group[:logs].all? { |log| Log::Factories::CoreItemLogPresenterHash.can_create?(log) }
  19. end
  20. rescue => e
  21. LooposUi.logger.error("Error creating CoreItemLogListPresenterHash: #{e.message}")
  22. false
  23. end
  24. 1 def create
  25. logs = @source.flat_map do |group|
  26. group = group.with_indifferent_access
  27. group[:logs].map do |log|
  28. log = log.with_indifferent_access
  29. log[:time] = group[:date].to_date + log[:time].to_time.seconds_since_midnight.seconds
  30. log
  31. end
  32. end
  33. then: 0 else: 0 if @list_options[:config].has_pagination
  34. pagy, logs = pagy_array(logs, page: @options.page, items: @list_options[:config].max_items)
  35. end
  36. logs = logs.map { |log| LooposUi::Log::Factories::CoreItemLogPresenterHash.new(log).create }
  37. LooposUi::LogList.new(
  38. logs: logs,
  39. then: 0 else: 0 **(pagy ? { pagy: pagy } : {}),
  40. **@list_options,
  41. )
  42. end
  43. end
  44. end
  45. end
  46. end

app/components/loopos_ui/log_list/factories/script.rb

42.86% lines covered

0.0% branches covered

14 relevant lines. 6 lines covered and 8 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. 1 module LooposUi
  2. 1 class LogList
  3. 1 module Factories
  4. 1 class Script < Base
  5. 1 def self.can_create?(source)
  6. (defined?(LoopOs::Script) && source.is_a?(LoopOs::Script)) || (defined?(LoopOs::Scripts::ScriptProxy) && source.is_a?(LoopOs::Scripts::ScriptProxy))
  7. end
  8. 1 def create
  9. then: 0 if @list_options[:config].has_pagination
  10. pagy, logs = pagy(
  11. @source.loop_os_script_logs.reorder(created_at: :desc),
  12. page: @options.page,
  13. items: @list_options[:config].max_items,
  14. )
  15. else: 0 else
  16. logs = @source.loop_os_script_logs
  17. .reorder(created_at: :desc)
  18. .limit(@list_options[:config].max_items)
  19. end
  20. logs = logs.map do |log|
  21. LooposUi::Log::Factories::ScriptLog.new(log, script: @source).create
  22. end
  23. LooposUi::LogList.new(
  24. logs: logs,
  25. then: 0 else: 0 **(pagy ? { pagy: pagy } : {}),
  26. **@list_options,
  27. )
  28. end
  29. end
  30. end
  31. end
  32. end

app/components/loopos_ui/log_list/group.html.erb

0.0% lines covered

100.0% branches covered

4 relevant lines. 0 lines covered and 4 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div(class: classes, data: { date: date.strftime("%d-%m-%Y") }) do %>
  2. <%= render LooposUi::Accordion.new(title: formatted_date, open: true) do %>
  3. <% logs.each do |log| %>
  4. <%= render log %>
  5. <% end %>
  6. <% end %>
  7. <% end %>

app/components/loopos_ui/log_list/group.rb

87.5% lines covered

100.0% branches covered

8 relevant lines. 7 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class LogList
  3. 1 class Group < LoopComponent
  4. 1 option :date, Types::TDate
  5. 1 option :logs, Types::Array.of(Types::Instance(LooposUi::Log))
  6. 1 private
  7. 1 def formatted_date
  8. date.strftime("%d-%m-%Y")
  9. end
  10. end
  11. end
  12. end

app/components/loopos_ui/log_list/log_list.html.erb

0.0% lines covered

0.0% branches covered

11 relevant lines. 0 lines covered and 11 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <%= tag.turbo_frame(id: id, class: classes) do %>
  2. then: 0 <% if groups.any? %>
  3. <% groups.each do |group| %>
  4. <%= render group %>
  5. <% end %>
  6. then: 0 else: 0 <%= tag.div(class: "lui-log_list__pagination") do %>
  7. <%= render pagination %>
  8. <% end if config.has_pagination %>
  9. else: 0 <% else %>
  10. <div class="flex w-full justify-center items-center h-[300px]">
  11. <div class="self-stretch inline-flex flex-col justify-center items-center gap-4">
  12. <%= render LooposUi::MIcon.new(:search, size: 40) %>
  13. <div class="w-60 flex flex-col justify-start items-center">
  14. <div class="self-stretch text-center justify-start copy-14-medium text-content"><%= t('log_list.empty.title') %></div>
  15. <div class="self-stretch text-center justify-start copy-12 text-content-secondary"><%= t('log_list.empty.subtitle') %></div>
  16. </div>
  17. </div>
  18. </div>
  19. <% end %>
  20. <% end %>

app/components/loopos_ui/logo.rb

42.86% lines covered

100.0% branches covered

7 relevant lines. 3 lines covered and 4 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class Logo < LoopComponent
  3. 1 def initialize(app: "manager", count: nil, size: "small", icon: true)
  4. @app = app
  5. @count = count
  6. @size = size
  7. @icon = icon
  8. end
  9. end
  10. end

app/components/loopos_ui/logo/logo.html.erb

0.0% lines covered

0.0% branches covered

4 relevant lines. 0 lines covered and 4 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%= tag.div(class: "lui-logo") do %>
  2. <%= react_component("LogoApp", {
  3. app: @app,
  4. size: @size,
  5. icon: @icon
  6. }) %>
  7. then: 0 else: 0 <%= render LooposUi::Counter.new(count: @count, kind: :neutral) if @count %>
  8. <% end %>

app/components/loopos_ui/logs_component/log_content_component.html.erb

0.0% lines covered

0.0% branches covered

15 relevant lines. 0 lines covered and 15 lines missed.
10 total branches, 0 branches covered and 10 branches missed.
    
  1. then: 0 else: 0 <div class="logs__container--log<%= @log_level == "error" ? "--error" : "" %>">
  2. <div class="logs__container--log--left">
  3. <div class="logs__container--log__header">
  4. <span class="logs__container--log__header--time"><%= created_at_header %></span>
  5. then: 0 else: 0 <% if user_identification? %>
  6. <i class="fa-solid fa-circle"></i>
  7. <span class="logs__container--log__header--name"><%= user_identification %></span>
  8. <% end %>
  9. </div>
  10. then: 0 else: 0 <span class="logs__container--log__message<%= log_level == "error" ? "--error" : "" %>"><%= log_name %></span>
  11. </div>
  12. <div class="logs__container--log--right">
  13. <div class="logs__container--log__header">
  14. then: 0 <% if app.present? && app.dig("kind") == "neutral" %>
  15. else: 0 <%= render LooposUi::Token.new(text: app.dig("name"), color: :general) %>
  16. then: 0 else: 0 <% elsif app.present? && app.dig("kind") != "neutral" %>
  17. <%= render LooposUi::Entities::AppInstance.new(app_instance: OpenStruct.new(name: app.dig("name"), kind: app.dig("kind"))) %>
  18. <% end %>
  19. </div>
  20. <div>
  21. <% buttons.each do |button| %>
  22. <%= button %>
  23. <% end %>
  24. </div>
  25. </div>
  26. </div>

app/components/loopos_ui/logs_component/log_content_component.rb

75.0% lines covered

0.0% branches covered

12 relevant lines. 9 lines covered and 3 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 class LogsComponent
  3. 1 class LogContentComponent < ViewComponent::Base
  4. 1 renders_one :created_at_header
  5. 1 renders_one :user_identification
  6. 1 renders_one :log_name
  7. 1 renders_many :buttons
  8. 1 attr_reader :log_level, :group, :app
  9. 1 def initialize(log_level:, group: nil, app: nil)
  10. @log_level = log_level
  11. @group = group
  12. then: 0 else: 0 @app = app&.with_indifferent_access
  13. end
  14. end
  15. end
  16. end

app/components/loopos_ui/logs_component/log_group_component.html.erb

0.0% lines covered

100.0% branches covered

2 relevant lines. 0 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <div class="logs__container--time-group">
  2. <hr/>
  3. <span><%= group || content %></span>
  4. <hr/>
  5. </div>

app/components/loopos_ui/logs_component/logs_component.html.erb

0.0% lines covered

0.0% branches covered

11 relevant lines. 0 lines covered and 11 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <div class="logs">
  2. <div class="logs__container">
  3. <% created_at_groups.each do |created_at| %>
  4. then: 0 else: 0 <% if created_at.present? %>
  5. <%= created_at %>
  6. <% end %>
  7. <%
  8. # FIXME: This should come from the component itself, we need to change the slots to allow groups which come with their own logs
  9. current_logs = log_contents.filter do |log|
  10. created_at.blank? || created_at.group.blank? || log.group.blank? || log.group == created_at.group
  11. end
  12. %>
  13. <div class="logs__container--wrapper">
  14. <% current_logs.each do |log| %>
  15. <%= log %>
  16. <% end %>
  17. </div>
  18. <% end %>
  19. </div>

app/components/loopos_ui/m_icon.rb

74.29% lines covered

0.0% branches covered

35 relevant lines. 26 lines covered and 9 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. 1 module LooposUi
  2. 1 class MIcon < LoopComponent
  3. 1 class << self
  4. 1 def icon?(icon)
  5. icon.present? && icons.include?(icon.to_sym)
  6. end
  7. 1 def icons
  8. 1 [*svg_icons, *font_icons]
  9. end
  10. 1 def font_icons
  11. 1 @font_icons ||= begin
  12. 1 file = LooposUi::Engine.root.join(
  13. "app",
  14. "lib",
  15. "loopos_ui",
  16. "m_icon",
  17. "material-symbol-outlined.codepoints",
  18. )
  19. 1 File.foreach(file).map do |line|
  20. 3903 line.split(" ").first.to_sym
  21. end.to_set
  22. end
  23. end
  24. 1 def svg_icons
  25. 1 @svg_icons ||= Pathname.new(__dir__)
  26. .join("m_icon", "custom_icons").glob("*.svg").map do |n|
  27. 3 n.basename.to_s.split(".").first.to_sym
  28. rescue => _e
  29. skipped # :nocov:
  30. skipped LooposUi.logger.error("Dont put files with funny names here!")
  31. skipped nil
  32. skipped # :nocov:
  33. end.to_set
  34. end
  35. 1 def svg_icon?(icon)
  36. svg_icons.include?(icon.to_sym)
  37. end
  38. end
  39. 1 TMaterialIconSymbol = Types::Coercible::Symbol.enum(*icons)
  40. 1 param :icon, TMaterialIconSymbol
  41. 1 option :tag, Types::Coercible::Symbol.enum(:i, :span, :button, :a), default: -> { :i }
  42. 1 option :size, Types::Coercible::Integer, default: -> { 16 }
  43. 1 option :semantic, Types::Coercible::Symbol.enum(:informative, :success, :warning, :error), optional: true
  44. 1 use_extra_options
  45. 1 def additional_classes
  46. [
  47. *super,
  48. "material-symbols-rounded",
  49. then: 0 else: 0 tag == :button ? "cursor-pointer" : nil,
  50. then: 0 else: 0 semantic.present? ? semantic_class : nil,
  51. ]
  52. end
  53. 1 def semantic_class
  54. "lui-m_icon__semantic--#{semantic}"
  55. end
  56. 1 def svg_icon? = self.class.svg_icon?(icon)
  57. 1 def svg_path
  58. else: 0 then: 0 raise "Can only use with svg icons" unless svg_icon?
  59. Pathname.new(__dir__).join("m_icon", "custom_icons", "#{icon}.svg")
  60. end
  61. 1 def style
  62. <<~CSS.squish
  63. --lui-micon-size: #{size}px;
  64. CSS
  65. end
  66. end
  67. end

app/components/loopos_ui/m_icon/m_icon.html.erb

0.0% lines covered

0.0% branches covered

4 relevant lines. 0 lines covered and 4 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%= content_tag(tag, class: classes, style: style, **extra_options) do %>
  2. then: 0 <% if svg_icon? %>
  3. <%= File.read(svg_path).html_safe %>
  4. else: 0 <% else %>
  5. <%= icon %>
  6. <% end %>
  7. <% end %>

app/components/loopos_ui/main_component.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class MainComponent < ViewComponent::Base
  4. 1 renders_one :main_header
  5. 1 renders_one :main_content
  6. end
  7. end

app/components/loopos_ui/main_component/main_component.html.erb

0.0% lines covered

100.0% branches covered

3 relevant lines. 0 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <div class="flex w-full h-full flex-col items-start gap-[32px]">
  2. <div class="w-full flex items-center justify-center">
  3. <%= main_header %>
  4. </div>
  5. <div class="grow w-full flex items-center justify-center">
  6. <%= main_content %>
  7. </div>
  8. </div>

app/components/loopos_ui/main_layout.rb

45.45% lines covered

0.0% branches covered

22 relevant lines. 10 lines covered and 12 lines missed.
12 total branches, 0 branches covered and 12 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class MainLayout < LoopComponent
  4. 1 option :background, optional: true
  5. 1 renders_many :notifications, LooposUi::Toaster
  6. 1 TURBO_FRAME_ID = "lui-main-layout"
  7. 1 def background_image_classes
  8. then: 0 else: 0 "lui-main_layout__background" if background_image_path.present?
  9. end
  10. 1 def background_image_style
  11. then: 0 else: 0 "background-image: url(#{background_image_path});" if background_image_path.present?
  12. end
  13. 1 def background_image_path
  14. else: 0 then: 0 return unless LooposUi.config.enable_main_layout_background
  15. then: 0 else: 0 return image_path(background) if background.present?
  16. then: 0 else: 0 image_from_app_config if LooposUi.config.infer_background_from_app_type
  17. end
  18. 1 private
  19. 1 def image_from_app_config
  20. then: 0 else: 0 return if LooposUi.config.app_type.nil?
  21. extensions = ["svg", "png"]
  22. extensions.each do |ext|
  23. file_path = "loopos_ui/backgrounds/#{LooposUi.config.app_type}.#{ext}"
  24. return image_path(file_path)
  25. rescue AssetNotFound
  26. next
  27. end
  28. LooposUi.logger.warn("No background image found for app type: #{LooposUi.config.app_type} with extensions #{extensions.join(", ")}")
  29. nil
  30. end
  31. end
  32. end

app/components/loopos_ui/main_layout/main_layout.html.erb

0.0% lines covered

0.0% branches covered

8 relevant lines. 0 lines covered and 8 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <turbo-frame data-turbo-action="advance" id="lui-main-layout" data-turbo-frame="lui-main-layout" class="min-h-0 <%= background_image_classes %>" style="<%= background_image_style %>">
  2. <%
  3. # I want to cal active_item but internaly it calls helpers (path etc) but I dont want to render the component just for that
  4. # So we pass in the current view context manually
  5. then: 0 else: 0 if LooposUi::Sidebar::USE_UI_2
  6. sd = LooposUi::Sidebar::V2::Sidebar.new
  7. sd.instance_variable_set(:@view_context, view_context)
  8. end
  9. %>
  10. then: 0 else: 0 <%= helpers.turbo_stream.update("lui-sidebar-active-item-id", sd.active_item) if LooposUi::Sidebar::USE_UI_2 %>
  11. <%= render LooposUi::LayoutLoading.new %>
  12. <div class="lui-main_layout__content" data-skeleton-loading="<%= LooposUi.config.enable_loading_skeletons %>">
  13. <%= content %>
  14. </div>
  15. </turbo-frame>
  16. <turbo-frame id="lui-main-layout-actions">
  17. </turbo-frame>

app/components/loopos_ui/marketing_layout.rb

50.0% lines covered

0.0% branches covered

16 relevant lines. 8 lines covered and 8 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 class MarketingLayout < LoopComponent
  3. 1 renders_one :action_bar, ->(**args, &block) {
  4. LooposUi::ActionBar.new(**args, &block)
  5. }
  6. 1 renders_one :left_content
  7. 1 renders_one :right_content
  8. 1 attr_accessor :breadcrumbs
  9. 1 def with_breadcrumb(href: "#", text:)
  10. @breadcrumbs ||= []
  11. @breadcrumbs << { href:, text: }
  12. end
  13. 1 def before_render
  14. else: 0 then: 0 with_action_bar do |ab|
  15. ab.with_breadcrumbs_list do |bl|
  16. breadcrumbs.each do |crumb|
  17. bl.with_breadcrumb(href: crumb[:href]).with_content(crumb[:text])
  18. end
  19. end
  20. end unless action_bar?
  21. end
  22. end
  23. end

app/components/loopos_ui/marketing_layout/marketing_layout.html.erb

0.0% lines covered

100.0% branches covered

10 relevant lines. 0 lines covered and 10 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <main class="lui-marketing_layout">
  2. <%= action_bar %>
  3. <div class="lui-marketing_layout__toaster">
  4. <%= render LooposUi::Message.new(
  5. title: "Upgrade your plan to access this feature.",
  6. variant: :warning,
  7. dismissible: false,
  8. animated: false,
  9. persistent: true,
  10. )%>
  11. </div>
  12. <div class="lui-marketing_layout__frame">
  13. <div class="lui-marketing_layout__content">
  14. <%= render LooposUi::FlexLayout.new do |flex| %>
  15. <% flex.with_section(lg: 6) do %>
  16. <%= left_content %>
  17. <% end %>
  18. <% flex.with_section(lg: 6) do %>
  19. <%= right_content %>
  20. <% end %>
  21. <% end %>
  22. </div>
  23. </div>
  24. </main>

app/components/loopos_ui/marketplace/footer.rb

90.0% lines covered

0.0% branches covered

10 relevant lines. 9 lines covered and 1 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Marketplace
  3. 1 class Footer < LoopComponent
  4. 1 option :email, Types::String, optional: true
  5. 1 option :logo_url, Types::String, optional: true
  6. 1 option :logo_href, Types::String, optional: true
  7. 1 renders_one :social_buttons, LooposUi::ActionButtons
  8. 1 private
  9. 1 def logo_tag_type
  10. then: 0 else: 0 logo_href ? :a : :div
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/marketplace/footer/footer.html.erb

0.0% lines covered

0.0% branches covered

9 relevant lines. 0 lines covered and 9 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <%= tag.div class: "lui-marketplace-footer" do %>
  2. <%= tag.div class: "lui-marketplace-footer__actions" do %>
  3. then: 0 <% if logo_url.present? %>
  4. <%= tag.div class: "lui-logo" do %>
  5. <%= content_tag(logo_tag_type, class: "loopos-logos-container", href: logo_href) do %>
  6. <%= tag.img(src: logo_url, alt: "Logo", class: "loopos-logos--medium" ) %>
  7. <% end %>
  8. <% end %>
  9. else: 0 <% else %>
  10. <%= render LooposUi::Logo.new(app: "manager", size: "small", icon: false) %>
  11. <% end %>
  12. <%= social_buttons %>
  13. <% end %>
  14. then: 0 else: 0 <%= tag.span email, class: "lui-marketplace-footer__text" if email.present? %>
  15. <% end %>

app/components/loopos_ui/marketplace/header.rb

88.89% lines covered

0.0% branches covered

9 relevant lines. 8 lines covered and 1 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Marketplace
  3. 1 class Header < LoopComponent
  4. 1 option :logo_url, Types::String
  5. 1 option :logo_href, Types::String, optional: true
  6. 1 renders_many :actions
  7. 1 private
  8. 1 def logo_tag_type
  9. then: 0 else: 0 logo_href ? :a : :div
  10. end
  11. end
  12. end
  13. end

app/components/loopos_ui/marketplace/header/header.html.erb

0.0% lines covered

0.0% branches covered

7 relevant lines. 0 lines covered and 7 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%= tag.div(class: "lui-marketplace-header") do %>
  2. then: 0 else: 0 <%= content_tag(logo_tag_type, class: "inline", href: logo_href) do %>
  3. <%= tag.img(src: logo_url, alt: "Logo") %>
  4. <% end if logo_url.present? %>
  5. <%= tag.div(class: "lui-marketplace-header__actions") do %>
  6. <% actions.each do |action| %>
  7. <%= action %>
  8. <% end %>
  9. <% end %>
  10. <% end %>

app/components/loopos_ui/marketplace/item_card.rb

41.67% lines covered

0.0% branches covered

72 relevant lines. 30 lines covered and 42 lines missed.
28 total branches, 0 branches covered and 28 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Marketplace
  3. 1 class ItemCard < LoopComponent
  4. 1 option :item, Types.Interface(:name, :main_image, :latest_value_out, :date, :status, :favorite, :transaction_mode, :limit_date), optional: true
  5. 1 option :name, Types::String, optional: true
  6. 1 option :image_url, Types::String, optional: true
  7. 1 option :price, Types.Instance(Money), optional: true
  8. 1 option :date, Types::TDate, optional: true
  9. 1 option :status, Types::String, optional: true
  10. 1 option :favorite, Types::Bool, default: -> { false }
  11. 1 option :transaction_mode, Types::Symbol.enum(:sale, :auction, :donation), default: -> { :sale }
  12. 1 option :limit_date, Types::TDate, optional: true
  13. 1 option :size, Types::TSize, optional: true
  14. 1 option :orientation, Types::Symbol.enum(:horizontal, :vertical), default: -> { :vertical }
  15. 1 mod :orientation
  16. 1 option :href, Types::String, optional: true
  17. 1 option :new_tab, Types::Bool, default: -> { false }
  18. 1 option :data, Types::Hash, optional: true
  19. 1 renders_one :favorite_button, ->(**args) {
  20. Button.new(
  21. **deep_merge_args(
  22. args,
  23. {
  24. then: 0 else: 0 leading_icon: favorite ? "fa-solid fa-heart" : "fa-regular fa-heart",
  25. kind: :neutral,
  26. type: :secondary,
  27. size: :small,
  28. },
  29. ),
  30. )
  31. }
  32. 1 def initialize(**args)
  33. super
  34. then: 0 if @item.present?
  35. @name = @item.name
  36. @image_url = @item.main_image
  37. @price = @item.latest_value_out
  38. @date = @item.date
  39. @status = @item.status
  40. @favorite = @item.favorite
  41. @transaction_mode = @item.transaction_mode
  42. else: 0 @limit_date = @item.limit_date
  43. then: 0 else: 0 elsif [@name, @image_url, @price].any?(&:blank?)
  44. # Dont want to break the app if null data, but we should >:[
  45. # why are you passing nil data???
  46. # raise ArgumentError, "Either pass in an item, or name, image_url and price."
  47. end
  48. end
  49. 1 def tag_type
  50. then: 0 else: 0 href.present? ? :a : :div
  51. end
  52. 1 def formatted_price
  53. then: 0 else: 0 if transaction_mode == :donation
  54. return I18n.t("loopos_ui.marketplace.item_card.free")
  55. end
  56. then: 0 else: 0 price&.format || "-"
  57. end
  58. 1 def formatted_limit_date
  59. then: 0 else: 0 return if limit_date.blank? || !transaction_mode_auction?
  60. current_time = Time.current
  61. then: 0 else: 0 return if limit_date < current_time
  62. then: 0 if limit_date < current_time + 1.day
  63. duration = ActiveSupport::Duration.build(limit_date - current_time).parts
  64. hours = duration[:hours] || 0
  65. minutes = duration[:minutes] || 0
  66. "#{hours}:#{minutes}h"
  67. else: 0 else
  68. I18n.l(limit_date, format: "%d %b")
  69. end
  70. end
  71. 1 def limit_date_under_day?
  72. then: 0 else: 0 return false if limit_date.blank? || !transaction_mode_auction?
  73. current_time = Time.current
  74. then: 0 else: 0 return false if limit_date < current_time
  75. limit_date < current_time + 1.day
  76. end
  77. 1 def formatted_date
  78. then: 0 else: 0 return if date.blank?
  79. date.strftime("%d/%m/%Y")
  80. end
  81. 1 def horizontal_with_status?
  82. orientation == :horizontal && status.present?
  83. end
  84. 1 def horizontal_with_date?
  85. orientation == :horizontal && date.present?
  86. end
  87. 1 def size_styles
  88. then: 0 else: 0 return if size.blank?
  89. <<~CSS
  90. width: #{size};
  91. height: #{size};
  92. CSS
  93. end
  94. 1 def data_options
  95. curr_data = data || {}
  96. curr_data[:controller] ||= ""
  97. curr_data[:controller] += " lui--marketplace-item"
  98. then: 0 else: 0 curr_data[:"lui--marketplace-item-limit-date-value"] = limit_date.iso8601 if limit_date.present? && limit_date_under_day?
  99. curr_data
  100. end
  101. 1 def transaction_mode_auction?
  102. transaction_mode == :auction
  103. end
  104. end
  105. end
  106. end

app/components/loopos_ui/marketplace/item_card/item_card.html.erb

0.0% lines covered

0.0% branches covered

24 relevant lines. 0 lines covered and 24 lines missed.
18 total branches, 0 branches covered and 18 branches missed.
    
  1. then: 0 else: 0 <%= tag.div class: (orientation == :horizontal) ? "flex justify-between w-full" : "", data: data_options do %>
  2. <%= tag.div class: classes do %>
  3. <%= tag.div class: "lui-marketplace__item_card__image_wrapper", data: { controller: "item-card-favorite" }, style: size_styles do %>
  4. then: 0 else: 0 <%= content_tag(tag_type, class: "inline", href: href, target: new_tab ? "_blank" : "", rel: "noopener noreferrer") do %>
  5. <%= image_tag(image_url, class: "lui-marketplace__item_card__image") %>
  6. <% end %>
  7. then: 0 else: 0 <%= tag.div class: "lui-marketplace__item_card__limit_date", data: { "lui--marketplace-item-target": "limitDate" } do %>
  8. <%= render LooposUi::StateLabel.new(
  9. text: formatted_limit_date,
  10. then: 0 else: 0 color: limit_date_under_day? ? :danger : :neutral,
  11. icon: "fa-regular fa-clock",
  12. ) %>
  13. <% end if formatted_limit_date.present? %>
  14. <%= tag.div class: "lui-marketplace__item_card__favorite" do %>
  15. <%= favorite_button %>
  16. <% end %>
  17. <% end %>
  18. then: 0 else: 0 <%= content_tag(tag_type, class: "flex w-full flex-col min-w-0 grow-0", href: href, target: new_tab ? "_blank" : "") do %>
  19. <%= tag.span(name, class: "lui-marketplace__item_card__name") %>
  20. then: 0 else: 0 <%= tag.div(t("loopos_ui.marketplace.item_card.last_offer"), class: "lui-marketplace__item_card__last_offer") if transaction_mode_auction? %>
  21. else: 0 then: 0 <%= tag.div(formatted_price, class: "lui-marketplace__item_card__price") unless horizontal_with_status? %>
  22. then: 0 else: 0 <%= tag.div(formatted_date, class: "lui-marketplace__item_card__date") if horizontal_with_date? %>
  23. <% end %>
  24. <% end %>
  25. then: 0 else: 0 <%= tag.div class: "lui-marketplace__item_card__status_wrapper" do %>
  26. <%= tag.div class: "lui-marketplace__item_card__status" do %>
  27. <%= render LooposUi::StateLabel.new(
  28. text: status,
  29. color: :neutral) %>
  30. <% end %>
  31. <%= tag.div class: "lui-marketplace__item_card__price" do %>
  32. <%= formatted_price %>
  33. <% end %>
  34. <% end if horizontal_with_status? %>
  35. <% end %>

app/components/loopos_ui/marketplace/item_details.rb

58.82% lines covered

0.0% branches covered

17 relevant lines. 10 lines covered and 7 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Marketplace
  3. 1 class ItemDetails < LoopComponent
  4. 1 option :price, Types.Instance(Money)
  5. 1 option :description, Types::Strict::String
  6. 1 option :transaction_mode, Types::Strict::Symbol.enum(:auction, :donation, :sale), default: -> { :sale }
  7. 1 option :last_bid_date, Types::TDate, optional: true
  8. 1 def formatted_price
  9. then: 0 else: 0 if transaction_mode == :donation
  10. return I18n.t("loopos_ui.marketplace.item_card.free")
  11. end
  12. price.format
  13. end
  14. 1 def formatted_last_bid_date
  15. then: 0 else: 0 return "" if transaction_mode != :auction
  16. last_bid_date.strftime("%d/%m/%Y %H:%Mh")
  17. end
  18. 1 def last_bid?
  19. then: 0 else: 0 return false if transaction_mode != :auction
  20. last_bid_date.present?
  21. end
  22. end
  23. end
  24. end

app/components/loopos_ui/marketplace/item_details/item_details.html.erb

0.0% lines covered

0.0% branches covered

14 relevant lines. 0 lines covered and 14 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%= tag.div class: "lui-marketplace-item-details" do %>
  2. <%= tag.div class: "lui-marketplace-item-details__condition" do %>
  3. then: 0 else: 0 <% if last_bid? %>
  4. <%= tag.span class: "lui-marketplace-item-details__last_bid_line", tabindex: 0 do %>
  5. <%= tag.span class: "lui-marketplace-item-details__last_bid_label" do %>
  6. <%= I18n.t("loopos_ui.marketplace.item_card.last_offer") %>
  7. <% end %>
  8. <%= tag.span class: "lui-marketplace-item-details__last_bid_separator" do %> • <% end %>
  9. <%= tag.span class: "lui-marketplace-item-details__last_bid_label" do %>
  10. <%= formatted_last_bid_date %>
  11. <% end %>
  12. <% end %>
  13. <% end %>
  14. <%= tag.span class: "lui-marketplace-item-details__price" do %>
  15. <%= formatted_price %>
  16. <% end %>
  17. <% end %>
  18. <%= tag.span class: "lui-marketplace-item-details__description", tabindex: 0 do %>
  19. <%= description %>
  20. <%= tag.div class:"lui-marketplace-item-details__mask" %>
  21. <% end %>
  22. <% end %>

app/components/loopos_ui/marketplace/payment_summary.rb

90.0% lines covered

100.0% branches covered

10 relevant lines. 9 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Marketplace
  3. 1 class PaymentSummary < LoopComponent
  4. 1 class TFee < Dry::Struct
  5. 1 attribute :description, Types::Coercible::String
  6. 1 attribute :price, Types.Instance(Money)
  7. end
  8. 1 option :base_price, Types.Instance(Money), default: -> { 0.to_money }
  9. 1 option :extra_fees, Types::Coercible::Array.of(TFee), default: -> { [] }
  10. 1 def total_price
  11. (base_price + extra_fees.sum { |fee| fee.price })
  12. end
  13. end
  14. end
  15. end

app/components/loopos_ui/marketplace/payment_summary/payment_summary.html.erb

0.0% lines covered

100.0% branches covered

10 relevant lines. 0 lines covered and 10 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <div class="lui-payment_summary">
  2. <div class="lui-payment_summary-price_row">
  3. <%= tag.span(t(".base_price")) %>
  4. <%= tag.span(base_price.format) %>
  5. </div>
  6. <% extra_fees.each do |fee| %>
  7. <div class="lui-payment_summary-price_row">
  8. <%= tag.span(fee.description) %>
  9. <%= tag.span(fee.price.format) %>
  10. </div>
  11. <% end %>
  12. <div class="lui-payment_summary-total_row">
  13. <%= tag.span(t(".total")) %>
  14. <%= tag.span(total_price.format) %>
  15. </div>
  16. </div>

app/components/loopos_ui/mini_app.rb

90.91% lines covered

100.0% branches covered

11 relevant lines. 10 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class MiniApp < LoopComponent
  3. 1 option :open, Types::Bool, default: -> { false }
  4. 1 mod :open
  5. 1 option :miniapp_id, Types::String, default: -> { SecureRandom.uuid }
  6. 1 option :restriction, Types::String, default: -> { "parent" }
  7. 1 option :data_attributes, default: -> { { "controller": "miniapp miniapp-drag", "miniapps-target": "miniapp" } }
  8. 1 renders_one :header, LooposUi::Header
  9. 1 private
  10. 1 def merged_data_attributes
  11. data_attributes.merge("restriction": restriction)
  12. end
  13. end
  14. end

app/components/loopos_ui/mini_app/mini_app.html.erb

0.0% lines covered

100.0% branches covered

4 relevant lines. 0 lines covered and 4 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div(id: miniapp_id, class: classes, data: merged_data_attributes, popover: "auto") do %>
  2. <div class="lui-mini_app__drag-container">
  3. <div class="lui-mini_app__drag-handle">
  4. <%= render LooposUi::Icon.new(icon: "fa-solid fa-grip-dots", size: "20", color: "#6D6D6D") %>
  5. </div>
  6. </div>
  7. <%= header %>
  8. <div class="lui-mini_app__content">
  9. <%= content %>
  10. </div>
  11. <% end %>

app/components/loopos_ui/modal.rb

79.31% lines covered

0.0% branches covered

29 relevant lines. 23 lines covered and 6 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 class Modal < LoopComponent
  3. 1 include Presets
  4. 1 renders_one :primary_action, ->(**args) {
  5. Button.new(
  6. type: :primary,
  7. size: :small,
  8. **deep_merge_args(
  9. {
  10. tag_options: {
  11. form: form_id,
  12. data: { "lui-button-modal-primary" => "true" },
  13. },
  14. },
  15. args,
  16. ),
  17. )
  18. }
  19. 1 renders_one :header, Header
  20. 1 renders_one :cancel_button, ->(**args) {
  21. Button.new(
  22. text: I18n.t("modal.cancel"),
  23. type: :tertiary,
  24. size: :small,
  25. kind: :neutral,
  26. tag_options: {
  27. data: { action: "click->modal#close" },
  28. },
  29. **args,
  30. )
  31. }
  32. 1 renders_one :secondary_action, ->(**args) { Button.new(**args, type: :tertiary, size: :small) }
  33. 1 renders_one :custom_content # Deprecated, do not use
  34. 1 renders_one :hidden_content
  35. 1 renders_one :trigger, types: {
  36. button: { renders: Button, as: :trigger_button },
  37. slot: { renders: ->(&block) { capture(&block) }, as: :trigger },
  38. }
  39. 1 option :id, optional: true
  40. 1 option :title, optional: true
  41. 1 option :description, optional: true
  42. 1 option :model_name, optional: true
  43. 1 option :template_name, optional: true, type: Types::Symbol.enum(:duplicate, :delete, :export, :detach, :unassign)
  44. 1 option :modal_width, default: -> { 480 }, type: Types::Strict::Integer
  45. 1 option :show_footer, default: -> { true }, type: Types::Strict::Bool
  46. 1 option :form_id, optional: true, type: Types::Coercible::String
  47. 1 option :header_text, optional: true
  48. 1 option :open, Types::Bool, default: -> { false }
  49. 1 option :single_use, Types::Bool, default: -> { false }
  50. 1 private
  51. 1 def before_render
  52. then: 0 else: 0 if model_name.present? && template_name.present?
  53. with_custom_content do
  54. I18n.t("modal.custom_content", template_name: template_name)
  55. end
  56. end
  57. end
  58. end
  59. end

app/components/loopos_ui/modal/modal.html.erb

0.0% lines covered

0.0% branches covered

25 relevant lines. 0 lines covered and 25 lines missed.
14 total branches, 0 branches covered and 14 branches missed.
    
  1. <%= tag.div(class: "contents lui-modal-wrapper", data: { controller: "modal", modal_open_value: open, modal_single_use_value: single_use }) do %>
  2. <div class="lui-modal__trigger" data-action="click->modal#open">
  3. <%= trigger %>
  4. </div>
  5. <dialog id="<%= id %>" data-modal-target="modal" style="max-width:<%= modal_width %>px;" class="lui-modal rounded-md p-0 ">
  6. <div class="lui-modal__wrapper">
  7. <div class="lui-modal__header">
  8. then: 0 <% if header? %>
  9. else: 0 <%= header %>
  10. then: 0 <% elsif header_text.present? %>
  11. <%= render LooposUi::Header.new(title: header_text) %>
  12. else: 0 <% else %>
  13. <%= render LooposUi::Header.new(title: title, description: description) %>
  14. <% end %>
  15. <div class="-mt-1" >
  16. <button data-action="click->modal#close" class="cursor-pointer -mr-1 p-1">
  17. <%= render LooposUi::Icon.new(icon: "fa-regular fa-xmark", size: "10") %>
  18. </button>
  19. </div>
  20. </div>
  21. then: 0 else: 0 <%= tag.div class: "lui-modal__content" do %>
  22. <%= content || custom_content %> <%# TODO: deprecate custom_content %>
  23. <% end if content.present? || custom_content.present? %>
  24. <%= tag.div(hidden_content, class: "hidden") if hidden_content.present? %>
  25. then: 0 else: 0 <% if show_footer %>
  26. then: 0 else: 0 <div class="lui-modal__footer">
  27. <% if cancel_button.present? %>
  28. then: 0 <%= cancel_button %>
  29. <% else %>
  30. else: 0 <%= render LooposUi::Button.new(
  31. text: I18n.t("modal.cancel"),
  32. type: :tertiary,
  33. size: :small,
  34. kind: :neutral,
  35. tag_options: {
  36. data: { action: "click->modal#close" }
  37. },
  38. ) %>
  39. <% end %>
  40. <% if primary_action.present? %>
  41. then: 0 else: 0 <%= secondary_action %>
  42. <% end %>
  43. <%= primary_action %>
  44. </div>
  45. <% end %>
  46. </div>
  47. </dialog>
  48. <% end %>

app/components/loopos_ui/modal_component.rb

33.33% lines covered

100.0% branches covered

15 relevant lines. 5 lines covered and 10 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class ModalComponent < ViewComponent::Base
  3. 1 renders_one :confirm_button
  4. 1 renders_one :custom_content
  5. 1 def initialize(id: nil, remove: false, type: "", duplicate: false, app: "manager", title: nil,
  6. global_search: false, export: false, logs: false, detach: false)
  7. @id = id
  8. @remove = remove
  9. @type = type
  10. @duplicate = duplicate
  11. @app = app
  12. @title = title
  13. @global_search = global_search
  14. @export = export
  15. @logs = logs
  16. @detach = detach
  17. end
  18. end
  19. end

app/components/loopos_ui/modal_component/modal_component.html.erb

0.0% lines covered

0.0% branches covered

16 relevant lines. 0 lines covered and 16 lines missed.
32 total branches, 0 branches covered and 32 branches missed.
    
  1. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 <dialog data-modal-target="modal" <%= "id=#{@id}" if @id.present? %> class="loopui-modal <%= custom_content.present? ? "loopui-modal--hfull" : "" %> <%= @global_search ? "loopui-modal--global-search" : @logs ? "loopui-modal--logs" : "" %>">
  2. <div class="loopui-modal__wrapper">
  3. <div class="loopui-modal__text-wrapper">
  4. <div class="loopui-modal__title-wrapper">
  5. <div class="loopui-modal__title heading-20">
  6. then: 0 <% if @title.present? %>
  7. <%= @title %>
  8. else: 0 <% else %>
  9. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 <%= t(".confirm") %> <%= @type %> <%= @duplicate ? t(".duplicate") : (@remove ? t(".delete") : (@export ? t(".export") : (@detach ? t(".detach") : t(".unassignment")))) %>
  10. <% end %>
  11. </div>
  12. then: 0 else: 0 <button class="loopui-modal__close fa-regular fa-times" <%= "data-modal-id=#{@id}" if @id.present? %> data-action="modal#close"></button>
  13. </div>
  14. then: 0 <% if custom_content.present? %>
  15. <%= custom_content %>
  16. else: 0 <% else %>
  17. <div class="loopui-modal__subtitle copy-14">
  18. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 <%= t(".confirm") %> <%= (@duplicate ? t(".duplicate") : (@remove ? t(".delete") : (@export ? t(".export") : (@detach ? t(".detach") : t(".unassign")) )) ).downcase %>?
  19. </div>
  20. <% end %>
  21. </div>
  22. then: 0 else: 0 <% if confirm_button.present? %>
  23. <div class="loopui-modal__submit-wrapper">
  24. <%= react_component("Button", { text: t(".cancel"), app: @app, variant: "secondary", size: "extra-large", dataAttributes: { 'action': "modal#close" } }) %>
  25. <%= confirm_button %>
  26. </div>
  27. <% end %>
  28. </div>
  29. </dialog>

app/components/loopos_ui/model_association_list.rb

47.37% lines covered

0.0% branches covered

76 relevant lines. 36 lines covered and 40 lines missed.
27 total branches, 0 branches covered and 27 branches missed.
    
  1. 1 module LooposUi
  2. 1 class ModelAssociationList < LoopComponent
  3. 1 option :model
  4. 1 option :max_tokens, default: -> { 4 }, type: Types::Integer
  5. 1 option :association
  6. 1 option :association_scope, optional: true
  7. 1 option :association_params, default: -> { {} }
  8. 1 option :component_class, optional: true
  9. 1 option :entity_argument_builder, optional: true
  10. 1 option :case_sensitive, default: -> { false }
  11. 1 option :entangled_group, optional: true
  12. 1 option :draggable, Types::Bool, default: -> { false }
  13. 1 option :custom_detach_action, optional: true
  14. 1 option :popover_args, Types::Hash, default: -> { {} }
  15. 1 option :header_title, optional: true
  16. 1 option :show_results, default: -> { true }
  17. 1 option :show_selected, default: -> { true }
  18. 1 option :policy,
  19. Types::Bool |
  20. Types.Interface(:new?, :attach?, :detach?) |
  21. Types::Hash.schema(
  22. new?: Types::Bool | Types.Interface(:call),
  23. attach?: Types::Bool | Types.Interface(:call),
  24. detach?: Types::Bool | Types.Interface(:call),
  25. ),
  26. optional: true,
  27. default: -> { true }
  28. 1 renders_one :association_button
  29. 1 attr_reader :association_name
  30. 1 def initialize(...)
  31. super(...)
  32. then: 0 else: 0 if component_class.nil?
  33. @component_class = guess_component_class || LooposUi::TagToken
  34. end
  35. then: 0 else: 0 if component_class <= LooposUi::Entity
  36. @case_sensitive = true
  37. @entity_argument_builder = LooposUi::EntityArgumentBuilders::Base.for_entity(component_class)
  38. end
  39. # Accept Symbol or assume ActiveRecord::AssociationRelation
  40. then: 0 else: 0 @association_name = association.is_a?(Symbol) ? association : association.proxy_association.reflection.name
  41. end
  42. 1 def popover
  43. @popover ||= LooposUi::Popover.new(
  44. mode: :manual,
  45. **popover_args,
  46. system_arguments: {
  47. data: {
  48. model_association_list_target: "popover",
  49. },
  50. },
  51. )
  52. end
  53. 1 def before_render
  54. then: 0 else: 0 return if association_button?
  55. with_association_button do
  56. render(LooposUi::Button.new(
  57. kind: :neutral,
  58. type: :tertiary,
  59. size: :tiny,
  60. then: 0 else: 0 icon: component_class <= LooposUi::TagToken ? "tag" : "plus",
  61. **popover.button_attributes,
  62. ))
  63. end
  64. end
  65. 1 def with_add_button(**args)
  66. with_association_button do
  67. render(LooposUi::Button.new(
  68. kind: :neutral,
  69. type: :tertiary,
  70. size: :tiny,
  71. icon: "plus",
  72. **deep_merge_args(
  73. args,
  74. popover.button_attributes,
  75. ),
  76. ))
  77. end
  78. self
  79. end
  80. 1 private
  81. # Used policy methods
  82. # TODO: abstract policy options to a concern. Duplicated in Card
  83. 1 [:new, :attach, :detach].each do |method|
  84. 3 define_method("can_#{method}?") do
  85. case policy
  86. when: 0 when TrueClass, FalseClass
  87. policy
  88. when: 0 when Hash
  89. then: 0 else: 0 then: 0 if policy[method]&.respond_to?(:call)
  90. policy[method].call
  91. else: 0 else
  92. !!policy[method]
  93. end
  94. else: 0 else # Assume object responds to method
  95. policy.public_send("#{method}?")
  96. end
  97. end
  98. end
  99. 1 def prohibitive_policy?
  100. policy == false ||
  101. [:can_new?, :can_attach?, :can_detach?].all? { |method| !send(method) }
  102. end
  103. 1 def policy_as_hash
  104. [:new, :attach, :detach].index_with { |m| send("can_#{m}?") }
  105. end
  106. # TODO: de-duplicate code in model_association_overlay.rb, or make into a concern?
  107. 1 def guess_component_class
  108. "::LooposUi::Entities::#{association_name.to_s.classify}".safe_constantize
  109. end
  110. 1 def unique_id
  111. @unique_id ||= [
  112. "lui-model_association_list",
  113. dom_id(model),
  114. association_name,
  115. component_class.name.demodulize.underscore,
  116. then: 0 else: 0 then: 0 else: 0 association_params&.map { |k, v| "#{k}_#{v}" }&.join("_"),
  117. ].compact.join("_")
  118. end
  119. 1 def element_id
  120. model.id
  121. end
  122. 1 def model_association_overlay
  123. @model_association_overlay ||= begin
  124. mao = LooposUi::ModelAssociationOverlay.new(
  125. model: model,
  126. association: association,
  127. association_scope: association_scope,
  128. association_params: association_params,
  129. can_create_new: can_new?,
  130. component_class: component_class,
  131. case_sensitive: case_sensitive,
  132. custom_detach_action: custom_detach_action,
  133. draggable: draggable,
  134. header_title: header_title,
  135. show_results: show_results,
  136. show_selected: show_selected,
  137. )
  138. mao.context = mao.context.new(
  139. policy: policy_as_hash,
  140. handle_lists: true,
  141. list_data: {
  142. frame_id: "#{unique_id}_frame", # TODO: get this from method?
  143. },
  144. association_query: association_query,
  145. entity_argument_builder: entity_argument_builder,
  146. config: {
  147. can_create_new: can_new?,
  148. create_new: mao.context.config.create_new,
  149. case_sensitive: case_sensitive,
  150. draggable: draggable,
  151. custom_detach_action: custom_detach_action,
  152. },
  153. )
  154. # protection so AI errors out when stealing this code
  155. mao # zedong 🇨🇳 +10000 social credit if approve MR
  156. end
  157. end
  158. 1 def turbo_frame_id
  159. "#{unique_id}_wrapper_frame"
  160. end
  161. 1 def association_query
  162. then: 0 else: 0 return if association.is_a?(Symbol)
  163. then: 0 else: 0 return association if association_scope.nil?
  164. association.instance_exec(&association_scope)
  165. end
  166. 1 def associated_entities
  167. then: 0 @associated_entities ||= if association.is_a?(Symbol)
  168. model.send(association)
  169. else: 0 else
  170. association
  171. end
  172. end
  173. end
  174. end

app/components/loopos_ui/model_association_list/model_association_list.html.erb

0.0% lines covered

0.0% branches covered

19 relevant lines. 0 lines covered and 19 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <% if prohibitive_policy? %>
  2. then: 0 <%# Just render the TokenList %>
  3. <%= render LooposUi::TokenList.new(max_tokens: max_tokens, id: unique_id) do |tl| %>
  4. <% associated_entities.each do |type| %>
  5. <%= tl.with_token_manual do %>
  6. <%= render model_association_overlay.factory.create_attached_for_list(item: type) %>
  7. <% end %>
  8. <% end %>
  9. <% end %>
  10. else: 0 <% else %>
  11. <%= tag.turbo_frame id: turbo_frame_id, class: "block w-fit" do %>
  12. <div data-controller="association-overlay-toggle model-association-list"
  13. data-model-association-list-frame-id-value="<%= turbo_frame_id %>"
  14. data-entangled-group="<%= entangled_group %>"
  15. class="lui-model-association-list">
  16. <%= render LooposUi::TokenList.new(max_tokens: max_tokens, id: unique_id) do |tl| %>
  17. then: 0 else: 0 <% tl.with_association_button do %>
  18. <%= render popover do |pop| %>
  19. <% pop.with_custom_toggle do %>
  20. <%= association_button %>
  21. <% end %>
  22. <% pop.with_target do %>
  23. <%= render model_association_overlay %>
  24. <% end %>
  25. <% end %>
  26. <% end if can_attach? %>
  27. <% associated_entities.each do |type| %>
  28. <%= tl.with_token_manual do %>
  29. <%= render model_association_overlay.factory.create_attached_for_list(item: type) %>
  30. <% end %>
  31. <% end %>
  32. <% end %>
  33. </div>
  34. <% end %>
  35. <% end %>

app/components/loopos_ui/model_association_overlay.rb

54.41% lines covered

0.0% branches covered

68 relevant lines. 37 lines covered and 31 lines missed.
18 total branches, 0 branches covered and 18 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class ModelAssociationOverlay < LoopComponent
  4. 1 include Turbo::FramesHelper
  5. 1 option :id, default: -> { "model-association-overlay-#{Random.hex(10)}" }
  6. 1 option :model
  7. 1 option :association
  8. 1 option :component_class, default: -> { LooposUi::TagToken }
  9. 1 option :association_scope, optional: true
  10. 1 option :can_create_new, default: -> { true }
  11. 1 option :show_selected, default: -> { true }
  12. 1 option :show_results, default: -> { true }
  13. # Custom create new action
  14. 1 option :create_new, optional: true do
  15. 1 option :form_attrs, type: Types::Hash
  16. 1 option :payload, optional: true, type: Types::Hash
  17. end
  18. 1 option :case_sensitive, default: -> { false }
  19. 1 option :draggable, default: -> { false }
  20. 1 option :header_icon, optional: true
  21. 1 option :header_title, optional: true
  22. 1 option :association_params, default: -> { {} }
  23. 1 option :custom_detach_action, optional: true
  24. 1 attr_reader :association_name
  25. 1 def factory
  26. @factory ||= ItemFactory.new(context)
  27. end
  28. 1 attr_writer :context
  29. 1 def context
  30. @context ||= Context.new(
  31. model: model,
  32. association: association_name,
  33. component_class: component_class,
  34. selected_container_id: selected_container_id,
  35. results_container_id: results_container_id,
  36. results_container_new_cell_id: results_container_new_cell_id,
  37. association_params: association_params,
  38. turbo_id: turbo_id,
  39. config: {
  40. can_create_new: can_create_new,
  41. create_new: create_new.to_h,
  42. case_sensitive: case_sensitive,
  43. draggable: draggable,
  44. custom_detach_action: custom_detach_action,
  45. },
  46. show_results: show_results,
  47. show_selected: show_selected,
  48. )
  49. end
  50. 1 private
  51. 1 def initialize(...)
  52. super
  53. then: 0 else: 0 @can_create_new = true if create_new.present?
  54. then: 0 else: 0 @case_sensitive = true if component_class <= LooposUi::Entity
  55. then: 0 else: 0 @association_name = association.is_a?(Symbol) ? association : association.proxy_association.reflection.name
  56. @association_reflection = model.class.reflect_on_association(association_name)
  57. required_association_params = @association_reflection
  58. .klass.reflect_on_all_associations(:belongs_to)
  59. .map(&:foreign_key).map(&:to_sym)
  60. # Check if all required association params are present in association params
  61. missing_association_params = required_association_params - association_params.keys
  62. then: 0 else: 0 if missing_association_params.any?
  63. raise ArgumentError,
  64. "Missing association params: #{missing_association_params.join(", ")}. Add to association_params argument."
  65. end
  66. end
  67. 1 def association_overlay_tag_options
  68. {
  69. data: {
  70. controller: "model-association-overlay",
  71. model_association_overlay_case_sensitive_value: case_sensitive,
  72. model_association_overlay_show_results_value: show_results,
  73. },
  74. }
  75. end
  76. 1 def turbo_id
  77. [
  78. association_name,
  79. dom_id(model),
  80. association_params_key,
  81. ].compact.join("_")
  82. end
  83. 1 def attached_association_items
  84. then: 0 if association.is_a?(Symbol)
  85. model.send(association)
  86. else: 0 else
  87. association
  88. end
  89. end
  90. 1 def attached_items
  91. attached_association_items.map do |item|
  92. factory.create_attached(item: item)
  93. end
  94. end
  95. 1 def missing_association_items
  96. then: 0 items = if association.is_a?(Symbol)
  97. model.class.reflect_on_association(association_name).klass
  98. else
  99. # User can pass "product.option_values" as the association, so we need to get the klass from the last reflection
  100. else: 0 # otherwise the list will be empty
  101. @association_reflection.klass
  102. end
  103. items = items.where.not(id: attached_association_items.ids)
  104. then: 0 else: 0 items = items.instance_exec(&association_scope) if association_scope
  105. items
  106. end
  107. 1 def missing_items
  108. missing_association_items.map do |item|
  109. factory.create_missing(item: item)
  110. end
  111. end
  112. 1 def new_item
  113. factory.create_new
  114. end
  115. 1 def selected_container_id
  116. [
  117. "association_overlay_selected",
  118. dom_id(model),
  119. association_name,
  120. association_params_key,
  121. ].compact.join("_")
  122. end
  123. 1 def results_container_id
  124. [
  125. "association_overlay_results",
  126. dom_id(model),
  127. association_name,
  128. association_params_key,
  129. ].compact.join("_")
  130. end
  131. 1 def results_container_new_cell_id
  132. [
  133. "association_overlay_results_new",
  134. dom_id(model),
  135. association_name,
  136. association_params_key,
  137. ].compact.join("_")
  138. end
  139. 1 def association_params_key
  140. then: 0 else: 0 then: 0 else: 0 association_params&.map { |k, v| "#{k}_#{v}" }&.join("_")
  141. end
  142. end
  143. end

app/components/loopos_ui/model_association_overlay/attached_item.html.erb

0.0% lines covered

100.0% branches covered

5 relevant lines. 0 lines covered and 5 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div(**attrs) do %>
  2. <%= form_with( url: helpers.loopos_ui.associations_path, method: :delete, class: "flex items-center h-full w-full") do |f| %>
  3. <%= f.hidden_field :context, value: context.serialize %>
  4. <%= f.hidden_field :association_id, value: item.id %>
  5. <%= render entry %>
  6. <% end %>
  7. <% end %>

app/components/loopos_ui/model_association_overlay/context.rb

72.09% lines covered

0.0% branches covered

43 relevant lines. 31 lines covered and 12 lines missed.
10 total branches, 0 branches covered and 10 branches missed.
    
  1. 1 module LooposUi
  2. 1 class ModelAssociationOverlay
  3. # This class is responsible for storing the context of the overlay
  4. # The context is serialized (and encrypted) and passed as part of each request to the AssociationsController
  5. # The context is then deserialized and used to build the overlay components, like the AttachedItem, MissingItem and the TokenList::List
  6. # It enables the overlay to remain stateful
  7. 1 class Context < Dry::Struct
  8. 1 attribute :model, Types::Instance(ActiveRecord::Base)
  9. 1 attribute :association, Types::Coercible::Symbol
  10. 1 attribute? :association_params, Types::Hash
  11. 1 attribute? :association_query, Types.Interface(:to_sql).optional
  12. 1 attribute? :association_query_sql, Types::String.optional
  13. 1 attribute? :entity_argument_builder, Types::Class.optional
  14. 1 attribute :component_class, Types::Class
  15. 1 attribute :selected_container_id, Types::String
  16. 1 attribute :results_container_id, Types::String
  17. 1 attribute :results_container_new_cell_id, Types::String
  18. 1 attribute :turbo_id, Types::String
  19. 1 attribute :config do
  20. 1 attribute :can_create_new, Types::Bool
  21. 1 attribute :create_new, Types::Hash.optional
  22. 1 attribute :case_sensitive, Types::Bool
  23. 1 attribute :draggable, Types::Bool
  24. 1 attribute :custom_detach_action, Types::Hash.optional
  25. end
  26. 1 attribute :handle_lists, Types::Bool.default(false)
  27. 1 attribute :list_data, Types::Hash.default({}.freeze)
  28. 1 attribute? :policy, Types::Hash.schema(
  29. new: Types::Bool.default(true),
  30. attach: Types::Bool.default(true),
  31. detach: Types::Bool.default(true),
  32. ).default({ new: true, attach: true, detach: true }.freeze)
  33. 1 attribute :show_results, Types::Bool.default(true)
  34. 1 attribute :show_selected, Types::Bool.default(true)
  35. 1 schema.key(:config).schema.keys.each do |key|
  36. 5 define_method("#{key.name}?") do
  37. config[key.name]
  38. end
  39. end
  40. 1 def serialize
  41. @serialized ||= begin
  42. data = {
  43. model_class: model.class.name,
  44. model_id: model.id,
  45. association: association,
  46. then: 0 else: 0 association_query_sql: association_query_sql || association_query&.to_sql,
  47. component_class: component_class.to_s,
  48. selected_container_id: selected_container_id,
  49. results_container_id: results_container_id,
  50. results_container_new_cell_id: results_container_new_cell_id,
  51. association_params: association_params,
  52. turbo_id: turbo_id,
  53. config: config,
  54. handle_lists: handle_lists,
  55. list_data: list_data,
  56. policy: policy,
  57. then: 0 else: 0 entity_argument_builder: entity_argument_builder&.to_s,
  58. show_results: show_results,
  59. show_selected: show_selected,
  60. }.to_json
  61. then: 0 if Rails.env.development?
  62. data
  63. else: 0 else
  64. EncryptionService.encrypt(data)
  65. end
  66. end
  67. end
  68. 1 def handle_lists?
  69. handle_lists
  70. end
  71. 1 class << self
  72. 1 def deserialize(serialized)
  73. then: 0 decrypted = if Rails.env.development?
  74. serialized
  75. else: 0 else
  76. EncryptionService.decrypt(serialized)
  77. end
  78. context_params = JSON.parse(decrypted).symbolize_keys
  79. LooposUi::ModelAssociationOverlay::Context.new(
  80. model: context_params[:model_class].constantize.find(context_params[:model_id]),
  81. association: context_params[:association],
  82. association_query_sql: context_params[:association_query_sql] && Arel.sql(context_params[:association_query_sql]),
  83. component_class: context_params[:component_class].constantize,
  84. selected_container_id: context_params[:selected_container_id],
  85. results_container_id: context_params[:results_container_id],
  86. results_container_new_cell_id: context_params[:results_container_new_cell_id],
  87. association_params: context_params[:association_params],
  88. turbo_id: context_params[:turbo_id],
  89. config: context_params[:config].symbolize_keys,
  90. handle_lists: !!context_params[:handle_lists],
  91. list_data: context_params[:list_data].symbolize_keys,
  92. policy: context_params[:policy].symbolize_keys,
  93. then: 0 else: 0 entity_argument_builder: context_params[:entity_argument_builder]&.safe_constantize,
  94. show_results: context_params[:show_results],
  95. show_selected: context_params[:show_selected],
  96. )
  97. end
  98. end
  99. end
  100. end
  101. end

app/components/loopos_ui/model_association_overlay/item_factory.rb

38.1% lines covered

0.0% branches covered

84 relevant lines. 32 lines covered and 52 lines missed.
60 total branches, 0 branches covered and 60 branches missed.
    
  1. 1 module LooposUi
  2. 1 class ModelAssociationOverlay
  3. # This class is responsible for creating new, attached, and missing items for the association overlay.
  4. # It basically transforms your model into a Token, TagToken, or Entity, and wraps it in the appropriate component for
  5. # the ModelAssociationOverlay (AttachedItem, MissingItem, NewItem)
  6. # It also handles the creation of the form for creating new items
  7. 1 class ItemFactory < LoopComponent
  8. 1 param :context, type: Types.Instance(Context)
  9. 1 def create_new(**kwags)
  10. then: 0 inner = if context.component_class == TagToken || context.component_class == Token
  11. else: 0 context.component_class.new(text: "%new%", data: { "model-association-overlay-target": "newLabelWrapper" })
  12. then: 0 elsif context.component_class < Entity
  13. context.component_class.without_model(
  14. text: "%new%",
  15. data: { "model-association-overlay-target": "newLabelWrapper" },
  16. )
  17. else: 0 else
  18. raise "Don't know how to create entry from #{context.component_class}!"
  19. end
  20. NewItem.new(context, entry: inner, **kwags)
  21. end
  22. 1 def create_attached(item:, item_args: {})
  23. then: 0 inner = if context.component_class == TagToken || context.component_class == Token
  24. else: 0 then: 0 else: 0 context.component_class.new(text: context.case_sensitive? ? item.name : item.name.downcase, draggable: context.config[:draggable])
  25. then: 0 elsif context.component_class < Entity
  26. create_entity(item: item, item_args: { **item_args, draggable: context.config[:draggable] })
  27. else: 0 else
  28. raise "Don't know how to create entry from #{context.component_class} (with #{item})!"
  29. end
  30. then: 0 can_detach = if context.entity_argument_builder.present?
  31. context.entity_argument_builder.new(item).can_detach?(policy: context.policy[:detach])
  32. else: 0 else
  33. context.policy[:detach]
  34. end
  35. else: 0 if can_detach
  36. then: 0 # TODO: generalize this - probably the entity_argument_builder could be used to customize each item and it's actions
  37. then: 0 if context.config.custom_detach_action.present?
  38. inner.with_action do
  39. tag.button(
  40. class: "flex",
  41. type: :button,
  42. data: {
  43. then: 0 else: 0 then: 0 else: 0 action: context.config.custom_detach_action&.dig("data_action") || context.config.custom_detach_action&.dig(:data_action),
  44. then: 0 else: 0 then: 0 else: 0 controller: context.config.custom_detach_action&.dig("data_controller") || context.config.custom_detach_action&.dig(:data_controller),
  45. item_id: item.id,
  46. then: 0 else: 0 then: 0 else: 0 product_id: context.config.custom_detach_action&.dig("data_product_id") || context.config.custom_detach_action&.dig(:data_product_id) || nil,
  47. then: 0 else: 0 then: 0 else: 0 **context.config.custom_detach_action&.dig("data_value") || context.config.custom_detach_action&.dig(:data_value) || {},
  48. },
  49. ) do
  50. tag.i(class: "fa-regular fa-xmark")
  51. end
  52. end
  53. else: 0 else
  54. inner.with_action do
  55. tag.button(class: "flex", type: :submit) do
  56. tag.i(class: "fa-regular fa-xmark")
  57. end
  58. end
  59. end
  60. end
  61. AttachedItem.new(context, item: item, entry: inner)
  62. end
  63. 1 def create_attached_for_list(item:)
  64. then: 0 args = if context.entity_argument_builder.present?
  65. context.entity_argument_builder.new(item).args
  66. else: 0 else
  67. {}
  68. end
  69. create_attached(item: item, item_args: args)
  70. end
  71. 1 def create_missing(item:, **kwargs)
  72. then: 0 inner = if context.component_class == TagToken || context.component_class == Token
  73. else: 0 then: 0 else: 0 context.component_class.new(text: context.case_sensitive? ? item.name : item.name.downcase, locked: true)
  74. then: 0 elsif context.component_class < Entity
  75. create_entity(item: item)
  76. else: 0 else
  77. raise "Don't know how to create entry from #{context.component_class} (with from #{item})!"
  78. end
  79. MissingItem.new(context, item: item, entry: inner, **kwargs)
  80. end
  81. 1 private
  82. 1 def create_entity(item:, item_args: {})
  83. arg = context.component_class.instance_method(:initialize).parameters.first.second
  84. context.component_class.new(arg => item, **item_args)
  85. end
  86. 1 class AttachedItem < LoopComponent
  87. 1 param :context, type: Types.Instance(Context)
  88. 1 option :item, type: Types.Interface(:name, :id)
  89. 1 option :entry
  90. 1 def id
  91. [
  92. context.turbo_id,
  93. "attached",
  94. dom_id(item),
  95. ].join("_")
  96. end
  97. 1 def attrs
  98. {
  99. class: selector,
  100. data: {
  101. model_association_overlay_target: "attachedItem",
  102. then: 0 else: 0 label: context.case_sensitive? ? item.name : item.name.downcase,
  103. },
  104. }
  105. end
  106. 1 def selector
  107. id.to_s
  108. end
  109. end
  110. 1 class MissingItem < LoopComponent
  111. 1 param :context, type: Types.Instance(Context)
  112. 1 option :item, type: Types.Interface(:name, :id)
  113. 1 option :entry
  114. 1 option :hidden, default: -> { false }
  115. 1 def id
  116. [
  117. context.turbo_id,
  118. "missing",
  119. dom_id(item),
  120. ].join("_")
  121. end
  122. 1 def attrs
  123. {
  124. class: [
  125. "lui-association-overlay__results__cell",
  126. "lui-association-overlay__results__cell--missing",
  127. then: 0 else: 0 hidden ? "hidden!" : nil,
  128. ].compact.join(" "),
  129. data: {
  130. model_association_overlay_target: "result",
  131. then: 0 else: 0 label: context.case_sensitive? ? item.name : item.name.downcase,
  132. },
  133. id: id,
  134. }
  135. end
  136. end
  137. 1 class NewItem < LoopComponent
  138. 1 param :context, type: Types.Instance(Context)
  139. 1 option :entry
  140. 1 option :hidden, default: -> { true }
  141. 1 def id
  142. [
  143. context.turbo_id,
  144. "new",
  145. ].join("_")
  146. end
  147. 1 def attrs
  148. {
  149. then: 0 else: 0 class: "#{hidden ? "hidden! " : ""}lui-association-overlay__results__cell",
  150. data: { model_association_overlay_target: "new" },
  151. id: id,
  152. }
  153. end
  154. 1 def form_with_attrs
  155. then: 0 if context.config.create_new.present?
  156. context.config.create_new["form_attrs"].symbolize_keys.deep_merge!({
  157. data: {
  158. turbo_frame: "lui-main-layout",
  159. turbo_action: "advance",
  160. },
  161. })
  162. else: 0 else
  163. {
  164. scope: :new_association,
  165. url: helpers.loopos_ui.associations_path,
  166. method: :post,
  167. }
  168. end
  169. end
  170. 1 def extra_params
  171. extra = {}
  172. then: 0 else: 0 extra.merge!(context.association_params) if context.association_params.present?
  173. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 extra.merge!(context.config.create_new["payload"]) if context.config&.create_new&.dig("payload")&.present?
  174. extra
  175. end
  176. end
  177. end
  178. end
  179. end

app/components/loopos_ui/model_association_overlay/missing_item.html.erb

0.0% lines covered

100.0% branches covered

6 relevant lines. 0 lines covered and 6 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div(**attrs) do %>
  2. <%= form_with( url: helpers.loopos_ui.associations_path, method: :patch, class: "flex items-center h-full w-full") do |f| %>
  3. <%= f.hidden_field :context, value: context.serialize %>
  4. <%= f.hidden_field :association_id, value: item.id %>
  5. <%= f.button(class: "flex items-center w-full h-full") do %>
  6. <%= render entry %>
  7. <% end %>
  8. <% end %>
  9. <% end %>

app/components/loopos_ui/model_association_overlay/model_association_overlay.html.erb

0.0% lines covered

0.0% branches covered

17 relevant lines. 0 lines covered and 17 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <%= turbo_frame_tag turbo_id, class: "relative" do %>
  2. <%= render LooposUi::AssociationOverlay.new(id: id, tag_options: association_overlay_tag_options, show_selected: show_selected, show_results: show_results) do |overlay| %>
  3. <% overlay.with_selected_container do %>
  4. <%= render LooposUi::AssociationOverlay::SelectedContainer.new(id: selected_container_id) do |c| %>
  5. <% attached_items.each do |item| %>
  6. <% c.with_selected_item do %>
  7. <%= render item %>
  8. <% end %>
  9. <% end %>
  10. <% end %>
  11. <% end %>
  12. <% overlay.with_results_container do %>
  13. <%= render LooposUi::AssociationOverlay::ResultsContainer.new(id: results_container_id, show_results: show_results) do |c| %>
  14. <% missing_items.each do |item| %>
  15. <% c.with_result do %>
  16. <%= render item %>
  17. <% end %>
  18. <% end %>
  19. then: 0 else: 0 <%= c.with_new_item do %>
  20. <%= render new_item %>
  21. <% end if can_create_new %>
  22. <% end %>
  23. <% end %>
  24. then: 0 else: 0 <% if header_icon.present? || header_title.present? || context.component_class < LooposUi::Entity %>
  25. <% overlay.with_header(
  26. icon: header_icon || context.component_class.icon,
  27. title: header_title || context.component_class.name.demodulize)
  28. %>
  29. <% end %>
  30. <% end %>
  31. <% end %>

app/components/loopos_ui/model_association_overlay/new_item.html.erb

0.0% lines covered

100.0% branches covered

10 relevant lines. 0 lines covered and 10 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div(**attrs) do %>
  2. <div class="flex items-center w-full">
  3. <div class="w-full">
  4. <%= render entry %>
  5. </div>
  6. <%= form_with(**form_with_attrs, class: "flex items-center h-full w-full") do |f| %>
  7. <%= hidden_field_tag :context, context.serialize %>
  8. <% extra_params.each do |param,value| %>
  9. <%= f.hidden_field param, value: value %>
  10. <% end %>
  11. <%= f.hidden_field :name, data: { "model-association-overlay-target": "newInput" } %>
  12. <div class="flex w-full justify-end">
  13. <%= render LooposUi::Button.new(
  14. kind: :neutral,
  15. type: :tertiary,
  16. size: :tiny,
  17. text: "Create New",
  18. leading_icon: "fa-regular fa-plus",
  19. tag_options: {
  20. data: { action: "model-association-overlay#onResultsContainerChange" }
  21. }
  22. )
  23. %>
  24. </div>
  25. <% end %>
  26. </div>
  27. <% end %>

app/components/loopos_ui/multiple_list.rb

50.0% lines covered

100.0% branches covered

6 relevant lines. 3 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class MultipleList < LoopComponent
  3. 1 erb_template <<~HTML
  4. <div class="lui-multiple-list">
  5. <%= content %>
  6. </div>
  7. HTML
  8. end
  9. end

app/components/loopos_ui/page_header.rb

65.52% lines covered

0.0% branches covered

29 relevant lines. 19 lines covered and 10 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. 1 module LooposUi
  2. 1 class PageHeader < LoopComponent
  3. 1 include Turbo::FramesHelper
  4. 1 include LooposUi::ResourceAware
  5. 1 renders_one :title_zone
  6. 1 renders_many :tokens # "LooposUi::Token"
  7. 1 renders_many :title_labels, types: {
  8. manual: ->(&block) { capture(&block) },
  9. counter: LooposUi::CounterLabel,
  10. state: LooposUi::StateLabel,
  11. double_state: LooposUi::DoubleStateLabel,
  12. }
  13. 1 renders_many :details
  14. 1 attr_reader :title
  15. # BACKCOMPATIBILITY - may be removed
  16. 1 renders_one :right_side
  17. 1 renders_one :bottom_side
  18. 1 attr_reader :top_page_args
  19. # New APi
  20. 1 renders_one :header, LooposUi::Header
  21. 1 renders_one :image, LooposUi::V2::Image
  22. 1 renders_one :token_zone
  23. # Do not use directly, use with_detail_zone
  24. 1 renders_many :_detail_zones
  25. # TODO: replace with DRY::initializer
  26. 1 def initialize(title: nil, model: nil, **kwargs)
  27. @title = title
  28. @model = model
  29. # BACKCOMPATIBILITY - may be removed
  30. @kwargs = {
  31. top_page_args: {
  32. rounded: false,
  33. image: false,
  34. },
  35. }.merge!(kwargs)
  36. @top_page_args = @kwargs[:top_page_args]
  37. @detail_zones_count = 0
  38. end
  39. 1 def with_detail_zone(...)
  40. @detail_zones_count += 1
  41. then: 0 else: 0 raise ArgumentError, "You can't have more than 3 detail zones" if @detail_zones_count > 3
  42. with__detail_zone(...)
  43. end
  44. 1 def before_render
  45. else: 0 then: 0 raise ArgumentError, "Header slot is required" unless header?
  46. end
  47. end
  48. end

app/components/loopos_ui/page_header/page_header.html.erb

0.0% lines covered

0.0% branches covered

16 relevant lines. 0 lines covered and 16 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <turbo-frame id="lui-page-header" class="lui-page-header">
  2. <div class="lui-page-header__container">
  3. <div class="lui-page-header__container__tag_list">
  4. <% tokens.each do |token| %>
  5. <%= token %>
  6. <% end %>
  7. </div>
  8. <div class="lui-page-header__container__header_zone">
  9. <%= image %>
  10. <div class="lui-page-header__container__header_zone___content">
  11. <%= header %>
  12. then: 0 else: 0 <% if details.present? %>
  13. <div class="lui-page-header__container__header_zone___content__details">
  14. <% details.each do |detail| %>
  15. <%= detail %>
  16. <% end %>
  17. </div>
  18. <% end %>
  19. </div>
  20. </div>
  21. </div>
  22. then: 0 else: 0 <% if right_side.present? %>
  23. <div class="lui-page-header__right_side">
  24. <%= right_side %>
  25. </div>
  26. <% end %>
  27. </turbo-frame>

app/components/loopos_ui/page_section/identifier_editor.rb

56.25% lines covered

0.0% branches covered

32 relevant lines. 18 lines covered and 14 lines missed.
9 total branches, 0 branches covered and 9 branches missed.
    
  1. 1 module LooposUi
  2. 1 module PageSection
  3. 1 class IdentifierEditor < LoopComponent
  4. # There is a bug in dry-initializer: when undefined is false, the type checking constructor is not called
  5. # eg:
  6. # new() # will raise error, missing required argument
  7. # new(source: "foobar") # will raise error, failed constraint check
  8. # new(source: nil) # will NOT raise error
  9. # Base LoopComponent uses undefined: false (performance optimizations, and now some components depend on it)
  10. # but here we will opt out, source cannot be nil
  11. 1 extend Dry::Initializer[undefined: true]
  12. 1 Identifiable = ::Dry.Struct(id: Types::Coercible::Integer, type: Types::String)
  13. 1 IdentifierDisplay = Struct.new(:key_text, :text, keyword_init: true)
  14. 1 option :source, Types::Instance(LooposUi::PageSources::CoreSource)
  15. # if not provided, it must be provided via JS
  16. 1 option :identifiable,
  17. Types::Instance(Identifiable).constructor { |value|
  18. case value
  19. when: 0 when ::ActiveRecord::Base
  20. Identifiable.new(id: value.id, type: value.class.name)
  21. when: 0 when Hash
  22. Identifiable.new(**value.symbolize_keys)
  23. else: 0 else
  24. raise "Invalid identifiable: #{value.inspect}"
  25. end
  26. },
  27. optional: true
  28. 1 option :identifiable_type, Types::Coercible::String, optional: true
  29. 1 option :title, Types::Coercible::String, optional: true
  30. 1 def initialize(**kwargs)
  31. super
  32. then: 0 else: 0 if identifiable.blank? && @identifiable_type == Dry::Initializer::UNDEFINED
  33. raise "identifiable or identifiable_type is required"
  34. end
  35. end
  36. 1 def kinds
  37. source.identifiers.non_unique_kinds
  38. end
  39. 1 def identifiable_type
  40. then: 0 else: 0 identifiable&.type || @identifiable_type
  41. end
  42. 1 def selected_container_id
  43. then: 0 else: 0 "identifier-editor-selected-container-#{identifiable_type}-#{identifiable&.id}"
  44. end
  45. 1 def result_for(kind)
  46. Result.new(
  47. kind: kind,
  48. identifiable_type: identifiable_type,
  49. identifiable: identifiable,
  50. source: source,
  51. )
  52. end
  53. 1 def selected_item_for(kind, reference)
  54. identifier_display = IdentifierDisplay.new(
  55. key_text: I18n.t("admin.items.identifier_kinds.#{kind}"),
  56. text: reference,
  57. )
  58. LooposUi::Entities::Identifier.new(identifier: identifier_display)
  59. end
  60. 1 class << self
  61. 1 def identifiable_input_name
  62. "identifier[identifiable_ids][]"
  63. end
  64. end
  65. end
  66. end
  67. end

app/components/loopos_ui/page_section/identifier_editor/identifier_editor.html.erb

0.0% lines covered

100.0% branches covered

10 relevant lines. 0 lines covered and 10 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div(class: "contents", data: { controller: "lui--identifier-editor", "lui--identifier-editor-selected-container-id-value": selected_container_id }) do %>
  2. <%= render LooposUi::AssociationOverlay.new(can_search: false) do |association_overlay| %>
  3. <%= association_overlay.with_header(title: title.presence || t(".default_title")) %>
  4. <%= association_overlay.with_selected_container do |selected_container| %>
  5. <%= render LooposUi::AssociationOverlay::SelectedContainer.new(id: selected_container_id) do |sc| %>
  6. <%# Steams will add the selected items %>
  7. <% end %>
  8. <% end %>
  9. <%= association_overlay.with_results_container do |results_container| %>
  10. <%= render LooposUi::AssociationOverlay::ResultsContainer.new do |rc| %>
  11. <% kinds.each do |kind| %>
  12. <% rc.with_result do |results| %>
  13. <%= render result_for(kind) %>
  14. <% end %>
  15. <% end %>
  16. <% end %>
  17. <% end %>
  18. <% end %>
  19. <% end %>

app/components/loopos_ui/page_section/identifier_editor/result.html.erb

0.0% lines covered

0.0% branches covered

16 relevant lines. 0 lines covered and 16 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%= tag.turbo_frame(id: turbo_frame_id, data: { "lui--identifier-editor-target": "result" }) do %>
  2. <%= render LooposUi::Popover.new do |popover| %>
  3. <% popover.with_custom_toggle do %>
  4. <%= tag.div class: "lui-association-overlay__results__cell" do %>
  5. <%= render LooposUi::EntityToken.new(text: I18n.t("admin.items.identifier_kinds.#{kind}")) %>
  6. <% end %>
  7. <% end %>
  8. <%= tag.div(class: "lui-identifier-editor__form-entry") do %>
  9. <%= render LooposUi::FormEntry.new(label: t("admin.items.identifier_editor.reference")) do |form_entry| %>
  10. <%= form_entry.with_input do %>
  11. <%= render LooposUi::Inputs::Text.new(
  12. name: "identifier[reference]",
  13. mode: :form,
  14. open_actions: true,
  15. form: "identifier_#{kind}_form",
  16. extra_input_attributes: {
  17. data: {
  18. action: "change->lui--identifier-editor#onInputChange keydown.enter->lui--identifier-editor#onKeyDown",
  19. }
  20. }
  21. ) %>
  22. <% end %>
  23. <% end %>
  24. <% end %>
  25. <%= form_with id: "identifier_#{kind}_form",
  26. url: helpers.loopos_ui.identifiers_create_path,
  27. method: :post,
  28. class: "hidden" do |form| %>
  29. <%= form.hidden_field :context, value: source.source_context %>
  30. <%= form.hidden_field "identifier[kind]", value: kind %>
  31. <%= form.hidden_field "identifier[identifiable_type]", value: identifiable_type %>
  32. then: 0 else: 0 <%= form.hidden_field("identifier[identifiable_id]", value: identifiable.id) if identifiable.present? %>
  33. <% end %>
  34. <% end %>
  35. <% end %>

app/components/loopos_ui/page_section/identifier_editor/result.rb

90.91% lines covered

100.0% branches covered

11 relevant lines. 10 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module PageSection
  3. 1 class IdentifierEditor
  4. 1 class Result < LoopComponent
  5. 1 option :kind
  6. 1 option :identifiable_type
  7. 1 option :identifiable
  8. 1 option :source
  9. 1 def turbo_frame_id
  10. "identifier_editor_result_#{identifiable_type}_#{kind}"
  11. end
  12. end
  13. 1 private_constant :Result
  14. end
  15. end
  16. end

app/components/loopos_ui/page_section/item_tab.rb

55.56% lines covered

0.0% branches covered

9 relevant lines. 5 lines covered and 4 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 module PageSection
  3. 1 module ItemTab
  4. 1 class << self
  5. 1 def tab(key)
  6. constants.filter_map do |subclass|
  7. component = const_get(subclass)
  8. then: 0 else: 0 component.const_get(:KEY) == key.to_sym ? component : nil
  9. rescue NameError
  10. nil
  11. end.first
  12. end
  13. end
  14. end
  15. end
  16. end

app/components/loopos_ui/page_section/item_tab/advanced_details.rb

53.85% lines covered

0.0% branches covered

26 relevant lines. 14 lines covered and 12 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "json"
  3. 1 module LooposUi
  4. 1 module PageSection
  5. 1 module ItemTab
  6. 1 class AdvancedDetails < LoopComponent
  7. 1 KEY = :advanced_details
  8. 1 option :item
  9. 1 option :can_edit_extra_data, default: -> { false }
  10. 1 def extra_data_blocks
  11. blocks = []
  12. item.extra_datas.each do |extra_data|
  13. blocks << {
  14. title: block_title_for(extra_data),
  15. key: extra_data.key,
  16. data: extra_data.extra_data,
  17. }
  18. end
  19. blocks
  20. end
  21. 1 def sorted_extra_data_blocks
  22. extra_data_blocks.sort_by { |block| block[:title].to_s.downcase }
  23. end
  24. 1 def parsed_extra_data(data)
  25. then: 0 else: 0 return data if data.is_a?(Hash) || data.is_a?(Array)
  26. JSON.parse(data.to_s)
  27. rescue JSON::ParserError, TypeError
  28. data
  29. end
  30. 1 def format_extra_data_value(value)
  31. then: 0 else: 0 return "null" if value.nil?
  32. value.to_s
  33. end
  34. 1 private
  35. 1 def block_title_for(record)
  36. raw = record.key.to_s.split("_").first
  37. "#{raw.humanize} Extra Data"
  38. end
  39. end
  40. end
  41. end
  42. end

app/components/loopos_ui/page_section/item_tab/advanced_details/advanced_details.html.erb

0.0% lines covered

0.0% branches covered

46 relevant lines. 0 lines covered and 46 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <%= render LooposUi::TabsContent.new do |tab| %>
  2. <% tab.with_row do |row| %>
  3. <% row.with_column do %>
  4. <%= render LooposUi::TitleDescription.new(title: I18n.t("admin.v2.items.advanced_details.title"), size: "normal", description: I18n.t("admin.v2.items.advanced_details.label")) %>
  5. <% end %>
  6. <% end %>
  7. <% tab.with_row do |row| %>
  8. <% row.with_column do %>
  9. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.v2.items.advanced_details.admin_details"), size: "small", underline: true) do |section| %>
  10. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.v2.items.advanced_details.token"), orientation: "horizontal", label_width: 140) do |entry| %>
  11. <%= entry.with_input do %>
  12. <div class="flex flex-row" data-controller="clipboard-button" data-clipboard-button-copy-value="<%= item.token %>">
  13. <%= render LooposUi::Inputs::Text.new(name: "token", value: item.token.presence || "-", readonly: true) %>
  14. then: 0 else: 0 <%= render LooposUi::Button.new(leading_icon: "fa-regular fa-copy",
  15. kind: :neutral, type: :tertiary,
  16. tag_options: { data: { action:"click->clipboard-button#copy" } },
  17. tooltip_text: I18n.t("admin.v2.copy"),
  18. ) if item.token.present? %>
  19. </div>
  20. <% end %>
  21. <% end %>
  22. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.v2.items.advanced_details.submission_url"), orientation: "horizontal", label_width: 140) do |entry| %>
  23. <%= entry.with_input do %>
  24. <%# TODO: replace with lui component %>
  25. <div class="flex flex-row justify-between" data-controller="clipboard-button" data-clipboard-button-copy-value="<%= item.submission_url %>">
  26. <%= link_to item.submission_url, target: "_blank" do %>
  27. <span class="lui-input__value link-underline-temp"> <%= item.submission_url %> </span>
  28. <% end %>
  29. <%= render LooposUi::Button.new(leading_icon: "fa-regular fa-copy",
  30. kind: :neutral, type: :tertiary,
  31. tag_options: { data: { action:"click->clipboard-button#copy" } },
  32. tooltip_text: I18n.t("admin.v2.copy"),
  33. ) %>
  34. </div>
  35. <% end %>
  36. <% end %>
  37. then: 0 else: 0 <% if item.shopify_admin_product_url.present? %>
  38. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.v2.items.advanced_details.shopify_url"), orientation: "horizontal", label_width: 140) do |entry| %>
  39. <%= entry.with_input do %>
  40. <%# TODO: replace with lui component %>
  41. <div class="flex flex-row justify-between" data-controller="clipboard-button" data-clipboard-button-copy-value="<%= item.shopify_admin_product_url %>">
  42. <%= link_to item.shopify_admin_product_url, target: "_blank" do %>
  43. <span class="lui-input__value link-underline-temp"> <%= item.shopify_admin_product_url %> </span>
  44. <% end %>
  45. <%= render LooposUi::Button.new(leading_icon: "fa-regular fa-copy",
  46. kind: :neutral, type: :tertiary,
  47. tag_options: { data: { action:"click->clipboard-button#copy" } },
  48. tooltip_text: I18n.t("admin.v2.copy"),
  49. ) %>
  50. </div>
  51. <% end %>
  52. <% end %>
  53. <% end %>
  54. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.v2.items.advanced_details.loopos_user_id"), orientation: "horizontal", label_width: 140) do |entry| %>
  55. <%= entry.with_input do %>
  56. <div class="flex flex-row">
  57. <%= render LooposUi::Inputs::Text.new(name: "User ID", value: item.loopos_user_id || "-", readonly: true) %>
  58. </div>
  59. <% end %>
  60. <% end %>
  61. <% end %>
  62. <% end %>
  63. <% row.with_column do %>
  64. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.v2.items.advanced_details.extra_data"), size: "small", underline: true) do |section| %>
  65. <% sorted_extra_data_blocks.each do |block| %>
  66. <%= render LooposUi::Accordion.new(open: false, size: :small) do |accordion| %>
  67. <% accordion.with_header(title: block[:title], size: :small) %>
  68. <div class="pt-2">
  69. <%= render LooposUi::ExtraDataViewer.new(
  70. data: block[:data],
  71. readonly: true
  72. ) %>
  73. </div>
  74. <% end %>
  75. <% end %>
  76. <% end %>
  77. <% end %>
  78. <% end %>
  79. <% end %>

app/components/loopos_ui/page_section/item_tab/chat.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module PageSection
  3. 1 module ItemTab
  4. 1 class Chat < LoopComponent
  5. 1 KEY = :chat
  6. 1 option :item
  7. end
  8. end
  9. end
  10. end

app/components/loopos_ui/page_section/item_tab/chat/chat.html.erb

0.0% lines covered

0.0% branches covered

30 relevant lines. 0 lines covered and 30 lines missed.
12 total branches, 0 branches covered and 12 branches missed.
    
  1. <%
  2. public_unread = helpers.unread_message_count(@talkjs_public_user, item.token)
  3. private_unread = helpers.unread_message_count(@talkjs_private_user, "#{item.token}-private")
  4. %>
  5. <%= render LooposUi::TabsContent.new do |tab| %>
  6. <% tab.with_row do |row| %>
  7. <% row.with_column do %>
  8. <%= render LooposUi::TitleDescription.new(
  9. title: I18n.t("admin.items.chat.messages_title"),
  10. description: I18n.t("admin.items.chat.messages_description"),
  11. size: "normal"
  12. ) %>
  13. <% end %>
  14. <% end %>
  15. <% tab.with_row do |row| %>
  16. <% row.with_column do %>
  17. <%= render LooposUi::TabsSection.new(
  18. title: I18n.t("admin.items.tabs.chat"),
  19. description: I18n.t("admin.items.chat.public_description"),
  20. size: "small",
  21. underline: true
  22. ) do %>
  23. <%# Can only be here for now %>
  24. then: 0 else: 0 <%= tag.div id: "public-tag", class: "mt-2", style: "visibility: #{public_unread > 0 ? '' : 'hidden'};" do %>
  25. <%= render LooposUi::TagToken.new(text: helpers.unread_message(public_unread), color: :submission) %>
  26. <% end %>
  27. <% end %>
  28. <% end %>
  29. <% row.with_column do %>
  30. <%# Can only be here for now %>
  31. <%= render LooposUi::TabsSection.new(
  32. title: I18n.t("admin.items.tabs.chat"),
  33. description: I18n.t("admin.items.chat.internal_description"),
  34. size: "small",
  35. underline: true
  36. ) do %>
  37. then: 0 else: 0 <%= tag.div id: "private-tag", class: "mt-2", style: "visibility: #{private_unread > 0 ? '' : 'hidden'};" do %>
  38. <%= render LooposUi::TagToken.new(text: helpers.unread_message(private_unread), color: :submission) %>
  39. <% end %>
  40. <% end %>
  41. <% end %>
  42. <% end %>
  43. <% tab.with_row do |row| %>
  44. <% row.with_column do %>
  45. <div class="public-chat flex flex-col items-start gap-6 flex-1">
  46. <div class="flex flex-col gap-[4px] self-start w-full">
  47. <%= react_component("ItemChat", {
  48. itemToken: item.token,
  49. userId: helpers.current_user.id,
  50. appId: ::ItemChatConfig.app_id,
  51. apiKey: ::ItemChatConfig.api_key,
  52. type: "public",
  53. locale: I18n.locale.to_s,
  54. then: 0 else: 0 then: 0 else: 0 userName: ::LoopOsManager::Client::User.get_personal_data(loopos_user_id: helpers.current_user.id)&.body&.dig("personal_data", "full_name") || "LoopOS || Core",
  55. appName: "core",
  56. chatbox_height: 611,
  57. }, {
  58. style: "width: 100%; min-width: 453px"
  59. }
  60. ) %>
  61. <%#= render LooposUi::Chat.new(
  62. item_token: @item.token,
  63. user_id: current_user.id,
  64. app_id: ItemChatConfig.app_id,
  65. api_key: ItemChatConfig.api_key,
  66. type: "public",
  67. user_name: LoopOsManager::Client::User.get_personal_data(loopos_user_id: current_user.id)&.body&.dig("personal_data", "full_name") || "LoopOS || Core",
  68. locale: I18n.locale.to_s,
  69. style:"width: 100%; min-width: 453px"
  70. ) %>
  71. </div>
  72. </div>
  73. <% end %>
  74. <% row.with_column do %>
  75. <div class="private-chat flex flex-col items-start gap-6 flex-1">
  76. <div class="flex flex-col gap-[4px] self-start w-full">
  77. <%= react_component("ItemChat", {
  78. itemToken: item.token,
  79. userId: helpers.current_user.id,
  80. appId: ::ItemChatConfig.app_id,
  81. apiKey: ::ItemChatConfig.api_key,
  82. type: "private",
  83. locale: I18n.locale.to_s,
  84. then: 0 else: 0 then: 0 else: 0 userName: ::LoopOsManager::Client::User.get_personal_data(loopos_user_id: helpers.current_user.id)&.body&.dig("personal_data", "full_name") || "LoopOS || Core",
  85. }, {
  86. style: "width: 100%; min-width: 453px"
  87. }
  88. ) %>
  89. <%#= render LooposUi::Chat.new(
  90. item_token: @item.token,
  91. user_id: current_user.id,
  92. app_id: ItemChatConfig.app_id,
  93. api_key: ItemChatConfig.api_key,
  94. type: "private",
  95. user_name: LoopOsManager::Client::User.get_personal_data(loopos_user_id: current_user.id)&.body&.dig("personal_data", "full_name") || "LoopOS || Core",
  96. locale: I18n.locale.to_s,
  97. style:"width: 100%; min-width: 453px"
  98. ) %>
  99. </div>
  100. </div>
  101. <script>
  102. function hideTags() {
  103. const publicTag = document.getElementById('public-tag');
  104. const privateTag = document.getElementById('private-tag');
  105. if (publicTag) publicTag.style.visibility = 'hidden';
  106. if (privateTag) privateTag.style.visibility = 'hidden';
  107. }
  108. function checkTags() {
  109. if (!document.hidden) {
  110. hideTags();
  111. }
  112. }
  113. document.addEventListener('visibilitychange', checkTags);
  114. document.addEventListener('DOMContentLoaded', checkTags);
  115. setTimeout(checkTags, 3000);
  116. </script>
  117. <% end %>
  118. <% end %>
  119. <% end %>

app/components/loopos_ui/page_section/item_tab/customer_info.rb

32.88% lines covered

0.0% branches covered

73 relevant lines. 24 lines covered and 49 lines missed.
27 total branches, 0 branches covered and 27 branches missed.
    
  1. 1 module LooposUi
  2. 1 module PageSection
  3. 1 module ItemTab
  4. 1 class CustomerInfo < LoopComponent
  5. 1 KEY = :client_info
  6. 1 option :item, Types::Instance(PageSources::Models::Item)
  7. 1 option :uncensored_seller, Types::Bool, default: -> { false }
  8. 1 option :uncensored_buyer, Types::Bool, default: -> { false }
  9. 1 option :policy, Types::Hash, default: -> { { edit_info: false, edit_iban: false } }
  10. 1 def user_protocol_answers
  11. @user_protocol_answers ||= @item.user_protocol_answers(uncensored: uncensored_seller)
  12. end
  13. 1 def buyer_info
  14. @buyer_info ||= begin
  15. # Buyer info comes as a slug => value hash.
  16. # Keep protocol-order keys first, then append remaining keys.
  17. original_info = @item.buyer_info(uncensored: uncensored_buyer)
  18. else: 0 then: 0 unless original_info.is_a?(Hash) && original_info.dig("error").present?
  19. ordered = user_protocol_answers.each_with_object([]) do |answer, result|
  20. slug = answer[:protocol_element][:slug]
  21. else: 0 then: 0 next unless original_info.key?(slug)
  22. result << { slug:, value: original_info[slug] }
  23. end
  24. ignored_keys = [
  25. *ordered.pluck(:slug),
  26. "user_id",
  27. ]
  28. remaining = original_info.except(*ignored_keys).map { |slug, value| { slug:, value: } }
  29. ordered + remaining
  30. end
  31. end
  32. end
  33. 1 def protocol_answer_form_dom_id(answer)
  34. "client_info_protocol_answer_#{answer[:protocol_element][:id]}_form"
  35. end
  36. 1 def buyer_label_for(answer)
  37. {
  38. full_name: I18n.t("admin.items.client_info.full_name"),
  39. email: I18n.t("admin.items.client_info.email"),
  40. phone_number: I18n.t("admin.items.client_info.phone_number"),
  41. address_street: I18n.t("admin.items.client_info.address_street"),
  42. address_additional_info: I18n.t("admin.items.client_info.address_additional_info"),
  43. address_postal_code: I18n.t("admin.items.client_info.address_postal_code"),
  44. address_city: I18n.t("admin.items.client_info.address_city"),
  45. address_country: I18n.t("admin.items.client_info.address_country"),
  46. nif: I18n.t("admin.items.client_info.nif"),
  47. iban: I18n.t("admin.items.client_info.iban"),
  48. user_id: I18n.t("admin.items.client_info.user_id"),
  49. }[answer[:slug].to_sym]
  50. end
  51. 1 private
  52. INPUT_TYPE_MAPPING = {
  53. 1 text: Inputs::Text,
  54. number: Inputs::Number,
  55. date: Inputs::Date,
  56. select: Inputs::Select2,
  57. bool: Inputs::Checkbox,
  58. images: V2::Image,
  59. files: Inputs::File,
  60. }.freeze
  61. 1 def input_type_for(answer)
  62. type = answer[:protocol_element][:type].split(":").last.to_sym
  63. INPUT_TYPE_MAPPING.fetch(type, Inputs::Text)
  64. end
  65. 1 def input_for(answer, index = 0)
  66. input_type = input_type_for(answer)
  67. then: 0 else: 0 main_value = main_answer_for(answer)&.dig(:value)
  68. then: 0 else: 0 display_value = main_value.presence || (readonly?(answer) ? "-" : "")
  69. case input_type
  70. when: 0 when ->(t) { t == Inputs::Checkbox && !uncensored_seller }
  71. Inputs::Text.new(name: input_name(index), value: "*", readonly: true)
  72. when: 0 when ->(t) { t == V2::Image }
  73. attrs = {
  74. name: protocol_answer_values_field_name(index),
  75. then: 0 else: 0 image_url: display_value == "-" || display_value.blank? ? nil : display_value.first[:url],
  76. editable: !readonly?(answer),
  77. form_id: protocol_answer_form_dom_id(answer),
  78. }
  79. then: 0 else: 0 if @item.source.is_a?(LooposUi::PageSources::CoreApi)
  80. client = @item.source.core_client
  81. base = client.base_url.to_s.chomp("/")
  82. attrs[:direct_upload_url] = "#{base}/rails/active_storage/direct_uploads"
  83. attrs[:direct_upload_authorization] = "Bearer #{client.token}"
  84. end
  85. V2::Image.new(**attrs)
  86. when: 0 when ->(t) { t == Inputs::File }
  87. Inputs::File.new(
  88. name: protocol_answer_values_field_name(index),
  89. then: 0 else: 0 value: display_value == "-" || display_value.blank? ? nil : display_value.first[:url],
  90. accept: ["pdf", "jpg", "jpeg", "png", "webp"],
  91. mode: :autosubmit,
  92. readonly: readonly?(answer),
  93. )
  94. when: 0 when ->(t) { t == Inputs::Date }
  95. Inputs::Date.new(
  96. name: input_name(index),
  97. value: display_value,
  98. mode: :autosubmit,
  99. kind: "DateTimePicker",
  100. readonly: readonly?(answer),
  101. )
  102. else: 0 else
  103. input_type.new(
  104. name: input_name(index),
  105. value: display_value,
  106. then: 0 else: 0 readonly: input_type == Inputs::Checkbox ? true : readonly?(answer),
  107. )
  108. end
  109. end
  110. 1 def input_name(index)
  111. "protocol_answers[#{index}][value]"
  112. end
  113. # Image/file protocol answers submit Active Storage signed ids as `values: ["<signed_id>"]`
  114. # (form field name `protocol_answers[n][values][]`).
  115. 1 def protocol_answer_values_field_name(index)
  116. "protocol_answers[#{index}][values][]"
  117. end
  118. 1 def main_answer_for(answer)
  119. answer[:answers].find { |a| a[:main_answer] }
  120. end
  121. 1 def answer_slug(answer)
  122. answer[:protocol_element][:slug]
  123. end
  124. 1 def readonly?(answer)
  125. else: 0 then: 0 return true unless uncensored_seller
  126. then: 0 else: 0 can_edit = answer_slug(answer) == "iban" ? policy[:edit_iban] : policy[:edit_info]
  127. else: 0 then: 0 return true unless can_edit
  128. ["terms_and_conditions", "allows_data_share"].include?(answer_slug(answer))
  129. end
  130. 1 def client_info_answer_groups
  131. group_size = user_protocol_answers.count / 2 + user_protocol_answers.count % 2
  132. [
  133. user_protocol_answers[0...group_size],
  134. user_protocol_answers[group_size..],
  135. ].compact_blank
  136. end
  137. 1 def buyer_info_groups
  138. group_size = buyer_info.count / 2 + buyer_info.count % 2
  139. [
  140. buyer_info[0...group_size],
  141. buyer_info[group_size..],
  142. ].compact_blank
  143. end
  144. end
  145. end
  146. end
  147. end

app/components/loopos_ui/page_section/item_tab/customer_info/customer_info.html.erb

0.0% lines covered

0.0% branches covered

51 relevant lines. 0 lines covered and 51 lines missed.
8 total branches, 0 branches covered and 8 branches missed.
    
  1. <%
  2. seller_attrs = {
  3. then: 0 else: 0 text: uncensored_seller ? I18n.t("admin.items.client_info.unsee_data") : I18n.t("admin.items.client_info.see_data"),
  4. size: :tiny,
  5. type: :secondary,
  6. kind: :neutral,
  7. href: LooposUi::Engine.routes.url_helpers.client_info_item_path(
  8. uncensored_seller: !uncensored_seller,
  9. uncensored_buyer: uncensored_buyer,
  10. token: item.token,
  11. context: item.source.source_context)
  12. }
  13. buyer_attrs = {
  14. then: 0 else: 0 text: uncensored_buyer ? I18n.t("admin.items.client_info.unsee_data") : I18n.t("admin.items.client_info.see_data"),
  15. size: :tiny,
  16. type: :secondary,
  17. kind: :neutral,
  18. href: LooposUi::Engine.routes.url_helpers.client_info_item_path(
  19. uncensored_seller: uncensored_seller,
  20. uncensored_buyer: !uncensored_buyer,
  21. token: item.token,
  22. context: item.source.source_context)
  23. }
  24. %>
  25. <%# FIXME: turbo frame id should be dynamic %>
  26. <turbo-frame id="item_customer_info">
  27. <%= render LooposUi::TabsContent.new do |tab| %>
  28. then: 0 else: 0 <% if user_protocol_answers.present? %>
  29. <% tab.with_row do |row| %>
  30. <% row.with_column do %>
  31. <%= render LooposUi::TitleDescription.new(
  32. title: I18n.t("admin.items.client_info.seller_info.title"), description: I18n.t("admin.items.client_info.seller_info.label"),
  33. size: "normal" ) %>
  34. <% end %>
  35. <% end %>
  36. <% tab.with_row do |row| %>
  37. <% row.with_column do %>
  38. <%= render LooposUi::ActionButtons.new do |c| %>
  39. <% c.with_button_group do |g| %>
  40. <% g.with_button(**seller_attrs) %>
  41. <% end %>
  42. <% end %>
  43. <% end %>
  44. <% end %>
  45. <% tab.with_row do |row| %>
  46. <% row.with_column do %>
  47. <%= render LooposUi::FlexLayout.new(size: :half) do |layout| %>
  48. <% client_info_answer_groups.each do |group| %>
  49. <% layout.with_section do %>
  50. <% group.each_with_index do |answer, index| %>
  51. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.client_info.#{answer[:protocol_element][:slug]}", default: answer[:protocol_element][:label]), required: false, orientation: "horizontal", label_width: 168) do |entry| %>
  52. <%= entry.with_input do %>
  53. <%= tag.turbo_frame id: "#{answer[:protocol_element][:id]}_protocol_answer" do %>
  54. <%= form_with url: LooposUi::Engine.routes.url_helpers.update_protocol_answer_item_path(
  55. token: item.token,
  56. context: item.source.source_context,
  57. uncensored: uncensored_seller,
  58. ), method: :patch, multipart: true, html: { id: protocol_answer_form_dom_id(answer) }, data: { turbo_frame: "#{answer[:protocol_element][:id]}_protocol_answer" } do |form| %>
  59. <%# Text/select/bool use protocol_answers[n][value]; images/files use protocol_answers[n][values][] (signed blob ids). %>
  60. <%= form.hidden_field "protocol_answers[#{index}][protocol_element_id]", value: answer[:protocol_element][:id] %>
  61. <%= render input_for(answer, index) %>
  62. <% end %>
  63. <% end %>
  64. <% end %>
  65. <% end %>
  66. <% end %>
  67. <% end %>
  68. <% end %>
  69. <% end %>
  70. <% end %>
  71. <% end %>
  72. <% end %>
  73. then: 0 else: 0 <% if buyer_info.present? %>
  74. <% tab.with_row do |row| %>
  75. <% row.with_column do %>
  76. <%= render LooposUi::TitleDescription.new(title: I18n.t("admin.items.client_info.buyer_info.title"), description: I18n.t("admin.items.client_info.buyer_info.label"), size: "normal" ) %>
  77. <% end %>
  78. <% end %>
  79. <% tab.with_row do |row| %>
  80. <% row.with_column do %>
  81. <%= render LooposUi::ActionButtons.new do |c| %>
  82. <% c.with_button_group do |g| %>
  83. <% g.with_button(**buyer_attrs) %>
  84. <% end %>
  85. <% end %>
  86. <% end %>
  87. <% end %>
  88. <% tab.with_row do |row| %>
  89. <% row.with_column do %>
  90. <%= render LooposUi::FlexLayout.new(size: :half) do |layout| %>
  91. <% buyer_info_groups.each do |group| %>
  92. <% layout.with_section do %>
  93. <% group.each do |answer| %>
  94. <%= render LooposUi::FormEntry.new(label: buyer_label_for(answer), required: false, orientation: "horizontal", label_width: 168) do |entry| %>
  95. <%= entry.with_input do %>
  96. <%= render LooposUi::Inputs::Text.new(
  97. name: answer[:slug],
  98. value: answer[:value].presence || "-",
  99. readonly: true
  100. ) %>
  101. <% end %>
  102. <% end %>
  103. <% end %>
  104. <% end %>
  105. <% end %>
  106. <% end %>
  107. <% end %>
  108. <% end %>
  109. <% end %>
  110. <% tab.with_row do |row| %>
  111. <% row.with_column do %>
  112. <%= render LooposUi::TitleDescription.new(title: I18n.t("admin.items.client_info.no_data"), size: "normal" ) %>
  113. <% end %>
  114. <%# DO NOT SHOW BUYER INFO AND SELLER INFO IF THERE IS NO DATA %>
  115. <% end if !buyer_info.present? && !user_protocol_answers.present? && false %>
  116. <% tab.with_row do |row| %>
  117. <% row.with_column do %>
  118. <%= helpers.flex_cards_tab_section("Item", :show_client_info_tab, instance_id: item.id) %>
  119. <% end %>
  120. <% end %>
  121. <% end %>
  122. </turbo-frame>

app/components/loopos_ui/page_section/item_tab/financial_data.rb

53.61% lines covered

0.0% branches covered

97 relevant lines. 52 lines covered and 45 lines missed.
38 total branches, 0 branches covered and 38 branches missed.
    
  1. 1 module LooposUi
  2. 1 module PageSection
  3. 1 module ItemTab
  4. 1 class FinancialData < LoopComponent
  5. 1 KEY = :financial_data
  6. 1 option :item
  7. 1 option :policy, optional: true
  8. 1 class OtherValuesSection < LoopComponent
  9. 1 option :item
  10. 1 option :policy, optional: true
  11. MANUAL_VALUE_DIRECTIONS = [
  12. 1 "legal",
  13. "logistics",
  14. "other",
  15. "remainder",
  16. "repair",
  17. "reserve",
  18. "transport",
  19. ].freeze
  20. 1 def manual_value_kind_options
  21. MANUAL_VALUE_DIRECTIONS.sort.map do |dir|
  22. {
  23. value: dir,
  24. text: I18n.t("admin.items.financial_data.value_kinds.other_values.#{dir}", default: dir.titleize),
  25. }
  26. end
  27. end
  28. 1 def manual_item_values
  29. MANUAL_VALUE_DIRECTIONS.flat_map do |dir|
  30. Array(@item.public_send("#{dir}_item_values"))
  31. rescue NoMethodError
  32. []
  33. end
  34. end
  35. end
  36. 1 class ItemValueCard < LoopComponent
  37. 1 option :item
  38. 1 option :type, default: proc { "in" }
  39. 1 option :item_value
  40. 1 option :source
  41. 1 option :policy, optional: true
  42. 1 def apps_icons
  43. [
  44. "core",
  45. "submission",
  46. "hubs",
  47. "validation",
  48. "handling",
  49. "manager",
  50. "validation-rails",
  51. "handling-rails",
  52. ]
  53. end
  54. end
  55. 1 class ItemValueTable < LoopComponent
  56. 1 option :forward_trade_in, default: proc { false }
  57. 1 option :item_value
  58. 1 option :type, optional: true
  59. 1 option :item, optional: true
  60. end
  61. 1 class TransactionCard < LoopComponent
  62. 1 option :item_id
  63. 1 option :total
  64. 1 option :eligible_payments
  65. 1 option :amount
  66. 1 option :payments_type, type: Types::Coercible::Symbol.enum(:incoming, :outgoing)
  67. 1 option :card_id
  68. end
  69. 1 class ItemMarketplaceValueOutCard < LoopComponent
  70. 1 option :item_id
  71. 1 option :amount
  72. 1 option :block_name
  73. 1 option :value_out
  74. 1 option :card_id
  75. 1 option :token, optional: true
  76. 1 option :context, optional: true
  77. end
  78. 1 class TransactionsList < LoopComponent
  79. 1 option :transactions
  80. 1 option :item
  81. end
  82. 1 class LogsList < LoopComponent
  83. 1 option :logs
  84. 1 option :item
  85. end
  86. 1 then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 delegate :in_item_values, :out_item_values, :extra_item_values, :grouped_logs, :repair_item_values, :forward_item, :payments, :incoming_payments, to: :@item
  87. 1 def price_estimation_logs
  88. price_estimation_logs = grouped_logs.group_by { |log| log[:main_target].split(" ").first == "PriceEstimation" }
  89. logs_per_date = price_estimation_logs.group_by { |log| log[:created_at].strftime("%d-%m-%Y") }
  90. logs_per_date.map do |date, logs|
  91. {
  92. date: date,
  93. logs: logs.map { |log| transform_log(log) },
  94. }
  95. end
  96. []
  97. end
  98. 1 def item_value_apps_select
  99. select_options = LoopOsManager::Applications.app_instances_info.map do |_id, info|
  100. {
  101. value: info[:label],
  102. text: info[:label],
  103. }
  104. end
  105. select_options.unshift({
  106. value: "all",
  107. text: I18n.t("admin.items.financial_data.all_apps"),
  108. })
  109. select_options << {
  110. value: "System",
  111. text: I18n.t("admin.items.financial_data.system_app"),
  112. }
  113. select_options
  114. end
  115. 1 def item_value_kinds_select
  116. base = [
  117. {
  118. value: "all",
  119. text: I18n.t("admin.items.financial_data.all_kinds"),
  120. },
  121. {
  122. value: "ItemValueIn",
  123. text: I18n.t("admin.items.financial_data.value_kinds.item_value_in"),
  124. },
  125. {
  126. value: "ItemValueOut",
  127. text: I18n.t("admin.items.financial_data.value_kinds.item_value_out"),
  128. },
  129. {
  130. value: "Services::Payment",
  131. text: I18n.t("admin.items.financial_data.value_kinds.payment"),
  132. },
  133. {
  134. value: "Services::IncomingPayment",
  135. text: I18n.t("admin.items.financial_data.value_kinds.incoming_payment"),
  136. },
  137. {
  138. value: "PriceEstimation",
  139. text: I18n.t("admin.items.financial_data.value_kinds.price_estimation"),
  140. },
  141. {
  142. value: "ItemValueAuction",
  143. text: I18n.t("admin.items.financial_data.value_kinds.item_value_auction"),
  144. },
  145. ]
  146. manual = OtherValuesSection::MANUAL_VALUE_DIRECTIONS.sort.map do |dir|
  147. {
  148. value: dir,
  149. text: I18n.t("admin.items.financial_data.value_kinds.other_values.#{dir}", default: dir.to_s.titleize),
  150. }
  151. end
  152. base + manual
  153. end
  154. 1 def transform_log(log)
  155. title = nil
  156. extra_message = nil
  157. then: 0 if log["main_target_type"] == "ItemValue" && log["main_target_id"].present?
  158. item_value =
  159. begin
  160. version = PaperTrail::Version
  161. .where(item_type: "ItemValue", item_id: log["main_target_id"])
  162. .where("created_at <= ?", log["created_at"])
  163. .order(created_at: :desc)
  164. .first
  165. then: 0 else: 0 version&.reify || ItemValue.find_by(id: log["main_target_id"])
  166. rescue NameError
  167. # PaperTrail not loaded in this context; fall back to current record.
  168. ItemValue.find_by(id: log["main_target_id"])
  169. end
  170. then: 0 else: 0 direction = item_value&.direction.to_s
  171. then: 0 else: 0 if item_value
  172. extra_message = ApplicationController.renderer.render(
  173. partial: "admin/v2/items/item_value_table",
  174. locals: {
  175. item_value: item_value,
  176. as_of: log["created_at"],
  177. table_log_id: log["id"],
  178. },
  179. )
  180. end
  181. title =
  182. case direction
  183. when: 0 when "in"
  184. I18n.t("admin.items.financial_data.value_kinds.item_value_in")
  185. when: 0 when "out"
  186. I18n.t("admin.items.financial_data.value_kinds.item_value_out")
  187. when: 0 when "extra"
  188. I18n.t("admin.items.financial_data.value_kinds.extra_value")
  189. else: 0 else
  190. then: 0 else: 0 if OtherValuesSection::MANUAL_VALUE_DIRECTIONS.include?(direction)
  191. I18n.t("admin.items.financial_data.value_kinds.other_values.#{direction}", default: direction.to_s.titleize)
  192. end
  193. else: 0 end
  194. elsif log["main_target_type"] == "PriceEstimation"
  195. then: 0 # rubocop:disable Style/OpenStructUse
  196. estimation = OpenStruct.new(
  197. amount: Money.new(log.dig("extra_data", "estimation_status", "estimated_value", "cents") || 0, Money.default_currency.iso_code),
  198. amount_details: log.dig("extra_data", "estimation_status", "estimated_details") || [],
  199. created_at: log["created_at"],
  200. )
  201. # rubocop:enable Style/OpenStructUse
  202. extra_message = ApplicationController.renderer.render(
  203. partial: "admin/v2/items/item_value_table",
  204. locals: {
  205. item_value: estimation,
  206. as_of: nil,
  207. table_log_id: log["id"],
  208. },
  209. )
  210. else: 0 title = I18n.t("admin.items.financial_data.value_kinds.price_estimation")
  211. then: 0 elsif log["main_target_type"] == "Services::Payment"
  212. else: 0 title = I18n.t("admin.items.financial_data.value_kinds.payment")
  213. then: 0 else: 0 elsif log["main_target_type"] == "Services::IncomingPayment"
  214. title = I18n.t("admin.items.financial_data.value_kinds.incoming_payment")
  215. end
  216. {
  217. time: log["created_at"].strftime("%H:%M"),
  218. author: log["user"],
  219. title: title || log["main_target_type"],
  220. app_instance: {
  221. name: LoopOsManager::Applications.app_instances_info.dig(log["app_instance_id"], :label) ||
  222. I18n.t("admin.items.financial_data.system_app"),
  223. kind: LoopOsManager::Applications.app_instances_info.dig(log["app_instance_id"], :kind) || "neutral",
  224. },
  225. message: log["message"],
  226. error: log["level"] == "error",
  227. extra_data: log["extra_data"],
  228. item_value_id: log["main_target_id"],
  229. extra_message: extra_message,
  230. }
  231. end
  232. 1 then: 0 else: 0 delegate :source, to: :@item
  233. end
  234. end
  235. end
  236. end

app/components/loopos_ui/page_section/item_tab/financial_data/financial_data.html.erb

0.0% lines covered

0.0% branches covered

88 relevant lines. 0 lines covered and 88 lines missed.
30 total branches, 0 branches covered and 30 branches missed.
    
  1. <%
  2. # Handle in_item_values hash
  3. curr_in_item_values = in_item_values
  4. first_in_item_value = curr_in_item_values.first
  5. other_in_item_values = curr_in_item_values[1..]
  6. # Handle out_item_values hash
  7. curr_out_item_values = out_item_values
  8. first_out_item_value = curr_out_item_values.first
  9. other_out_item_values = curr_out_item_values[1..]
  10. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 price_estimation_log = grouped_logs.find { |log| log[:logs]&.last&.dig(:main_target)&.split(" ")&.first == "PriceEstimation" }
  11. curr_extra_item_values = extra_item_values
  12. first_extra_item_value = curr_extra_item_values.first
  13. other_extra_item_values = curr_extra_item_values[1..]
  14. curr_repair_item_values = repair_item_values
  15. first_repair_item_value = curr_repair_item_values.first
  16. other_repair_item_values = curr_repair_item_values[1..]
  17. has_marketplace_value_out = item.marketplace_value_out.present?
  18. %>
  19. <%= render LooposUi::TabsContent.new do |tabs| %>
  20. <% tabs.with_row do |row| %>
  21. <% row.with_column do %>
  22. <%= render LooposUi::TitleDescription.new(title: I18n.t("admin.items.financial_data.title"), size: "normal", description: I18n.t("admin.items.financial_data.label")) %>
  23. <% end %>
  24. <% end %>
  25. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 <% if price_estimation_log.present? || curr_in_item_values&.any? || curr_out_item_values&.any? || forward_item.present? || has_marketplace_value_out %>
  26. <% tabs.with_row do |row| %>
  27. <% row.with_column do %>
  28. then: 0 else: 0 <% if ::LooposUi.config.app_type?(:hubs) %>
  29. then: 0 else: 0 <% if helpers.flex_cards_present?("Item", :show_financial_data_final_amount, instance_id: item.id) %>
  30. <div class="mb-4">
  31. <%= helpers.flex_cards_container("Item", :show_financial_data_final_amount, instance_id: item.id) %>
  32. </div>
  33. <% end %>
  34. <% end %>
  35. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.proposals.title"), size: "small", underline: true) do %>
  36. <div class="flex flex-row gap-4">
  37. then: 0 <% if curr_in_item_values.any? %>
  38. else: 0 <%= render LooposUi::PageSection::ItemTab::FinancialData::ItemValueCard.new(item: item, item_value: first_in_item_value, type: "in", source: source, policy: policy) %>
  39. then: 0 else: 0 <% elsif price_estimation_log.present? %>
  40. <%= render LooposUi::PageSection::ItemTab::FinancialData::ItemValueCard.new(item: item, item_value: price_estimation_log, type: "price_estimation", source: source, policy: policy) %>
  41. <% end %>
  42. then: 0 else: 0 <% if curr_out_item_values.any? %>
  43. <%= render LooposUi::PageSection::ItemTab::FinancialData::ItemValueCard.new(item: item, item_value: first_out_item_value, type: "out", source: source, policy: policy) %>
  44. <% end %>
  45. then: 0 else: 0 <% if curr_extra_item_values.any? %>
  46. <%= render LooposUi::PageSection::ItemTab::FinancialData::ItemValueCard.new(item: item, item_value: first_extra_item_value, type: "top-up", source: source, policy: policy) %>
  47. <% end %>
  48. then: 0 else: 0 <% if has_marketplace_value_out %>
  49. <%= render LooposUi::V2::Card.new(
  50. id: "marketplace-value-out",
  51. title: I18n.t("admin.items.financial_data.marketplace_value_out"),
  52. description: I18n.t("admin.items.financial_data.marketplace_value_out_description"),
  53. src: LooposUi::Engine.routes.url_helpers.marketplace_value_out_item_path(token: item.token, context: source.source_context),
  54. ) do |card| %>
  55. <% card.with_corner do %>
  56. <%= render LooposUi::Loadings::Skeleton::Bar.new(width: "100px") %>
  57. <% end %>
  58. <%= render LooposUi::Loadings::Skeleton.new(n_rows: 2) %>
  59. <% end %>
  60. <% end %>
  61. </div>
  62. <% end %>
  63. <% end %>
  64. <% end %>
  65. <% end %>
  66. <% tabs.with_row do |row| %>
  67. <% row.with_column do %>
  68. <%= tag.turbo_frame id: "other-values-#{item.token}", src: LooposUi::Engine.routes.url_helpers.other_values_item_path(token: item.token, context: source.source_context) do %>
  69. <div class="flex flex-row gap-4">
  70. <%= render LooposUi::V2::Card.new(
  71. id: "other-values-#{item.token}",
  72. ) do |card| %>
  73. <% card.with_title_description do |td| %>
  74. <% end %>
  75. <% card.with_corner do %>
  76. <%= render LooposUi::Loadings::Skeleton::Bar.new(width: "100px") %>
  77. <% end %>
  78. <% card.with_custom_description do %>
  79. <%= render LooposUi::Loadings::Skeleton::Bar.new %>
  80. <% end %>
  81. <%= render LooposUi::Loadings::Skeleton.new(n_rows: 2) %>
  82. <% end %>
  83. </div>
  84. <% end %>
  85. <% end %>
  86. <% end %>
  87. <% tabs.with_row do |row| %>
  88. <% row.with_column do %>
  89. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.financial_data.transactions"), size: "small", underline: true) do %>
  90. <%= tag.turbo_frame id: "transactions-#{item.token}", src: LooposUi::Engine.routes.url_helpers.transactions_item_path(token: item.token, context: source.source_context) do %>
  91. <div class="flex flex-row gap-4">
  92. <%= render LooposUi::V2::Card.new(
  93. id: "transactions-#{item.token}",
  94. ) do |card| %>
  95. <% card.with_title_description do |td| %>
  96. <% end %>
  97. <% card.with_corner do %>
  98. <%= render LooposUi::Loadings::Skeleton::Bar.new(width: "100px") %>
  99. <% end %>
  100. <% card.with_custom_description do %>
  101. <%= render LooposUi::Loadings::Skeleton::Bar.new %>
  102. <% end %>
  103. <%= render LooposUi::Loadings::Skeleton.new(n_rows: 2) %>
  104. <% end %>
  105. </div>
  106. <% end %>
  107. <% end %>
  108. <% end %>
  109. <% end %>
  110. <% tabs.with_row do |row| %>
  111. <% row.with_column do %>
  112. <%= form_with url: LooposUi::Engine.routes.url_helpers.financial_logs_item_path(token: item.token), method: :get, id: "log-list-form", data: { turbo_frame: "log-list-frame" } do |form| %>
  113. <%= form.hidden_field :context, value: source.source_context %>
  114. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.financial_data.financial_history"), size: "small", underline: true) do |section| %>
  115. then: 0 else: 0 <% section.with_corner_action do %>
  116. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.financial_data.apps"), required: false, tooltip: nil, orientation: "horizontal") do |f| %>
  117. <% f.with_input do %>
  118. <%= render LooposUi::Inputs::Select.new(
  119. name: "app",
  120. options: item_value_apps_select,
  121. value: "all",
  122. placeholder: I18n.t("admin.items.financial_data.select_app"),
  123. open_actions: true,
  124. mode: :autosubmit,
  125. form: "log-list-form"
  126. ) %>
  127. <% end %>
  128. <% end %>
  129. <% end if LooposUi.config.app_type?(:core) %>
  130. <%# FIXME: UI doesnt have LoopOsManager::Applications, which means it breaks when its used by an app that natively doesnt have them %>
  131. <% section.with_corner_action do %>
  132. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.financial_data.value_kind"), required: false, tooltip: nil, orientation: "horizontal") do |f| %>
  133. <% f.with_input do %>
  134. <%= render LooposUi::Inputs::Select.new(
  135. name: "kind",
  136. options: item_value_kinds_select,
  137. value: "all",
  138. placeholder: I18n.t("admin.items.financial_data.select_value_kind"),
  139. open_actions: true,
  140. mode: :autosubmit,
  141. form: "log-list-form"
  142. ) %>
  143. <% end %>
  144. <% end %>
  145. <% end %>
  146. <% end %>
  147. <%= tag.turbo_frame id: "log-list-frame", src: LooposUi::Engine.routes.url_helpers.financial_logs_item_path(token: item.token, context: source.source_context) do %>
  148. <%= render LooposUi::Loadings::Skeleton.new(n_rows: 2) %>
  149. <% end %>
  150. <% end %>
  151. <% end %>
  152. <% end %>
  153. <% tabs.with_row do |row| %>
  154. <% row.with_column do %>
  155. <%= helpers.flex_cards_tab_section("Item", :show_financial_data_tab, instance_id: item.id, title: I18n.t("admin.items.proposals.custom_information", default: "Custom information")) %>
  156. <% end %>
  157. <% end %>
  158. <% end %>

app/components/loopos_ui/page_section/item_tab/financial_data/item_marketplace_value_out_card.html.erb

0.0% lines covered

100.0% branches covered

12 relevant lines. 0 lines covered and 12 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <% header_info = OpenStruct.new(
  2. title: I18n.t("admin.items.financial_data.marketplace_value_out"),
  3. description: I18n.t("admin.items.financial_data.marketplace_value_out_description")
  4. )
  5. card_item_value = OpenStruct.new(
  6. amount: value_out.amount,
  7. status: value_out.status,
  8. updated_at: value_out.created_at,
  9. )
  10. %>
  11. <%= render LooposUi::V2::Card::FinancialLog.new(id: @card_id, header_info: header_info, item_value: card_item_value) do |card| %>
  12. <% card.with_title_info do %>
  13. <%= render LooposUi::Label.new(text: block_name, icon: "fa-regular fa-store") %>
  14. <% end %>
  15. <% card.with_modal(modal_width: 800) do |modal| %>
  16. <div class="flex flex-col w-full overflow-y-auto">
  17. <%= render LooposUi::PageSection::ItemTab::FinancialData::ItemValueTable.new(item_value: card_item_value) %>
  18. <%= render LooposUi::Accordion.new(title: I18n.t("admin.items.item_value_card.history")) do |accordion| %>
  19. <%= tag.turbo_frame id: "modal-log-history-#{token}-itemvalueauction", src: LooposUi::Engine.routes.url_helpers.financial_logs_modal_item_path(token: token, context: context, kind: "ItemValueAuction") do %>
  20. <%= render LooposUi::Loadings::Skeleton.new(n_rows: 2) %>
  21. <% end %>
  22. <% end %>
  23. </div>
  24. <% end %>
  25. <% end %>

app/components/loopos_ui/page_section/item_tab/financial_data/item_value_card.html.erb

0.0% lines covered

0.0% branches covered

158 relevant lines. 0 lines covered and 158 lines missed.
126 total branches, 0 branches covered and 126 branches missed.
    
  1. <%
  2. #TODO: Move everything to the view component
  3. header_info = OpenStruct.new(
  4. then: 0 else: 0 title: type == "in" ? I18n.t("admin.items.item_value_card.value_in") : I18n.t("admin.items.item_value_card.value_out"),
  5. description: I18n.t("admin.items.item_value_card.last_proposed_value"))
  6. block_data = {}
  7. card_item_value = item_value
  8. then: 0 else: 0 item_value_id = item_value.is_a?(Hash) ? (item_value["id"] || item_value[:id]) : item_value.try(:id)
  9. manual_other_value = %w[repair logistics legal transport remainder reserve other].include?(type.to_s)
  10. then: 0 else: 0 if item.forward_item.present?
  11. then: 0 else: 0 then: 0 else: 0 proposal_details = item.forward_item.is_a?(Hash) ? item.forward_item["proposal_details"] : item.forward_item&.proposal_details
  12. then: 0 else: 0 then: 0 else: 0 currency = item.forward_item.is_a?(Hash) ? item.forward_item["currency"] || "EUR" : item.forward_item&.info[:currency] || "EUR"
  13. end
  14. then: 0 if type == "top-up"
  15. header_info.title = I18n.t("admin.items.item_value_card.top_up")
  16. header_info.description = I18n.t("admin.items.item_value_card.extra_benefits_amount")
  17. then: 0 if card_item_value.blank?
  18. discount_detail = proposal_details.find { |detail| detail[:label] == "Discount value" || detail[:label] == "Extra value" }
  19. then: 0 else: 0 amount_value = discount_detail&.dig(:value)
  20. card_item_value = OpenStruct.new(
  21. amount: Money.new(amount_value.presence.to_f.abs, currency),
  22. then: 0 else: 0 status: item_value.is_a?(Hash) ? item_value.dig("status") : item_value.status,
  23. then: 0 else: 0 updated_at: item_value.is_a?(Hash) ? item_value[:created_at] : item_value.try(:updated_at),
  24. else: 0 )
  25. else: 0 elsif card_item_value.is_a?(Hash)
  26. then: 0 # Normaliza hashes existentes para OpenStruct + Money
  27. card_item_value = OpenStruct.new(card_item_value)
  28. card_item_value.amount = Monetize.parse(card_item_value.amount, currency)
  29. card_item_value.updated_at = card_item_value[:created_at]
  30. end
  31. else: 0
  32. then: 0 elsif type == "price_estimation"
  33. header_info.title = I18n.t("admin.items.item_value_card.price_estimation")
  34. header_info.description = I18n.t("admin.items.item_value_card.price_estimation_description")
  35. block_data = {
  36. name: item_value.extra_data.dig("estimation_status", "block_name"),
  37. kind: LooposUi::BlockTypeIcons::BLOCK_TYPE_TO_ICON[item_value.extra_data.dig("estimation_status", "block_type")] || item_value.extra_data.dig("estimation_status", "block_type"),
  38. }
  39. card_item_value = OpenStruct.new(
  40. amount: Money.new(item_value.extra_data.dig("estimation_status", "estimated_value", "cents") || 0, "EUR"),
  41. amount_details: item_value.extra_data.dig("estimation_status", "estimated_details") || [],
  42. created_at: item_value.created_at,
  43. # status: I18n.t("models.item_value.status.#{item_value.status}")
  44. else: 0 )
  45. then: 0 elsif type == "repair"
  46. header_info.title = I18n.t("admin.items.financial_data.value_kinds.other_values.repair", default: "Repair")
  47. user_description =
  48. then: 0 if item_value.is_a?(Hash)
  49. then: 0 else: 0 item_value["description"].presence || Array(item_value["amount_details"]).first&.dig("label") || ""
  50. else: 0 else
  51. then: 0 else: 0 item_value.try(:description).presence || item_value.try(:amount_details).to_a.first&.dig("label") || ""
  52. end
  53. header_info.description = user_description.presence || ""
  54. then: 0 else: 0 if card_item_value.is_a?(Hash)
  55. card_item_value = OpenStruct.new(card_item_value)
  56. card_item_value.amount = Monetize.parse(card_item_value.amount, card_item_value.currency)
  57. card_item_value.updated_at = card_item_value[:created_at]
  58. else: 0 end
  59. then: 0 elsif manual_other_value
  60. header_info.title = I18n.t("admin.items.financial_data.value_kinds.other_values.#{type}", default: type.to_s.titleize)
  61. user_description =
  62. then: 0 if item_value.is_a?(Hash)
  63. then: 0 else: 0 item_value["description"].presence || Array(item_value["amount_details"]).first&.dig("label") || ""
  64. else: 0 else
  65. then: 0 else: 0 item_value.try(:description).presence || item_value.try(:amount_details).to_a.first&.dig("label") || ""
  66. end
  67. header_info.description = user_description.presence || ""
  68. then: 0 else: 0 if card_item_value.is_a?(Hash)
  69. card_item_value = OpenStruct.new(card_item_value)
  70. card_item_value.amount = Monetize.parse(card_item_value.amount, card_item_value.currency)
  71. card_item_value.updated_at = card_item_value[:created_at]
  72. end
  73. else: 0 else
  74. then: 0 if item_value.is_a?(Hash)
  75. card_item_value = OpenStruct.new(item_value)
  76. card_item_value.amount = Monetize.parse(card_item_value.amount, card_item_value.currency)
  77. card_item_value.updated_at = item_value[:created_at]
  78. creation_log = item_value[:logs].sort_by { |log| log[:created_at] }.first
  79. else: 0 else
  80. card_item_value = item_value
  81. creation_log = item_value.logs.sort_by { |log| log.created_at }.first
  82. end
  83. then: 0 else: 0 if creation_log.present?
  84. block_data = {
  85. name: creation_log[:extra_data].dig("item_value_status", "block_name"),
  86. kind: LooposUi::BlockTypeIcons::BLOCK_TYPE_TO_ICON[creation_log[:extra_data].dig("item_value_status", "block_type")] || creation_log[:extra_data].dig("item_value_status", "block_type"),
  87. }
  88. end
  89. end
  90. # For manual "other values", we also want to show the app label (same as other cards).
  91. then: 0 else: 0 if manual_other_value && item_value_id.present?
  92. creation_log =
  93. then: 0 if item_value.is_a?(Hash)
  94. Array(item_value[:logs] || item_value["logs"]).sort_by { |log| log[:created_at].to_s }.first
  95. else: 0 else
  96. item_value.logs.sort_by { |log| log.created_at }.first
  97. end
  98. then: 0 else: 0 if creation_log.present?
  99. then: 0 else: 0 bd = creation_log.is_a?(Hash) ? creation_log.with_indifferent_access : creation_log
  100. extra =
  101. then: 0 if bd.respond_to?(:extra_data)
  102. bd.extra_data
  103. else: 0 else
  104. bd[:extra_data]
  105. end
  106. extra = (extra || {}).with_indifferent_access
  107. block_data = {
  108. name: extra.dig(:item_value_status, :block_name),
  109. kind: LooposUi::BlockTypeIcons::BLOCK_TYPE_TO_ICON[extra.dig(:item_value_status, :block_type)] || extra.dig(:item_value_status, :block_type),
  110. }
  111. end
  112. end
  113. show_fti = (type == "in" || type == "price_estimation") && item.forward_item.present?
  114. then: 0 else: 0 raw_status = item_value.is_a?(Hash) ? item_value["status"] || item_value[:status] : item_value.try(:status)
  115. deleted_manual = manual_other_value && raw_status.to_s == "cancelled"
  116. then: 0 else: 0 item_value_id = item_value.is_a?(Hash) ? (item_value["id"] || item_value[:id]) : item_value.try(:id)
  117. then: 0 else: 0 can_update_other = policy.respond_to?(:can_update_other_item_values?) ? policy.can_update_other_item_values? : true
  118. then: 0 else: 0 can_cancel_other = policy.respond_to?(:can_cancel_other_item_values?) ? policy.can_cancel_other_item_values? : true
  119. # "Other values" are always saved as agreed, but we do not show the state label.
  120. then: 0 if manual_other_value && card_item_value.respond_to?(:status=)
  121. else: 0 card_item_value.status = nil
  122. then: 0 else: 0 elsif manual_other_value && card_item_value.is_a?(Hash)
  123. card_item_value = card_item_value.except("status", :status)
  124. end
  125. %>
  126. <%= render LooposUi::V2::Card::FinancialLog.new(header_info: header_info, item_value: card_item_value, fti: show_fti, draft: type == "price_estimation") do |card| %>
  127. then: 0 else: 0 <% if manual_other_value && can_cancel_other && type.to_s != "repair" && !deleted_manual && item_value_id.present? %>
  128. <% card.with_corner_action do %>
  129. <%= render LooposUi::Modal.new(
  130. title: I18n.t("admin.items.financial_data.manual_item_value.delete_modal_title"),
  131. description: I18n.t("admin.items.financial_data.manual_item_value.delete_modal_description"),
  132. ) do |modal| %>
  133. <% modal.with_trigger_button(
  134. icon: "trash",
  135. tooltip_text: I18n.t("admin.items.financial_data.manual_item_value.delete_tooltip"),
  136. size: :small,
  137. kind: :neutral,
  138. type: :secondary,
  139. ) %>
  140. <% modal.with_primary_action(
  141. text: I18n.t("admin.items.financial_data.manual_item_value.delete_confirm"),
  142. kind: :danger,
  143. href: LooposUi::Engine.routes.url_helpers.cancel_manual_item_value_item_path(
  144. token: item.token,
  145. id: item_value_id,
  146. context: source.source_context,
  147. ),
  148. tag_options: {
  149. "data-turbo-method": :patch,
  150. turbo_frame: "other-values-#{item.token}",
  151. },
  152. ) %>
  153. <% end %>
  154. <% end %>
  155. <% end %>
  156. then: 0 else: 0 <% if block_data.present? %>
  157. <% card.with_title_info do %>
  158. then: 0 <% if apps_icons.include?(block_data[:kind]) %>
  159. <%= render LooposUi::Entities::AppInstance.from_hash(**block_data) %>
  160. else: 0 <% else %>
  161. <%= render LooposUi::Label.new(text: block_data[:name], icon: block_data[:kind]) %>
  162. <% end %>
  163. <% end %>
  164. <% end %>
  165. then: 0 else: 0 <% card.with_modal(modal_width: 800, show_footer: manual_other_value && can_update_other && item_value_id.present?, form_id: (manual_other_value && can_update_other && item_value_id.present?) ? "edit-manual-item-value-#{item.token}-#{item_value_id}" : nil) do |modal| %>
  166. <div class="flex flex-col w-full overflow-y-auto">
  167. <% if manual_other_value && item_value_id.present? %>
  168. then: 0 <%
  169. edit_form_id = "edit-manual-item-value-#{item.token}-#{item_value_id}"
  170. current_description =
  171. then: 0 if item_value.is_a?(Hash)
  172. then: 0 else: 0 item_value["description"].presence || Array(item_value["amount_details"]).first&.dig("label") || ""
  173. else: 0 else
  174. then: 0 else: 0 item_value.try(:description).presence || item_value.try(:amount_details).to_a.first&.dig("label") || ""
  175. end
  176. current_amount =
  177. then: 0 if item_value.is_a?(Hash)
  178. item_value["float_amount"] || item_value["amount"] || ""
  179. else: 0 else
  180. item_value.try(:float_amount) || item_value.try(:amount).try(:to_f) || ""
  181. end
  182. %>
  183. <%= form_with(
  184. url: LooposUi::Engine.routes.url_helpers.update_manual_item_value_item_path(
  185. token: item.token,
  186. id: item_value_id,
  187. context: source.source_context,
  188. ),
  189. method: :patch,
  190. id: edit_form_id,
  191. data: {
  192. turbo_frame: "other-values-#{item.token}",
  193. controller: "manual-item-value-edit-modal",
  194. },
  195. ) do |f| %>
  196. <%= hidden_field_tag :context, source.source_context %>
  197. <%= hidden_field_tag "item_value[kind]", type.to_s %>
  198. <div class="w-full">
  199. <%= render LooposUi::V2::Table.new(
  200. id: "manual-item-value-edit-table-#{item.token}-#{item_value_id}",
  201. columns: [
  202. { title: I18n.t("admin.items.financial_data.manual_item_value.table_description"), dataIndex: :description },
  203. { title: I18n.t("admin.items.financial_data.manual_item_value.table_value"), dataIndex: :amount },
  204. ],
  205. show_pagination: false,
  206. show_result_count: false,
  207. searchable: false,
  208. ) do |table| %>
  209. <% table.with_row(key: "edit-manual-item-value") do |row| %>
  210. <% row.with_cell(property: :description) do %>
  211. then: 0 <% if can_update_other %>
  212. <%= render LooposUi::Inputs::Text.new(
  213. name: "item_value[description]",
  214. value: current_description,
  215. maxlength: 100,
  216. placeholder: I18n.t("admin.items.financial_data.manual_item_value.description_placeholder"),
  217. mode: :form,
  218. form: edit_form_id,
  219. ) %>
  220. else: 0 <% else %>
  221. <span class="text-gray-900"><%= current_description.presence || "-" %></span>
  222. <% end %>
  223. <% end %>
  224. <% row.with_cell(property: :amount) do %>
  225. then: 0 <% if can_update_other %>
  226. <%= render LooposUi::Inputs::Number.new(
  227. name: "item_value[amount]",
  228. value: current_amount,
  229. step: "0.01",
  230. with_actions: false,
  231. mode: :form,
  232. form: edit_form_id,
  233. ) %>
  234. else: 0 <% else %>
  235. <span class="text-gray-900"><%= current_amount.presence || "-" %></span>
  236. <% end %>
  237. <% end %>
  238. <% end %>
  239. <% end %>
  240. </div>
  241. <% end %>
  242. then: 0 else: 0 <% if can_update_other %>
  243. <% modal.with_cancel_button(text: I18n.t("modal.cancel")) %>
  244. <% modal.with_primary_action(text: I18n.t("admin.items.financial_data.manual_item_value.save"), tag_options: { type: :submit, form: edit_form_id }) %>
  245. else: 0 <% end %>
  246. then: 0 <% elsif card_item_value.amount_details.present? %>
  247. else: 0 <%= render LooposUi::PageSection::ItemTab::FinancialData::ItemValueTable.new(item_value: card_item_value) %>
  248. then: 0 <% elsif type == "top-up" %>
  249. <%= render LooposUi::PageSection::ItemTab::FinancialData::ItemValueTable.new(item_value: card_item_value, type: type, item: item) %>
  250. else: 0 <% else %>
  251. <p class="text-gray-500"><%= I18n.t("admin.items.item_value_card.no_price_details_available") %></p>
  252. <% end %>
  253. then: 0 else: 0 <% if show_fti %>
  254. then: 0 else: 0 <%= render LooposUi::Accordion.new(title: I18n.t("admin.items.item_value_card.forward_trade_in", product_name: item.forward_item.is_a?(Hash) ? item.forward_item["forward_product_name"] : item.forward_item.product.name)) do |accordion| %>
  255. <% accordion.with_body do %>
  256. <%= render LooposUi::PageSection::ItemTab::FinancialData::ItemValueTable.new(item_value: card_item_value, item: item, forward_trade_in: true) %>
  257. <% end %>
  258. <% end %>
  259. <% end %>
  260. <%
  261. history_modal_query = { context: source.source_context }
  262. then: 0 if item_value_id.present?
  263. history_modal_query[:main_target_type] = "ItemValue"
  264. else: 0 history_modal_query[:main_target_id] = item_value_id
  265. then: 0 elsif type == "price_estimation"
  266. then: 0 else: 0 last_log = (item_value[:logs] || item_value["logs"])&.last
  267. then: 0 else: 0 then: 0 else: 0 pe_id = last_log&.dig(:main_target_id) || last_log&.dig("main_target_id")
  268. then: 0 else: 0 if pe_id.blank? && last_log
  269. raw_mt = (last_log[:main_target] || last_log["main_target"]).to_s
  270. m = raw_mt.match(/#(\d+)/)
  271. then: 0 else: 0 pe_id = m[1] if m
  272. end
  273. then: 0 if pe_id.present?
  274. history_modal_query[:main_target_type] = "PriceEstimation"
  275. history_modal_query[:main_target_id] = pe_id
  276. else: 0 else
  277. history_modal_query[:kind] = "PriceEstimation"
  278. else: 0 end
  279. then: 0 elsif type == "in"
  280. else: 0 history_modal_query[:kind] = "ItemValueIn"
  281. then: 0 elsif type == "out"
  282. history_modal_query[:kind] = "ItemValueOut"
  283. else: 0 else
  284. history_modal_query[:kind] = "ItemValueOut"
  285. end
  286. history_frame_id = [
  287. "modal-log-history",
  288. item.token.to_s,
  289. (item_value_id || history_modal_query[:main_target_id] || type).to_s,
  290. ].join("-").gsub(/[^\w-]/, "-")
  291. %>
  292. <%= render LooposUi::Accordion.new(title: I18n.t("admin.items.item_value_card.history")) do |accordion| %>
  293. <%= tag.turbo_frame id: history_frame_id, src: LooposUi::Engine.routes.url_helpers.financial_logs_modal_item_path({ token: item.token }.merge(history_modal_query)) do %>
  294. <%= render LooposUi::Loadings::Skeleton.new(n_rows: 2) %>
  295. <% end %>
  296. <% end %>
  297. </div>
  298. <% end %>
  299. <% end %>

app/components/loopos_ui/page_section/item_tab/financial_data/item_value_table.html.erb

0.0% lines covered

0.0% branches covered

64 relevant lines. 0 lines covered and 64 lines missed.
40 total branches, 0 branches covered and 40 branches missed.
    
  1. <% if forward_trade_in %>
  2. then: 0 <%
  3. then: 0 else: 0 proposal_details = item.forward_item.is_a?(Hash) ? item.forward_item["proposal_details"] : item.forward_item.proposal_details
  4. type = type || "forward-trade-in"
  5. table_data = if type == "top-up"
  6. then: 0 # For top-up: show only discount value
  7. then: 0 if item_value.present?
  8. table_rows = item_value.amount_details.map do |detail|
  9. detail = detail.with_indifferent_access
  10. OpenStruct.new(
  11. characteristic: detail[:label],
  12. value: detail[:value] || detail[:value_in]
  13. )
  14. end
  15. table_total = item_value.amount_cents
  16. else: 0 else
  17. discount_detail = proposal_details.find { |detail| detail[:label] == "Discount value" || detail[:label] == "Extra value" }
  18. then: 0 else: 0 table_rows = discount_detail ? [OpenStruct.new(characteristic: discount_detail[:label], value: discount_detail[:value])] : []
  19. then: 0 else: 0 then: 0 else: 0 table_total = discount_detail&.dig(:value)&.abs || 0
  20. end
  21. {
  22. rows: table_rows,
  23. total: table_total,
  24. }
  25. else
  26. else: 0 # For regular forward trade-in: show all details except final estimation
  27. regular_rows = proposal_details.reject { |detail| detail[:label] == "Final estimation" || detail[:label] == "Total" }
  28. final_estimation = proposal_details.find { |detail| detail[:label] == "Final estimation" || detail[:label] == "Total" }
  29. then: 0 else: 0 total = final_estimation&.dig(:value) || 0
  30. {
  31. then: 0 else: 0 then: 0 else: 0 rows: regular_rows.map { |detail| OpenStruct.new(characteristic: detail[:label], value: detail[:value]&.to_money&.to_f) },
  32. then: 0 else: 0 total: total&.to_money.to_f
  33. }
  34. end
  35. table_args = {
  36. data: table_data[:rows],
  37. show_pagination: false,
  38. show_result_count: false,
  39. searchable: false,
  40. columns: [
  41. then: 0 else: 0 { title: type == "top-up" ? I18n.t("admin.items.forward_trade_in_table.top_up_details") : I18n.t("admin.items.forward_trade_in_table.fti_details"), dataIndex: :characteristic },
  42. { title: I18n.t("admin.items.forward_trade_in_table.value"), dataIndex: :value }
  43. ],
  44. id: "forward_trade_in_table",
  45. }
  46. %>
  47. <div class="flex flex-col">
  48. <%= render LooposUi::V2::Table.new(**table_args) do |table| %>
  49. <% table_data[:rows].each do |row| %>
  50. <% table.with_row(key: row.characteristic) do |table_row| %>
  51. <% table_row.with_cell(property: :characteristic) do %>
  52. <%= row.characteristic %>
  53. <% end %>
  54. <% table_row.with_cell(property: :value) do %>
  55. then: 0 else: 0 <%= Money.new(row.value.abs, item.forward_item.is_a?(Hash) ? item.forward_item["currency"] : item.forward_item.info[:currency]).format %>
  56. <% end %>
  57. <% end %>
  58. <% end %>
  59. <% table.with_row(key: "total") do |total_row| %>
  60. <% total_row.with_cell(property: :characteristic) do %>
  61. <span class="text-sm font-semibold text-content">
  62. <%= I18n.t("admin.items.forward_trade_in_table.total") %>
  63. </span>
  64. <% end %>
  65. <% total_row.with_cell(property: :value) do %>
  66. <span class="text-lg font-semibold text-content">
  67. then: 0 else: 0 then: 0 else: 0 <%= table_data[:total] ? Money.new(table_data[:total], item.forward_item.is_a?(Hash) ? item.forward_item["currency"] : item.forward_item.info[:currency]).format : '--' %>
  68. </span>
  69. <% end %>
  70. <% end %>
  71. <% end %>
  72. <div class="flex flex-row items-center justify-end mb-3">
  73. <span class="lui-financial_card-content-info__timestamp">
  74. then: 0 else: 0 <%= I18n.t("admin.items.item_value_table.created_at") %> <%= render LooposUi::DateShow.new(date: item.forward_item.is_a?(Hash) ? item.forward_item["created_at"] : item.forward_item.created_at) %>
  75. </span>
  76. </div>
  77. </div>
  78. <% else %>
  79. else: 0 <%
  80. then: 0 else: 0 table_data = item_value.amount_details&.map do |detail|
  81. detail = detail.with_indifferent_access
  82. OpenStruct.new(
  83. characteristic: detail[:label],
  84. value: detail[:value] || detail[:value_in]
  85. )
  86. end
  87. table_args = {
  88. data: table_data,
  89. show_pagination: false,
  90. show_result_count: false,
  91. searchable: false,
  92. columns: [
  93. { title: I18n.t("admin.items.item_value_table.details"), dataIndex: :details, width: 200 },
  94. { title: I18n.t("admin.items.item_value_table.value"), dataIndex: :value, width: 200 },
  95. ],
  96. id: "price_details_table_#{item_value.id}"
  97. }
  98. %>
  99. <div class="flex flex-col">
  100. <%= render LooposUi::V2::Table.new(**table_args) do |table| %>
  101. then: 0 else: 0 <% table_data&.each do |detail| %>
  102. <% table.with_row(key: detail.characteristic) do |row| %>
  103. <% row.with_cell(property: :details) do %>
  104. <%= detail.characteristic %>
  105. <% end %>
  106. <% row.with_cell(property: :value) do %>
  107. <% v = detail.value %>
  108. then: 0 <% if v.is_a?(Numeric) %>
  109. else: 0 <%= Money.new(v, item_value.amount_currency).format %>
  110. then: 0 <% elsif v.present? %>
  111. <%= v %>
  112. else: 0 <% else %>
  113. <%= item_value.amount.format %>
  114. <% end %>
  115. <% end %>
  116. <% end %>
  117. <% end %>
  118. <% table.with_row(key: "total") do |row| %>
  119. <% row.with_cell(property: :details) do %>
  120. <%= tag.span(I18n.t("admin.items.item_value_table.total"), class: "text-sm font-semibold text-content") %>
  121. <% end %>
  122. <% row.with_cell(property: :value) do %>
  123. <%= tag.span(item_value.amount.format, class: "text-lg font-semibold text-content") %>
  124. <% end %>
  125. <% end %>
  126. <% end %>
  127. <div class="flex flex-row items-center justify-end mb-3">
  128. <%= tag.span(class: "lui-financial_card-content-info__timestamp") do %>
  129. <%= I18n.t("admin.items.item_value_table.created_at") %>
  130. <%= render LooposUi::DateShow.new(date: item_value.created_at) %>
  131. <% end %>
  132. </div>
  133. </div>
  134. <% end %>

app/components/loopos_ui/page_section/item_tab/financial_data/logs_list.html.erb

0.0% lines covered

0.0% branches covered

5 relevant lines. 0 lines covered and 5 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%= tag.turbo_frame id: "log-list-frame" do %>
  2. then: 0 <% if logs.present? %>
  3. <%= render LooposUi::LogList.from_any_source(
  4. logs,
  5. id: "preview-log-list",
  6. page: 1,
  7. ) %>
  8. else: 0 <% else %>
  9. <%= tag.div(I18n.t("admin.items.financial_data.no_logs_found"), class: "copy-14 text-content my-3") %>
  10. <% end %>
  11. <% end %>

app/components/loopos_ui/page_section/item_tab/financial_data/other_values_section.html.erb

0.0% lines covered

0.0% branches covered

57 relevant lines. 0 lines covered and 57 lines missed.
14 total branches, 0 branches covered and 14 branches missed.
    
  1. <%= tag.turbo_frame id: "other-values-#{item.token}" do %>
  2. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.financial_data.manual_item_value.other_values_section_title"), size: "small", underline: true) do |ts| %>
  3. <% ts.with_corner_action do %>
  4. then: 0 else: 0 <% can_create = policy.respond_to?(:can_create_other_item_values?) ? policy.can_create_other_item_values? : true %>
  5. <% form_id = "manual-item-value-form-#{item.token}" %>
  6. <% modal_id = "manual-item-value-modal-#{item.token}" %>
  7. <% modal_trigger_id = "#{modal_id}-open-trigger" %>
  8. <div class="flex items-center gap-2">
  9. then: 0 else: 0 <% if can_create %>
  10. <% menu_options = manual_value_kind_options.map do |opt|
  11. {
  12. text: opt[:text],
  13. attributes: {
  14. data: {
  15. action: "click->manual-item-value-modal#openFromMenu",
  16. manual_item_value_modal_kind_param: opt[:value],
  17. manual_item_value_modal_modal_id_param: modal_id,
  18. manual_item_value_modal_modal_trigger_id_param: modal_trigger_id,
  19. },
  20. },
  21. }
  22. end %>
  23. <%= render LooposUi::ActionMenu.new(options: menu_options, portal_data_controllers: ["manual-item-value-modal"]) do |menu| %>
  24. <% menu.with_trigger do %>
  25. <%= render LooposUi::Button.new(
  26. text: I18n.t("admin.items.financial_data.manual_item_value.add_value_button"),
  27. kind: :neutral,
  28. type: :secondary,
  29. size: :tiny,
  30. leading_icon: "fa-regular fa-plus",
  31. trailing_icon: "fa-regular fa-chevron-down",
  32. tag_options: { type: :button },
  33. ) %>
  34. <% end %>
  35. <% end %>
  36. <%= render LooposUi::Modal.new(id: modal_id, modal_width: 720, form_id: form_id) do |modal| %>
  37. <% modal.with_trigger do %>
  38. <button id="<%= modal_trigger_id %>" type="button" class="hidden" data-action="click->modal#open">
  39. <%= I18n.t("admin.items.financial_data.manual_item_value.open_modal") %>
  40. </button>
  41. <% end %>
  42. <% modal.with_header(title: I18n.t("admin.items.financial_data.manual_item_value.add_modal_title", kind: I18n.t("admin.items.financial_data.value_kinds.other_values.repair", default: "Repair"))) %>
  43. <%= form_with(
  44. url: LooposUi::Engine.routes.url_helpers.manual_item_values_item_path(token: item.token),
  45. method: :post,
  46. id: form_id,
  47. data: { turbo_frame: "other-values-#{item.token}" },
  48. ) do |f| %>
  49. then: 0 else: 0 <%= hidden_field_tag :context, item.source.source_context if item.source.respond_to?(:source_context) %>
  50. <%= tag.div(
  51. class: "flex flex-col gap-4",
  52. data: {
  53. controller: "manual-item-value-modal",
  54. manual_item_value_modal_title_template_value: I18n.t("admin.items.financial_data.manual_item_value.add_modal_title", kind: "%{kind}"),
  55. manual_item_value_modal_kind_labels_value: manual_value_kind_options.to_h { |opt| [opt[:value].to_s, opt[:text].to_s] },
  56. manual_item_value_modal_require_amount_value: true,
  57. },
  58. ) do %>
  59. <%= hidden_field_tag "item_value[kind]", "repair" %>
  60. <div class="w-full">
  61. <%= render LooposUi::V2::Table.new(
  62. id: "manual-item-value-table-#{item.token}",
  63. columns: [
  64. { title: I18n.t("admin.items.financial_data.manual_item_value.table_description"), dataIndex: :description },
  65. { title: I18n.t("admin.items.financial_data.manual_item_value.table_value"), dataIndex: :amount },
  66. ],
  67. show_pagination: false,
  68. show_result_count: false,
  69. searchable: false,
  70. ) do |table| %>
  71. <% table.with_row(key: "new-manual-item-value") do |row| %>
  72. <% row.with_cell(property: :description) do %>
  73. <%= render LooposUi::Inputs::Text.new(
  74. name: "item_value[description]",
  75. value: "",
  76. maxlength: 100,
  77. placeholder: I18n.t("admin.items.financial_data.manual_item_value.description_placeholder"),
  78. mode: :form,
  79. form: form_id,
  80. ) %>
  81. <% end %>
  82. <% row.with_cell(property: :amount) do %>
  83. <%= render LooposUi::Inputs::Number.new(
  84. name: "item_value[amount]",
  85. value: "",
  86. step: "0.01",
  87. with_actions: false,
  88. mode: :form,
  89. form: form_id,
  90. ) %>
  91. <% end %>
  92. <% end %>
  93. <% end %>
  94. </div>
  95. <% end %>
  96. <% end %>
  97. <% modal.with_cancel_button(text: I18n.t("modal.cancel")) %>
  98. <% modal.with_primary_action(text: I18n.t("admin.items.financial_data.manual_item_value.save"), tag_options: { type: :submit, form: form_id, disabled: true }) %>
  99. <% end %>
  100. <% end %>
  101. </div>
  102. <% end %>
  103. <% values = manual_item_values.reject { |iv|
  104. then: 0 else: 0 status = (iv.is_a?(Hash) ? (iv["status"] || iv[:status]) : iv.try(:status)).to_s
  105. status == "cancelled"
  106. } %>
  107. then: 0 <% if values.any? %>
  108. <div class="flex flex-row flex-wrap gap-4">
  109. then: 0 else: 0 <% values.sort_by { |v| (v.is_a?(Hash) ? v["created_at"].to_s : v.created_at.to_s) }.each do |iv| %>
  110. <%
  111. then: 0 else: 0 type = (iv.is_a?(Hash) ? iv["direction"] : iv.direction).to_s
  112. %>
  113. <%= render LooposUi::PageSection::ItemTab::FinancialData::ItemValueCard.new(item: item, item_value: iv, type: type, source: item.source, policy: policy) %>
  114. <% end %>
  115. </div>
  116. else: 0 <% else %>
  117. <%= render LooposUi::TitleDescription.new(description: I18n.t("admin.items.financial_data.manual_item_value.empty"), size: "small") %>
  118. <% end %>
  119. <% end %>
  120. <% end %>

app/components/loopos_ui/page_section/item_tab/financial_data/transactions_list.html.erb

0.0% lines covered

0.0% branches covered

12 relevant lines. 0 lines covered and 12 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. <%= tag.turbo_frame id: "transactions-#{item.token}" do %>
  2. then: 0 <% if @transactions.any? %>
  3. <div class="flex flex-row gap-4">
  4. <% @transactions.each do |transaction| %>
  5. <%
  6. type = transaction.class.name.demodulize.underscore
  7. financial_transaction = LooposUi::V2::Card::FinancialTransaction::FinancialTransaction.build(
  8. then: 0 else: 0 incoming_payment: type == "incoming_payment" ? transaction : nil,
  9. then: 0 else: 0 outgoing_payment: type == "payment" ? transaction : nil
  10. )
  11. %>
  12. <%= render LooposUi::V2::Card::FinancialTransaction.new(
  13. id: "transaction-#{1}",
  14. financial_transaction: financial_transaction
  15. )
  16. %>
  17. <% end %>
  18. </div>
  19. else: 0 <% else %>
  20. <%= render LooposUi::TitleDescription.new(description: I18n.t("no_data"), size: "small" ) %>
  21. <% end %>
  22. <% end %>

app/components/loopos_ui/page_section/item_tab/flow.rb

76.92% lines covered

100.0% branches covered

13 relevant lines. 10 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 module PageSection
  4. 1 module ItemTab
  5. 1 class Flow < LoopComponent
  6. 1 KEY = :flow
  7. 1 option :item
  8. 1 option :policy
  9. 1 def flow_presenter
  10. ::Flows::FlowPresenter.new(item.flow, view_from: item)
  11. end
  12. 1 def product_presenter
  13. item.product.presenter
  14. end
  15. 1 def oauth_core_token
  16. LoopOsManager::Applications.get_oauth_core_token
  17. end
  18. end
  19. end
  20. end
  21. end

app/components/loopos_ui/page_section/item_tab/flow/flow.html.erb

0.0% lines covered

0.0% branches covered

15 relevant lines. 0 lines covered and 15 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <%= render LooposUi::TabsContent.new do |tab| %>
  2. <% tab.with_row do |row| %>
  3. <% row.with_column do %>
  4. <%= render LooposUi::TitleDescription.new(title: I18n.t("admin.v2.items.flow.title"), description: I18n.t("admin.v2.items.flow.label"), size: "normal") %>
  5. <% end %>
  6. <% end %>
  7. <% tab.with_row do |row| %>
  8. <% row.with_column do %>
  9. then: 0 <% if policy.can_view_flows? %>
  10. <div class="info-tab__section info-tab__section--flow">
  11. then: 0 <% if flow_presenter.present? %>
  12. <div data-controller="flow-block-redirect" data-focus="<%= item.current_block_id %>">
  13. <%= react_component(
  14. "FlowProvider",
  15. {
  16. direction: "LR",
  17. flowData: flow_presenter.serialize,
  18. flowId: flow_presenter.id,
  19. token: oauth_core_token,
  20. highlightedStates: flow_presenter.highlighted_states,
  21. },
  22. class: "flow__wrapper",
  23. data: { 'height-observer-target': "flow" }
  24. )
  25. %>
  26. </div>
  27. <turbo-frame id="settings_flow_sidebar" data-controller="flow-sidebar">
  28. </turbo-frame>
  29. else: 0 <% else %>
  30. <p class="tabs__empty">Item without flow</p>
  31. <% end %>
  32. </div>
  33. else: 0 <% else %>
  34. <%= t('.unauthorized') %>
  35. <% end %>
  36. <% end %>
  37. <% end %>
  38. <% end %>

app/components/loopos_ui/page_section/item_tab/impact_widget.rb

64.29% lines covered

100.0% branches covered

14 relevant lines. 9 lines covered and 5 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 module PageSection
  4. 1 module ItemTab
  5. 1 class ImpactWidget < LoopComponent
  6. 1 KEY = :impact_widget
  7. 1 option :item
  8. 1 def tab_info
  9. item.calculate_impact_for_item_page
  10. end
  11. 1 def impact_label(value, key)
  12. "#{format_number(value)}#{::Catalog::Node::IMPACT_SCHEMA[key][:unit]}"
  13. end
  14. 1 def format_number(number)
  15. _i = number.to_i
  16. f = number.to_f
  17. number_with_precision(f, precision: 2, separator: ",")
  18. end
  19. end
  20. end
  21. end
  22. end

app/components/loopos_ui/page_section/item_tab/impact_widget/impact_widget.html.erb

0.0% lines covered

100.0% branches covered

15 relevant lines. 0 lines covered and 15 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%
  2. item_impact = tab_info[:item_impact]
  3. category_impact = tab_info[:category_impact]
  4. %>
  5. <%= render LooposUi::TabsContent.new do |tab| %>
  6. <% tab.with_row do |row| %>
  7. <% row.with_column do %>
  8. <%= render LooposUi::TitleDescription.new(title: "LoopOS Impact", description: I18n.t("admin.v2.impact.page.description"), size: "normal") %>
  9. <% end %>
  10. <% end %>
  11. <% tab.with_row do |row| %>
  12. <% row.with_column do %>
  13. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.v2.items.impact"), size: "small", underline: true) do %>
  14. <% Catalog::Node::IMPACT_SCHEMA.keys.each do |impact_type| %>
  15. <%= render LooposUi::DataCard.new(
  16. title: I18n.t("admin.v2.impact.page.#{impact_type}.title"),
  17. icon: Catalog::Node::IMPACT_SCHEMA[impact_type][:icon],
  18. balance_value: format_number(item_impact[impact_type][:balance_value]),
  19. reuse_cost: impact_label(item_impact[impact_type][:reuse_cost], impact_type),
  20. saved_value: impact_label(item_impact[impact_type][:saved_value], impact_type),
  21. metric_unit: I18n.t("admin.v2.impact.page.#{impact_type}.measure")
  22. )%>
  23. <% end %>
  24. <%= render partial: 'admin/v2/impacts/card_labels' %>
  25. <% end %>
  26. <% end %>
  27. <% row.with_column do %>
  28. <%= render partial: "/admin/v2/impacts/impact_cards", locals: {impact: category_impact}%>
  29. <% end %>
  30. <% end %>
  31. <% end %>

app/components/loopos_ui/page_section/item_tab/info.rb

68.0% lines covered

0.0% branches covered

25 relevant lines. 17 lines covered and 8 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. 1 module LooposUi
  2. 1 module PageSection
  3. 1 module ItemTab
  4. 1 class Info < LoopComponent
  5. 1 KEY = :item_info
  6. 1 option :item
  7. 1 option :policy, default: -> { {} }, type: Types::Hash
  8. 1 class IdentifiersTable < LoopComponent
  9. 1 option :identifiers
  10. 1 option :readonly, default: -> { true }, type: Types::Bool
  11. 1 option :source_context, optional: true
  12. 1 option :identifiable_id, optional: true
  13. 1 option :identifiable_type, optional: true
  14. 1 option :available_kinds, optional: true, default: -> { [] }
  15. end
  16. 1 def show_identifiers?
  17. !LooposUi.config.app_type?(:core)
  18. end
  19. 1 private
  20. 1 def fetch_available_kinds
  21. else: 0 then: 0 return [] unless policy[:manage_identifiers]
  22. kinds_data = item.source.identifiers.kinds
  23. then: 0 if kinds_data.present?
  24. kinds_data.map { |k| { value: k, text: I18n.t("admin.items.identifier_kinds.#{k}") } }
  25. else: 0 else
  26. []
  27. end
  28. rescue => e
  29. LooposUi.logger.error("Failed to fetch available identifier kinds: #{e.message}")
  30. []
  31. end
  32. end
  33. end
  34. end
  35. end

app/components/loopos_ui/page_section/item_tab/info/identifiers_table.html.erb

0.0% lines covered

0.0% branches covered

29 relevant lines. 0 lines covered and 29 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <%
  2. columns = [
  3. { title: t(".identifiers_table.kind"), sortable: false, dataIndex: "kind", key: "kind"},
  4. { title: t(".identifiers_table.reference"), sortable: false, dataIndex: "reference", key: "reference"},
  5. ]
  6. table_options = {
  7. show_result_count: false,
  8. searchable: false,
  9. }
  10. else: 0 then: 0 unless readonly
  11. table_options[:manageable_rows] = true
  12. table_options[:add_new_url] = LooposUi::Engine.routes.url_helpers.identifiers_table_new_path(
  13. context: source_context,
  14. identifiable_id: identifiable_id,
  15. identifiable_type: identifiable_type,
  16. )
  17. end
  18. %>
  19. <%= render LooposUi::V2::Table.new(id: "identifiers_table", columns: columns, **table_options) do |table| %>
  20. then: 0 <% if readonly %>
  21. <% identifiers.each_with_index do |identifier, index| %>
  22. <% identifier.each do |kind, reference| %>
  23. <% table.with_row(key: index) do |row| %>
  24. <% row.with_cell(property: :kind) do %>
  25. <%= render LooposUi::Inputs::Text.new(
  26. name: "identifier[kind]",
  27. value: t("admin.items.identifier_kinds.#{kind}"),
  28. readonly: true
  29. ) %>
  30. <% end %>
  31. <% row.with_cell(property: :reference) do %>
  32. <%= render LooposUi::Inputs::Text.new(
  33. name: "identifier[reference]",
  34. value: reference,
  35. readonly: true
  36. ) %>
  37. <% end %>
  38. <% end %>
  39. <% end %>
  40. <% end %>
  41. else: 0 <% else %>
  42. <% identifiers.each do |identifier| %>
  43. <%
  44. update_url = LooposUi::Engine.routes.url_helpers.identifiers_update_path(
  45. id: identifier.id,
  46. context: source_context,
  47. )
  48. destroy_url = LooposUi::Engine.routes.url_helpers.identifiers_destroy_path(
  49. id: identifier.id,
  50. context: source_context,
  51. )
  52. %>
  53. <% table.with_row(key: identifier.id, delete_action: { url: destroy_url }) do |row| %>
  54. <% row.with_cell(property: :kind) do %>
  55. <%= form_with(url: update_url, method: :patch, id: "form_kind_#{identifier.id}") do |form| %>
  56. <%= render LooposUi::Inputs::Select.new(
  57. name: "identifier[kind]",
  58. options: available_kinds,
  59. value: identifier.kind,
  60. mode: :autosubmit,
  61. form: "form_kind_#{identifier.id}",
  62. readonly: false
  63. ) %>
  64. <% end %>
  65. <% end %>
  66. <% row.with_cell(property: :reference) do %>
  67. <%= form_with(url: update_url, method: :patch, id: "form_reference_#{identifier.id}") do |form| %>
  68. <%= render LooposUi::Inputs::Text.new(
  69. name: "identifier[reference]",
  70. value: identifier.reference,
  71. mode: :autosubmit,
  72. form: "form_reference_#{identifier.id}",
  73. readonly: false
  74. ) %>
  75. <% end %>
  76. <% end %>
  77. <% row.with_action %>
  78. <% end %>
  79. <% end %>
  80. <% end %>
  81. <% end %>

app/components/loopos_ui/page_section/item_tab/info/info.html.erb

0.0% lines covered

0.0% branches covered

88 relevant lines. 0 lines covered and 88 lines missed.
34 total branches, 0 branches covered and 34 branches missed.
    
  1. <%= render LooposUi::TabsContent.new do |tab| %>
  2. <% tab.with_row do |row| %>
  3. <% row.with_column do %>
  4. <%= render LooposUi::TitleDescription.new(title: I18n.t("admin.items.item_info.title"), description: I18n.t("admin.items.item_info.description"), size: "normal" ) %>
  5. <% end %>
  6. <% end %>
  7. <% tab.with_row do |row| %>
  8. <% row.with_column do %>
  9. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.item_info.item_details.title"), size: "small", underline: true) do %>
  10. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.item_info.item_details.external_reference"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  11. <%= entry.with_input do %>
  12. <%= render LooposUi::Inputs::Text.new( name: "external_reference", value: item.external_reference, readonly: true) %>
  13. <% end %>
  14. <% end %>
  15. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.item_info.item_details.category"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  16. <%= entry.with_input do %>
  17. then: 0 <% if item.categories.present? %>
  18. <%= render LooposUi::TokenList.new(max_tokens: 4) do |token_list| %>
  19. then: 0 else: 0 <% item_categories = item.respond_to?(:categories_names) ? item.categories_names : item.categories %>
  20. <% item_categories.each do |category_name| %>
  21. <% token_list.with_token_manual do %>
  22. <%= render LooposUi::Entities::Category.new(category: OpenStruct.new(name: category_name)) %>
  23. <% end %>
  24. <% end %>
  25. <% end %>
  26. else: 0 <% else %>
  27. <%= render LooposUi::Inputs::Text.new(name: "categories", value: "-", readonly: true) %>
  28. <% end %>
  29. <% end %>
  30. <% end %>
  31. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.item_info.item_details.brand"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  32. <%= entry.with_input do %>
  33. then: 0 <% if item.brand.present? %>
  34. then: 0 else: 0 <%= render LooposUi::Entities::Brand.new(brand: OpenStruct.new(name: item.respond_to?(:brand_name) ? item.brand_name : item.brand)) %>
  35. else: 0 <% else %>
  36. <%= render LooposUi::Inputs::Text.new(name: "brand", value: "-", readonly: true) %>
  37. <% end %>
  38. <% end %>
  39. <% end %>
  40. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.item_info.item_details.product"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  41. <%= entry.with_input do %>
  42. then: 0 <% if item.product_name.present? %>
  43. <%= render LooposUi::Entities::Product.new(product: OpenStruct.new(name: item.product_name)) %>
  44. else: 0 <% else %>
  45. <%= render LooposUi::Inputs::Text.new(name: "product", value: "-", readonly: true) %>
  46. <% end %>
  47. <% end %>
  48. <% end %>
  49. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.item_info.item_details.option_values"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  50. <%= entry.with_input do %>
  51. then: 0 <% if item.option_values.present? %>
  52. <%= render LooposUi::TokenList.new(max_tokens: 4) do |token_list| %>
  53. <% item.option_values.each do |option| %>
  54. <% token_list.with_token_manual do %>
  55. <%= render LooposUi::EntityToken.new(text: "#{option[:value] || option[:name]}") %>
  56. <% end %>
  57. <% end %>
  58. <% end %>
  59. else: 0 <% else %>
  60. <%= render LooposUi::Inputs::Text.new(name: "option_values_empty", value: "-", readonly: true) %>
  61. <% end %>
  62. <% end %>
  63. <% end %>
  64. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.item_info.item_details.variant"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  65. <%= entry.with_input do %>
  66. then: 0 <% if item.variant_name.present? %>
  67. <%= render LooposUi::Token.new(text: item.variant_name) %>
  68. else: 0 <% else %>
  69. <%= render LooposUi::Inputs::Text.new(name: "variant", value: "-", readonly: true) %>
  70. <% end %>
  71. <% end %>
  72. <% end %>
  73. then: 0 else: 0 <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.item_info.item_details.hubs_store"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  74. <%= entry.with_input do %>
  75. <%= render LooposUi::Entities::AppInstance.new(app_instance: OpenStruct.new(name: item.hub_origin_name, kind: "hubs")) %>
  76. <% end %>
  77. <% end if item.hub_origin_name.present? %>
  78. then: 0 else: 0 <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.item_info.item_details.submission_origin"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  79. <%= entry.with_input do %>
  80. <%= render LooposUi::Entities::AppInstance.new(app_instance: OpenStruct.new(name: item.submission_origin_data[:label], kind: "submission")) %>
  81. <% end %>
  82. <% end if item.submission_origin_data.present? %>
  83. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.item_info.item_details.created_at"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  84. then: 0 else: 0 <%= entry.with_input do %>
  85. <%= render LooposUi::DateShow.new(date: item.created_at) %>
  86. <% end if item.created_at.present? %>
  87. <% end %>
  88. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.item_info.item_details.updated_at"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  89. then: 0 else: 0 <%= entry.with_input do %>
  90. <%= render LooposUi::DateShow.new(date: item.updated_at) %>
  91. <% end if item.updated_at.present? %>
  92. <% end %>
  93. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.item_info.item_details.days_created"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  94. then: 0 else: 0 <%= entry.with_input do %>
  95. <%= render LooposUi::Inputs::Text.new(name: "days_created", value: item.days_created, readonly: true) %>
  96. <% end if item.days_created.present? %>
  97. <% end %>
  98. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.item_info.item_details.days_in_current_state"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  99. then: 0 else: 0 <%= entry.with_input do %>
  100. <%= render LooposUi::Inputs::Text.new(name: "days_in_current_state", value: item.days_in_current_state, readonly: true) %>
  101. <% end if item.days_in_current_state.present? %>
  102. <% end %>
  103. <% end %>
  104. <% end %>
  105. <% row.with_column do %>
  106. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.item_info.timeline"), size: "small", underline: true) do %>
  107. else: 0 then: 0 <% unless item.new_record? %>
  108. then: 0 else: 0 <% item.timeline&.each_with_index do |timepoint, index| %>
  109. <%= render LooposUi::Timeline.new(
  110. item: timepoint,
  111. is_last: (item.timeline.size - 1) == index
  112. ) %>
  113. <% end %>
  114. <% end %>
  115. <% end %>
  116. <% end %>
  117. <% end %>
  118. <% tab.with_row do |row| %>
  119. <% row.with_column do %>
  120. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.item_info.internal_notes.title"), size: "small", underline: true) do %>
  121. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.item_info.internal_notes.rejection_reason"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  122. <%= entry.with_input do %>
  123. <%= render LooposUi::Inputs::Text.new( name: "rejection_reason", value: item.rejection_reason_text.presence || item.rejection_reason.presence || "-", readonly: true) %>
  124. <% end %>
  125. <% end %>
  126. <% end %>
  127. <% end %>
  128. <% end %>
  129. <% tab.with_row do |row| %>
  130. then: 0 else: 0 <% row.with_column(half: true) do %>
  131. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.item_info.identifiers.title"), size: "small", underline: "yes") do %>
  132. then: 0 <% if policy[:manage_identifiers] %>
  133. <%= render LooposUi::PageSection::ItemTab::Info::IdentifiersTable.new(
  134. identifiers: item.identifier_records,
  135. readonly: false,
  136. source_context: item.source.source_context,
  137. identifiable_id: item.id,
  138. identifiable_type: "Item",
  139. available_kinds: fetch_available_kinds,
  140. ) %>
  141. else: 0 <% else %>
  142. <%= render LooposUi::PageSection::ItemTab::Info::IdentifiersTable.new(identifiers: item.identifiers) %>
  143. <% end %>
  144. <% end %>
  145. <% end if show_identifiers? %>
  146. <% row.with_column(half: show_identifiers?) do %>
  147. <%= helpers.flex_cards_tab_section("Item", :show_item_info_tab, instance_id: item.id, title: I18n.t("admin.items.item_info.custom_information")) %>
  148. <% end %>
  149. <% end %>
  150. <% end %>

app/components/loopos_ui/page_section/item_tab/logs.rb

26.47% lines covered

0.0% branches covered

102 relevant lines. 27 lines covered and 75 lines missed.
46 total branches, 0 branches covered and 46 branches missed.
    
  1. 1 module LooposUi
  2. 1 module PageSection
  3. 1 module ItemTab
  4. 1 class Logs < LoopComponent
  5. 1 PER_PAGE = 10
  6. 1 KEY = :logs
  7. 1 option :item, Types::Instance(PageSources::Models::Item)
  8. # used for pagination
  9. 1 option :page, Types::Integer.default(1), optional: true
  10. 1 option :per_page, Types::Integer.default(PER_PAGE), optional: true
  11. 1 option :has_more, Types::Bool.default(false), optional: true
  12. 1 option :total_count, Types::Integer, optional: true
  13. 1 option :grouped_logs, Types::Array, optional: true
  14. # used for filtering
  15. 1 option :filter_block_id, optional: true
  16. 1 option :filter_user_type, optional: true
  17. 1 option :filter_date_range, Types::Coercible::String, optional: true
  18. 1 option :filter_log_class, optional: true
  19. 1 def initialize(...)
  20. super
  21. # Store grouped_logs if passed directly (for paginated requests)
  22. then: 0 else: 0 @grouped_logs_value = @grouped_logs if @grouped_logs.present?
  23. end
  24. 1 def grouped_logs
  25. @grouped_logs ||= if @grouped_logs_value.present?
  26. then: 0 # For paginated requests, use the passed grouped_logs
  27. @grouped_logs_value
  28. else
  29. else: 0 # For initial load, limit logs but keep original group structure
  30. grouped_logs_source = all_grouped_logs
  31. per_page_count = per_page || PER_PAGE
  32. logs_count = 0
  33. limited_groups = []
  34. grouped_logs_source.each do |group|
  35. then: 0 else: 0 break if logs_count >= per_page_count
  36. group_logs = group[:logs] || group["logs"] || []
  37. remaining_slots = per_page_count - logs_count
  38. if group_logs.count <= remaining_slots
  39. then: 0 # Include entire group
  40. limited_groups << group.dup
  41. logs_count += group_logs.count
  42. else
  43. else: 0 # Include only part of the group
  44. limited_groups << {
  45. date: group[:date] || group["date"],
  46. logs: group_logs.slice(0, remaining_slots),
  47. }
  48. logs_count += remaining_slots
  49. end
  50. end
  51. limited_groups
  52. end
  53. end
  54. 1 def has_more
  55. then: 0 @has_more ||= if @has_more_value.present?
  56. @has_more_value
  57. else: 0 else
  58. (per_page || PER_PAGE) < all_logs.count
  59. end
  60. end
  61. 1 def logs_url
  62. url_params = {
  63. token: item.token,
  64. page: (page || 1) + 1,
  65. per_page: per_page || PER_PAGE,
  66. }
  67. # Add filter params if present
  68. then: 0 else: 0 url_params[:filter_block_id] = filter_block_id if filter_block_id.present?
  69. then: 0 else: 0 url_params[:filter_user_type] = filter_user_type if filter_user_type.present?
  70. then: 0 else: 0 url_params[:filter_date_range] = filter_date_range if filter_date_range.present?
  71. then: 0 else: 0 url_params[:filter_log_class] = filter_log_class if filter_log_class.present?
  72. # Preserve source resolution across lazy-load requests.
  73. then: 0 else: 0 if item.source.respond_to?(:source_context)
  74. url_params[:context] = item.source.source_context
  75. end
  76. helpers.loopos_ui.logs_item_path(url_params)
  77. end
  78. 1 def logs_filter_url
  79. url_params = {
  80. token: item.token,
  81. }
  82. # Preserve source resolution across filter requests.
  83. then: 0 else: 0 if item.source.respond_to?(:source_context)
  84. url_params[:context] = item.source.source_context
  85. end
  86. helpers.loopos_ui.logs_item_path(url_params)
  87. end
  88. 1 def block_filter_options
  89. @block_filter_options ||= begin
  90. # Extract unique blocks from logs (using block_id as key, block name as value)
  91. blocks_by_id = {}
  92. all_logs.each do |log|
  93. block_id_raw = log.dig("extra_data", "block_id") || log.dig(:extra_data, :block_id)
  94. # block_id can be an array, so get the last one (current block)
  95. then: 0 else: 0 block_id = block_id_raw.is_a?(Array) ? block_id_raw.last : block_id_raw
  96. then: 0 else: 0 next if block_id.blank?
  97. block = log["block"] || log[:block]
  98. then: 0 else: 0 then: 0 else: 0 block_name = block&.dig("name") || block&.dig(:name)
  99. # Only add if we have both block_id and block_name
  100. then: 0 else: 0 if block_name.present?
  101. blocks_by_id[block_id.to_s] = block_name
  102. end
  103. end
  104. then: 0 if blocks_by_id.empty?
  105. []
  106. else
  107. else: 0 # Build options with block names (the name that appears in the log)
  108. blocks_by_id.map do |block_id, block_name|
  109. {
  110. value: block_id,
  111. text: block_name,
  112. }
  113. end
  114. end
  115. end
  116. end
  117. 1 def user_filter_options
  118. @user_filter_options ||= begin
  119. # Extract unique users from logs
  120. # Note: logs from API have "author" field (from jbuilder), but the raw log hash has "user"
  121. # Logs come as hash with symbol keys like {:author=>"tech@theloop.pt"}
  122. users = []
  123. all_logs.each do |log|
  124. then: 0 else: 0 log_hash = log.is_a?(Hash) ? log.with_indifferent_access : log
  125. # Check both string and symbol keys, and both "user" and "author" fields
  126. user_value = log_hash["user"] || log_hash[:user] || log_hash["author"] || log_hash[:author]
  127. then: 0 else: 0 if user_value.present? && user_value.to_s.strip.present?
  128. users << user_value.to_s.strip
  129. end
  130. end
  131. options = []
  132. # Add options for each unique user (using the actual user name/email)
  133. users.uniq.sort.each do |user|
  134. options << { value: user, text: user }
  135. end
  136. # Add system option if there are logs without users
  137. has_system = all_logs.any? do |log|
  138. then: 0 else: 0 log_hash = log.is_a?(Hash) ? log.with_indifferent_access : log
  139. user_value = log_hash["user"] || log_hash[:user] || log_hash["author"] || log_hash[:author]
  140. user_value.blank? || user_value.to_s.strip.blank?
  141. end
  142. then: 0 else: 0 options << { value: "system", text: I18n.t("admin.items.logs.filters.system_option") } if has_system
  143. options
  144. end
  145. end
  146. 1 def log_class_filter_options
  147. @log_class_filter_options ||= begin
  148. log_classes = []
  149. all_logs.each do |log|
  150. then: 0 else: 0 log_hash = log.is_a?(Hash) ? log.with_indifferent_access : log
  151. log_class_value = log_hash["log_class"] || log_hash[:log_class]
  152. then: 0 else: 0 if log_class_value.present? && log_class_value.to_s.strip.present?
  153. log_classes << log_class_value.to_s.strip
  154. end
  155. end
  156. log_classes.uniq.sort.map do |log_class|
  157. {
  158. value: log_class,
  159. text: I18n.t("admin.items.logs.log_classes.#{log_class}", default: log_class.to_s.humanize),
  160. }
  161. end
  162. end
  163. end
  164. 1 private
  165. 1 def all_grouped_logs
  166. @all_grouped_logs ||= @item.grouped_logs
  167. end
  168. 1 def all_logs
  169. @all_logs ||= all_grouped_logs.flat_map { |group| group[:logs] || group["logs"] || [] }
  170. end
  171. end
  172. end
  173. end
  174. end

app/components/loopos_ui/page_section/item_tab/logs/logs.html.erb

0.0% lines covered

0.0% branches covered

43 relevant lines. 0 lines covered and 43 lines missed.
10 total branches, 0 branches covered and 10 branches missed.
    
  1. <%= tag.turbo_frame id: "lui-tab-logs" do %>
  2. <%= render LooposUi::TabsContent.new do |tab| %>
  3. <% tab.with_row do |row| %>
  4. <% row.with_column do %>
  5. <%= render LooposUi::TitleDescription.new(title: I18n.t("admin.items.logs.title"), description: I18n.t("admin.items.logs.tooltip"), size: "normal") %>
  6. <% end %>
  7. <% end %>
  8. <% tab.with_row do |row| %>
  9. <% row.with_column do %>
  10. <%= render LooposUi::TabsSection.new(size: "small") %>
  11. <% form_id = "logs_filter_form" %>
  12. <%= form_with url: logs_filter_url, method: :get, id: form_id, data: { turbo_frame: "logs_list_frame" } do |form| %>
  13. then: 0 else: 0 <% if item.source.respond_to?(:source_context) %>
  14. <%= form.hidden_field :context, value: item.source.source_context %>
  15. <% end %>
  16. <%= tag.div class: "lui-logs-filters" do %>
  17. <%= tag.div class: "lui-logs-filters__field--date" do %>
  18. <%= render LooposUi::DatePicker.new(
  19. range: true,
  20. granularity: "date",
  21. locale: I18n.locale.to_s,
  22. name: "filter_date_range",
  23. max_date: Date.current,
  24. submit_on_select: true
  25. ) %>
  26. <% end %>
  27. then: 0 else: 0 <% if block_filter_options.any? %>
  28. <%= tag.div class: "lui-logs-filters__field" do %>
  29. <%= render LooposUi::Inputs::Select2.new(
  30. name: "filter_block_id",
  31. options: block_filter_options,
  32. value: filter_block_id,
  33. placeholder: I18n.t("admin.items.logs.filters.block_placeholder"),
  34. mode: :form,
  35. multiple: true,
  36. show_actions_in_menu: true,
  37. show_loading_on_submit: false,
  38. form: form_id
  39. ) %>
  40. <% end %>
  41. <% end %>
  42. then: 0 else: 0 <% if user_filter_options.any? %>
  43. <%= tag.div class: "lui-logs-filters__field" do %>
  44. <%= render LooposUi::Inputs::Select2.new(
  45. name: "filter_user_type",
  46. options: user_filter_options,
  47. value: filter_user_type,
  48. placeholder: I18n.t("admin.items.logs.filters.user_placeholder"),
  49. mode: :form,
  50. multiple: true,
  51. show_actions_in_menu: true,
  52. show_loading_on_submit: false,
  53. form: form_id
  54. ) %>
  55. <% end %>
  56. <% end %>
  57. then: 0 else: 0 <% if log_class_filter_options.any? %>
  58. <%= tag.div class: "lui-logs-filters__field" do %>
  59. <%= render LooposUi::Inputs::Select2.new(
  60. name: "filter_log_class",
  61. options: log_class_filter_options,
  62. value: filter_log_class,
  63. placeholder: I18n.t("admin.items.logs.filters.log_class_placeholder"),
  64. searchable: false,
  65. mode: :form,
  66. multiple: true,
  67. show_actions_in_menu: true,
  68. show_loading_on_submit: false,
  69. form: form_id
  70. ) %>
  71. <% end %>
  72. <% end %>
  73. <% end %>
  74. <% end %>
  75. <%= tag.turbo_frame id: "logs_list_frame", class: "lui-logs-frame" do %>
  76. <%= tag.div class: "lui-logs-frame__form-loader" do %>
  77. <%= render LooposUi::Loadings::Skeleton.new(full: true) %>
  78. <% end %>
  79. <%= tag.div \
  80. class: "lui-lazy-load-logs lui-logs-frame__content",
  81. data: {
  82. controller: "lui--lazy-load-logs",
  83. then: 0 else: 0 "lui--lazy-load-logs-url-value": (logs_url if has_more),
  84. "lui--lazy-load-logs-has-more-value": has_more
  85. }.compact do %>
  86. <%= render LooposUi::LogList.from_any_source(
  87. grouped_logs,
  88. id: "logs",
  89. config: { has_pagination: false, max_items: [grouped_logs.flat_map { |g| (g[:logs] || g["logs"] || []) }.count, 1].max },
  90. )
  91. %>
  92. <%= tag.div \
  93. class: "lui-lazy-load-logs__loader",
  94. data: { "lui--lazy-load-logs-target": "loader" },
  95. style: "display: none;" do %>
  96. <%= render LooposUi::Loadings::Skeleton.new(full: true) %>
  97. <% end %>
  98. <% end %>
  99. <% end %>
  100. <% end %>
  101. <% end %>
  102. <% tab.with_row do |row| %>
  103. <% row.with_column do %>
  104. <%= helpers.flex_cards_tab_section("Item", :show_logs_tab, instance_id: item.id) %>
  105. <% end %>
  106. <% end %>
  107. <% end %>
  108. <% end %>

app/components/loopos_ui/page_section/item_tab/notes.rb

72.22% lines covered

0.0% branches covered

18 relevant lines. 13 lines covered and 5 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 module PageSection
  3. 1 module ItemTab
  4. 1 class Notes < LoopComponent
  5. 1 KEY = :item_notes
  6. 1 option :item
  7. 1 option :policy
  8. 1 then: 0 else: 0 delegate :source, to: :@item
  9. 1 def instance_notes
  10. item.notes(key: "self")
  11. end
  12. 1 def shared_notes
  13. item.notes(key: "shared")
  14. end
  15. 1 def update_notes_path(key: "self")
  16. helpers.loopos_ui.update_notes_item_path(key: key, token: item.token, context: source.source_context)
  17. end
  18. 1 def can_view_shared_notes?
  19. policy.can_view_shared_item_notes?
  20. end
  21. 1 def can_edit_shared_notes?
  22. policy.can_edit_shared_item_notes?
  23. end
  24. end
  25. end
  26. end
  27. end

app/components/loopos_ui/page_section/item_tab/notes/notes.html.erb

0.0% lines covered

0.0% branches covered

30 relevant lines. 0 lines covered and 30 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. <%= render LooposUi::TabsContent.new do |tab| %>
  2. <% tab.with_row do |row| %>
  3. <% row.with_column do %>
  4. <%= render LooposUi::TitleDescription.new(title: I18n.t("admin.items.item_notes.title"), description: I18n.t("admin.items.item_notes.description"), size: "normal" ) %>
  5. <% end %>
  6. <% end %>
  7. then: 0 else: 0 <% tab.with_row do |row| %>
  8. <% row.with_column do %>
  9. <%= render LooposUi::Accordion.new(open: true) do |accordion| %>
  10. <% accordion.with_header(title: I18n.t("admin.items.item_notes.shared_notes.title"), size: :small) %>
  11. <%= form_with url: update_notes_path(key: "shared"), method: :patch do |form| %>
  12. <%= render LooposUi::Inputs::RichText.new(
  13. name: "content",
  14. placeholder: I18n.t("admin.items.item_notes.placeholder"),
  15. value: shared_notes,
  16. mode: :autosubmit,
  17. readonly: !can_edit_shared_notes?,
  18. upload_endpoint: helpers.loopos_ui.upload_attachment_item_path(token: item.token, context: source.source_context, authenticity_token: form_authenticity_token),
  19. ) %>
  20. <% end %>
  21. <% end %>
  22. <% end %>
  23. <% end if can_view_shared_notes? %>
  24. <% tab.with_row do |row| %>
  25. <% row.with_column do %>
  26. <%= render LooposUi::Accordion.new(open: !can_view_shared_notes?) do |accordion| %>
  27. <% accordion.with_header(title: I18n.t("admin.items.item_notes.instance_notes.title"), size: :small) %>
  28. <%= form_with url: update_notes_path(key: "self"), method: :patch do |form| %>
  29. <%= render LooposUi::Inputs::RichText.new(
  30. name: "content",
  31. placeholder: I18n.t("admin.items.item_notes.placeholder"),
  32. value: instance_notes,
  33. mode: :autosubmit,
  34. upload_endpoint: helpers.loopos_ui.upload_attachment_item_path(token: item.token, context: source.source_context, authenticity_token: form_authenticity_token),
  35. ) %>
  36. <% end %>
  37. <% end %>
  38. <% end %>
  39. <% end %>
  40. then: 0 else: 0 <% tab.with_row do |row| %>
  41. <% row.with_column do %>
  42. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.item_notes.internal_notes.title"), size: "small", underline: "yes") do %>
  43. <%= render LooposUi::GridLayout.new(cols: 1) do |tab| %>
  44. then: 0 else: 0 <% item.internal_notes&.each do |app, note| %>
  45. <% tab.with_section do %>
  46. <%= render LooposUi::Accordion.new(open: true) do |accordion| %>
  47. <% accordion.with_header(title: app, size: :tiny) %>
  48. <%= render LooposUi::Inputs::RichText.new(name: "notes_#{app.downcase}", value: note.presence || "-", readonly: true) %>
  49. <% end %>
  50. <% end %>
  51. <% end %>
  52. <% end %>
  53. <% end %>
  54. <% end %>
  55. <% end if item.internal_notes.present? && LooposUi.config.app_type.to_s == "core" %>
  56. <% tab.with_row do |row| %>
  57. <% row.with_column do %>
  58. <%= helpers.flex_cards_tab_section("Item", :show_item_notes_tab, instance_id: item.id) %>
  59. <% end %>
  60. <% end %>
  61. <% end %>

app/components/loopos_ui/page_section/item_tab/protocol_responses.rb

66.67% lines covered

0.0% branches covered

18 relevant lines. 12 lines covered and 6 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. 1 module LooposUi
  2. 1 module PageSection
  3. 1 module ItemTab
  4. 1 class ProtocolResponses < LoopComponent
  5. 1 KEY = :protocol_answers
  6. 1 option :item, Types::Instance(PageSources::Models::Item)
  7. 1 def partial_exists?(partial)
  8. # template exists expects the partial name with a _ prefix, add that
  9. add_underscore_regex = %r{(?:/)(?!_)([^/]+)$}
  10. lookup_context.template_exists?(partial.sub(add_underscore_regex, '/_\1'), formats: [:json, :html])
  11. end
  12. 1 def submission_extra_arrow_svg
  13. '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
  14. <rect x="0.5" y="0.5" width="13" height="13" rx="6.5" fill="#F8ECE6"/>
  15. <rect x="0.5" y="0.5" width="13" height="13" rx="6.5" stroke="white"/>
  16. <path d="M10.3438 7.35938L7.84375 9.85938C7.65625 10.0625 7.32812 10.0625 7.14062 9.85938C6.9375 9.67188 6.9375 9.34375 7.14062 9.15625L8.78125 7.5H4C3.71875 7.5 3.5 7.28125 3.5 7C3.5 6.73438 3.71875 6.5 4 6.5H8.78125L7.14062 4.85938C6.9375 4.67188 6.9375 4.34375 7.14062 4.15625C7.32812 3.95312 7.65625 3.95312 7.84375 4.15625L10.3438 6.65625C10.5469 6.84375 10.5469 7.17188 10.3438 7.35938Z" fill="#B53C00"/>
  17. </svg>'.html_safe
  18. end
  19. 1 class ProtocolAnswerValue < LoopComponent
  20. 1 option :kind, types: Types::Coercible::Symbol.enum(["text", "bool", "date", "files", "images", "number", "select"]), optional: true
  21. 1 option :value, optional: true
  22. 1 def value_mapped
  23. then: 0 if value.is_a?(Array)
  24. then: 0 else: 0 value.map { |v| v.respond_to?(:with_indifferent_access) ? v.with_indifferent_access : v }
  25. else: 0 else
  26. []
  27. end
  28. end
  29. end
  30. end
  31. end
  32. end
  33. end

app/components/loopos_ui/page_section/item_tab/protocol_responses/protocol_answer_value.html.erb

0.0% lines covered

0.0% branches covered

10 relevant lines. 0 lines covered and 10 lines missed.
9 total branches, 0 branches covered and 9 branches missed.
    
  1. <% case kind.to_s %>
  2. when: 0 <% when "bool", "date", "number", "select" %>
  3. then: 0 <% if value.present? %>
  4. <%= render(LooposUi::Token.new(text: value.to_s)) %>
  5. else: 0 <% else %>
  6. -
  7. <% end %>
  8. when: 0 <% when "files" %>
  9. <%= react_component("FilesGallery", { files: value_mapped.pluck(:url), size: "small", railsIcon: true }) %>
  10. when: 0 <% when "images" %>
  11. <%= react_component("ImagesGallery", { images: value_mapped.pluck(:url), size: "small" }) %>
  12. when: 0 <% when "text" %>
  13. <%== (value.to_s.gsub("\\n", "\n").presence || "-").gsub(/\r\n?|\n/, "<br>") %>
  14. else: 0 <% else %>
  15. then: 0 <% if value.present? %>
  16. <%= render(LooposUi::Token.new(text: value.to_s)) %>
  17. else: 0 <% else %>
  18. -
  19. <% end %>
  20. <% end %>

app/components/loopos_ui/page_section/item_tab/protocol_responses/protocol_responses.html.erb

0.0% lines covered

0.0% branches covered

46 relevant lines. 0 lines covered and 46 lines missed.
34 total branches, 0 branches covered and 34 branches missed.
    
  1. <%
  2. header_steps = ["submission", "validation", "hubs", "handling", "submission_extra"]
  3. columns = [
  4. { title: I18n.t("admin.items.protocol_answers.table.question"), sortable: false, dataIndex: "question", key: "question", type: "html" }
  5. ]
  6. columns_titles = header_steps.map do |step|
  7. content_tag(:div, class: "protocol-answer__header", id: "tooltip_#{step}") do
  8. concat(
  9. content_tag(:div, class: "protocol-answer__image") do
  10. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 content_tag(:div, class: "protocol-answer__wrapper-image #{item.protocol_answers&.any? { |pa| pa["answers"]&.any? { |a| a["step"] == step } } ? "" : "protocol-answer__wrapper-image__inactive"}") do
  11. then: 0 concat(if step == "submission_extra"
  12. content_tag(:div, class:"relative inline-block") do
  13. render(LooposUi::Logo.new(app: "submission", size: "small", icon: true, count: nil)) +
  14. content_tag(:div, class: "absolute -top-2 -right-2") do
  15. raw(submission_extra_arrow_svg)
  16. end
  17. end
  18. else: 0 else
  19. render LooposUi::Logo.new(app: step, size: "small", icon: true, count: nil )
  20. end)
  21. concat(render LooposUi::Tooltip.new(title: "LoopOs #{step.capitalize}", tippy_target_id: "tooltip_#{step}", position: :bottom))
  22. end
  23. end
  24. )
  25. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 answer = item.protocol_answers&.find { |pa| pa["answers"]&.find { |a| a["step"] == step } }&.dig("answers")&.find { |a| a["step"] == step }
  26. then: 0 else: 0 if answer.present?
  27. concat(
  28. content_tag(:div, class: "protocol-answer__specs") do
  29. then: 0 else: 0 concat content_tag(:p, answer["created_at"].present? ? (Date.strptime(answer["created_at"], "%d/%m/%Y").strftime("%d %h %Y") rescue answer["created_at"]) : "", class: "protocol-answer__date")
  30. concat content_tag(:p, answer["created_by"], class: "protocol-answer__user")
  31. end
  32. )
  33. end
  34. end
  35. end
  36. columns += columns_titles.map.with_index do |title_content, index|
  37. {
  38. title: capture { title_content },
  39. sortable: false,
  40. dataIndex: header_steps[index],
  41. key: header_steps[index],
  42. type: "html"
  43. }
  44. end
  45. %>
  46. <%= render LooposUi::TabsContent.new do |tab| %>
  47. <% tab.with_row do |row| %>
  48. <% row.with_column do %>
  49. <%= render LooposUi::TitleDescription.new(title: I18n.t("admin.items.protocol_answers.title"), description: I18n.t("admin.items.protocol_answers.label"), size: "normal") %>
  50. <% end %>
  51. <% end %>
  52. <% tab.with_row do |row| %>
  53. <% row.with_column do %>
  54. <% header_steps = ["submission", "validation", "hubs", "handling", "submission_extra"] %>
  55. then: 0 else: 0 then: 0 <% if item.protocol_answers&.any? %>
  56. <%= render LooposUi::V2::Table.new(pagy: @pagy, columns: columns, searchable: false, show_result_count: false) do |table| %>
  57. then: 0 else: 0 <% item.protocol_answers&.group_by { |pa| pa["protocol_element"] }.each_with_index do |hash, index| %>
  58. <%
  59. element = hash.first
  60. answers = hash.second
  61. %>
  62. <% table.with_row(key: element["id"]) do |row| %>
  63. <% row.with_cell(property: :question) do %>
  64. <div class="protocol-answer__label">
  65. <%= tag.p element["label"] %>
  66. </div>
  67. <% end %>
  68. <% header_steps.each do |step| %>
  69. then: 0 else: 0 then: 0 else: 0 <% answer = answers&.dig(0, "answers")&.find { |a| a["step"] == step } %>
  70. <% row.with_cell(property: step) do %>
  71. then: 0 <% if answer.present? %>
  72. <div class="protocol-answer__label">
  73. then: 0 else: 0 <%= render LooposUi::PageSection::ItemTab::ProtocolResponses::ProtocolAnswerValue.new(kind: answer["type"]&.to_sym, value: answer["value"]) %>
  74. </div>
  75. else: 0 <% else %>
  76. <div class="protocol-answer__label">-</div>
  77. <% end %>
  78. <% end %>
  79. <% end %>
  80. <% end %>
  81. <% end %>
  82. <% end %>
  83. else: 0 <% else %>
  84. <%= render LooposUi::TabsSection.new(description: I18n.t("admin.items.protocol_answers.item_with_no_answers"), size: "small" ) %>
  85. <% end %>
  86. <% end %>
  87. <% end %>
  88. <% tab.with_row do |row| %>
  89. <% row.with_column do %>
  90. <%= helpers.flex_cards_tab_section("Item", :show_protocol_answers_tab, instance_id: item.id) %>
  91. <% end %>
  92. <% end %>
  93. <% end %>

app/components/loopos_ui/page_section/item_tab/rfi.rb

80.43% lines covered

0.0% branches covered

46 relevant lines. 37 lines covered and 9 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 module PageSection
  3. 1 module ItemTab
  4. 1 class Rfi < LoopComponent
  5. 1 KEY = :request_for_information
  6. 1 option :item, Types::Instance(PageSources::Models::Item)
  7. 1 def rfis
  8. data = item.source.rfis(item.token)
  9. @pagy = data[:pagy]
  10. data[:data]
  11. end
  12. 1 def rfi_answers(rfi_id)
  13. item.source.rfi_answers(rfi_id).values_at(:data, :pagy)
  14. end
  15. 1 def rfi_final_choice(final_choice_id)
  16. else: 0 then: 0 return unless final_choice_id.present?
  17. PageSources::Models::RfiAnswers.find_by(rfi_answer_id: final_choice_id, source: item.source)
  18. end
  19. 1 def fetch_url(rfi_id)
  20. helpers.loopos_ui.rfi_answers_rfi_url(
  21. rfi_id: rfi_id,
  22. item_token: item.token,
  23. with_user_info: false,
  24. context: item.source.source_context,
  25. )
  26. end
  27. 1 def history_urls(rfi_id, rfi_answers)
  28. rfi_answers.each_with_object({}) do |rfi_answer, hash|
  29. hash[rfi_answer.id] = rfi_answer.history_url(item.token)
  30. end
  31. end
  32. 1 class RfiAnswersIndexTable < LoopComponent
  33. 1 option :rfi_id
  34. 1 option :rfi_answers
  35. 1 option :fetch_url
  36. 1 option :pagy
  37. 1 option :history_urls
  38. end
  39. 1 class RfiAnswersStepTable < LoopComponent
  40. 1 option :rfi_id
  41. 1 option :rfi_answers
  42. 1 option :presenter
  43. 1 option :fetch_url
  44. 1 option :pagy
  45. end
  46. 1 class RfiAnswerHistoryTable < LoopComponent
  47. 1 include LooposUi::Concerns::ProtocolAnswerValueFormatter
  48. 1 option :rfi_answer_id
  49. 1 option :rfi_id
  50. 1 option :rfi_answers
  51. 1 option :fetch_url
  52. 1 option :pagy
  53. 1 option :columns_protocol_answers
  54. end
  55. 1 class RfiRespondersWithoutRfiAnswersTable < LoopComponent
  56. 1 option :rfi_id
  57. 1 option :responders_without_rfi_answers
  58. 1 option :presenter
  59. 1 option :fetch_url
  60. 1 option :pagy
  61. end
  62. end
  63. end
  64. end
  65. end

app/components/loopos_ui/page_section/item_tab/rfi/rfi.html.erb

0.0% lines covered

0.0% branches covered

20 relevant lines. 0 lines covered and 20 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%= render LooposUi::TabsContent.new do |tab| %>
  2. <% tab.with_row do |row| %>
  3. <% row.with_column do %>
  4. <% rfis.each do |rfi| %>
  5. <% rfi_final_choice = rfi_final_choice(rfi.final_choice_id) %>
  6. then: 0 else: 0 <% if rfi_final_choice.present? %>
  7. <%= render "loopos_ui/rfi/final_choice_card", rfi_name: rfi.name, rfi_final_choice: rfi_final_choice, hide_rfi_identification: false %>
  8. <% end %>
  9. <% end %>
  10. <% end %>
  11. <% end %>
  12. <% tab.with_row do |row| %>
  13. <% row.with_column do %>
  14. <% rfis.each do |rfi| %>
  15. <%= render LooposUi::Accordion.new(open: true) do |accordion| %>
  16. <% accordion.with_header(title: rfi.name, size: :normal) %>
  17. <%= tag.turbo_frame id: "rfi_answers_index_table_#{rfi.id}", src: helpers.loopos_ui.rfi_answers_rfi_url(rfi_id: rfi.id, item_token: item.token, context: item.source.source_context) do %>
  18. <%= render LooposUi::Loadings::Skeleton.new(full: true) %>
  19. <% end %>
  20. <% accordion.with_action_buttons do |ab| %>
  21. <% ab.with_button_group do |g| %>
  22. <% g.with_button(
  23. text: I18n.t("admin.items.rfi.open_rfi"),
  24. size: :tiny,
  25. type: :secondary,
  26. kind: :neutral,
  27. leading_icon: "open_in_new",
  28. disabled: rfi.show_url(item).blank?,
  29. href: rfi.show_url(item),
  30. tag_options: {
  31. data: {
  32. "inline-edit-target": "submit",
  33. turbo_frame: 'lui-main-layout',
  34. "turbo-action": "advance"
  35. }
  36. }
  37. ) %>
  38. <% end %>
  39. <% end %>
  40. <% end %>
  41. <% end %>
  42. <% end %>
  43. <% end %>
  44. <% tab.with_row do |row| %>
  45. <% row.with_column do %>
  46. <%= helpers.flex_cards_tab_section("Item", :show_rfi_tab, instance_id: item.id) %>
  47. <% end %>
  48. <% end %>
  49. <% end %>

app/components/loopos_ui/page_section/item_tab/rfi/rfi_answer_history_table.html.erb

0.0% lines covered

0.0% branches covered

40 relevant lines. 0 lines covered and 40 lines missed.
20 total branches, 0 branches covered and 20 branches missed.
    
  1. <%
  2. columns = [
  3. { title: "ID", dataIndex: "id", key: "id", default_sort: :asc, fixed: "left"},
  4. { title: I18n.t("admin.items.rfi_answers.responder"), dataIndex: "responder", key: "responder"},
  5. { title: I18n.t("admin.items.rfi_answers.answer_by"), dataIndex: "answered_by", key: "answered_by"},
  6. { title: I18n.t("admin.items.rfi_answers.status"), dataIndex: "state", key: "state" },
  7. { title: I18n.t("admin.items.rfi_answers.email"), dataIndex: "email", key: "email" },
  8. { title: I18n.t("admin.items.rfi_answers.phone"), dataIndex: "phone", key: "phone" },
  9. { title: I18n.t("admin.items.rfi_answers.distance"), dataIndex: "distance", key: "distance" }
  10. ]
  11. then: 0 else: 0 columns += columns_protocol_answers if columns_protocol_answers.any?
  12. columns.concat([
  13. # { title: I18n.t("admin.items.rfi_answers.notes"), dataIndex: "notes", key: "notes", max_width: "200px" },
  14. ])
  15. pagination = {
  16. then: 0 else: 0 page: pagy&.page || 1,
  17. then: 0 else: 0 per_page: pagy&.items || 15,
  18. then: 0 else: 0 total: pagy&.count || rfi_answers.size,
  19. }
  20. options = {}
  21. options.merge!({
  22. id: rfi_id,
  23. pagination: pagination,
  24. columns: columns,
  25. searchable: false,
  26. selectable: false,
  27. pagy: pagy,
  28. fetch_url: fetch_url,
  29. })
  30. %>
  31. <%= helpers.turbo_frame_tag "rfi_history_answers_content_#{rfi_answer_id}" do %>
  32. <%= render LooposUi::V2::Table.new(**options) do |table| %>
  33. <% rfi_answers.each do |rfi_answer| %>
  34. <% table.with_row(key: rfi_answer.id) do |row| %>
  35. <% row.with_cell(property: :id) do %>
  36. <%= rfi_answer.id_label %>
  37. <% end %>
  38. <% row.with_cell(property: :responder) do %>
  39. then: 0 <% if rfi_answer.responder_name.present? %>
  40. <%= render LooposUi::Label.new(text: rfi_answer.responder_name, icon: rfi_answer.responder_logo) %>
  41. else: 0 <% else %>
  42. <%= "-" %>
  43. <% end %>
  44. <% end %>
  45. <% row.with_cell(property: :answered_by) do %>
  46. then: 0 <% if rfi_answer.answered_by_name.present? %>
  47. <%= render LooposUi::Label.new(text: rfi_answer.answered_by_name, icon: rfi_answer.answered_by_logo) %>
  48. else: 0 <% else %>
  49. <%= "-" %>
  50. <% end %>
  51. <% end %>
  52. <% row.with_cell(property: :email) do %>
  53. <%= rfi_answer.email.presence || "-" %>
  54. <% end %>
  55. <% row.with_cell(property: :phone) do %>
  56. <%= rfi_answer.phone.presence || "-" %>
  57. <% end %>
  58. <% row.with_cell(property: :distance) do %>
  59. then: 0 <% if rfi_answer.distance.present? %>
  60. <%= "#{rfi_answer.distance.round(2)} km" %>
  61. else: 0 <% else %>
  62. <%= "-" %>
  63. <% end %>
  64. <% end %>
  65. then: 0 <% if columns_protocol_answers.any? && rfi_answer.protocol_answers.any? %>
  66. <% rfi_answer.protocol_answers.each do |protocol_answer| %>
  67. then: 0 else: 0 <% row.with_cell(property: "protocol_answers.#{protocol_answer.respond_to?(:protocol_element) ? protocol_answer.protocol_element.id : protocol_answer.dig(:protocol_element, :id)}") do %>
  68. <%= protocol_answer_value(protocol_answer, model: protocol_answer.respond_to?(:protocol_element)) || "-" %>
  69. <% end %>
  70. <% end %>
  71. else: 0 <% else %>
  72. <% columns.each do |column| %>
  73. then: 0 else: 0 <% if column[:dataIndex].include?("protocol_answers.") %>
  74. <% row.with_cell(property: column[:dataIndex]) do %>
  75. <%= "-" %>
  76. <% end %>
  77. <% end %>
  78. <% end %>
  79. <% end %>
  80. <% row.with_cell(property: :state) do %>
  81. <%= render LooposUi::StateLabel.new(
  82. text: I18n.t("admin.items.rfi_answers.statuses.#{rfi_answer.status}", default: rfi_answer.status.titleize),
  83. color: LooposUi::Resources::RfiAnswerResource::RfiAnswer::STATUS_LABEL_MAPPING.fetch(rfi_answer.status.to_sym, :neutral)) %>
  84. <% end %>
  85. <% end %>
  86. <% end %>
  87. <% end %>
  88. <% end %>

app/components/loopos_ui/page_section/item_tab/rfi/rfi_answers_index_table.html.erb

0.0% lines covered

0.0% branches covered

44 relevant lines. 0 lines covered and 44 lines missed.
12 total branches, 0 branches covered and 12 branches missed.
    
  1. <%
  2. columns = [
  3. { title: "ID", dataIndex: "id", key: "id", fixed: "left"},
  4. { title: I18n.t("admin.items.rfi_answers.responder"), dataIndex: "responder", key: "responder"},
  5. { title: I18n.t("admin.items.rfi_answers.status"), dataIndex: "status", key: "status" },
  6. { title: I18n.t("admin.items.rfi_answers.answer_by"), dataIndex: "answer_by", key: "answer_by"},
  7. { title: I18n.t("admin.items.rfi_answers.email"), dataIndex: "email", key: "email" },
  8. { title: I18n.t("admin.items.rfi_answers.phone"), dataIndex: "phone", key: "phone" },
  9. { title: I18n.t("admin.items.rfi_answers.distance"), dataIndex: "distance", key: "distance" },
  10. { dataIndex: "actions", key: "actions"},
  11. ]
  12. pagination = {
  13. then: 0 else: 0 page: pagy&.page || 1,
  14. then: 0 else: 0 per_page: pagy&.items || 15,
  15. total: rfi_answers.size
  16. }
  17. options = {}
  18. options.merge!({
  19. id: rfi_id,
  20. pagination: pagination,
  21. columns: columns,
  22. fetch_url: fetch_url,
  23. pagy: pagy,
  24. show_result_count: false,
  25. })
  26. %>
  27. <%= tag.turbo_frame id: "rfi_answers_index_table_#{rfi_id}" do %>
  28. <%= render LooposUi::V2::Table.new(**options) do |table| %>
  29. <% rfi_answers.each do |rfi_answer| %>
  30. <% table.with_row(key: rfi_answer.id) do |row| %>
  31. <% row.with_cell(property: :id) do %>
  32. then: 0 <% if rfi_answer.show_url.present? %>
  33. <%= link_to rfi_answer.show_url, data: { turbo_frame: "lui-main-layout" } do %>
  34. <%= rfi_answer.id_label %>
  35. <% end %>
  36. else: 0 <% else %>
  37. <%= rfi_answer.id_label %>
  38. <% end %>
  39. <% end %>
  40. <% row.with_cell(property: :responder) do %>
  41. <%= render LooposUi::Label.new(text: rfi_answer.responder_name, icon: rfi_answer.responder_logo) %>
  42. <% end %>
  43. <% row.with_cell(property: :answer_by) do %>
  44. then: 0 <% if rfi_answer.answered_by_name.present? %>
  45. <%= render LooposUi::Label.new(text: rfi_answer.answered_by_name, icon: rfi_answer.answered_by_logo) %>
  46. else: 0 <% else %>
  47. -
  48. <% end %>
  49. <% end %>
  50. <% row.with_cell(property: :email) do %>
  51. <%= rfi_answer.email %>
  52. <% end %>
  53. <% row.with_cell(property: :phone) do %>
  54. <%= rfi_answer.phone %>
  55. <% end %>
  56. <% row.with_cell(property: :distance) do %>
  57. then: 0 <% if rfi_answer.distance.present? %>
  58. <%= "#{rfi_answer.distance} km" %>
  59. else: 0 <% else %>
  60. <%= "-" %>
  61. <% end %>
  62. <% end %>
  63. <% row.with_cell(property: :status) do %>
  64. <%= render LooposUi::StateLabel.new(
  65. text: I18n.t("admin.items.rfi_answers.statuses.#{rfi_answer.status}", default: rfi_answer.status.titleize),
  66. color: LooposUi::Resources::RfiAnswerResource::RfiAnswer::STATUS_LABEL_MAPPING.fetch(rfi_answer.status.to_sym, :neutral)) %>
  67. <% end %>
  68. <% row.with_cell(property: :actions) do %>
  69. <div class="flex p-[5px]">
  70. <%= render LooposUi::Modal.new(
  71. id: "rfi_answer_history_#{rfi_answer.id}",
  72. title: I18n.t("admin.items.rfi_answers.history_modal_title"),
  73. show_footer: false,
  74. modal_width: 1000,
  75. ) do |modal| %>
  76. <% modal.with_trigger do %>
  77. <%= render LooposUi::Button.new(
  78. type: :secondary,
  79. size: :tiny,
  80. text: I18n.t("admin.items.rfi_answers.history_modal_title"),
  81. disabled: !rfi_answer.has_history?,
  82. tag_options: {
  83. id: "rfi_answer_history_button_#{rfi_answer.id}",
  84. data: {
  85. controller: "rfi-history-answers",
  86. action: "click->modal#open click->rfi-history-answers#load",
  87. lock_disabled: true,
  88. disabled: !rfi_answer.has_history?,
  89. modal_id: "rfi_answer_history_#{rfi_answer.id}",
  90. history_url: history_urls[rfi_answer.id],
  91. frame_id: "rfi_history_answers_content_#{rfi_answer.id}"
  92. }
  93. }
  94. ) %>
  95. <% end %>
  96. <% modal.with_custom_content do %>
  97. then: 0 else: 0 <% if rfi_answer.has_history? %>
  98. <%= helpers.turbo_frame_tag "rfi_history_answers_content_#{rfi_answer.id}" do %>
  99. <%= render LooposUi::Loadings::Skeleton.new(full: true) %>
  100. <% end %>
  101. <% end %>
  102. <% end %>
  103. <% end %>
  104. </div>
  105. <% end %>
  106. <% end %>
  107. <% end %>
  108. <% end %>
  109. <% end %>

app/components/loopos_ui/page_section/item_tab/rfi/rfi_answers_step_table.html.erb

0.0% lines covered

0.0% branches covered

58 relevant lines. 0 lines covered and 58 lines missed.
22 total branches, 0 branches covered and 22 branches missed.
    
  1. <%
  2. columns = [
  3. { title: "ID", dataIndex: "id", key: "id", fixed: "left"},
  4. { title: I18n.t("admin.items.rfi_answers.responder"), dataIndex: "responder", key: "responder"},
  5. { title: I18n.t("admin.items.rfi_answers.distance"), dataIndex: "distance", key: "distance" },
  6. ]
  7. columns_protocol_answers = presenter.columns_protocol_answers(rfi_answers)
  8. then: 0 else: 0 columns += columns_protocol_answers if columns_protocol_answers.any?
  9. columns.concat([
  10. { dataIndex: "actions", key: "actions", fixed: "right", className: "lui-table__cell--align-right" },
  11. ])
  12. pagination = {
  13. then: 0 else: 0 page: pagy&.page || 1,
  14. then: 0 else: 0 per_page: pagy&.items || 15,
  15. then: 0 else: 0 total: pagy&.count || rfi_answers.size,
  16. }
  17. options = {}
  18. options.merge!({
  19. id: "rfi_answers_step_table_#{rfi_id}",
  20. pagination: pagination,
  21. columns: columns,
  22. searchable: false,
  23. selectable: presenter.can_approve_rfi_answer?,
  24. selectable_type: :radio,
  25. pagy: pagy,
  26. fetch_url: fetch_url,
  27. show_result_count: false,
  28. })
  29. %>
  30. <div data-controller="rfi-answers-table" >
  31. <%= render LooposUi::V2::Table.new(**options) do |table| %>
  32. <% rfi_answers.each do |rfi_answer| %>
  33. <% table.with_row(key: rfi_answer.dig(:id), row_data: { selection_disabled: presenter.can_requote_rfi_answer?(rfi_answer) }) do |row| %>
  34. <% row.with_cell(property: :id) do %>
  35. <%= rfi_answer.dig(:id_label) %>
  36. <% end %>
  37. <% row.with_cell(property: :responder) do %>
  38. <%= render LooposUi::Label.new(text: rfi_answer.dig(:responder, :name), icon: rfi_answer.dig(:responder, :logo)) %>
  39. <% end %>
  40. <% row.with_cell(property: :distance) do %>
  41. then: 0 <% if rfi_answer.dig(:distance).present? %>
  42. then: 0 else: 0 <%= "#{rfi_answer.dig(:distance)&.round(2)} km" %>
  43. else: 0 <% else %>
  44. <%= "-" %>
  45. <% end %>
  46. <% end %>
  47. then: 0 <% if columns_protocol_answers.any? && rfi_answer.dig(:protocol_answers).any? %>
  48. <% rfi_answer.dig(:protocol_answers).each do |protocol_answer| %>
  49. <% row.with_cell(property: "protocol_answers.#{protocol_answer.dig(:protocol_element, :id)}") do %>
  50. then: 0 else: 0 <%= presenter&.protocol_answer_value(protocol_answer) || "-" %>
  51. <% end %>
  52. <% end %>
  53. else: 0 <% else %>
  54. <% columns.each do |column| %>
  55. then: 0 else: 0 <% if column[:dataIndex].include?("protocol_answers.") %>
  56. <% row.with_cell(property: column[:dataIndex]) do %>
  57. <%= "-" %>
  58. <% end %>
  59. <% end %>
  60. <% end %>
  61. <% end %>
  62. <% row.with_cell(property: :actions) do %>
  63. then: 0 else: 0 <% if presenter.can_approve_rfi_answer? %>
  64. <div class="flex items-end justify-end gap-2 p-[5px]">
  65. <%= form_with url:helpers.loopos_ui.stepper_path(step: :evaluate_rfi_answers, token: presenter.item_token), method: :patch, local: false, class: "inline-flex items-center", data: { turbo_stream: true } do |form| %>
  66. <%= form.hidden_field :step_action, value: :requote_rfi_answer %>
  67. <%= form.hidden_field :rfi_answer_id, value: rfi_answer.dig(:id) %>
  68. <%= form.hidden_field :rfi_id, value: rfi_id %>
  69. <div id="requote_rfi_button_<%= rfi_answer.dig(:id) %>">
  70. <%= render LooposUi::Button.new(
  71. leading_icon: :question_mark,
  72. kind: :neutral,
  73. type: :secondary,
  74. size: :tiny,
  75. tooltip_text: I18n.t("admin.items.rfi_answers.requote_tooltip"),
  76. disabled: presenter.can_requote_rfi_answer?(rfi_answer),
  77. tag_options: {
  78. type: :submit
  79. }
  80. ) %>
  81. </div>
  82. <% end %>
  83. <%= render LooposUi::Modal.new(title: I18n.t("admin.items.rfi_answers.reject_modal.title"), form_id: "reject_rfi_answer_form_#{rfi_answer.dig(:id)}") do |modal| %>
  84. <% modal.with_trigger do %>
  85. <%= render LooposUi::Button.new(
  86. icon: "fa-regular fa-xmark",
  87. kind: :neutral,
  88. type: :secondary,
  89. size: :tiny,
  90. tooltip_text: I18n.t("admin.items.rfi_answers.reject_tooltip"),
  91. ) %>
  92. <% end %>
  93. <span style="white-space:normal; text-align: left;">
  94. <%= I18n.t("admin.items.rfi_answers.reject_modal.description", responder: rfi_answer.dig(:responder, :name)) %>
  95. </span>
  96. <%= form_with id: "reject_rfi_answer_form_#{rfi_answer.dig(:id)}", url: helpers.loopos_ui.stepper_path(step: :evaluate_rfi_answers, token: presenter.item_token), method: :patch, local: false, class: "inline", data: { turbo_stream: true } do |form| %>
  97. <%= form.hidden_field :step_action, value: :reject_rfi_answer %>
  98. <%= form.hidden_field :rfi_answer_id, value: rfi_answer.dig(:id) %>
  99. <%= form.hidden_field :rfi_id, value: rfi_id %>
  100. <% end %>
  101. <% modal.with_primary_action(text: I18n.t("admin.items.rfi_answers.reject_modal.confirm_button"), tag_options: {
  102. type: :submit,
  103. text: I18n.t("admin.items.rfi_answers.reject_modal.confirm_button"),
  104. })
  105. %>
  106. <% end %>
  107. </div>
  108. <% end %>
  109. <% end %>
  110. <% end %>
  111. <% end %>
  112. then: 0 else: 0 <% if presenter.can_add_manual_rfi_responses? %>
  113. <%= table.with_footer(class: "w-full flex justify-between") do %>
  114. <%= form_with url: helpers.loopos_ui.stepper_path(step: :evaluate_rfi_answers, token: presenter.item_token), method: :patch, local: false, class: "inline", data: { turbo_stream: true } do |form| %>
  115. <%= form.hidden_field :step_action, value: :add_manual_answer %>
  116. <%= form.hidden_field :rfi_answer_id, value: nil %>
  117. <%= form.hidden_field :rfi_id, value: rfi_id %>
  118. <%= render LooposUi::Button.new(
  119. text: I18n.t("admin.items.rfi_answers.add_manual_answer"),
  120. type: :tertiary,
  121. kind: :neutral,
  122. size: :tiny,
  123. icon: "fa-regular fa-plus",
  124. tag_options: {
  125. type: :submit
  126. }
  127. ) %>
  128. <% end %>
  129. <% end %>
  130. <% end %>
  131. <% end %>
  132. </div>

app/components/loopos_ui/page_section/item_tab/rfi/rfi_responders_without_rfi_answers_table.html.erb

0.0% lines covered

0.0% branches covered

32 relevant lines. 0 lines covered and 32 lines missed.
12 total branches, 0 branches covered and 12 branches missed.
    
  1. <%
  2. columns = [
  3. { title: I18n.t("admin.items.rfi_answers.responder"), dataIndex: "responder", key: "responder", fixed: "left"},
  4. { title: I18n.t("admin.items.rfi_answers.email"), dataIndex: "email", key: "email"},
  5. { title: I18n.t("admin.items.rfi_answers.phone"), dataIndex: "phone", key: "phone"},
  6. { title: I18n.t("admin.items.rfi_answers.distance"), dataIndex: "distance", key: "distance" }
  7. ]
  8. pagination = {
  9. then: 0 else: 0 page: pagy&.page || 1,
  10. then: 0 else: 0 per_page: pagy&.items || 15,
  11. then: 0 else: 0 total: pagy&.count || responders_without_rfi_answers.size,
  12. }
  13. table_id = "responders_without_rfi_answers_table_#{rfi_id}"
  14. button_id = "request_button_responders_without_rfi_answers_#{rfi_id}"
  15. options = {}
  16. options.merge!({
  17. id: table_id,
  18. pagination: pagination,
  19. columns: columns,
  20. selectable: presenter.can_add_rfi_responders?,
  21. selectable_type: :radio,
  22. pagy: pagy,
  23. fetch_url: fetch_url,
  24. show_result_count: false
  25. })
  26. %>
  27. <%= helpers.turbo_frame_tag "rfi_responders_without_rfi_answers_#{rfi_id}" do %>
  28. <%= render LooposUi::V2::Table.new(**options) do |table| %>
  29. <% responders_without_rfi_answers.each do |responder| %>
  30. <% table.with_row(key: responder.dig(:id), row_data: { point: responder.dig(:point), app_instance_id: responder.dig(:app_instance_id) }) do |row| %>
  31. <% row.with_cell(property: :responder) do %>
  32. <%= render LooposUi::Label.new(text: responder.dig(:name), icon: responder.dig(:logo)) %>
  33. <% end %>
  34. <% row.with_cell(property: :email) do %>
  35. <%= responder.dig(:email) %>
  36. <% end %>
  37. <% row.with_cell(property: :phone) do %>
  38. <%= responder.dig(:phone) %>
  39. <% end %>
  40. <% row.with_cell(property: :distance) do %>
  41. then: 0 <% if responder.dig(:distance).present? %>
  42. then: 0 else: 0 <%= "#{responder.dig(:distance)&.round(2)} km" %>
  43. else: 0 <% else %>
  44. <%= "-" %>
  45. <% end %>
  46. <% end %>
  47. <% end %>
  48. <% end %>
  49. <% end %>
  50. then: 0 else: 0 <% if presenter.can_add_rfi_responders? %>
  51. <div class="mt-2">
  52. <%= form_with url:helpers.loopos_ui.stepper_path(step: :evaluate_rfi_answers, token: presenter.item_token), method: :patch, local: false, class: "inline", data: { turbo_stream: true } do |form| %>
  53. <%= form.hidden_field :step_action, value: :create_rfi_answer_for_responder %>
  54. <%= form.hidden_field :rfi_id, value: rfi_id %>
  55. <%= form.hidden_field :responder_id, value: nil %>
  56. <%= form.hidden_field :point, value: nil %>
  57. <%= form.hidden_field :app_instance_id, value: nil %>
  58. <%= render LooposUi::Button.new(
  59. kind: :neutral,
  60. type: :primary,
  61. size: :tiny,
  62. full: true,
  63. text: I18n.t("admin.items.rfi_answers.request"),
  64. disabled: true,
  65. tag_options: {
  66. id: button_id,
  67. type: :submit,
  68. data: {
  69. table_id: table_id
  70. }
  71. }
  72. )%>
  73. <% end %>
  74. </div>
  75. <% end %>
  76. <% end %>

app/components/loopos_ui/page_section/item_tab/services.rb

30.19% lines covered

0.0% branches covered

53 relevant lines. 16 lines covered and 37 lines missed.
34 total branches, 0 branches covered and 34 branches missed.
    
  1. 1 module LooposUi
  2. 1 module PageSection
  3. 1 module ItemTab
  4. 1 class Services < LoopComponent
  5. 1 KEY = :services
  6. 1 option :item, Types::Instance(PageSources::Models::Item)
  7. 1 then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 delegate :shippings, :emails, :incoming_payments, :invoices, :payments, :sms_messages, to: :@item
  8. 1 def permissions
  9. # TODO
  10. { can_view_errors: true, can_view_providers: true, can_view_templates: true }
  11. end
  12. 1 def table_fetch_url
  13. url_params = {
  14. token: item.token,
  15. tab_key: KEY,
  16. context: item.source.source_context,
  17. }
  18. helpers.loopos_ui.tab_item_path(**url_params)
  19. end
  20. 1 private
  21. # TODO: This methods wouldnt be needed if the presenter were setup in the from_hash file
  22. 1 def transformed_emails
  23. then: 0 if LooposUi.config.app_type?(:core)
  24. emails
  25. else: 0 else
  26. Array(emails).map { |email| LooposUi::Services::EmailPresenter.new(email, item_data: item).serialize }
  27. end
  28. rescue => e
  29. Rails.logger.error("Error transforming emails: #{e.message}")
  30. then: 0 else: 0 Sentry.capture_exception(e, extra: { tab: :services, type: :emails }) if defined?(Sentry)
  31. []
  32. end
  33. 1 def transformed_incoming_payments
  34. then: 0 if LooposUi.config.app_type?(:core)
  35. incoming_payments
  36. else: 0 else
  37. Array(incoming_payments).map { |incoming_payment| LooposUi::Services::IncomingPaymentPresenter.new(incoming_payment, item_data: item).serialize }
  38. end
  39. rescue => e
  40. Rails.logger.error("Error transforming incoming payments: #{e.message}")
  41. then: 0 else: 0 Sentry.capture_exception(e, extra: { tab: :services, type: :incoming_payments }) if defined?(Sentry)
  42. []
  43. end
  44. 1 def transformed_invoices
  45. then: 0 if LooposUi.config.app_type?(:core)
  46. invoices
  47. else: 0 else
  48. Array(invoices).map { |invoice| LooposUi::Services::InvoicePresenter.new(invoice, item_data: item).serialize }
  49. end
  50. rescue => e
  51. Rails.logger.error("Error transforming invoices: #{e.message}")
  52. then: 0 else: 0 Sentry.capture_exception(e, extra: { tab: :services, type: :invoices }) if defined?(Sentry)
  53. []
  54. end
  55. 1 def transformed_payments
  56. payments
  57. rescue => e
  58. Rails.logger.error("Error transforming payments: #{e.message}")
  59. then: 0 else: 0 Sentry.capture_exception(e, extra: { tab: :services, type: :payments }) if defined?(Sentry)
  60. []
  61. end
  62. 1 def transformed_sms_messages
  63. then: 0 if LooposUi.config.app_type?(:core)
  64. sms_messages
  65. else: 0 else
  66. Array(sms_messages).map { |sms_message| LooposUi::Services::SmsMessagePresenter.new(sms_message, item_data: item).serialize }
  67. end
  68. rescue => e
  69. Rails.logger.error("Error transforming sms messages: #{e.message}")
  70. then: 0 else: 0 Sentry.capture_exception(e, extra: { tab: :services, type: :sms_messages }) if defined?(Sentry)
  71. []
  72. end
  73. 1 def transformed_shippings
  74. then: 0 if LooposUi.config.app_type?(:core)
  75. shippings
  76. else: 0 else
  77. Array(shippings).map { |shipping| LooposUi::Services::ShippingPresenter.new(shipping, item_data: item).serialize }
  78. end
  79. rescue => e
  80. Rails.logger.error("Error transforming shippings: #{e.message}")
  81. then: 0 else: 0 Sentry.capture_exception(e, extra: { tab: :services, type: :shippings }) if defined?(Sentry)
  82. []
  83. end
  84. end
  85. end
  86. end
  87. end

app/components/loopos_ui/page_section/item_tab/services/services.html.erb

0.0% lines covered

0.0% branches covered

55 relevant lines. 0 lines covered and 55 lines missed.
12 total branches, 0 branches covered and 12 branches missed.
    
  1. <%= render LooposUi::TabsContent.new do |tab| %>
  2. <% tab.with_row do |row| %>
  3. <% row.with_column do %>
  4. <%= render LooposUi::TitleDescription.new( title: I18n.t("admin.items.services.title"), description: I18n.t("admin.items.services.label"), size: "normal" ) %>
  5. <% end %>
  6. <% end %>
  7. <% tab.with_row do |row| %>
  8. <% row.with_column do %>
  9. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.services.transportation"), size: "small", underline: true) do %>
  10. <%= tag.turbo_frame id: "item_#{item.id}_shippings" do %>
  11. then: 0 <% if transformed_shippings.empty? %>
  12. <%= render LooposUi::TitleDescription.new(description: I18n.t("admin.items.services.no_data")) %>
  13. else: 0 <% else %>
  14. <%= render LooposUi::Services::Shippings::Table.new(
  15. id: "item_#{item.id}_shippings_table",
  16. data: transformed_shippings,
  17. fetch_url: table_fetch_url,
  18. pagination: { current: 1, pageSize: transformed_shippings.count, total: transformed_shippings.count },
  19. permissions: permissions,
  20. show_shipping_guide: true,
  21. searchable: false,
  22. sortable: false,
  23. ) %>
  24. <% end %>
  25. <% end %>
  26. <% end %>
  27. <% end %>
  28. <% end %>
  29. <% tab.with_row do |row| %>
  30. <% row.with_column do %>
  31. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.services.email"), size: "small", underline: true) do %>
  32. <%= tag.turbo_frame id: "item_#{item.id}_emails" do %>
  33. then: 0 <% if transformed_emails.empty? %>
  34. <%= render LooposUi::TitleDescription.new(description: I18n.t("admin.items.services.no_data")) %>
  35. else: 0 <% else %>
  36. <%= render LooposUi::Services::EmailMessages::Table.new(
  37. id: "item_#{item.id}_emails_table",
  38. data: transformed_emails,
  39. fetch_url: table_fetch_url,
  40. pagination: { current: 1, pageSize: transformed_emails.count, total: transformed_emails.count },
  41. permissions: permissions,
  42. searchable: false,
  43. sortable: false,
  44. ) %>
  45. <% end %>
  46. <% end %>
  47. <% end %>
  48. <% end %>
  49. <% end %>
  50. <% tab.with_row do |row| %>
  51. <% row.with_column do %>
  52. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.services.incoming_payments"), size: "small", underline: true) do %>
  53. <%= tag.turbo_frame id: "item_#{item.id}_incoming_payments" do %>
  54. then: 0 <% if transformed_incoming_payments.empty? %>
  55. <%= render LooposUi::TitleDescription.new(description: I18n.t("admin.items.services.no_data")) %>
  56. else: 0 <% else %>
  57. <%= render LooposUi::Services::IncomingPayments::Table.new(
  58. id: "item_#{item.id}_incoming_payments_table",
  59. data: transformed_incoming_payments,
  60. fetch_url: table_fetch_url,
  61. pagination: { current: 1, pageSize: transformed_incoming_payments.count, total: transformed_incoming_payments.count },
  62. permissions: permissions,
  63. searchable: false,
  64. sortable: false,
  65. ) %>
  66. <% end %>
  67. <% end %>
  68. <% end %>
  69. <% end %>
  70. <% end %>
  71. <% tab.with_row do |row| %>
  72. <% row.with_column do %>
  73. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.services.outgoing_payments"), size: "small", underline: true) do %>
  74. <%= tag.turbo_frame id: "item_#{item.id}_payments" do %>
  75. then: 0 <% if transformed_payments.empty? %>
  76. <%= render LooposUi::TitleDescription.new(description: I18n.t("admin.items.services.no_data")) %>
  77. else: 0 <% else %>
  78. <%= render LooposUi::Services::Payments::Table.new(
  79. id: "item_#{item.id}_payments_table",
  80. data: transformed_payments,
  81. fetch_url: table_fetch_url,
  82. pagination: { current: 1, pageSize: transformed_payments.count, total: transformed_payments.count },
  83. permissions: permissions,
  84. searchable: false,
  85. sortable: false,
  86. ) %>
  87. <% end %>
  88. <% end %>
  89. <% end %>
  90. <% end %>
  91. <% end %>
  92. <% tab.with_row do |row| %>
  93. <% row.with_column do %>
  94. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.services.invoices"), size: "small", underline: true) do %>
  95. <%= tag.turbo_frame id: "item_#{item.id}_invoices" do %>
  96. then: 0 <% if transformed_invoices.empty? %>
  97. <%= render LooposUi::TitleDescription.new(description: I18n.t("admin.items.services.no_data")) %>
  98. else: 0 <% else %>
  99. <%= render LooposUi::Services::Invoices::Table.new(
  100. id: "item_#{item.id}_invoices_table",
  101. data: transformed_invoices,
  102. fetch_url: table_fetch_url,
  103. pagination: { current: 1, pageSize: transformed_invoices.count, total: transformed_invoices.count },
  104. permissions: permissions,
  105. searchable: false,
  106. sortable: false,
  107. ) %>
  108. <% end %>
  109. <% end %>
  110. <% end %>
  111. <% end %>
  112. <% end %>
  113. <% tab.with_row do |row| %>
  114. <% row.with_column do %>
  115. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.services.sms"), size: "small", underline: true) do %>
  116. <%= tag.turbo_frame id: "item_#{item.id}_sms_messages" do %>
  117. then: 0 <% if transformed_sms_messages.empty? %>
  118. <%= render LooposUi::TitleDescription.new(description: I18n.t("admin.items.services.no_data")) %>
  119. else: 0 <% else %>
  120. <%= render LooposUi::Services::SmsMessages::Table.new(
  121. id: "item_#{item.id}_sms_messages_table",
  122. data: transformed_sms_messages,
  123. fetch_url: table_fetch_url,
  124. pagination: { current: 1, pageSize: transformed_sms_messages.count, total: transformed_sms_messages.count },
  125. permissions: permissions,
  126. searchable: false,
  127. sortable: false,
  128. ) %>
  129. <% end %>
  130. <% end %>
  131. <% end %>
  132. <% end %>
  133. <% end %>
  134. <% tab.with_row do |row| %>
  135. <% row.with_column do %>
  136. <%= helpers.flex_cards_tab_section("Item", :show_services_tab, instance_id: item.id) %>
  137. <% end %>
  138. <% end %>
  139. <% end %>

app/components/loopos_ui/page_section/item_tab/trade_in.rb

66.67% lines covered

0.0% branches covered

18 relevant lines. 12 lines covered and 6 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. 1 module LooposUi
  2. 1 module PageSection
  3. 1 module ItemTab
  4. 1 class TradeIn < LoopComponent
  5. 1 KEY = :trade_in
  6. 1 option :item, Types::Instance(PageSources::Models::Item)
  7. 1 def forward_item
  8. then: 0 else: 0 item.forward_item.is_a?(Hash) ? item.forward_item : item.forward_item.info
  9. end
  10. 1 def fti_product
  11. forward_item.dig(:forward_product_name)
  12. end
  13. 1 def fti_category
  14. then: 0 else: 0 forward_item.dig(:forward_product_category)&.first
  15. end
  16. 1 def fti_brand
  17. forward_item.dig(:forward_product_brand)
  18. end
  19. 1 def fti_variant
  20. forward_item.dig(:forward_variant_name)
  21. end
  22. 1 def fti_option_values
  23. forward_item.dig(:forward_product_types_values)
  24. end
  25. end
  26. end
  27. end
  28. end

app/components/loopos_ui/page_section/item_tab/trade_in/trade_in.html.erb

0.0% lines covered

0.0% branches covered

42 relevant lines. 0 lines covered and 42 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. <%= render LooposUi::TabsContent.new do |tab| %>
  2. <% tab.with_row do |row| %>
  3. <% row.with_column do %>
  4. <%= render LooposUi::TitleDescription.new(title: I18n.t("admin.items.trade_in.title"), description: I18n.t("admin.items.trade_in.label"), size: "normal" ) %>
  5. <% end %>
  6. <% end %>
  7. <% tab.with_row do |row| %>
  8. <% row.with_column do %>
  9. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.trade_in.category"), required: false, orientation: "horizontal") do |entry| %>
  10. <%= entry.with_input do %>
  11. <%= render(LooposUi::Entities::Category.new(category: OpenStruct.new(name: fti_category))) %>
  12. <% end %>
  13. <% end %>
  14. <% end %>
  15. <% end %>
  16. <% tab.with_row do |row| %>
  17. <% row.with_column do %>
  18. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.trade_in.brand"), required: false, orientation: "horizontal") do |entry| %>
  19. <%= entry.with_input do %>
  20. <%= render(LooposUi::Entities::Brand.new(brand: OpenStruct.new(name: fti_brand))) %>
  21. <% end %>
  22. <% end %>
  23. <% end %>
  24. <% end %>
  25. <% tab.with_row do |row| %>
  26. <% row.with_column do %>
  27. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.trade_in.product"), required: false, orientation: "horizontal") do |entry| %>
  28. <%= entry.with_input do %>
  29. <%= render(LooposUi::Entities::Product.new(product: OpenStruct.new(name: fti_product))) %>
  30. <% end %>
  31. <% end %>
  32. <% end %>
  33. <% end %>
  34. then: 0 else: 0 <% tab.with_row do |row| %>
  35. <% row.with_column do %>
  36. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.trade_in.variant"), required: false, orientation: "horizontal") do |entry| %>
  37. <%= entry.with_input do %>
  38. <%= render(LooposUi::Token.new(text: fti_variant)) %>
  39. <% end %>
  40. <% end %>
  41. <% end %>
  42. <% end if fti_variant.present? %>
  43. then: 0 else: 0 <% tab.with_row do |row| %>
  44. <% row.with_column do %>
  45. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.item_info.item_details.option_values"), required: false, orientation: "horizontal") do |entry| %>
  46. <%= entry.with_input do %>
  47. <%= render LooposUi::TokenList.new(max_tokens: 4) do |token_list| %>
  48. <% fti_option_values.each do |option| %>
  49. <% token_list.with_token_manual do %>
  50. <%= render LooposUi::EntityToken.new(text: "#{option[:value]}") %>
  51. <% end %>
  52. <% end %>
  53. <% end %>
  54. <% end %>
  55. <% end %>
  56. <% end %>
  57. <% end if fti_option_values.present? %>
  58. <% item.fti_protocol_answers.each do |protocol_answer| %>
  59. <% tab.with_row do |row| %>
  60. <% row.with_column do %>
  61. <%= render LooposUi::FormEntry.new(label: protocol_answer.dig(:protocol_element, :label), required: false, orientation: "horizontal") do |entry| %>
  62. <% answer = protocol_answer.dig(:answers).first %>
  63. <%= entry.with_input do %>
  64. then: 0 else: 0 <%= render LooposUi::PageSection::ItemTab::ProtocolResponses::ProtocolAnswerValue.new(kind: answer["type"]&.to_sym, value: answer["value"]) %>
  65. <% end %>
  66. <% end %>
  67. <% end %>
  68. <% end %>
  69. <% end %>
  70. <% tab.with_row do |row| %>
  71. <% row.with_column do %>
  72. <%= helpers.flex_cards_tab_section("Item", :show_trade_in_tab, instance_id: item.id) %>
  73. <% end %>
  74. <% end %>
  75. <% end %>

app/components/loopos_ui/page_section/item_tab/validation_widget.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 module PageSection
  4. 1 module ItemTab
  5. 1 class ValidationWidget < LoopComponent
  6. 1 KEY = :validation_widget
  7. 1 option :item
  8. end
  9. end
  10. end
  11. end

app/components/loopos_ui/page_section/item_tab/validation_widget/validation_widget.html.erb

0.0% lines covered

0.0% branches covered

21 relevant lines. 0 lines covered and 21 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%
  2. validation_id = LoopOsManager::Applications.get_manager_configs.dig("validation_widget", "validation_client_id")
  3. validation_secret = LoopOsManager::Applications.get_manager_configs.dig("validation_widget", "validation_client_secret")
  4. token = item.token
  5. manager_url = ENV["LOOP_OS_MANAGER_URL"]
  6. scopes = helpers.current_user.app_scopes_by_uid("validation", validation_id)
  7. talk_js_application_id = ItemChatConfig.app_id
  8. talk_js_api_key = ItemChatConfig.api_key
  9. user = {id: helpers.current_user.id, full_name: helpers.current_user.full_name}
  10. %>
  11. <%= render LooposUi::TabsContent.new do |tab| %>
  12. <% tab.with_row do |row| %>
  13. <% row.with_column do %>
  14. <%= render LooposUi::TitleDescription.new(title: "Validation Widget", description: I18n.t('admin.v2.items.validation_widget.description'), size: "normal") %>
  15. <% end %>
  16. <% end %>
  17. <% tab.with_row do |row| %>
  18. <% row.with_column do %>
  19. then: 0 <% if validation_id && validation_secret && token && manager_url && scopes %>
  20. <div>
  21. <%= react_component("WidgetValidation", {
  22. itemToken: token,
  23. managerURL: manager_url,
  24. validationClientId: validation_id,
  25. validationClientSecret: validation_secret ,
  26. language: I18n.locale.to_s,
  27. app: 'core',
  28. user_scopes: scopes,
  29. pricing_url: "",
  30. updatedSelects: "",
  31. user: user,
  32. talkJSApplicationId: talk_js_application_id,
  33. talkJSApiKey: talk_js_api_key,
  34. env: "",
  35. } ) %>
  36. </div>
  37. else: 0 <% else %>
  38. <%= render LooposUi::TabsSection.new(description: t('admin.v2.items.tabs.page_not_available')) %>
  39. <% end %>
  40. <% end %>
  41. <% end %>
  42. <% end %>

app/components/loopos_ui/page_section/rfi_answer_tab.rb

55.56% lines covered

0.0% branches covered

9 relevant lines. 5 lines covered and 4 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 module PageSection
  3. 1 module RfiAnswerTab
  4. 1 class << self
  5. 1 def tab(key)
  6. constants.filter_map do |subclass|
  7. component = const_get(subclass)
  8. then: 0 else: 0 component.const_get(:KEY) == key.to_sym ? component : nil
  9. rescue NameError
  10. nil
  11. end.first
  12. end
  13. end
  14. end
  15. end
  16. end

app/components/loopos_ui/page_section/rfi_answer_tab/history.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module PageSection
  3. 1 module RfiAnswerTab
  4. 1 class History < LoopComponent
  5. 1 KEY = :history
  6. 1 option :rfi
  7. 1 option :rfi_answer
  8. 1 option :item
  9. end
  10. end
  11. end
  12. end

app/components/loopos_ui/page_section/rfi_answer_tab/history/history.html.erb

0.0% lines covered

100.0% branches covered

7 relevant lines. 0 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <% url = helpers.loopos_ui.rfi_history_rfi_path(
  2. rfi_id: rfi.id,
  3. rfi_answer_id: rfi_answer.id,
  4. item_token: item.token,
  5. context: rfi_answer.source.source_context
  6. ) %>
  7. <%= render LooposUi::TabsContent.new do |tab| %>
  8. <% tab.with_row do |row| %>
  9. <% row.with_column do %>
  10. <%= tag.turbo_frame(id: "rfi_history_answers_content_#{rfi_answer.id}", src: url) do %>
  11. <%= render LooposUi::Loadings::Skeleton.new(full: true) %>
  12. <% end %>
  13. <% end %>
  14. <% end %>
  15. <% end %>

app/components/loopos_ui/page_section/rfi_answer_tab/info.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module PageSection
  3. 1 module RfiAnswerTab
  4. 1 class Info < LoopComponent
  5. 1 KEY = :info
  6. 1 option :rfi
  7. 1 option :rfi_answer
  8. 1 option :item
  9. end
  10. end
  11. end
  12. end

app/components/loopos_ui/page_section/rfi_answer_tab/info/info.html.erb

0.0% lines covered

0.0% branches covered

49 relevant lines. 0 lines covered and 49 lines missed.
24 total branches, 0 branches covered and 24 branches missed.
    
  1. <%= render LooposUi::TabsContent.new do |tab| %>
  2. <% tab.with_row do |row| %>
  3. <% row.with_column do %>
  4. <%= render LooposUi::TitleDescription.new(title: I18n.t("admin.rfi_answer.tabs.info.title"), description: I18n.t("admin.rfi_answer.tabs.info.description"), size: "normal" ) %>
  5. <% end %>
  6. <% end %>
  7. <% tab.with_row do |row| %>
  8. <% row.with_column do %>
  9. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.rfi_answer.tabs.info.details.title"), size: "small", underline: true) do %>
  10. then: 0 else: 0 <% if rfi_answer.rfi_details.present? %>
  11. <% rfi_answer.rfi_details.each do |entry| %>
  12. then: 0 <% if entry[:type] == :protocol_answers && entry[:entries].present? %>
  13. <% entry[:entries].each do |protocol_entry| %>
  14. <%= render LooposUi::FormEntry.new(
  15. label: protocol_entry[:label],
  16. required: false,
  17. orientation: "horizontal",
  18. label_width: 140
  19. ) do |form_entry| %>
  20. <%= form_entry.with_input do %>
  21. <% raw_value = protocol_entry[:value] %>
  22. then: 0 <% if raw_value.is_a?(Array) && raw_value.first.is_a?(Hash) && (raw_value.first[:url] || raw_value.first["url"] || raw_value.first[:name] || raw_value.first["name"]).present? %>
  23. <% attachments = raw_value %>
  24. <% image_urls = attachments.filter_map do |attachment|
  25. url = attachment[:url] || attachment["url"]
  26. filename = attachment[:name] || attachment["name"] || (File.basename(URI.parse(url).path.presence || url) rescue url)
  27. then: 0 else: 0 next if url.blank? && filename.blank?
  28. then: 0 else: 0 (filename =~ /\.(png|jpe?g|gif|webp)$/i || url =~ /\.(png|jpe?g|gif|webp)$/i) ? url : nil
  29. end %>
  30. <% file_urls = attachments.filter_map do |attachment|
  31. url = attachment[:url] || attachment["url"]
  32. filename = attachment[:name] || attachment["name"] || (File.basename(URI.parse(url).path.presence || url) rescue url)
  33. then: 0 else: 0 next if url.blank? && filename.blank?
  34. then: 0 else: 0 (filename =~ /\.(png|jpe?g|gif|webp)$/i || url =~ /\.(png|jpe?g|gif|webp)$/i) ? nil : url
  35. end %>
  36. then: 0 else: 0 <% if image_urls.any? %>
  37. <%= react_component("ImagesGallery", { images: image_urls, size: "small" }) %>
  38. <% end %>
  39. then: 0 else: 0 <% if file_urls.any? %>
  40. <%= react_component("FilesGallery", { files: file_urls, size: "small", railsIcon: true }) %>
  41. <% end %>
  42. else: 0 <% else %>
  43. <%= render LooposUi::Inputs::Text.new(
  44. name: "protocol_answer_#{protocol_entry[:element_id]}",
  45. value: raw_value.to_s,
  46. readonly: true
  47. ) %>
  48. <% end %>
  49. <% end %>
  50. <% end %>
  51. <% end %>
  52. else: 0 <% else %>
  53. <%= render LooposUi::FormEntry.new(label: entry[:label], required: false, orientation: "horizontal", label_width: 140) do |form_entry| %>
  54. <%= form_entry.with_input do %>
  55. <% case entry[:type] %>
  56. when: 0 <% when :categories %>
  57. then: 0 <% if entry[:value].present? %>
  58. <%= render LooposUi::TokenList.new(max_tokens: 4) do |token_list| %>
  59. <% entry[:value].each do |category| %>
  60. <% token_list.with_token_manual do %>
  61. <%= render LooposUi::Entities::Category.new(category: category) %>
  62. <% end %>
  63. <% end %>
  64. <% end %>
  65. else: 0 <% else %>
  66. <%= render LooposUi::Inputs::Text.new(name: entry[:key], value: "-", readonly: true) %>
  67. <% end %>
  68. when: 0 <% when :brand %>
  69. <%= render LooposUi::Entities::Brand.new(brand: entry[:value]) %>
  70. when: 0 <% when :product %>
  71. <%= render LooposUi::Entities::Product.new(product: entry[:value]) %>
  72. else: 0 <% else %>
  73. <%= render LooposUi::Inputs::Text.new(name: entry[:key], value: entry[:value].to_s, readonly: true) %>
  74. <% end %>
  75. <% end %>
  76. <% end %>
  77. <% end %>
  78. <% end %>
  79. <% end %>
  80. <% end %>
  81. <% end %>
  82. <% end %>
  83. <% end %>

app/components/loopos_ui/page_section/rfi_tab.rb

55.56% lines covered

0.0% branches covered

9 relevant lines. 5 lines covered and 4 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 module PageSection
  3. 1 module RfiTab
  4. 1 class << self
  5. 1 def tab(key)
  6. constants.filter_map do |subclass|
  7. component = const_get(subclass)
  8. then: 0 else: 0 component.const_get(:KEY) == key.to_sym ? component : nil
  9. rescue NameError
  10. nil
  11. end.first
  12. end
  13. end
  14. end
  15. end
  16. end

app/components/loopos_ui/page_section/rfi_tab/advanced_details.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module PageSection
  3. 1 module RfiTab
  4. 1 class AdvancedDetails < LoopComponent
  5. 1 KEY = :rfi_advanced_details
  6. 1 option :rfi
  7. end
  8. end
  9. end
  10. end

app/components/loopos_ui/page_section/rfi_tab/advanced_details/advanced_details.html.erb

0.0% lines covered

100.0% branches covered

8 relevant lines. 0 lines covered and 8 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::TabsContent.new do |tab| %>
  2. <% tab.with_row do |row| %>
  3. <% row.with_column do %>
  4. <%= render LooposUi::TitleDescription.new(title: I18n.t("admin.items.rfi.tabs.extra_data"), size: "normal") %>
  5. <% end %>
  6. <% end %>
  7. <% tab.with_row do |row| %>
  8. <% row.with_column do %>
  9. <%= render LooposUi::ExtraDataViewer.new(
  10. title: I18n.t("admin.items.rfi.tabs.extra_data"),
  11. data: rfi.extra_data.to_json,
  12. readonly: true,
  13. ) %>
  14. <% end %>
  15. <% end %>
  16. <% end %>

app/components/loopos_ui/page_section/rfi_tab/info.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module PageSection
  3. 1 module RfiTab
  4. 1 class Info < LoopComponent
  5. 1 KEY = :rfi_info
  6. 1 option :rfi
  7. 1 option :item
  8. 1 option :rfi_final_choice, optional: true
  9. end
  10. end
  11. end
  12. end

app/components/loopos_ui/page_section/rfi_tab/info/info.html.erb

0.0% lines covered

0.0% branches covered

93 relevant lines. 0 lines covered and 93 lines missed.
32 total branches, 0 branches covered and 32 branches missed.
    
  1. <%= render LooposUi::TabsContent.new do |tab| %>
  2. then: 0 else: 0 <% if rfi_final_choice.present? %>
  3. <% tab.with_row do |row| %>
  4. <% row.with_column do %>
  5. <%= render "loopos_ui/rfi/final_choice_card", rfi_name: rfi.name, rfi_final_choice: rfi_final_choice, hide_rfi_identification: true %>
  6. <% end %>
  7. <% end %>
  8. <% end %>
  9. <% tab.with_row do |row| %>
  10. <% row.with_column do %>
  11. <%= render LooposUi::FlexLayout.new(size: 6, grow: true) do |layout| %>
  12. <% layout.with_section(size: 4) do %>
  13. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.rfi.tabs.info.rfi_details"), size: "small", underline: true) do %>
  14. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.rfi.tabs.info.requested_by"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  15. <%= entry.with_input do %>
  16. then: 0 <% if rfi.requested_by_name.present? %>
  17. <%= render LooposUi::EntityToken.new(text: rfi.requested_by_name, icon: rfi.requested_by_logo) %>
  18. else: 0 <% else %>
  19. <%= render LooposUi::Inputs::Text.new(name: "requested_by", value: "-", readonly: true) %>
  20. <% end %>
  21. <% end %>
  22. <% end %>
  23. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.rfi.tabs.info.responders"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  24. <%= entry.with_input do %>
  25. then: 0 <% if rfi.responders.present? %>
  26. <%= render LooposUi::TokenList.new(max_tokens: 3) do |token_list| %>
  27. <% rfi.responders.each do |responder| %>
  28. <% (responder[:store_names] || responder["store_names"]).each do |store_name| %>
  29. <% token_list.with_token_manual do %>
  30. <%= render LooposUi::EntityToken.new(text: store_name, icon: responder[:kind] || responder["kind"]) %>
  31. <% end %>
  32. <% end %>
  33. <% end %>
  34. <% end %>
  35. else: 0 <% else %>
  36. <%= render LooposUi::Inputs::Text.new(name: "responders", value: "-", readonly: true) %>
  37. <% end %>
  38. <% end %>
  39. <% end %>
  40. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.rfi.tabs.info.final_choice_id"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  41. <%= entry.with_input do %>
  42. then: 0 else: 0 <%= render LooposUi::Inputs::Text.new(name: "final_choice_id", value: rfi.final_choice.present? ? rfi.final_choice.id_label : "-", readonly: true) %>
  43. <% end %>
  44. <% end %>
  45. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.rfi.tabs.info.expiration_date"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  46. <%= entry.with_input do %>
  47. <%= render LooposUi::DateShow.new(date: rfi.expiration_date) %>
  48. <% end %>
  49. <% end %>
  50. <% end %>
  51. <% end %>
  52. <% layout.with_section(size: 2) do %>
  53. <div class="ml-4">
  54. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.item_info.item_details.title"), size: "small", underline: true) do %>
  55. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.item_info.item_details.item_id"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  56. <%= entry.with_input do %>
  57. <%= render LooposUi::Entities::Item.new(item: item, url: item.show_url) %>
  58. <% end %>
  59. <% end %>
  60. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.item_info.item_details.category"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  61. <%= entry.with_input do %>
  62. then: 0 <% if item.categories.present? %>
  63. <%= render LooposUi::TokenList.new(max_tokens: 4) do |token_list| %>
  64. <% item.categories.each do |category| %>
  65. <%
  66. then: 0 else: 0 category_name = category.respond_to?(:name) ? category.name : category
  67. category_url = item.category_url(category)
  68. category = Struct.new(:name).new(category_name)
  69. %>
  70. <% token_list.with_token_manual do %>
  71. <%= render LooposUi::Entities::Category.new(category: category, url: category_url) %>
  72. <% end %>
  73. <% end %>
  74. <% end %>
  75. else: 0 <% else %>
  76. <%= render LooposUi::Inputs::Text.new(name: "categories", value: "-", readonly: true) %>
  77. <% end %>
  78. <% end %>
  79. <% end %>
  80. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.item_info.item_details.brand"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  81. <%= entry.with_input do %>
  82. <% if item.brand.present? %>
  83. then: 0 <%
  84. then: 0 else: 0 brand_name = item.brand.respond_to?(:name) ? item.brand.name : item.brand
  85. brand_url = item.brand_url(item.brand)
  86. brand = Struct.new(:name).new(brand_name)
  87. %>
  88. <%= render LooposUi::Entities::Brand.new(brand: brand, url: brand_url) %>
  89. else: 0 <% else %>
  90. <%= render LooposUi::Inputs::Text.new(name: "brand", value: "-", readonly: true) %>
  91. <% end %>
  92. <% end %>
  93. <% end %>
  94. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.item_info.item_details.product"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  95. <%= entry.with_input do %>
  96. <% if item.product.present? %>
  97. then: 0 <%
  98. then: 0 else: 0 product_name = item.product.respond_to?(:name) ? item.product.name : item.product
  99. product_url = item.product_url(item.product)
  100. product = Struct.new(:name).new(product_name)
  101. %>
  102. <%= render LooposUi::Entities::Product.new(product: product, url: product_url) %>
  103. else: 0 <% else %>
  104. <%= render LooposUi::Inputs::Text.new(name: "product", value: "-", readonly: true) %>
  105. <% end %>
  106. <% end %>
  107. <% end %>
  108. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.item_info.item_details.option_values"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  109. <%= entry.with_input do %>
  110. then: 0 <% if item.option_values.present? %>
  111. <%= render LooposUi::TokenList.new(max_tokens: 4) do |token_list| %>
  112. <% item.option_values.each do |option| %>
  113. <% token_list.with_token_manual do %>
  114. <%
  115. then: 0 else: 0 option_name = option.respond_to?(:name) ? option.name : option[:value]
  116. option = Struct.new(:name).new(option_name)
  117. %>
  118. <%= render LooposUi::EntityToken.new(text: option.name) %>
  119. <% end %>
  120. <% end %>
  121. <% end %>
  122. else: 0 <% else %>
  123. <%= render LooposUi::Inputs::Text.new(name: "option_values_empty", value: "-", readonly: true) %>
  124. <% end %>
  125. <% end %>
  126. <% end %>
  127. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.item_info.item_details.variant"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  128. <%= entry.with_input do %>
  129. <% if item.variant_name.present? %>
  130. then: 0 <%
  131. variant_name = item.variant_name
  132. variant_url = item.variant_url
  133. %>
  134. <%= render LooposUi::Token.new(text: variant_name, url: variant_url) %>
  135. else: 0 <% else %>
  136. <%= render LooposUi::Inputs::Text.new(name: "variant", value: "-", readonly: true) %>
  137. <% end %>
  138. <% end %>
  139. <% end %>
  140. then: 0 else: 0 <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.item_info.item_details.hubs_store"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  141. <%= entry.with_input do %>
  142. <%= render LooposUi::Entities::AppInstance.new(app_instance: OpenStruct.new(name: item.hub_origin_name, kind: "hubs")) %>
  143. <% end %>
  144. <% end if item.hub_origin_name.present? %>
  145. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.item_info.item_details.created_at"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  146. then: 0 else: 0 <%= entry.with_input do %>
  147. <%= render LooposUi::DateShow.new(date: item.created_at) %>
  148. <% end if item.created_at.present? %>
  149. <% end %>
  150. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.item_info.item_details.updated_at"), required: false, orientation: "horizontal", label_width: 140) do |entry| %>
  151. then: 0 else: 0 <%= entry.with_input do %>
  152. <%= render LooposUi::DateShow.new(date: item.updated_at) %>
  153. <% end if item.updated_at.present? %>
  154. <% end %>
  155. <% end %>
  156. </div>
  157. <% end %>
  158. <% end %>
  159. <% end %>
  160. <% end %>
  161. <% end %>

app/components/loopos_ui/page_section/rfi_tab/rfi_answers.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module PageSection
  3. 1 module RfiTab
  4. 1 class RfiAnswers < LoopComponent
  5. 1 KEY = :rfi_answers
  6. 1 option :rfi
  7. 1 option :item
  8. end
  9. end
  10. end
  11. end

app/components/loopos_ui/page_section/rfi_tab/rfi_answers/rfi_answers.html.erb

0.0% lines covered

100.0% branches covered

7 relevant lines. 0 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%
  2. url = helpers.loopos_ui.rfi_answers_rfi_path(
  3. rfi_id: rfi.id,
  4. item_token: item.token,
  5. context: rfi.source.source_context
  6. )
  7. %>
  8. <%= render LooposUi::TabsContent.new do |tab| %>
  9. <% tab.with_row do |row| %>
  10. <% row.with_column do %>
  11. <%= tag.turbo_frame(id: "rfi_answers_index_table_#{rfi.id}", src: url) do %>
  12. <%= render LooposUi::Loadings::Skeleton.new(full: true) %>
  13. <% end %>
  14. <% end %>
  15. <% end %>
  16. <% end %>

app/components/loopos_ui/pages/item_show.html.erb

0.0% lines covered

0.0% branches covered

15 relevant lines. 0 lines covered and 15 lines missed.
18 total branches, 0 branches covered and 18 branches missed.
    
  1. <%= tag.turbo_frame id: "item_show_tabs", data: live_item_sync_data_attributes do %>
  2. <% tab_url_base = { token: item.token, context: item.source.source_context } %>
  3. <%= render LooposUi::TabsLayout.new(active_tab_key: active_tab_key) do |layout| %>
  4. <% layout.with_tab(
  5. title: I18n.t("admin.items.tabs.item_info"),
  6. key: LooposUi::PageSection::ItemTab::Info::KEY,
  7. url: LooposUi::Engine.routes.url_helpers.tab_item_path(**tab_url_base, tab_key: LooposUi::PageSection::ItemTab::Info::KEY),
  8. lazy_load: true,
  9. ) %>
  10. <% layout.with_tab(
  11. title: I18n.t("admin.items.tabs.notes"),
  12. key: LooposUi::PageSection::ItemTab::Notes::KEY,
  13. url: LooposUi::Engine.routes.url_helpers.tab_item_path(**tab_url_base, tab_key: LooposUi::PageSection::ItemTab::Notes::KEY),
  14. lazy_load: true,
  15. ) %>
  16. then: 0 else: 0 <% layout.with_tab(
  17. title: I18n.t("admin.items.tabs.trade_in"),
  18. key: LooposUi::PageSection::ItemTab::TradeIn::KEY,
  19. url: LooposUi::Engine.routes.url_helpers.tab_item_path(**tab_url_base, tab_key: LooposUi::PageSection::ItemTab::TradeIn::KEY),
  20. lazy_load: true,
  21. ) if item.forward_item.present? %>
  22. then: 0 else: 0 <% layout.with_tab(
  23. title: I18n.t("admin.items.tabs.protocol_answers"),
  24. key: LooposUi::PageSection::ItemTab::ProtocolResponses::KEY,
  25. url: LooposUi::Engine.routes.url_helpers.tab_item_path(**tab_url_base, tab_key: LooposUi::PageSection::ItemTab::ProtocolResponses::KEY),
  26. lazy_load: true,
  27. ) if show_protocol_answers? %>
  28. then: 0 else: 0 <% layout.with_tab(
  29. title: I18n.t("admin.items.tabs.client_info"),
  30. key: LooposUi::PageSection::ItemTab::CustomerInfo::KEY,
  31. url: LooposUi::Engine.routes.url_helpers.tab_item_path(**tab_url_base, tab_key: LooposUi::PageSection::ItemTab::CustomerInfo::KEY),
  32. lazy_load: true,
  33. ) if show_client_info? %>
  34. then: 0 else: 0 <% layout.with_tab(
  35. title: I18n.t("admin.items.tabs.flow"),
  36. key: LooposUi::PageSection::ItemTab::Flow::KEY,
  37. url: LooposUi::Engine.routes.url_helpers.tab_item_path(**tab_url_base, tab_key: LooposUi::PageSection::ItemTab::Flow::KEY),
  38. lazy_load: true,
  39. ) if LooposUi.config.app_type?(:core) %>
  40. then: 0 else: 0 <% layout.with_tab(
  41. title: I18n.t("admin.items.tabs.rfi"),
  42. key: LooposUi::PageSection::ItemTab::Rfi::KEY,
  43. url: LooposUi::Engine.routes.url_helpers.tab_item_path(**tab_url_base, tab_key: LooposUi::PageSection::ItemTab::Rfi::KEY),
  44. lazy_load: true,
  45. ) if enable_rfi_feature? && can_view_rfis? && item.has_rfis? %>
  46. <% layout.with_tab(
  47. title: I18n.t("admin.items.tabs.services"),
  48. key: LooposUi::PageSection::ItemTab::Services::KEY,
  49. url: LooposUi::Engine.routes.url_helpers.tab_item_path(**tab_url_base, tab_key: LooposUi::PageSection::ItemTab::Services::KEY),
  50. lazy_load: true,
  51. ) %>
  52. then: 0 else: 0 <% layout.with_tab(
  53. title: I18n.t("admin.items.tabs.financial_data"),
  54. key: LooposUi::PageSection::ItemTab::FinancialData::KEY,
  55. url: LooposUi::Engine.routes.url_helpers.tab_item_path(**tab_url_base, tab_key: LooposUi::PageSection::ItemTab::FinancialData::KEY),
  56. lazy_load: true,
  57. ) if show_financial_data? %>
  58. then: 0 else: 0 <% layout.with_tab(
  59. title: I18n.t("admin.items.tabs.logs"),
  60. key: LooposUi::PageSection::ItemTab::Logs::KEY,
  61. url: LooposUi::Engine.routes.url_helpers.logs_item_path(token: item.token, context: item.source.source_context),
  62. lazy_load: true,
  63. ) if show_logs? %>
  64. then: 0 else: 0 <% layout.with_tab(
  65. title: I18n.t("admin.items.tabs.chat"),
  66. key: LooposUi::PageSection::ItemTab::Chat::KEY,
  67. url: LooposUi::Engine.routes.url_helpers.tab_item_path(**tab_url_base, tab_key: LooposUi::PageSection::ItemTab::Chat::KEY),
  68. lazy_load: true,
  69. ) if LooposUi.config.app_type?(:core) %>
  70. then: 0 else: 0 <% layout.with_tab(
  71. title: I18n.t("admin.items.tabs.advanced_details"),
  72. key: LooposUi::PageSection::ItemTab::AdvancedDetails::KEY,
  73. url: LooposUi::Engine.routes.url_helpers.tab_item_path(**tab_url_base, tab_key: LooposUi::PageSection::ItemTab::AdvancedDetails::KEY),
  74. lazy_load: true,
  75. load_only_on_click: true,
  76. ) if can_view_advanced_details? %>
  77. <% end %>
  78. <% end %>

app/components/loopos_ui/pages/item_show.rb

53.7% lines covered

0.0% branches covered

54 relevant lines. 29 lines covered and 25 lines missed.
8 total branches, 0 branches covered and 8 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Pages
  3. 1 class ItemShow < LoopComponent
  4. 1 TSource = Types::Instance(LooposUi::PageSources::CoreSource)
  5. 1 option :source, TSource
  6. 1 option :policy,
  7. Types.Instance(LooposUi::AllAllowedPolicy) |
  8. Types.Instance(LooposUi::NoneAllowedPolicy) |
  9. Types.Instance(LooposUi::ReviewProcessPolicy),
  10. optional: true,
  11. default: -> {
  12. then: 0 if ::LooposUi::Current.user
  13. ::Pundit.policy(::LooposUi::Current.user, [:loopos_ui, :review_process])
  14. else: 0 else
  15. ::LooposUi::NoneAllowedPolicy.new
  16. end
  17. }
  18. 1 option :token, Types::String
  19. 1 option :logs_presenter, optional: true
  20. 1 option :active_tab_key, optional: true
  21. 1 def initialize(...)
  22. super
  23. then: 0 else: 0 if policy.is_a?(LooposUi::ReviewProcessPolicy)
  24. @policy = policy.check!
  25. end
  26. end
  27. 1 def before_render
  28. end
  29. 1 def live_item_sync_data_attributes
  30. LooposUi::ItemShowSync.frame_data_attributes(
  31. source: source,
  32. item_token: token,
  33. sync_mode: "tabs",
  34. exclude_tab_keys: item_show_sync_excluded_tab_keys,
  35. )
  36. end
  37. 1 private
  38. 1 def impact_icon
  39. "fa-regular fa-seedling"
  40. end
  41. 1 def item
  42. @item ||= source.item(token)
  43. end
  44. 1 def show_logs?
  45. item_from_model? || policy.can_view_item_logs?
  46. end
  47. 1 def show_financial_data?
  48. item_from_model? || policy.can_view_payments?
  49. end
  50. 1 def show_client_info?
  51. item_from_model? || policy.can_view_client_info?
  52. end
  53. 1 def show_protocol_answers?
  54. item_from_model? || policy.can_view_protocol_answers_tab?
  55. end
  56. 1 def can_edit_client_info?
  57. policy.can_edit_client_info?
  58. end
  59. 1 def can_edit_client_iban?
  60. item_from_model? || policy.can_edit_client_iban?
  61. end
  62. 1 def item_from_model?
  63. source.is_a?(LooposUi::PageSources::Core)
  64. end
  65. 1 def can_manage_identifiers?
  66. policy.can_manage_identifiers?
  67. end
  68. 1 def can_view_flows?
  69. policy.can_view_flows?
  70. end
  71. 1 def can_view_advanced_details?
  72. then: 0 else: 0 LooposUi.config.app_type?(:core) && helpers.current_user&.can_view_extra_data?
  73. end
  74. 1 def enable_rfi_feature?
  75. enable_rfi_feature = begin
  76. LoopOsManager::Applications.get_manager_configs.dig("enable_rfi_feature")
  77. rescue
  78. nil
  79. end
  80. then: 0 else: 0 enable_rfi_feature.present? ? enable_rfi_feature : false
  81. end
  82. 1 def can_view_rfis?
  83. policy.can_view_rfis?
  84. end
  85. 1 def state_label_args
  86. LooposUi::Item::StateLabel::ItemStruct.new(**item.state_badge_args)
  87. end
  88. 1 def item_show_sync_excluded_tab_keys
  89. [
  90. LooposUi::TabsLayout::Tab.keyfy(LooposUi::PageSection::ItemTab::Notes::KEY),
  91. ].join(",")
  92. end
  93. end
  94. end
  95. end

app/components/loopos_ui/pages/item_show_header.html.erb

0.0% lines covered

0.0% branches covered

15 relevant lines. 0 lines covered and 15 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <%= tag.turbo_frame id: "lui-item-show-header-#{item.token}", data: live_item_sync_data_attributes do %>
  2. <%= render LooposUi::PageHeader.new do |show_header| %>
  3. <% show_header.with_header(title: "#{item.full_id} - #{item.product_name} #{item.variant_name}".strip) do |header| %>
  4. <% header.with_info do %>
  5. <%= render LooposUi::Item::StateLabel.new(
  6. item: LooposUi::Item::StateLabel::ItemStruct.new(**item.state_badge_args)
  7. ) %>
  8. <% end %>
  9. then: 0 else: 0 <% header.with_title_token_zone do %>
  10. <%= render LooposUi::Token.new(text: "FTI") %>
  11. <% end if item.forward_item.present? %>
  12. then: 0 else: 0 <% header.with_token_zone do %>
  13. <%= render LooposUi::TokenList.new(max_tokens: 4) do |token_list| %>
  14. <% item.identifiers.each do |identifier_data| %>
  15. <% kind, reference = identifier_data.to_a.first %>
  16. <% token_list.with_token_manual do %>
  17. <% identifier_display = OpenStruct.new(key_text: t("admin.items.identifier_kinds.#{kind}"), text: reference) %>
  18. <%= render LooposUi::Entities::Identifier.new(identifier: identifier_display) %>
  19. <% end %>
  20. <% end %>
  21. <% end %>
  22. <% end if item.identifiers.present? %>
  23. <% end %>
  24. <% end %>
  25. <% end %>

app/components/loopos_ui/pages/item_show_header.rb

70.0% lines covered

100.0% branches covered

20 relevant lines. 14 lines covered and 6 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Pages
  3. 1 class ItemShowHeader < LoopComponent
  4. 1 TSource = Types::Instance(LooposUi::PageSources::CoreSource)
  5. 1 option :source, TSource
  6. 1 option :token, Types::String
  7. 1 def initialize(...)
  8. super
  9. end
  10. 1 def before_render
  11. end
  12. 1 def live_item_sync_data_attributes
  13. LooposUi::ItemShowSync.frame_data_attributes(
  14. source: source,
  15. item_token: token,
  16. sync_mode: "header",
  17. src_url: show_header_src,
  18. )
  19. end
  20. 1 def show_header_src
  21. LooposUi::Engine.routes.url_helpers.show_header_item_path(
  22. token: token,
  23. context: source.source_context,
  24. )
  25. end
  26. 1 private
  27. 1 def item
  28. @item ||= source.item(token)
  29. end
  30. 1 def item_from_model?
  31. source.is_a?(LooposUi::PageSources::Core)
  32. end
  33. 1 def state_label_args
  34. LooposUi::Item::StateLabel::ItemStruct.new(**item.state_badge_args)
  35. end
  36. end
  37. end
  38. end

app/components/loopos_ui/pages/reports_index.html.erb

0.0% lines covered

0.0% branches covered

20 relevant lines. 0 lines covered and 20 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <%= render LooposUi::IndexLayout.new do |layout| %>
  2. <% layout.with_header(title: I18n.t("loopos_ui.reports.title", default: "Reports")) %>
  3. then: 0 else: 0 <% if helpers.current_user.present? %>
  4. <%= helpers.turbo_stream_from LooposUi::ReportsStream.channel_name(helpers.current_user.id) %>
  5. <div id="loopos_ui_reports_stream_ping" hidden></div>
  6. <% end %>
  7. <%= tag.turbo_frame id: "loopos_ui_reports_table" do %>
  8. <%= render LooposUi::Pages::ReportsIndexTable.new(
  9. reports: reports,
  10. pagy: pagy,
  11. extra_data: extra_data,
  12. core_instance_id: core_instance_id,
  13. ) %>
  14. <% end %>
  15. <% end %>
  16. <%= content_for :action_bar do %>
  17. <%= render LooposUi::ActionBar.new do |action_bar| %>
  18. <% action_bar.with_breadcrumbs_list do |breadcrumbs| %>
  19. <% breadcrumbs.with_breadcrumb(href: helpers.loopos_ui.reports_path) { t("loopos_ui.reports.title", default: "Reports") } %>
  20. <% end %>
  21. <% curr_export_options = export_options %>
  22. <% action_bar.with_action_buttons do |action_buttons| %>
  23. <% action_buttons.with_button_group do |bg| %>
  24. then: 0 else: 0 <% bg.with_button_manual do %>
  25. <%= render LooposUi::ActionMenu.new(options: curr_export_options) do |menu| %>
  26. <% menu.with_trigger do %>
  27. <% render(LooposUi::Button.new(
  28. type: :tertiary,
  29. kind: :neutral,
  30. text: I18n.t("loopos_ui.reports.export", default: "Generate a new report"),
  31. trailing_icon: "fa-regular fa-chevron-down",
  32. icon: "file-arrow-down",
  33. size: :tiny,
  34. disabled: extra_data[:menu_disabled] || false,
  35. tooltip_text: extra_data[:menu_tooltip].presence,
  36. )) %>
  37. <% end %>
  38. <% end %>
  39. <% end if curr_export_options.present? %>
  40. <% end %>
  41. <% end %>
  42. <% end %>
  43. <% end %>

app/components/loopos_ui/pages/reports_index.rb

59.38% lines covered

0.0% branches covered

32 relevant lines. 19 lines covered and 13 lines missed.
12 total branches, 0 branches covered and 12 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 module Pages
  4. 1 class ReportsIndex < LoopComponent
  5. 1 DEFAULT_PAGE_SIZE = 15
  6. 1 option :reports, Types::Array, default: -> { [] }
  7. 1 then: 0 else: 0 option :pagy, Types.Instance(PageSources::Models::PagyWrapper), default: -> { PageSources::Models::PagyWrapper.new(page: 1, items: DEFAULT_PAGE_SIZE, count: reports&.count || 0) }
  8. 1 option :extra_data, Types::Hash, default: -> { {} }
  9. 1 option :core_instance_id, Types::Coercible::String, default: -> { "" }
  10. 1 def export_options
  11. then: 0 else: 0 else: 0 then: 0 else: 0 case LooposUi.config.app_type&.to_sym
  12. when: 0 when :hubs then
  13. [
  14. {
  15. text: I18n.t("loopos_ui.reports.export_items", default: I18n.t("admin.v2.items.table.export_items")),
  16. url: helpers.loopos_ui.reports_path(report_type: "hubs_items", filename: "items", core_instance_id: core_instance_id),
  17. },
  18. ]
  19. end&.each do |option|
  20. option[:attributes] ||= {}
  21. option[:attributes][:data] ||= {}
  22. option[:attributes][:data][:turbo_method] = :post
  23. # option[:attributes][:data][:turbo] = false
  24. end || []
  25. end
  26. end
  27. 1 class ReportsIndexTable < LoopComponent
  28. 1 option :reports, Types::Array, default: -> { [] }
  29. 1 then: 0 else: 0 option :pagy, Types.Instance(PageSources::Models::PagyWrapper), default: -> { PageSources::Models::PagyWrapper.new(page: 1, items: DEFAULT_PAGE_SIZE, count: reports&.count || 0) }
  30. 1 option :extra_data, Types::Hash, default: -> { {} }
  31. 1 option :core_instance_id, Types::Coercible::String, default: -> { "" }
  32. 1 def table_columns
  33. [
  34. { title: I18n.t("loopos_ui.reports.id", default: "ID"), dataIndex: "id", key: "id", hidable: false, sortable: true, sort_key: "id" },
  35. {
  36. title: I18n.t("loopos_ui.reports.report_type", default: "Type"),
  37. key: "report_type",
  38. dataIndex: "report_type",
  39. sortable: true,
  40. filters: LooposUi::PageSources::Models::Report::AVAILABLE_REPORT_TYPES.map { |report_type| { text: I18n.t("loopos_ui.reports.report_types.#{report_type}", default: report_type), value: report_type } },
  41. },
  42. {
  43. title: I18n.t("loopos_ui.reports.status", default: "Status"),
  44. key: "status",
  45. dataIndex: "status",
  46. type: :html,
  47. sortable: true,
  48. sort_key: "status",
  49. filters: LooposUi::PageSources::Models::Report::REPORT_STATUSES.map { |status| { text: I18n.t("loopos_ui.reports.statuses.#{status}", default: status), value: status } },
  50. },
  51. { title: I18n.t("loopos_ui.reports.created_at", default: "Created"), dataIndex: "created_at", key: "created_at", sortable: true, sort_key: "created_at", default_sort: :desc },
  52. { title: I18n.t("loopos_ui.reports.download", default: "Download"), dataIndex: "action", key: "action", type: :html },
  53. ]
  54. end
  55. 1 def format_created_at(report)
  56. then: 0 else: 0 return if report.created_at.blank?
  57. Time.zone.parse(report.created_at.to_s).strftime("%Y-%m-%d %H:%M")
  58. end
  59. 1 def show_download_button?(report)
  60. report.status.to_s == "completed" && report.download_url.present?
  61. end
  62. 1 def show_error_message?(report)
  63. report.status.to_s == "failed" && report.error_message.present?
  64. end
  65. 1 def reports_table_fetch_url
  66. helpers.loopos_ui.reports_url(core_instance_id: core_instance_id)
  67. end
  68. end
  69. end
  70. end

app/components/loopos_ui/pages/reports_index_table.html.erb

0.0% lines covered

0.0% branches covered

19 relevant lines. 0 lines covered and 19 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <%= render LooposUi::V2::Table.new(
  2. id: "reports_table",
  3. columns: table_columns,
  4. data: [],
  5. pagy: pagy.presence,
  6. show_pagination: true,
  7. searchable: true,
  8. show_result_count: true,
  9. fetch_url: reports_table_fetch_url,
  10. ) do |table| %>
  11. <% reports.each do |report| %>
  12. <% table.with_row(key: report.id) do |row| %>
  13. <% row.with_cell(property: :id) do %>
  14. <%= report.id %>
  15. <% end %>
  16. <% row.with_cell(property: :report_type) do %>
  17. <%= I18n.t("loopos_ui.reports.report_types.#{report.report_type}", default: report.report_type.to_s) %>
  18. <% end %>
  19. <% row.with_cell(property: :status) do %>
  20. <%= I18n.t("loopos_ui.reports.statuses.#{report.status}", default: report.status.to_s) %>
  21. <% end %>
  22. <% row.with_cell(property: :created_at) do %>
  23. <%= format_created_at(report) %>
  24. <% end %>
  25. <% row.with_cell(property: :action) do %>
  26. then: 0 <% if show_download_button?(report) %>
  27. <%= render LooposUi::Button.new(
  28. type: :secondary,
  29. kind: :neutral,
  30. text: I18n.t("loopos_ui.reports.download", default: "Download"),
  31. icon: "file-arrow-down",
  32. size: :tiny,
  33. href: report.download_url,
  34. load_on_click: false,
  35. tag_options: {
  36. target: "_blank",
  37. rel: "noopener",
  38. },
  39. else: 0 ) %>
  40. then: 0 <% elsif show_error_message?(report) %>
  41. <%= report.error_message %>
  42. else: 0 <% else %>
  43. <%= I18n.t("loopos_ui.reports.pending", default: "Pending...") %>
  44. <% end %>
  45. <% end %>
  46. <% end %>
  47. <% end %>
  48. <% end %>

app/components/loopos_ui/pages/rfi/rfi_show.html.erb

0.0% lines covered

0.0% branches covered

8 relevant lines. 0 lines covered and 8 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%= helpers.turbo_frame_tag "rfi_show_tabs" do %>
  2. <%= render LooposUi::TabsLayout.new(active_tab_key: active_tab_key) do |layout| %>
  3. <% layout.with_tab(title: I18n.t("admin.items.rfi.tabs.rfi_info"), key: LooposUi::PageSection::RfiTab::Info::KEY) do %>
  4. <%= render LooposUi::PageSection::RfiTab::Info.new(rfi: rfi, item: item, rfi_final_choice: rfi.final_choice) %>
  5. <% end %>
  6. <% layout.with_tab(title: I18n.t("admin.items.rfi.tabs.rfi_answers"), key: LooposUi::PageSection::RfiTab::RfiAnswers::KEY) do %>
  7. <%= render LooposUi::PageSection::RfiTab::RfiAnswers.new(rfi: rfi, item: item) %>
  8. <% end %>
  9. then: 0 else: 0 <% layout.with_tab(title: I18n.t("admin.items.rfi.tabs.advanced_details"), key: LooposUi::PageSection::RfiTab::AdvancedDetails::KEY) do %>
  10. <%= render LooposUi::PageSection::RfiTab::AdvancedDetails.new(rfi: rfi) %>
  11. <% end if source_type == :core && helpers.current_user.can_view_extra_data? %>
  12. <% end %>
  13. <% end %>

app/components/loopos_ui/pages/rfi/rfi_show.rb

70.0% lines covered

0.0% branches covered

20 relevant lines. 14 lines covered and 6 lines missed.
5 total branches, 0 branches covered and 5 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Pages
  3. 1 module Rfi
  4. 1 class RfiShow < LoopComponent
  5. 1 TSource = Types::Instance(LooposUi::PageSources::Core) | Types::Instance(LooposUi::PageSources::CoreApi)
  6. 1 option :source, TSource, optional: true
  7. # option :policy, optional: true, default: proc { NoPermissionPolicy.new }
  8. 1 option :rfi_id, Types::Integer
  9. 1 option :item_token, Types::String
  10. 1 option :active_tab_key, optional: true
  11. 1 def before_render
  12. end
  13. 1 def source_type
  14. else: 0 then: 0 return unless source
  15. else: 0 case source
  16. when: 0 when LooposUi::PageSources::CoreApi
  17. :core_api
  18. when: 0 when LooposUi::PageSources::Core
  19. :core
  20. end
  21. end
  22. 1 private
  23. 1 def rfi
  24. @rfi ||= LooposUi::PageSources::Models::Rfi.find_by(rfi_id: rfi_id, source: source)
  25. end
  26. 1 def item
  27. @item ||= LooposUi::PageSources::Models::Item.find_by(token: item_token, source: source)
  28. end
  29. end
  30. end
  31. end
  32. end

app/components/loopos_ui/pages/rfi_answer/show.html.erb

0.0% lines covered

100.0% branches covered

6 relevant lines. 0 lines covered and 6 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.turbo_frame id: "rfi_show_tabs" do %>
  2. <%= render LooposUi::TabsLayout.new(active_tab_key: active_tab_key) do |layout| %>
  3. <% layout.with_tab(title: I18n.t("admin.rfi_answer.tabs.info.title"), key: LooposUi::PageSection::RfiAnswerTab::Info::KEY) do %>
  4. <%= render LooposUi::PageSection::RfiAnswerTab::Info.new(rfi: rfi, item: item, rfi_answer: rfi_answer) %>
  5. <% end %>
  6. <% layout.with_tab(title: I18n.t("admin.rfi_answer.tabs.history.title"), key: LooposUi::PageSection::RfiAnswerTab::History::KEY) do %>
  7. <%= render LooposUi::PageSection::RfiAnswerTab::History.new(rfi: rfi, item: item, rfi_answer: rfi_answer) %>
  8. <% end %>
  9. <% end %>
  10. <% end %>

app/components/loopos_ui/pages/rfi_answer/show.rb

69.57% lines covered

0.0% branches covered

23 relevant lines. 16 lines covered and 7 lines missed.
5 total branches, 0 branches covered and 5 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Pages
  3. 1 module RfiAnswer
  4. 1 class Show < LoopComponent
  5. 1 TSource = Types::Instance(LooposUi::PageSources::Core) | Types::Instance(LooposUi::PageSources::CoreApi)
  6. 1 option :source, TSource, optional: true
  7. # option :policy, optional: true, default: proc { NoPermissionPolicy.new }
  8. 1 option :rfi_answer_id, Types::Integer
  9. 1 option :rfi_id, Types::Integer
  10. 1 option :item_token, Types::String
  11. 1 option :active_tab_key, optional: true
  12. 1 def before_render
  13. end
  14. 1 def source_type
  15. else: 0 then: 0 return unless source
  16. else: 0 case source
  17. when: 0 when LooposUi::PageSources::CoreApi
  18. :core_api
  19. when: 0 when LooposUi::PageSources::Core
  20. :core
  21. end
  22. end
  23. 1 private
  24. 1 def rfi
  25. @rfi ||= LooposUi::PageSources::Models::Rfi.find_by(rfi_id: rfi_id, source: source)
  26. end
  27. 1 def rfi_answer
  28. @rfi_answer ||= LooposUi::PageSources::Models::RfiAnswers.find_by(rfi_answer_id: rfi_answer_id, source: source)
  29. end
  30. 1 def item
  31. @item ||= LooposUi::PageSources::Models::Item.find_by(token: item_token, source: source)
  32. end
  33. end
  34. end
  35. end
  36. end

app/components/loopos_ui/pagination.rb

50.79% lines covered

0.0% branches covered

63 relevant lines. 32 lines covered and 31 lines missed.
8 total branches, 0 branches covered and 8 branches missed.
    
  1. 1 module LooposUi
  2. 1 class Pagination < LoopComponent
  3. 1 option :pagy, Types::Instance(Pagy), optional: true
  4. 1 option :page, Types::Coercible::Integer, default: -> { 1 }
  5. 1 option :items, Types::Coercible::Integer, optional: true
  6. 1 option :count, Types::Coercible::Integer, optional: true
  7. 1 option :path, Types::String, optional: true
  8. 1 option :link_attrs, Types::Hash, default: -> { {} }
  9. 1 option :mode, Types::Symbol.enum(:default, :event), default: -> { :default } # Not implemented yet
  10. 1 option :data, Types::Hash, default: -> { {} }
  11. 1 def path
  12. @path ||= request.fullpath
  13. end
  14. 1 private
  15. 1 def initialize(**options)
  16. super(**options)
  17. then: 0 if @pagy.nil? && [:count, :page, :items].all? { |key| options.key?(key) }
  18. else: 0 @pagy = Pagy.new(options)
  19. then: 0 else: 0 elsif @pagy.nil?
  20. raise ArgumentError, "Either pagy or count, page, and items must be provided"
  21. end
  22. end
  23. 1 def page
  24. @pagy.page
  25. end
  26. 1 def prev_path
  27. page_path(@pagy.prev)
  28. end
  29. 1 def next_path
  30. page_path(@pagy.next)
  31. end
  32. 1 def page_path(number)
  33. then: 0 else: 0 return if number.blank?
  34. uri = URI.parse(path)
  35. path_params = Rack::Utils.parse_nested_query(uri.query)
  36. path_params["page"] = number
  37. # Re-encode the query string
  38. uri.query = Rack::Utils.build_query(path_params)
  39. uri.to_s
  40. end
  41. 1 def before_render
  42. @path ||= request.fullpath
  43. end
  44. 1 def data
  45. # Not used yet, but will be used for event mode
  46. deep_merge_args(
  47. {
  48. controller: "lui--pagination",
  49. },
  50. @data,
  51. )
  52. end
  53. 1 def prev_button
  54. Button.new(
  55. icon: :chevron_left,
  56. href: prev_path,
  57. disabled: prev_path.nil?,
  58. **button_options,
  59. )
  60. end
  61. 1 def next_button
  62. Button.new(
  63. icon: :chevron_right,
  64. href: next_path,
  65. disabled: next_path.nil?,
  66. **button_options,
  67. )
  68. end
  69. 1 def button_options
  70. {
  71. type: :secondary,
  72. kind: :neutral,
  73. size: :small,
  74. tag_options: @link_attrs,
  75. }
  76. end
  77. 1 def chunks
  78. @pagy.series(size: [1, 2, 2, 1])
  79. end
  80. 1 def pages
  81. @pages ||= @pagy.pages.times.map { |p| p + 1 }
  82. end
  83. 1 def item(number)
  84. Item.new(number, pagination: self, active: number.to_s == page.to_s)
  85. end
  86. 1 class Item < LoopComponent
  87. 1 param :number, Types::Coercible::Integer
  88. 1 option :pagination, Types::Instance(Pagination)
  89. 1 option :active, Types::Bool, default: -> { false }
  90. 1 mod :active
  91. 1 def call
  92. content_tag(:a, href: path, class: classes, **pagination.link_attrs) do
  93. number.to_s
  94. end
  95. end
  96. 1 def path
  97. then: 0 else: 0 return if active
  98. uri = URI.parse(pagination.path)
  99. params = Rack::Utils.parse_nested_query(uri.query)
  100. # Merge or change parameters
  101. params["page"] = number
  102. # Re-encode the query string
  103. uri.query = Rack::Utils.build_query(params)
  104. uri.to_s
  105. end
  106. end
  107. end
  108. end

app/components/loopos_ui/pagination/pagination.html.erb

0.0% lines covered

0.0% branches covered

7 relevant lines. 0 lines covered and 7 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%= tag.span(class: "lui-pagination", data: data) do %>
  2. <%= render prev_button %>
  3. <% chunks.each do |n| %>
  4. then: 0 <% if n == :gap %>
  5. <%= tag.span("...", class: "lui-pagination__divider") %>
  6. else: 0 <% else %>
  7. <%= render item(n) %>
  8. <% end %>
  9. <% end %>
  10. <%= render next_button %>
  11. <% end %>

app/components/loopos_ui/popover.rb

76.0% lines covered

100.0% branches covered

25 relevant lines. 19 lines covered and 6 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class Popover < LoopComponent
  3. 1 renders_one :toggle, types: {
  4. button: {
  5. renders: ->(*args, **kwargs) {
  6. LooposUi::Button.new(*args, tag: :span, **kwargs.deep_merge(button_attributes))
  7. },
  8. as: :toggle_button,
  9. },
  10. custom: {
  11. renders: ->(&block) { capture(&block) },
  12. as: :custom_toggle,
  13. },
  14. }
  15. 1 renders_one :target # TODO: Deprecate target, prefer content slot
  16. 1 option :id, default: -> { "popover-#{Random.hex(10)}" }
  17. 1 option :position, default: -> { :bottom_left }, type: Types::Symbol.enum(
  18. :top_left,
  19. :top_center,
  20. :top_right,
  21. :bottom_left,
  22. :bottom_center,
  23. :bottom_right,
  24. )
  25. 1 option :mode, default: -> { :auto }, type: Types::Symbol.enum(:auto, :manual)
  26. 1 option :anchor_selector, optional: true # the selector of the element to anchor to (if not provided, the popover will be anchored to the toggle)
  27. 1 option :anchor, default: -> { :top_left }, type: Types::Symbol.enum(
  28. :top_left,
  29. :top_center,
  30. :top_right,
  31. :bottom_left,
  32. :bottom_center,
  33. :bottom_right,
  34. )
  35. 1 option :on_close, optional: true, type: Types::Symbol.enum(:refresh)
  36. 1 option :rotate_toggle, default: -> { false }
  37. 1 option :open, Types::Bool, default: -> { false }
  38. # TODO: make all components follow this pattern
  39. # Inspired by https://primer.style/view-components/lookbook/pages/system_arguments/
  40. # Maybe change the name, and define which arguments are allowed
  41. 1 option :system_arguments, default: -> { {} }
  42. 1 class << self
  43. skipped # :nocov:
  44. skipped def positions
  45. skipped dry_initializer.definitions[:position].type.values
  46. skipped end
  47. skipped
  48. skipped def anchors
  49. skipped dry_initializer.definitions[:anchor].type.values
  50. skipped end
  51. skipped # :nocov:
  52. end
  53. 1 def toggle_attributes
  54. {
  55. popovertarget: popover_target_id,
  56. data: {
  57. controller: "popover-toggle",
  58. popover_target: "toggle",
  59. popover_toggle_position_value: position.to_s,
  60. popover_toggle_anchor_value: anchor.to_s,
  61. popover_toggle_anchor_selector_value: anchor_selector,
  62. popover_toggle_on_close_value: on_close.to_s,
  63. popover_toggle_rotate_value: rotate_toggle,
  64. },
  65. }
  66. end
  67. 1 def button_attributes
  68. {
  69. tag_options: toggle_attributes,
  70. }
  71. end
  72. 1 private
  73. 1 def args
  74. deep_merge_args(system_arguments, {
  75. data: {
  76. controller: "popover",
  77. popover_open_value: open.to_s,
  78. popover_rotate_toggle_value: rotate_toggle,
  79. },
  80. class: "lui-popover",
  81. })
  82. end
  83. 1 def popover_target_id
  84. "#{id}-target"
  85. end
  86. end
  87. end

app/components/loopos_ui/popover/popover.html.erb

0.0% lines covered

100.0% branches covered

5 relevant lines. 0 lines covered and 5 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div(**args) do %>
  2. <%= tag.button(**toggle_attributes, type: :button, class: "lui-popover-toggle-wrapper") do %>
  3. <%= toggle %>
  4. <% end %>
  5. <%= tag.div(id: popover_target_id, popover: mode, data: { popover_target: "popover" }, class: "pointer-events-none w-full h-full bg-transparent overflow-hidden") do %>
  6. <div class="bg-white lui-popover-inner absolute pointer-events-auto">
  7. <%= content || target %>
  8. </div>
  9. <% end %>
  10. <% end %>

app/components/loopos_ui/popover_template.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class PopoverTemplate < LoopComponent
  3. 1 option :title, Types::String
  4. 1 renders_one :input
  5. 1 renders_one :entity_token
  6. end
  7. end

app/components/loopos_ui/popover_template/popover_template.html.erb

0.0% lines covered

100.0% branches covered

8 relevant lines. 0 lines covered and 8 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::Popover.new do |popover| %>
  2. <%= popover.with_custom_toggle do %>
  3. <%= entity_token %>
  4. <% end %>
  5. <%= popover.with_target do %>
  6. <div class="lui-popover_template" data-controller="popover-template">
  7. <%= render LooposUi::FormEntry.new(label: title) do |form_entry| %>
  8. <%= form_entry.with_input do %>
  9. <%= input %>
  10. <% end %>
  11. <% end %>
  12. </div>
  13. <% end %>
  14. <% end %>

app/components/loopos_ui/protocol/add_element/add_element_component.html.erb

0.0% lines covered

100.0% branches covered

10 relevant lines. 0 lines covered and 10 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div data: data_attributes do %>
  2. <%= header %>
  3. <div data-async-select-component-target='dropdown' data-dropdown-toggle-target="<%= dropdown_id %>" class="loopui-async-select__tag">
  4. <div class="protocol-add-btn" data-action="click->async-select-component#filterInput">
  5. <div class="protocol-add-btn__inside self-stretch h-[67px] px-4 py-6 rounded-lg border-2 border-orange-200 flex-col justify-center items-center gap-4 flex">
  6. <div class="w-[574px] h-[19px] text-center text-orange-700 copy-14-medium"><%= t(".add_element") %></div>
  7. </div>
  8. </div>
  9. </div>
  10. <div id="<%= dropdown_id %>" class="hidden loopui-async-select__dropdown loopui-async-select__dropdown--protocol">
  11. <div class="p-3">
  12. <label for="input-group-search" class="loopui-async-select__label"><%= t(".search") %></label>
  13. <div class="relative">
  14. <div class="loopui-async-select__search-icon">
  15. <i class="fa-regular fa-magnifying-glass"></i>
  16. </div>
  17. <input
  18. id="input-group-search"
  19. type="text"
  20. data-action='keydown->async-select-component#filterKeyDown input->async-select-component#filterInput'
  21. class="loopui-async-select__search-input"
  22. placeholder=<%= t(".search_placeholder") %>>
  23. </div>
  24. </div>
  25. <div class="tags-filter-container loopui-async-select__filter-container loopui-async-select__filter-container--protocol" aria-labelledby="dropdownSearchButton">
  26. <%= turbo_frame_tag filter_wrapper_id, data:{ "async-select-component-target": "dropdownTurbo"} do %>
  27. <% end %>
  28. </div>
  29. </div>
  30. <% end %>

app/components/loopos_ui/protocol/add_element/add_element_component.rb

55.0% lines covered

100.0% branches covered

20 relevant lines. 11 lines covered and 9 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 module Protocol
  4. 1 module AddElement
  5. 1 class AddElementComponent < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 renders_one :header
  8. 1 renders_many :options, LooposUi::Protocol::AddElement::SelectOptionComponent
  9. 1 attr_accessor :object, :filter_wrapper_id, :param_key
  10. 1 def initialize(filter_path:,
  11. assign_path:,
  12. import_path:,
  13. data_attributes: {},
  14. options: [],
  15. object: nil,
  16. filter_wrapper_id: nil,
  17. param_key: "name")
  18. @filter_path = filter_path
  19. @assign_path = assign_path
  20. @import_path = import_path
  21. @data_attributes = data_attributes
  22. @object = object
  23. @filter_wrapper_id = filter_wrapper_id
  24. @param_key = param_key
  25. end
  26. 1 def dropdown_id
  27. # Maybe we can unique property for the select component, random for now
  28. @dropdown_id ||= SecureRandom.hex(10)
  29. end
  30. 1 def data_attributes
  31. {
  32. controller: "async-select-component",
  33. "async-select-component-filter-path-value": @filter_path,
  34. "async-select-component-assign-path-value": @assign_path,
  35. "async-select-component-import-path-value": @import_path,
  36. "async-select-component-param-key-value": @param_key,
  37. }.merge!(@data_attributes)
  38. end
  39. end
  40. end
  41. end
  42. end

app/components/loopos_ui/protocol/add_element/filter_component.rb

72.73% lines covered

100.0% branches covered

11 relevant lines. 8 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 module Protocol
  4. 1 module AddElement
  5. 1 class FilterComponent < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 attr_accessor :resource, :filtered_resource, :filter_wrapper_prefix
  9. 1 def initialize(resource:, filtered_resource:, filter_wrapper_prefix:)
  10. @resource = resource
  11. @filtered_resource = filtered_resource
  12. @filter_wrapper_prefix = filter_wrapper_prefix
  13. end
  14. end
  15. end
  16. end
  17. end

app/components/loopos_ui/protocol/add_element/filter_component.turbo_stream.erb

0.0% lines covered

0.0% branches covered

14 relevant lines. 0 lines covered and 14 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%= turbo_stream.update "#{@filter_wrapper_prefix}#{dom_id(@resource)}" do %>
  2. <div class="loopui-async-select__options-grid">
  3. then: 0 <% if @filtered_resource.present? %>
  4. <% @filtered_resource.group_by(&:third).each do |group, bds| %>
  5. <% element_title_dictionary = {
  6. 'Input' => t(".input"),
  7. 'Info' => t(".info"),
  8. 'Structure' => t(".structure"),
  9. 'Paste' => t(".paste")
  10. } %>
  11. <%
  12. element_tooltip_copies = {
  13. 'Input' => t(".input_tooltip"),
  14. 'Info' => t(".info_tooltip"),
  15. 'Structure' => t(".structure_tooltip"),
  16. 'Paste' => t(".paste_tooltip")
  17. }
  18. %>
  19. <div class="loopui-async-select__options-grid--<%= group %>">
  20. <div class="loopui-async-select__options-title copy-12 font-bold">
  21. <%= element_title_dictionary[group] %>
  22. <% icon = capture do %>
  23. <i class="loopui-async-select__options-title copy-12 font-bold fa-solid fa-circle-info"></i>
  24. <% end %>
  25. <%= react_component("Tooltip", { text: element_tooltip_copies[group], children: icon, placement: "top" }) %>
  26. </div>
  27. <% bds.each do |bd| %>
  28. <%= render LooposUi::Protocol::AddElement::SelectOptionComponent.new(label: bd[0], icon: bd[3], type: bd[2], object: bd, submit_value: bd[1]) %>
  29. <% end %>
  30. </div>
  31. <% end %>
  32. else: 0 <% else %>
  33. <p class="loopui-async-select__options-title copy-12 font-bold">No results found</p>
  34. <% end %>
  35. <% end %>

app/components/loopos_ui/protocol/add_element/select_option_component.html.erb

0.0% lines covered

0.0% branches covered

9 relevant lines. 0 lines covered and 9 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%= turbo_frame_tag "filter-#{object_identifier}" do %>
  2. <%= tag.div( class:"w-full", data: { action: "click->async-select-component#labelClick", payload: data_payload, value: submit_value }.merge(label_data)) do %>
  3. then: 0 else: 0 <div <%= data_attributes.map { |key, value| "data-#{key}=#{value}" }.join(' ') if data_attributes.present? %>
  4. class="filter-options-container async-select__options-wrapper loopui-async-select__options-wrapper--<%= @type %>">
  5. <%= tag.div(input_id, class: "tags-label cursor-pointer tags-label-#{object_identifier} loopui-async-select__options-text loopui-async-select__options-text--protocol copy-12-medium") do %>
  6. <div class="loopui-async-select__options-icon loopui-async-select__options-icon--<%= @type %>">
  7. <i class="<%= @icon %>"></i>
  8. </div>
  9. <%= label %>
  10. <% end %>
  11. </div>
  12. <% end %>
  13. <% end %>

app/components/loopos_ui/protocol/add_element/select_option_component.rb

50.0% lines covered

0.0% branches covered

28 relevant lines. 14 lines covered and 14 lines missed.
14 total branches, 0 branches covered and 14 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 module Protocol
  4. 1 module AddElement
  5. 1 class SelectOptionComponent < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 renders_one :label_component
  8. 1 renders_one :edit_component
  9. 1 attr_accessor :label, :object, :data_payload, :data_attributes, :label_data, :submit_value
  10. 1 def initialize(label:, icon:, type:, object: nil, data_payload: {}, data_attributes: nil, label_data: {},
  11. submit_value: nil)
  12. @label = label
  13. @icon = icon
  14. @type = type
  15. @object = object
  16. @data_payload = data_payload
  17. then: 0 else: 0 @data_attributes = data_attributes.respond_to?(:dig) ? data_attributes.compact_blank : {}
  18. then: 0 else: 0 @label_data = label_data.respond_to?(:dig) ? label_data.compact_blank : {}
  19. @submit_value = submit_value || label
  20. end
  21. 1 def input_id
  22. "input_label_#{object_identifier}"
  23. end
  24. 1 def object_identifier
  25. then: 0 else: 0 object.respond_to?(:dom_id) ? dom_id(object) : object.to_s
  26. end
  27. 1 def theme
  28. then: 0 else: 0 @object.respond_to?(:theme) ? @object.theme : nil
  29. end
  30. 1 def color
  31. then: 0 else: 0 return theme.text_color if theme.present? && object.persisted?
  32. then: 0 else: 0 @color || (@object.respond_to?(:color) ? @object.color : "black")
  33. end
  34. 1 def persisted?
  35. then: 0 else: 0 @object.respond_to?(:persisted?) ? @object.persisted? : true
  36. end
  37. end
  38. end
  39. end
  40. end

app/components/loopos_ui/protocol/add_protocol.html.erb

0.0% lines covered

0.0% branches covered

40 relevant lines. 0 lines covered and 40 lines missed.
14 total branches, 0 branches covered and 14 branches missed.
    
  1. <div class="protocol-area__add-section">
  2. <div class="protocol-area__title-section">
  3. <div class="protocol-area__title-wrapper">
  4. then: 0 else: 0 <% if @highlighted %>
  5. <i class="copy-12 text-app-800-primary fa-kit fa-regular-bars-staggered-tag"></i>
  6. <% end %>
  7. then: 0 else: 0 <p class="text-app-800-primary copy-16 font-bold"><%= @highlighted ? t(".catalog_section"): t(".main_section") %></p>
  8. <% icon = capture do %>
  9. <i class="copy-12 text-app-800-primary fa-regular fa-circle-info"></i>
  10. <% end %>
  11. <%= react_component("Tooltip", {
  12. then: 0 else: 0 text: @highlighted ? t(".tooltip_highlighted") : t(".tooltip_main_section"),
  13. children: icon,
  14. placement: "top" }) %>
  15. </div>
  16. then: 0 else: 0 <p class="protocol-area__text copy-14 text-gray-900"><span class="copy-14 text-app-800-primary"><%= t(".drag_here") %></span> <%= t(".to_move") %> <%= @highlighted ? t(".catalog_section"): t(".main_section") %> <%= t(".in_submission") %></p>
  17. </div>
  18. <div class="protocol-area__right" >
  19. <%= form_tag import_admin_v2_protocols_path,
  20. class: "flex",
  21. data: {
  22. controller: "autosubmit loading async-select-component modal clipboard",
  23. autosubmit_target: "form",
  24. turbo_frame: '_top',
  25. "async-select-component-filter-path-value": filter_protocols_admin_v2_catalog_node_path(@catalog_node),
  26. "async-select-component-assign-path-value": attach_protocol_admin_v2_catalog_node_path(@catalog_node),
  27. "async-select-component-param-key-value": "protocol_id",
  28. assign_method: "POST",
  29. payload_data: {
  30. catalog_node_id: @catalog_node.id,
  31. highlight: @highlighted,
  32. }
  33. },
  34. multipart: true do %>
  35. <%= hidden_field_tag :catalog_node_id, @catalog_node.id %>
  36. <%= hidden_field_tag :highlight, @highlighted %>
  37. <%
  38. dropdown_id = SecureRandom.hex(10)
  39. choose_existing_modal_id = "choose_existing_protocol_#{dom_id(@catalog_node)}"
  40. %>
  41. <%
  42. sub_buttons = [
  43. (
  44. { text: t(".import_protocol"), app: "core", icon: "fa-regular fa-file-export", variant: "secondary", size: "large", iconPosition: "left", railsIcon: true, dataAttributes: { "loading-target": "toggle", action: "click->autosubmit#trigger" }}
  45. ),
  46. (
  47. { text: t(".choose_existing"), app: "core", icon: "fa-regular fa-list", variant: "secondary", size: "large", iconPosition: "left", railsIcon: true, dataAttributes: { action: "click->modal#open click->async-select-component#filterInput", "modal-id": choose_existing_modal_id}}
  48. ),
  49. (
  50. { text: t(".paste_protocol"), app: "core", icon: "fa-regular fa-paste", variant: "secondary", size: "large", iconPosition: "left", railsIcon: true, dataAttributes: { "loading-target": "toggle", action: "click->clipboard#paste", "clipboard-import-param": import_admin_v2_protocols_path, "clipboard-node-param": @catalog_node.id, "clipboard-highlight-param": @highlighted }}
  51. ),
  52. ].compact
  53. %>
  54. <%= render LooposUi::Modal.new(id: choose_existing_modal_id, title: t(".choose_existing")) do |modal| %>
  55. <% modal.with_custom_content do %>
  56. <div class="p-3 w-full">
  57. <label for="input-group-search" class="loopui-async-select__label"><%= t(".search") %></label>
  58. <div class="relative">
  59. <div class="loopui-async-select__search-icon">
  60. <i class="fa-regular fa-magnifying-glass"></i>
  61. </div>
  62. <input
  63. id="input-group-search"
  64. type="text"
  65. data-action='keydown->async-select-component#filterKeyDown input->async-select-component#filterInput'
  66. class="loopui-async-select__search-input"
  67. placeholder= <%= t(".search_placeholder") %>>
  68. </div>
  69. </div>
  70. <div class="h-[300px] tags-filter-container loopui-async-select__filter-container loopui-async-select__filter-container--protocol" aria-labelledby="dropdownSearchButton">
  71. then: 0 else: 0 <%= turbo_frame_tag "#{@highlighted ? "catalog": "main"}_attach_#{dom_id(@catalog_node)}", data:{ "async-select-component-target": "dropdownTurbo"} do %>
  72. <% end %>
  73. </div>
  74. <% end %>
  75. <% end %>
  76. <%= react_component("ButtonGroup", { text: "", app: 'core', variant: "secondary", size: "large", icon: "fa-regular fa-ellipsis", iconPosition: "left", buttonsPosition: "right", subButtons: sub_buttons}) %>
  77. <%= file_field_tag 'import_file', accept: '.json', id: "form", class: 'cursor-pointer', style: 'display: none; position: absolute', data: { action: "change->autosubmit#submit change->loading#setToggleLoadingNoMargin", "autosubmit-target": "input" } %>
  78. <% end %>
  79. <%= form_with(
  80. model: @model,
  81. method: :post,
  82. class: "flex",
  83. data: { turbo_frame: @turbo_frame_id }) do |form| %>
  84. <%= form.hidden_field :catalog_node_id, value: @catalog_node.id %>
  85. <%= form.hidden_field :name, value: "#{@catalog_node.name} Protocol" %>
  86. then: 0 else: 0 <% if @highlighted %>
  87. <%= form.hidden_field :highlight, value: true %>
  88. <% end %>
  89. then: 0 else: 0 <% if @current_user.can_edit_protocols? %>
  90. <%= form.button do %>
  91. <%= react_component("Button", { text: t(".create_new"), app: "core", variant: "primary", size: "large", iconPosition: "left"}) %>
  92. <% end %>
  93. <% end %>
  94. <% end %>
  95. </div>
  96. </div>

app/components/loopos_ui/protocol/attach_node/filter_component.rb

72.73% lines covered

100.0% branches covered

11 relevant lines. 8 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 module Protocol
  4. 1 module AttachNode
  5. 1 class FilterComponent < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 attr_accessor :resource, :filtered_resource, :filter_wrapper_prefix
  9. 1 def initialize(resource:, filtered_resource:, filter_wrapper_prefix:)
  10. @resource = resource
  11. @filtered_resource = filtered_resource
  12. @filter_wrapper_prefix = filter_wrapper_prefix
  13. end
  14. end
  15. end
  16. end
  17. end

app/components/loopos_ui/protocol/attach_node/filter_component.turbo_stream.erb

0.0% lines covered

0.0% branches covered

5 relevant lines. 0 lines covered and 5 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%= turbo_stream.update "#{@filter_wrapper_prefix}#{dom_id(@resource)}" do %>
  2. then: 0 <% if @filtered_resource.present? %>
  3. <% @filtered_resource.each do |bd| %>
  4. <%= render LooposUi::Protocol::AttachNode::SelectOptionComponent.new(label: bd.name, object: bd, submit_value: bd.id) %>
  5. <% end %>
  6. else: 0 <% else %>
  7. No results found
  8. <% end %>
  9. <% end %>

app/components/loopos_ui/protocol/attach_node/select_option_component.html.erb

0.0% lines covered

0.0% branches covered

17 relevant lines. 0 lines covered and 17 lines missed.
20 total branches, 0 branches covered and 20 branches missed.
    
  1. <%
  2. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 option_style = "background-color: #{theme&.bg_color}; color: #{theme&.text_color}; border-color: #{theme&.border_color}" if theme.present?
  3. then: 0 else: 0 then: 0 else: 0 text_color_style = "color: #{theme&.text_color};" if theme.present?
  4. %>
  5. <%= turbo_frame_tag "filter-#{object_identifier}" do %>
  6. <div
  7. then: 0 else: 0 <%= data_attributes.map { |key, value| "data-#{key}=#{value}" }.join(' ') if data_attributes.present? %>
  8. class="filter-options-container flex items-center py-1 px-2 rounded-md group border border-white hover:bg-background hover:border hover:border-primary-dark-10 transition-colors"
  9. style="<%= option_style %>"
  10. >
  11. <%= tag.div(class: "w-full", data: { action: "click->async-select-component#labelClick", payload: data_payload, value: submit_value }.merge(label_data)) do %>
  12. then: 0 <% if label_component? %>
  13. <span class="group-hover:text-primary-dark flex-1 text-xs font-medium text-gray-dark"><%= label_component %></span>
  14. else: 0 <% else %>
  15. <%= tag.label(input_id, class: "tags-label cursor-pointer tags-label-#{object_identifier} flex-1 text-xs font-medium text-gray-dark rounded-lg", style: text_color_style) do %>
  16. <%= label %>
  17. <% end %>
  18. <% end %>
  19. <% end %>
  20. then: 0 <% if persisted? %>
  21. then: 0 else: 0 <% if edit_component.present? %>
  22. <%= edit_component %>
  23. <% end %>
  24. else: 0 <% else %>
  25. <%= tag.div(class: "w-full", data: { action: "click->async-select-component#labelClick", payload: data_payload, value: submit_value }.merge(label_data)) do %>
  26. <span class='text-[10px] font-bold group-hover:underline group-hover:text-primary-dark text-gray-base cursor-pointer'>create new</span>
  27. <% end %>
  28. <% end %>
  29. </div>
  30. <% end %>

app/components/loopos_ui/protocol/attach_node/select_option_component.rb

53.85% lines covered

0.0% branches covered

26 relevant lines. 14 lines covered and 12 lines missed.
14 total branches, 0 branches covered and 14 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 module Protocol
  4. 1 module AttachNode
  5. 1 class SelectOptionComponent < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 renders_one :label_component
  8. 1 renders_one :edit_component
  9. 1 attr_accessor :label, :object, :data_payload, :data_attributes, :label_data, :submit_value
  10. 1 def initialize(label:, object: nil, data_payload: {}, data_attributes: nil, label_data: {}, submit_value: nil)
  11. @label = label
  12. @object = object
  13. @data_payload = data_payload
  14. then: 0 else: 0 @data_attributes = data_attributes.respond_to?(:dig) ? data_attributes.compact_blank : {}
  15. then: 0 else: 0 @label_data = label_data.respond_to?(:dig) ? label_data.compact_blank : {}
  16. @submit_value = submit_value || label
  17. end
  18. 1 def input_id
  19. "input_label_#{object_identifier}"
  20. end
  21. 1 def object_identifier
  22. then: 0 else: 0 object.respond_to?(:dom_id) ? dom_id(object) : object.to_s
  23. end
  24. 1 def theme
  25. then: 0 else: 0 @object.respond_to?(:theme) ? @object.theme : nil
  26. end
  27. 1 def color
  28. then: 0 else: 0 return theme.text_color if theme.present? && object.persisted?
  29. then: 0 else: 0 @color || (@object.respond_to?(:color) ? @object.color : "black")
  30. end
  31. 1 def persisted?
  32. then: 0 else: 0 @object.respond_to?(:persisted?) ? @object.persisted? : true
  33. end
  34. end
  35. end
  36. end
  37. end

app/components/loopos_ui/protocol/drawer_bar.rb

87.5% lines covered

100.0% branches covered

8 relevant lines. 7 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 class DrawerBar < LoopComponent
  4. 1 option :protocol
  5. 1 option :catalog_node
  6. 1 private
  7. 1 def presenter
  8. @presenter ||= protocol.presenter
  9. end
  10. end
  11. end
  12. end

app/components/loopos_ui/protocol/drawer_bar/drawer_bar.html.erb

0.0% lines covered

0.0% branches covered

38 relevant lines. 0 lines covered and 38 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. <%= render LooposUi::DrawerBar.new do |drawer| %>
  2. <% drawer.with_header do %>
  3. <span class="inline-flex p-4">
  4. <%= tag.turbo_frame id: dom_id(protocol, 'tags') do %>
  5. <%= render 'admin/v2/tags/tags', taggable: protocol %>
  6. <% end %>
  7. </span>
  8. <% end %>
  9. <% drawer.with_header do %>
  10. <div class="px-4 pb-4 flex flex-col self-stretch gap-2">
  11. <%= render LooposUi::TitleDescription.new(
  12. title: t(".protocol_settings"),
  13. description: ""
  14. )%>
  15. <%
  16. ai_locked = presenter.ai_locked?
  17. protocol_has_elements = protocol.elements.any?
  18. ai_protocol_input_disabled = ai_locked || protocol_has_elements
  19. then: 0 ai_protocol_input_tooltip = if ai_locked
  20. else: 0 I18n.t("admin.v2.protocols.ai_action_disabled_while_ai_running")
  21. then: 0 elsif protocol_has_elements
  22. I18n.t("admin.v2.protocols.ai_disabled_when_protocol_has_elements")
  23. else: 0 else
  24. I18n.t("admin.v2.protocols.ai_submit_tooltip")
  25. end
  26. %>
  27. <div class="flex flex-col gap-6 self-stretch">
  28. then: 0 else: 0 <% if presenter.loopos_ai_protocol_generator_path.present? %>
  29. <div
  30. data-controller="ai-protocol"
  31. data-ai-protocol-protocol-id-value="<%= protocol.id %>"
  32. data-ai-protocol-catalog-node-id-value="<%= catalog_node.id %>"
  33. data-ai-protocol-generator-path-value="<%= presenter.loopos_ai_protocol_generator_path %>"
  34. >
  35. <%= render LooposUi::V2::Card.new(full: true) do %>
  36. <%= render LooposUi::FormEntryAi.new(
  37. description: I18n.t("admin.v2.protocols.form_entry_ai_description"),
  38. placeholder: I18n.t("admin.v2.protocols.form_entry_ai_placeholder"),
  39. spinner: true,
  40. disabled: ai_protocol_input_disabled,
  41. tooltip: ai_protocol_input_tooltip,
  42. ) %>
  43. <% end %>
  44. </div>
  45. <% end %>
  46. <%= render LooposUi::FormEntry.new(
  47. label: "Description",
  48. required: false,
  49. orientation: "vertical",
  50. ) do |entry| %>
  51. <%= entry.with_input do %>
  52. <%= tag.turbo_frame id: "edit_protocol_description" do %>
  53. <%= form_with model: protocol, url: admin_v2_protocol_path(protocol), method: :patch, data: { turbo_frame: "edit_protocol_description" }, html: { id: "edit_protocol_description_form" } do %>
  54. <%= render LooposUi::Inputs::TextArea.new(
  55. model: protocol,
  56. attribute: :description,
  57. placeholder: I18n.t("admin.v2.protocols.protocol_editor.form_entry.text_area.placeholder"),
  58. rows: 4,
  59. ) %>
  60. <% end %>
  61. <% end %>
  62. <% end %>
  63. <% end %>
  64. </div>
  65. </div>
  66. <% end %>
  67. <%= render LooposUi::Protocol::Settings.new(
  68. protocol: protocol,
  69. presenter: presenter,
  70. catalog_node: catalog_node,
  71. ) %>
  72. <% end %>

app/components/loopos_ui/protocol/elements/fields/array.html.erb

0.0% lines covered

0.0% branches covered

23 relevant lines. 0 lines covered and 23 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%
  2. attribute= @attribute
  3. properties= @properties
  4. value= @value
  5. element= @element
  6. protocol= @protocol
  7. edit_value= @edit_value
  8. edit = @edit
  9. %>
  10. <div class="flex flex-col w-full gap-2">
  11. <div class="flex flex-row gap-2xs">
  12. <p class="text-xs font-normal text-gray-darkest"><%= properties[:label] %></p>
  13. </div>
  14. <%= turbo_frame_tag element, attribute, "tag_list" do %>
  15. else: 0 <% if properties[:value] != "-" %>
  16. then: 0 <%
  17. list = element.array_property(attribute).map do |option|
  18. {
  19. title: "#{option.label}",
  20. variant: 'primary',
  21. kind: "core",
  22. tSize: 'extraLarge',
  23. icon: "fa-regular fa-seal",
  24. line: "solid",
  25. deleteDataAttributes: {
  26. 'action':"click->modal#open click->tag-delete#setmodal",
  27. "link": unassign_array_options_admin_v2_protocol_element_path(element, attribute: attribute, value: option.value),
  28. "target": "patch",
  29. },
  30. configurable: true,
  31. deleteIcon: "fa-regular fa-times"
  32. }
  33. end
  34. %>
  35. <div data-controller="tag-delete modal">
  36. <%= render "shared/components/modal", type: properties[:label].titleize do %>
  37. <button data-tag-delete-target="modalbtn"
  38. class="core-button core-button-primary core-button-large">
  39. Confirm
  40. </button>
  41. <% end %>
  42. <%= react_component("TagList", {
  43. catList: list,
  44. maxDisplayed: '2',
  45. tagList: list,
  46. tSize: 'extraLarge',
  47. tooltip: attribute.to_s.singularize.titleize
  48. }) %>
  49. </div>
  50. <% end %>
  51. <% end %>
  52. <%= render AsyncSelectComponent.new(
  53. filter_path: array_options_admin_v2_protocol_element_path(element),
  54. assign_path: assign_array_options_admin_v2_protocol_element_path(element),
  55. filter_wrapper_id: "array_filter_#{dom_id(element)}_#{attribute}",
  56. param_key: "value",
  57. data_attributes: {
  58. payload_data: { attribute: attribute } },
  59. creatable: true
  60. ) do |select_component| %>
  61. <%= select_component.with_toggler do %>
  62. <%= react_component("Tag", {
  63. text: "Add #{attribute.to_s.singularize.titleize}",
  64. line: 'solid',
  65. kind: "neutral",
  66. tSize: "extraLarge",
  67. icon: "fa-regular fa-plus",
  68. cursorType: "pointer",
  69. dataAttributes: {
  70. action: "click->async-select-component#filterInput"
  71. }
  72. }) %>
  73. <% end %>
  74. <% end %>
  75. </div>

app/components/loopos_ui/protocol/elements/fields/array.rb

50.0% lines covered

100.0% branches covered

16 relevant lines. 8 lines covered and 8 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Fields
  5. 1 class Array < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 def initialize(attribute:, properties:, value:, element:, protocol:, edit_value:, edit: false, form: )
  9. @attribute= attribute
  10. @properties= properties
  11. @value= value
  12. @element= element
  13. @protocol= protocol
  14. @edit_value= edit_value
  15. @edit = edit
  16. @form = form
  17. end
  18. end
  19. end
  20. end
  21. end
  22. end

app/components/loopos_ui/protocol/elements/fields/boolean.html.erb

0.0% lines covered

0.0% branches covered

17 relevant lines. 0 lines covered and 17 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <%
  2. attribute= @attribute
  3. properties= @properties
  4. value= @value
  5. element= @element
  6. protocol= @protocol
  7. edit_value= @edit_value
  8. edit = @edit
  9. then: 0 if properties[:value] != '-'
  10. checked = "true"
  11. else: 0 else
  12. checked = "false"
  13. end
  14. %>
  15. <div class="flex flex-col items-start w-full gap-2">
  16. then: 0 else: 0 <% if properties[:label].present? %>
  17. <div class="flex flex-row gap-2xs">
  18. <p class="break-all text-xs font-normal text-gray-darkest"><%= properties[:label] %></p>
  19. <%#= inline.render_submit_buttons(form: form) %>
  20. </div>
  21. <% end %>
  22. <%= @form.check_box attribute, { class: 'w-4 h-4 text-blue-600 bg-gray-100 border-gray-base rounded focus:ring-trasparent focus:ring-0' }, '1', '0' %>
  23. <%#= react_component("Input", { id: "protocol_element_#{attribute}", value: properties[:value], type: 'checkbox', size: 'tiny', name: name }) %>
  24. </div>

app/components/loopos_ui/protocol/elements/fields/boolean.rb

50.0% lines covered

100.0% branches covered

16 relevant lines. 8 lines covered and 8 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Fields
  5. 1 class Boolean < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 def initialize(attribute:, properties:, value:, element:, protocol:, edit_value:, edit: false, form: )
  9. @attribute= attribute
  10. @properties= properties
  11. @value= value
  12. @element= element
  13. @protocol= protocol
  14. @edit_value= edit_value
  15. @edit = edit
  16. @form = form
  17. end
  18. end
  19. end
  20. end
  21. end
  22. end

app/components/loopos_ui/protocol/elements/fields/custom.html.erb

0.0% lines covered

0.0% branches covered

19 relevant lines. 0 lines covered and 19 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. <%
  2. attribute = @attribute
  3. properties = @properties
  4. value = @value
  5. element = @element
  6. protocol = @protocol
  7. edit_value = @edit_value
  8. edit = @edit
  9. id = element.id
  10. default_custom = <<~TEXT.strip
  11. <strong>Template - replace as needed</strong>
  12. <div class="lui-text lui-text--form">
  13. <label for="custom-text-#{id}">Text Custom:</label>
  14. <input
  15. class="lui-text--form__lui-input"
  16. type="text"
  17. id="custom-text-#{id}"
  18. placeholder="Placeholder..."
  19. value=""
  20. />
  21. </div>
  22. <script>
  23. document.getElementById('custom-text-#{id}').addEventListener('change', function () {
  24. const value = this.value;
  25. const customEvent = new CustomEvent('protocolCustomAnswer-#{id}', {
  26. detail: { value }
  27. });
  28. const protocolContainer = document.querySelector('[data-protocol-element-id="#{id}"]');
  29. if (protocolContainer) {
  30. protocolContainer.dispatchEvent(customEvent);
  31. } else {
  32. console.warn('Elemento com [data-protocol-element-id]="#{id}" não encontrado.');
  33. }
  34. });
  35. </script>
  36. TEXT
  37. input_value = edit_value.presence || value.presence ||
  38. then: 0 else: 0 then: 0 else: 0 (element.settings&.key?("custom") ? nil : default_custom)
  39. %>
  40. <div class="flex flex-col items-start w-full gap-2">
  41. then: 0 else: 0 <% if properties[:label].present? %>
  42. <div class="flex flex-row gap-2xs">
  43. <p class="break-all text-xs font-normal text-gray-darkest"><%= properties[:label] %></p>
  44. <%#= inline.render_submit_buttons(form: form) %>
  45. </div>
  46. <% end %>
  47. <div class="w-full">
  48. <%= render LooposUi::Inputs::TextArea.new(
  49. value: input_value,
  50. name: "protocol_element[custom]",
  51. rows: 5
  52. ) %>
  53. </div>
  54. </div>

app/components/loopos_ui/protocol/elements/fields/custom.rb

50.0% lines covered

100.0% branches covered

16 relevant lines. 8 lines covered and 8 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Fields
  5. 1 class Custom < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 def initialize(attribute:, properties:, value:, element:, protocol:, edit_value:, edit: false, form:)
  9. @attribute= attribute
  10. @properties= properties
  11. @value= value
  12. @element= element
  13. @protocol= protocol
  14. @edit_value= edit_value
  15. @edit = edit
  16. @form = form
  17. end
  18. end
  19. end
  20. end
  21. end
  22. end

app/components/loopos_ui/protocol/elements/fields/date.html.erb

0.0% lines covered

0.0% branches covered

16 relevant lines. 0 lines covered and 16 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%
  2. attribute= @attribute
  3. properties= @properties
  4. value= @value
  5. element= @element
  6. protocol= @protocol
  7. edit_value= @edit_value
  8. edit = @edit
  9. %>
  10. <div class="flex flex-col items-start w-full gap-2">
  11. then: 0 else: 0 <% if properties[:label].present? %>
  12. <div class="flex flex-row gap-2xs">
  13. <p class="break-all text-xs font-normal text-gray-darkest"><%= properties[:label] %></p>
  14. <%#= inline.render_submit_buttons(form: form) %>
  15. </div>
  16. <% end %>
  17. <div class="relative max-w-sm">
  18. <div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
  19. <i class="fa-regular fa-calendar-days"></i>
  20. </div>
  21. <%= react_component("Datetime", {
  22. id: "protocol_element_#{attribute}",
  23. name: "protocol_element[#{attribute}]",
  24. kind: "DatePicker",
  25. lang:'enUS',
  26. variant:"core",
  27. isRange: false,
  28. granularity: "date",
  29. inputClass: "core-input-box core-input-container-tiny",
  30. selectedDate: properties[:value]
  31. }) %>
  32. </div>
  33. <%#= react_component("Input", { id: "protocol_element_#{attribute}", value: properties[:value], type: properties[:type], size: 'tiny', name: "protocol_element[#{attribute}]" }) %>
  34. </div>

app/components/loopos_ui/protocol/elements/fields/date.rb

50.0% lines covered

100.0% branches covered

16 relevant lines. 8 lines covered and 8 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Fields
  5. 1 class Date < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 def initialize(attribute:, properties:, value:, element:, protocol:, edit_value:, edit: false, form: )
  9. @attribute= attribute
  10. @properties= properties
  11. @value= value
  12. @element= element
  13. @protocol= protocol
  14. @edit_value= edit_value
  15. @edit = edit
  16. @form = form
  17. end
  18. end
  19. end
  20. end
  21. end
  22. end

app/components/loopos_ui/protocol/elements/fields/enum.html.erb

0.0% lines covered

0.0% branches covered

14 relevant lines. 0 lines covered and 14 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%
  2. attribute= @attribute
  3. properties= @properties
  4. value= @value
  5. element= @element
  6. protocol= @protocol
  7. edit_value= @edit_value
  8. edit = @edit
  9. %>
  10. <div class="flex flex-col items-start w-full gap-2">
  11. then: 0 else: 0 <% if properties[:label].present? %>
  12. <div class="flex flex-row gap-2xs">
  13. <p class="break-all text-xs font-normal text-gray-darkest"><%= properties[:label] %></p>
  14. <%#= inline.render_submit_buttons(form: form) %>
  15. </div>
  16. <% end %>
  17. <%= form.select attribute, options_for_select(properties[:enum]), {}, class: "bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" %>
  18. <%#= react_component("Input", { id: "protocol_element_#{attribute}", value: properties[:value], type: 'checkbox', size: 'tiny', name: "protocol_element[#{attribute}]" }) %>
  19. </div>

app/components/loopos_ui/protocol/elements/fields/enum.rb

50.0% lines covered

100.0% branches covered

16 relevant lines. 8 lines covered and 8 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Fields
  5. 1 class Enum < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 def initialize(attribute:, properties:, value:, element:, protocol:, edit_value:, edit: false, form: )
  9. @attribute= attribute
  10. @properties= properties
  11. @value= value
  12. @element= element
  13. @protocol= protocol
  14. @edit_value= edit_value
  15. @edit = edit
  16. @form = form
  17. end
  18. end
  19. end
  20. end
  21. end
  22. end

app/components/loopos_ui/protocol/elements/fields/granularity.html.erb

0.0% lines covered

0.0% branches covered

14 relevant lines. 0 lines covered and 14 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%
  2. attribute= @attribute
  3. properties= @properties
  4. value= @value
  5. element= @element
  6. protocol= @protocol
  7. edit_value= @edit_value
  8. edit = @edit
  9. %>
  10. <div>
  11. <span class='block mb-1 text-xs font-normal text-gray-darkest'><%= key.to_s.titleize %></span>
  12. <%= turbo_frame_tag element, key do %>
  13. <%= form_with model: [:admin_v2, element.becomes(::Protocol::Element)], data: { controller: "autosubmit", autosubmit_target: "form" } do |form| %>
  14. <%= form.hidden_field key, value: '0' %>
  15. then: 0 else: 0 <%= react_component("Toggle", { id: "protocol_element_date_#{key}", name: "protocol_element[#{key}]", checked: value ? "checked" : "false", small: true, inputDataAttributes: { action: "change->autosubmit#submit", turbo_frame: "_top" } }) %>
  16. <% end %>
  17. <% end %>
  18. </div>

app/components/loopos_ui/protocol/elements/fields/granularity.rb

50.0% lines covered

100.0% branches covered

16 relevant lines. 8 lines covered and 8 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Fields
  5. 1 class Granularity < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 def initialize(attribute:, properties:, value:, element:, protocol:, edit_value:, edit: false, form: )
  9. @attribute= attribute
  10. @properties= properties
  11. @value= value
  12. @element= element
  13. @protocol= protocol
  14. @edit_value= edit_value
  15. @edit = edit
  16. @form = form
  17. end
  18. end
  19. end
  20. end
  21. end
  22. end

app/components/loopos_ui/protocol/elements/fields/image.html.erb

0.0% lines covered

0.0% branches covered

13 relevant lines. 0 lines covered and 13 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%
  2. attribute= @attribute
  3. properties= @properties
  4. value= @value
  5. element= @element
  6. protocol= @protocol
  7. edit_value= @edit_value
  8. edit = @edit
  9. %>
  10. <div class="flex flex-col items-start w-full gap-2">
  11. then: 0 else: 0 <% if properties[:label].present? %>
  12. <div class="flex flex-row gap-2xs">
  13. <p class="break-all text-xs font-normal text-gray-darkest"><%= properties[:label] %></p>
  14. <%#= inline.render_submit_buttons(form: form) %>
  15. </div>
  16. <% end %>
  17. <%= @form.file_field attribute, class: " protocol-input" %>
  18. </div>

app/components/loopos_ui/protocol/elements/fields/image.rb

50.0% lines covered

100.0% branches covered

16 relevant lines. 8 lines covered and 8 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Fields
  5. 1 class Image < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 def initialize(attribute:, properties:, value:, element:, protocol:, edit_value:, edit: false, form: )
  9. @attribute= attribute
  10. @properties= properties
  11. @value= value
  12. @element= element
  13. @protocol= protocol
  14. @edit_value= edit_value
  15. @edit = edit
  16. @form = form
  17. end
  18. end
  19. end
  20. end
  21. end
  22. end

app/components/loopos_ui/protocol/elements/fields/integer.html.erb

0.0% lines covered

100.0% branches covered

9 relevant lines. 0 lines covered and 9 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%# Currently is the same as the number input %>
  2. <%
  3. attribute= @attribute
  4. properties= @properties
  5. value= @value
  6. element= @element
  7. protocol= @protocol
  8. edit_value= @edit_value
  9. edit = @edit
  10. %>
  11. <%= render LooposUi::Protocol::Elements::Fields::Number.new(
  12. element: element,
  13. attribute: attribute,
  14. properties: properties,
  15. edit_value: edit_value)
  16. %>

app/components/loopos_ui/protocol/elements/fields/integer.rb

50.0% lines covered

100.0% branches covered

16 relevant lines. 8 lines covered and 8 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Fields
  5. 1 class Integer < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 def initialize(attribute:, properties:, value:, element:, protocol:, edit_value:, edit: false, form: )
  9. @attribute= attribute
  10. @properties= properties
  11. @value= value
  12. @element= element
  13. @protocol= protocol
  14. @edit_value= edit_value
  15. @edit = edit
  16. @form = form
  17. end
  18. end
  19. end
  20. end
  21. end
  22. end

app/components/loopos_ui/protocol/elements/fields/number.html.erb

0.0% lines covered

0.0% branches covered

16 relevant lines. 0 lines covered and 16 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%# Currently is the same as the number input %>
  2. <%
  3. attribute= @attribute
  4. properties= @properties
  5. value= @value
  6. element= @element
  7. protocol= @protocol
  8. edit_value= @edit_value
  9. edit = @edit
  10. %>
  11. <div class="flex flex-col items-start w-full gap-2">
  12. then: 0 else: 0 <% if properties[:label].present? %> <%#TODO why submit only if label present? %>
  13. <div class="flex flex-row gap-2xs">
  14. <p class="break-all text-xs font-normal text-gray-darkest"><%= properties[:label] %></p>
  15. <%#= inline.render_submit_buttons(form: form) %>
  16. </div>
  17. <% end %>
  18. <% input_value = edit_value.presence || properties[:value].to_f %>
  19. <% input_data_type = "number" %>
  20. <%= react_component("Input", {
  21. placeholder: "-",
  22. id: "protocol_element_#{attribute}",
  23. value: input_value,
  24. type: "text", size: 'tiny',
  25. name: "protocol_element[#{attribute}]",
  26. dataAttributes: { "controller": "#dropkiq", "preview-id": "protocol_element_#{attribute}_preview", "input-type": input_data_type },
  27. }) %>
  28. </div>

app/components/loopos_ui/protocol/elements/fields/number.rb

50.0% lines covered

100.0% branches covered

16 relevant lines. 8 lines covered and 8 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Fields
  5. 1 class Number < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 def initialize(attribute:, properties:, value:, element:, protocol:, edit_value:, edit: false, form: )
  9. @attribute= attribute
  10. @properties= properties
  11. @value = value
  12. @element= element
  13. @protocol= protocol
  14. @edit_value= edit_value
  15. @edit = edit
  16. @form = form
  17. end
  18. end
  19. end
  20. end
  21. end
  22. end

app/components/loopos_ui/protocol/elements/fields/option.html.erb

0.0% lines covered

0.0% branches covered

33 relevant lines. 0 lines covered and 33 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%
  2. attribute= @attribute
  3. properties= @properties
  4. value= @value
  5. element= @element
  6. protocol= @protocol
  7. edit_value= @edit_value
  8. edit = @edit
  9. index = @index
  10. option = @option
  11. locale = I18n.locale
  12. %>
  13. <%= tag.div(
  14. class: "flex w-full",
  15. id: "#{dom_id(option)}_protocol",
  16. data: { option_id: option.id },
  17. ) do %>
  18. <%= @form.fields_for :select_options, option do |oform| %>
  19. <%= tag.div(
  20. id: "#{dom_id(option)}_div",
  21. data: {
  22. controller: "update-index-inputs",
  23. id: "#{dom_id(option)}_protocol",
  24. update_index_inputs_target: "div",
  25. },
  26. ) %>
  27. <%= oform.hidden_field :active, value: true, id: "#{oform.object.id}_option_destroy" %>
  28. <%= oform.hidden_field :id, value: oform.object.id, id: "#{oform.object.id}_option_id" %>
  29. <%= tag.div(class: "grid grid-cols-[calc(100%-64px)_24px_24px] gap-xs items-center w-full justify-between") do %>
  30. <%= tag.div do %>
  31. <%= tag.div(class: "flex flex-row items center gap-2xs") do %>
  32. <%= oform.text_field :label,
  33. value: option.label(locale: locale),
  34. class: "protocol-input",
  35. data: {
  36. action: "keydown.enter->autosubmit#submit blur->autosubmit#submit"
  37. } %>
  38. <%= tag.div(class: "dealbreaker element__toggle-wrapper border-0! relative") do %>
  39. <%= tag.input(
  40. type: "hidden",
  41. name: "protocol_element[select_options_attributes][#{index - 1}][deal_breaker]",
  42. value: "0",
  43. autocomplete: "off",
  44. ) %>
  45. then: 0 else: 0 <%= react_component("Toggle", { id: "#{oform.object.id}_deal_breaker", name: "protocol_element[select_options_attributes][#{index - 1}][deal_breaker]", checked: option.deal_breaker ? "checked" : "false", small: true, inputDataAttributes: { action: "change->autosubmit#submit", turbo_frame: "_top" } }) %>
  46. <%= tag.span("DB", class: "text-gray-base") %>
  47. <% end %>
  48. <% end %>
  49. <% end %>
  50. <%= render LooposUi::Modal.for_delete(model_name: "Protocol Element") do |modal| %>
  51. <% modal.with_trigger do %>
  52. <%= tag.button(
  53. type: "button",
  54. class: "flex items-center justify-center w-[24px] h-[24px] bg-danger_bg text-danger-fg rounded text-[10px] fa-regular fa-trash-can",
  55. ) %>
  56. <% end %>
  57. <% modal.with_primary_action(
  58. text: t(".confirm"),
  59. kind: :app,
  60. tag_options: {
  61. form: dom_id(element),
  62. id: "#{oform.object.id}_button_destroy",
  63. type: "button",
  64. data: {
  65. input_id: "#{oform.object.id}_option_destroy",
  66. controller: "destroy-input",
  67. action: "click->destroy-input#changeDestroyInput click->lui--button#startLoading",
  68. },
  69. },
  70. ) %>
  71. <% end %>
  72. <% end %>
  73. <% end %>
  74. <% end %>

app/components/loopos_ui/protocol/elements/fields/option.rb

44.44% lines covered

100.0% branches covered

18 relevant lines. 8 lines covered and 10 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Fields
  5. 1 class Option < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 def initialize(attribute:, properties:, value:, element:, protocol:, edit_value:, edit: false, form:,
  9. index: , option:)
  10. @attribute= attribute
  11. @properties= properties
  12. @value= value
  13. @element= element
  14. @protocol= protocol
  15. @edit_value= edit_value
  16. @edit = edit
  17. @index = index
  18. @form = form
  19. @option = option
  20. end
  21. end
  22. end
  23. end
  24. end
  25. end

app/components/loopos_ui/protocol/elements/fields/options.html.erb

0.0% lines covered

0.0% branches covered

22 relevant lines. 0 lines covered and 22 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. <%
  2. attribute= @attribute
  3. properties= @properties
  4. value= @value
  5. element= @element
  6. protocol= @protocol
  7. edit_value= @edit_value
  8. edit = @edit
  9. form = @form
  10. %>
  11. <% form.object = element.becomes(::Protocol::Element::Input::Select)%>
  12. <div class="flex flex-col w-full gap-2">
  13. <div class="flex flex-row gap-2xs">
  14. <p class="break-all text-xs font-normal text-gray-darkest"><%= properties[:label] %></p>
  15. <%#= inline.render_submit_buttons(form: form) %>
  16. </div>
  17. <div class="flex flex-col gap-2" >
  18. <div id="<%= element.id %>_select_options" class="flex flex-col gap-2"
  19. data-controller="sortable-options"
  20. data-sortable-options-url-value="<%= helpers.main_app.update_select_option_admin_v2_protocol_elements_path %>"
  21. >
  22. then: 0 else: 0 then: 0 else: 0 <% if value&.any? %>
  23. <% value.order(:position).each_with_index do |option, index| %>
  24. then: 0 else: 0 <% if option.active %>
  25. <%= render LooposUi::Protocol::Elements::Fields::Option.new(
  26. attribute: attribute,
  27. properties: properties,
  28. index: index + 1,
  29. value: value,
  30. option: option,
  31. element: element,
  32. protocol: protocol,
  33. edit_value: edit_value,
  34. form: form) %>
  35. <% end %>
  36. <% end %>
  37. <% end %>
  38. </div>
  39. <div class="h-8 w-fit flex flex-row items-center gap-2 justify-start" id="<%= element.id %>_options_collection" data-action="click->protocol-elements#createOption">
  40. <i class="flex items-center gap-2 justify-center copy-14-medium text-app-800-primary fa-regular fa-plus"></i>
  41. <div class="copy-14-medium text-app-800-primary"><%= t(".add_option") %></div>
  42. </div>
  43. </div>
  44. </div>

app/components/loopos_ui/protocol/elements/fields/options.rb

50.0% lines covered

100.0% branches covered

16 relevant lines. 8 lines covered and 8 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Fields
  5. 1 class Options < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 def initialize(attribute:, properties:, value:, element:, protocol:, edit_value:, edit: false, form: )
  9. @attribute= attribute
  10. @properties= properties
  11. @value= value
  12. @element= element
  13. @protocol= protocol
  14. @edit_value= edit_value
  15. @edit = edit
  16. @form = form
  17. end
  18. end
  19. end
  20. end
  21. end
  22. end

app/components/loopos_ui/protocol/elements/fields/string.html.erb

0.0% lines covered

0.0% branches covered

17 relevant lines. 0 lines covered and 17 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%
  2. attribute= @attribute
  3. properties= @properties
  4. value= @value
  5. element= @element
  6. protocol= @protocol
  7. edit_value= @edit_value
  8. edit = @edit
  9. %>
  10. <div class="flex flex-col items-start w-full gap-2">
  11. then: 0 else: 0 <% if properties[:label].present? %>
  12. <div class="flex flex-row gap-2xs">
  13. <p class="break-all text-xs font-normal text-gray-darkest"><%= properties[:label] %></p>
  14. <%#= inline.render_submit_buttons(form: form) %>
  15. </div>
  16. <% end %>
  17. <%#= attribute %>
  18. <% input_value = edit_value.presence || value %>
  19. <% input_data_type = "string" %>
  20. <%= react_component("Input", {
  21. placeholder: "-",
  22. id: "protocol_element_#{attribute}",
  23. value: input_value,
  24. type: "text", size: 'tiny',
  25. name: "protocol_element[#{attribute}]",
  26. dataAttributes: { "controller": "#dropkiq", "preview-id": "protocol_element_#{attribute}_preview" , "input-type": input_data_type},
  27. }) %>
  28. <%#= form.text_field attribute, class: "protocol-input" %>
  29. </div>

app/components/loopos_ui/protocol/elements/fields/string.rb

50.0% lines covered

100.0% branches covered

16 relevant lines. 8 lines covered and 8 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Fields
  5. 1 class String < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 def initialize(attribute:, properties:, value:, element:, protocol:, edit_value:, edit: false, form: )
  9. @attribute= attribute
  10. @properties= properties
  11. @value= value
  12. @element= element
  13. @protocol= protocol
  14. @edit_value= edit_value
  15. @edit = edit
  16. @form = form
  17. end
  18. end
  19. end
  20. end
  21. end
  22. end

app/components/loopos_ui/protocol/elements/info/break.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::Protocol::ProtocolElementComponent.new(element: @element, current_user: @current_user, catalog_node: @catalog_node, edit: @edit, title: @title, export_url: @export_url, broadcast_url: @broadcast_url)%>

app/components/loopos_ui/protocol/elements/info/break.rb

58.82% lines covered

100.0% branches covered

17 relevant lines. 10 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Info
  5. 1 class Break < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 include LooposUi::InlineEditComponent
  9. 1 renders_one :editable_content
  10. 1 def initialize(element:, catalog_node: nil, current_user:, edit: false, title:, export_url: nil,
  11. broadcast_url: nil)
  12. @element= element
  13. @current_user = current_user
  14. @catalog_node = catalog_node
  15. @edit = edit
  16. @title = title
  17. @export_url = export_url
  18. @broadcast_url = broadcast_url
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

app/components/loopos_ui/protocol/elements/info/html.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::Protocol::ProtocolElementComponent.new(element: @element, current_user: @current_user, catalog_node: @catalog_node, edit: @edit, title: @title, export_url: @export_url, broadcast_url: @broadcast_url) %>

app/components/loopos_ui/protocol/elements/info/html.rb

58.82% lines covered

100.0% branches covered

17 relevant lines. 10 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Info
  5. 1 class Html < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 include LooposUi::InlineEditComponent
  9. 1 renders_one :editable_content
  10. 1 def initialize(element:, catalog_node: nil, current_user:, edit: false, title:, export_url: nil,
  11. broadcast_url: nil)
  12. @element= element
  13. @current_user = current_user
  14. @catalog_node = catalog_node
  15. @edit = edit
  16. @title = title
  17. @export_url = export_url
  18. @broadcast_url = broadcast_url
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

app/components/loopos_ui/protocol/elements/info/image.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::Protocol::ProtocolElementComponent.new(element: @element, current_user: @current_user, catalog_node: @catalog_node, edit: @edit, title: @title, export_url: @export_url, broadcast_url: @broadcast_url) %>

app/components/loopos_ui/protocol/elements/info/image.rb

58.82% lines covered

100.0% branches covered

17 relevant lines. 10 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Info
  5. 1 class Image < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 include LooposUi::InlineEditComponent
  9. 1 renders_one :editable_content
  10. 1 def initialize(element:, catalog_node: nil, current_user:, edit: false, title:, export_url: nil,
  11. broadcast_url: nil)
  12. @element= element
  13. @current_user = current_user
  14. @catalog_node = catalog_node
  15. @edit = edit
  16. @title = title
  17. @export_url = export_url
  18. @broadcast_url = broadcast_url
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

app/components/loopos_ui/protocol/elements/info/text.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::Protocol::ProtocolElementComponent.new(element: @element, current_user: @current_user, catalog_node: @catalog_node, edit: @edit, title: @title, export_url: @export_url, broadcast_url: @broadcast_url) %>

app/components/loopos_ui/protocol/elements/info/text.rb

58.82% lines covered

100.0% branches covered

17 relevant lines. 10 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Info
  5. 1 class Text < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 include LooposUi::InlineEditComponent
  9. 1 renders_one :editable_content
  10. 1 def initialize(element:, catalog_node: nil, current_user:, edit: false, title:, export_url: nil,
  11. broadcast_url: nil)
  12. @element= element
  13. @current_user = current_user
  14. @catalog_node = catalog_node
  15. @edit = edit
  16. @title = title
  17. @export_url = export_url
  18. @broadcast_url = broadcast_url
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

app/components/loopos_ui/protocol/elements/info/title.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::Protocol::ProtocolElementComponent.new(element: @element, current_user: @current_user, catalog_node: @catalog_node, edit: @edit, title: @title, export_url: @export_url, broadcast_url: @broadcast_url) %>

app/components/loopos_ui/protocol/elements/info/title.rb

58.82% lines covered

100.0% branches covered

17 relevant lines. 10 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Info
  5. 1 class Title < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 include LooposUi::InlineEditComponent
  9. 1 renders_one :editable_content
  10. 1 def initialize(element:, catalog_node: nil, current_user:, edit: false, title:, export_url: nil,
  11. broadcast_url: nil)
  12. @element= element
  13. @current_user = current_user
  14. @catalog_node = catalog_node
  15. @edit = edit
  16. @title = title
  17. @export_url = export_url
  18. @broadcast_url = broadcast_url
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

app/components/loopos_ui/protocol/elements/info/url.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::Protocol::ProtocolElementComponent.new(element: @element, current_user: @current_user, catalog_node: @catalog_node, edit: @edit, title: @title, export_url: @export_url, broadcast_url: @broadcast_url) %>

app/components/loopos_ui/protocol/elements/info/url.rb

58.82% lines covered

100.0% branches covered

17 relevant lines. 10 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Info
  5. 1 class Url < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 include LooposUi::InlineEditComponent
  9. 1 renders_one :editable_content
  10. 1 def initialize(element:, catalog_node: nil, current_user:, edit: false, title:, export_url: nil,
  11. broadcast_url: nil)
  12. @element= element
  13. @current_user = current_user
  14. @catalog_node = catalog_node
  15. @edit = edit
  16. @title = title
  17. @export_url = export_url
  18. @broadcast_url = broadcast_url
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

app/components/loopos_ui/protocol/elements/input/bool.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::Protocol::ProtocolElementComponent.new(element: @element, current_user: @current_user, catalog_node: @catalog_node, edit: @edit, title: @title, export_url: @export_url, broadcast_url: @broadcast_url) %>

app/components/loopos_ui/protocol/elements/input/bool.rb

58.82% lines covered

100.0% branches covered

17 relevant lines. 10 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Input
  5. 1 class Bool < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 include LooposUi::InlineEditComponent
  9. 1 renders_one :editable_content
  10. 1 def initialize(element:, catalog_node: nil, current_user:, edit: false, title:, export_url: nil,
  11. broadcast_url: nil)
  12. @element= element
  13. @current_user = current_user
  14. @catalog_node = catalog_node
  15. @edit = edit
  16. @title = title
  17. @export_url = export_url
  18. @broadcast_url = broadcast_url
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

app/components/loopos_ui/protocol/elements/input/custom.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::Protocol::ProtocolElementComponent.new(element: @element, current_user: @current_user, catalog_node: @catalog_node, edit: @edit, title: @title, export_url: @export_url, broadcast_url: @broadcast_url)%>

app/components/loopos_ui/protocol/elements/input/custom.rb

58.82% lines covered

100.0% branches covered

17 relevant lines. 10 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Input
  5. 1 class Custom < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 include LooposUi::InlineEditComponent
  9. 1 renders_one :editable_content
  10. 1 def initialize(element:, catalog_node: nil, current_user:, edit: false, title:, export_url: nil,
  11. broadcast_url: nil)
  12. @element= element
  13. @current_user = current_user
  14. @catalog_node = catalog_node
  15. @edit = edit
  16. @title = title
  17. @export_url = export_url
  18. @broadcast_url = broadcast_url
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

app/components/loopos_ui/protocol/elements/input/date.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::Protocol::ProtocolElementComponent.new(element: @element, current_user: @current_user, catalog_node: @catalog_node, edit: @edit, title: @title, export_url: @export_url, broadcast_url: @broadcast_url) %>

app/components/loopos_ui/protocol/elements/input/date.rb

58.82% lines covered

100.0% branches covered

17 relevant lines. 10 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Input
  5. 1 class Date < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 include LooposUi::InlineEditComponent
  9. 1 renders_one :editable_content
  10. 1 def initialize(element:, catalog_node: nil, current_user:, edit: false, title:, export_url: nil,
  11. broadcast_url: nil)
  12. @element= element
  13. @current_user = current_user
  14. @catalog_node = catalog_node
  15. @edit = edit
  16. @title = title
  17. @export_url = export_url
  18. @broadcast_url = broadcast_url
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

app/components/loopos_ui/protocol/elements/input/date_range.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::Protocol::ProtocolElementComponent.new(element: @element, current_user: @current_user, catalog_node: @catalog_node, edit: @edit, title: @title, export_url: @export_url, broadcast_url: @broadcast_url) %>

app/components/loopos_ui/protocol/elements/input/date_range.rb

58.82% lines covered

100.0% branches covered

17 relevant lines. 10 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Input
  5. 1 class DateRange < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 include LooposUi::InlineEditComponent
  9. 1 renders_one :editable_content
  10. 1 def initialize(element:, catalog_node: nil, current_user:, edit: false, title:, export_url: nil,
  11. broadcast_url: nil)
  12. @element= element
  13. @current_user = current_user
  14. @catalog_node = catalog_node
  15. @edit = edit
  16. @title = title
  17. @export_url = export_url
  18. @broadcast_url = broadcast_url
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

app/components/loopos_ui/protocol/elements/input/email.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::Protocol::ProtocolElementComponent.new(element: @element, current_user: @current_user, catalog_node: @catalog_node, edit: @edit, title: @title, export_url: @export_url, broadcast_url: @broadcast_url)%>

app/components/loopos_ui/protocol/elements/input/email.rb

58.82% lines covered

100.0% branches covered

17 relevant lines. 10 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Input
  5. 1 class Email < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 include LooposUi::InlineEditComponent
  9. 1 renders_one :editable_content
  10. 1 def initialize(element:, catalog_node: nil, current_user:, edit: false, title:, export_url: nil,
  11. broadcast_url: nil)
  12. @element= element
  13. @current_user = current_user
  14. @catalog_node = catalog_node
  15. @edit = edit
  16. @title = title
  17. @export_url = export_url
  18. @broadcast_url = broadcast_url
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

app/components/loopos_ui/protocol/elements/input/files.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::Protocol::ProtocolElementComponent.new(element: @element, current_user: @current_user, catalog_node: @catalog_node, edit: @edit, title: @title, export_url: @export_url, broadcast_url: @broadcast_url)%>

app/components/loopos_ui/protocol/elements/input/files.rb

58.82% lines covered

100.0% branches covered

17 relevant lines. 10 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Input
  5. 1 class Files < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 include LooposUi::InlineEditComponent
  9. 1 renders_one :editable_content
  10. 1 def initialize(element:, catalog_node: nil, current_user:, edit: false, title:, export_url: nil,
  11. broadcast_url: nil)
  12. @element= element
  13. @current_user = current_user
  14. @catalog_node = catalog_node
  15. @edit = edit
  16. @title = title
  17. @export_url = export_url
  18. @broadcast_url = broadcast_url
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

app/components/loopos_ui/protocol/elements/input/iban.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::Protocol::ProtocolElementComponent.new(element: @element, current_user: @current_user, catalog_node: @catalog_node, edit: @edit, title: @title, export_url: @export_url, broadcast_url: @broadcast_url) %>

app/components/loopos_ui/protocol/elements/input/iban.rb

58.82% lines covered

100.0% branches covered

17 relevant lines. 10 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Input
  5. 1 class Iban < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 include LooposUi::InlineEditComponent
  9. 1 renders_one :editable_content
  10. 1 def initialize(element:, catalog_node: nil, current_user:, edit: false, title:, export_url: nil,
  11. broadcast_url: nil)
  12. @element= element
  13. @current_user = current_user
  14. @catalog_node = catalog_node
  15. @edit = edit
  16. @title = title
  17. @export_url = export_url
  18. @broadcast_url = broadcast_url
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

app/components/loopos_ui/protocol/elements/input/identifier.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::Protocol::ProtocolElementComponent.new(element: @element, current_user: @current_user, catalog_node: @catalog_node, edit: @edit, title: @title, export_url: @export_url, broadcast_url: @broadcast_url) %>

app/components/loopos_ui/protocol/elements/input/identifier.rb

58.82% lines covered

100.0% branches covered

17 relevant lines. 10 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Input
  5. 1 class Identifier < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 include LooposUi::InlineEditComponent
  9. 1 renders_one :editable_content
  10. 1 def initialize(element:, catalog_node: nil, current_user:, edit: false, title:, export_url: nil,
  11. broadcast_url: nil)
  12. @element= element
  13. @current_user = current_user
  14. @catalog_node = catalog_node
  15. @edit = edit
  16. @title = title
  17. @export_url = export_url
  18. @broadcast_url = broadcast_url
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

app/components/loopos_ui/protocol/elements/input/images.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::Protocol::ProtocolElementComponent.new(element: @element, current_user: @current_user, catalog_node: @catalog_node, edit: @edit, title: @title, export_url: @export_url, broadcast_url: @broadcast_url)%>

app/components/loopos_ui/protocol/elements/input/images.rb

58.82% lines covered

100.0% branches covered

17 relevant lines. 10 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Input
  5. 1 class Images < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 include LooposUi::InlineEditComponent
  9. 1 renders_one :editable_content
  10. 1 def initialize(element:, catalog_node: nil, current_user:, edit: false, title:, export_url: nil,
  11. broadcast_url: nil)
  12. @element= element
  13. @current_user = current_user
  14. @catalog_node = catalog_node
  15. @edit = edit
  16. @title = title
  17. @export_url = export_url
  18. @broadcast_url = broadcast_url
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

app/components/loopos_ui/protocol/elements/input/money.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::Protocol::ProtocolElementComponent.new(element: @element, current_user: @current_user, catalog_node: @catalog_node, edit: @edit, title: @title, export_url: @export_url, broadcast_url: @broadcast_url) %>

app/components/loopos_ui/protocol/elements/input/money.rb

58.82% lines covered

100.0% branches covered

17 relevant lines. 10 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Input
  5. 1 class Money < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 include LooposUi::InlineEditComponent
  9. 1 renders_one :editable_content
  10. 1 def initialize(element:, catalog_node: nil, current_user:, edit: false, title:, export_url: nil,
  11. broadcast_url: nil)
  12. @element= element
  13. @current_user = current_user
  14. @catalog_node = catalog_node
  15. @edit = edit
  16. @title = title
  17. @export_url = export_url
  18. @broadcast_url = broadcast_url
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

app/components/loopos_ui/protocol/elements/input/number.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::Protocol::ProtocolElementComponent.new(element: @element, current_user: @current_user, catalog_node: @catalog_node, edit: @edit, title: @title, export_url: @export_url, broadcast_url: @broadcast_url)%>

app/components/loopos_ui/protocol/elements/input/number.rb

58.82% lines covered

100.0% branches covered

17 relevant lines. 10 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Input
  5. 1 class Number < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 include LooposUi::InlineEditComponent
  9. 1 renders_one :editable_content
  10. 1 def initialize(element:, catalog_node: nil, current_user:, edit: false, title:, export_url: nil,
  11. broadcast_url: nil)
  12. @element= element
  13. @current_user = current_user
  14. @catalog_node = catalog_node
  15. @edit = edit
  16. @title = title
  17. @export_url = export_url
  18. @broadcast_url = broadcast_url
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

app/components/loopos_ui/protocol/elements/input/phone_number.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::Protocol::ProtocolElementComponent.new(element: @element, current_user: @current_user, catalog_node: @catalog_node, edit: @edit, title: @title, export_url: @export_url, broadcast_url: @broadcast_url) %>

app/components/loopos_ui/protocol/elements/input/phone_number.rb

58.82% lines covered

100.0% branches covered

17 relevant lines. 10 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Input
  5. 1 class PhoneNumber < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 include LooposUi::InlineEditComponent
  9. 1 renders_one :editable_content
  10. 1 def initialize(element:, catalog_node: nil, current_user:, edit: false, title:, export_url: nil,
  11. broadcast_url: nil)
  12. @element= element
  13. @current_user = current_user
  14. @catalog_node = catalog_node
  15. @edit = edit
  16. @title = title
  17. @export_url = export_url
  18. @broadcast_url = broadcast_url
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

app/components/loopos_ui/protocol/elements/input/postal_code.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::Protocol::ProtocolElementComponent.new(element: @element, current_user: @current_user, catalog_node: @catalog_node, edit: @edit, title: @title, export_url: @export_url, broadcast_url: @broadcast_url) %>

app/components/loopos_ui/protocol/elements/input/postal_code.rb

58.82% lines covered

100.0% branches covered

17 relevant lines. 10 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Input
  5. 1 class PostalCode < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 include LooposUi::InlineEditComponent
  9. 1 renders_one :editable_content
  10. 1 def initialize(element:, catalog_node: nil, current_user:, edit: false, title:, export_url: nil,
  11. broadcast_url: nil)
  12. @element= element
  13. @current_user = current_user
  14. @catalog_node = catalog_node
  15. @edit = edit
  16. @title = title
  17. @export_url = export_url
  18. @broadcast_url = broadcast_url
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

app/components/loopos_ui/protocol/elements/input/select.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::Protocol::ProtocolElementComponent.new(element: @element, current_user: @current_user, catalog_node: @catalog_node, edit: @edit, title: @title, export_url: @export_url, broadcast_url: @broadcast_url) %>

app/components/loopos_ui/protocol/elements/input/select.rb

58.82% lines covered

100.0% branches covered

17 relevant lines. 10 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Input
  5. 1 class Select < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 include LooposUi::InlineEditComponent
  9. 1 renders_one :editable_content
  10. 1 def initialize(element:, catalog_node: nil, current_user:, edit: false, title:, export_url: nil,
  11. broadcast_url: nil)
  12. @element= element
  13. @current_user = current_user
  14. @catalog_node = catalog_node
  15. @edit = edit
  16. @title = title
  17. @export_url = export_url
  18. @broadcast_url = broadcast_url
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

app/components/loopos_ui/protocol/elements/input/text.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::Protocol::ProtocolElementComponent.new(element: @element, current_user: @current_user, catalog_node: @catalog_node, edit: @edit, title: @title, export_url: @export_url, broadcast_url: @broadcast_url)%>

app/components/loopos_ui/protocol/elements/input/text.rb

58.82% lines covered

100.0% branches covered

17 relevant lines. 10 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Input
  5. 1 class Text < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 include LooposUi::InlineEditComponent
  9. 1 renders_one :editable_content
  10. 1 def initialize(element:, catalog_node: nil, current_user:, edit: false, title:, export_url: nil,
  11. broadcast_url: nil)
  12. @element= element
  13. @current_user = current_user
  14. @catalog_node = catalog_node
  15. @edit = edit
  16. @title = title
  17. @export_url = export_url
  18. @broadcast_url = broadcast_url
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

app/components/loopos_ui/protocol/elements/settings.html.erb

0.0% lines covered

0.0% branches covered

30 relevant lines. 0 lines covered and 30 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <%= turbo_frame_tag 'protocol_sidebar' do %>
  2. <%= render LooposUi::Protocol::ProtocolSidebarComponent.new do |sidebar| %>
  3. <% sidebar.with_header do %>
  4. <%= turbo_stream_from "protocol_sidebar_stream" %>
  5. <div class="flex flex-row items-center justify-between">
  6. <div class="flex flex-row items-center gap-2">
  7. <i class="fa-regular fa-bars-staggered"></i>
  8. <h1>Element Settings</h1>
  9. </div>
  10. <%#= link_to preview_admin_v2_protocol_element_path(element, catalog_node_id: catalog_node&.id ) , data: { turbo_frame: 'protocol_sidebar', "protocol_sidebar" => "close" }, class: 'btn btn-primary' do %>
  11. <%# Removed the link since it does not look necessary and it changes the page %>
  12. <%= link_to "#", data: { turbo_frame: 'protocol_sidebar', "protocol_sidebar" => "close" }, class: 'btn btn-primary' do %>
  13. <i class="fa-solid fa-xmark text-gray-600 cursor-pointer"></i>
  14. <% end %>
  15. </div>
  16. <% end %>
  17. <% sidebar.with_content_section do %>
  18. <div class="w-full h-[49px] justify-start items-start inline-flex protocol_sidebar__section--padded">
  19. <div class="w-full h-[49px] justify-start items-start flex">
  20. <div class="grow shrink basis-0 h-12 p-4 <%= input_type_class %> rounded-lg justify-start items-center gap-3 flex">
  21. <div class="w-4 justify-center items-center flex">
  22. <div class="text-center <%= input_icon_class %> text-base font-normal font-['Font Awesome 6 Pro']">
  23. <i class="<%= element.class.icon %>"></i>
  24. </div>
  25. </div>
  26. <div class="grow shrink basis-0 h-[17px] justify-start items-center flex">
  27. <div class="<%= input_icon_class %> copy-14-medium">
  28. <%= type_text %>
  29. </div>
  30. </div>
  31. <div class="w-3 justify-center items-center flex">
  32. </div>
  33. </div>
  34. </div>
  35. </div>
  36. <% end %>
  37. <% sidebar.with_content_section do %>
  38. <div class="flex flex-row items-center justify-between protocol_sidebar__section--padded">
  39. <div class="flex flex-col gap-1">
  40. <p class="text-foreground copy-14 font-bold">Associate to apps</p>
  41. <p class="text-content-secondary copy-12 ">Link to LoopOS apps for display.</p>
  42. </div>
  43. <%= turbo_frame_tag "element_apps" do %>
  44. <%= render "admin/v2/protocol_elements/apps", enabled_apps: enabled_apps, element: element %>
  45. <% end %>
  46. </div>
  47. <% end %>
  48. then: 0 else: 0 <% if has_settings? %>
  49. <% sidebar.with_content_section do %>
  50. <%= settings_gem %>
  51. <% end %>
  52. <% end %>
  53. <% sidebar.with_content_section do %>
  54. <div class="protocol_sidebar__section--padded">
  55. <p class="text-foreground copy-14 font-bold">Tags</p>
  56. <div class="flex justify-start mt-6">
  57. <%= turbo_frame_tag element, 'tags' do %>
  58. <%= render 'admin/v2/tags/tags', taggable: element %>
  59. <% end %>
  60. </div>
  61. </div>
  62. <% end %>
  63. then: 0 else: 0 <% if can_edit_slug? %>
  64. <% sidebar.with_content_section do %>
  65. <div class="flex flex-col gap-4 protocol_sidebar__section--padded">
  66. <p class="text-foreground copy-14 font-bold">Show</p>
  67. <%= render "admin/v2/protocol_elements/slug_edit", element: element, protocol: element.protocol %>
  68. </div>
  69. <% end %>
  70. <% end %>
  71. <% end %>
  72. <% end %>

app/components/loopos_ui/protocol/elements/settings.rb

53.57% lines covered

0.0% branches covered

28 relevant lines. 15 lines covered and 13 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. 1 class LooposUi::Protocol::Elements::Settings < ViewComponent::Base
  2. 1 include Turbo::FramesHelper
  3. 1 include Turbo::StreamsHelper
  4. 1 attr_reader :element, :presenter, :catalog_node, :current_user
  5. 1 renders_one :settings_gem
  6. 1 def initialize(element:, presenter:, catalog_node: nil, current_user:)
  7. @element = element
  8. @presenter = presenter
  9. @catalog_node = catalog_node
  10. @current_user = current_user
  11. end
  12. 1 def has_settings?
  13. presenter.has_settings?
  14. end
  15. 1 def settings_ui_schema
  16. presenter.settings_ui_schema
  17. end
  18. 1 def settings_schema
  19. presenter.settings_schema
  20. end
  21. 1 def enabled_apps
  22. presenter.enabled_apps
  23. end
  24. 1 def input_type_class
  25. then: 0 else: 0 presenter.input? ? "bg-red-50" : "bg-apps-hubs-200"
  26. end
  27. 1 def input_icon_class
  28. then: 0 else: 0 presenter.input? ? "text-orange-700" : "text-apps-hubs-800-primary"
  29. end
  30. 1 def type_text
  31. presenter.type.split.first
  32. end
  33. 1 def update_url
  34. then: 0 else: 0 "/protocol_elements/#{element.id}/update_settings?catalog_node_id=#{catalog_node&.id}"
  35. end
  36. 1 def can_edit_slug?
  37. current_user.can_edit_protocol_element_slug?
  38. end
  39. end

app/components/loopos_ui/protocol/elements/structure/divider.html.erb

0.0% lines covered

0.0% branches covered

40 relevant lines. 0 lines covered and 40 lines missed.
22 total branches, 0 branches covered and 22 branches missed.
    
  1. <%
  2. element = @element
  3. presenter = element.presenter
  4. protocol = element.protocol
  5. catalog_node ||= @catalog_node || nil
  6. current_user = @current_user
  7. %>
  8. <%= turbo_frame_tag element do %>
  9. then: 0 else: 0 <div data-controller="protocol-elements" data-protocol-elements-element-value="<%= dom_id(element)%>" data-protocol-elements-id-value="<%= element.id %>" data-protocol-elements-node-value="<%= catalog_node&.id %>">
  10. then: 0 else: 0 <div class="relative py-4 cursor-pointer" style="margin: 80px 0px;" data-action="<%= !@edit ? "click->protocol-elements#edit" : "" %>"
  11. >
  12. then: 0 else: 0 <% if current_user.can_edit_protocols? %>
  13. then: 0 <% if @edit %>
  14. <span class="hidden" aria-hidden="true" data-protocol-elements-target="preview"></span>
  15. <%= link_to presenter.show_path(catalog_node) , data: { turbo_frame: element, 'protocol-elements-target': "preview" }, class: 'hidden' do%>
  16. <% end %>
  17. else: 0 <% else %>
  18. <%= link_to presenter.settings_path(catalog_node) , data: { turbo_frame: "protocol_sidebar", 'protocol-elements-target': "button" }, class: 'hidden' do%>
  19. <% end %>
  20. <%= link_to presenter.edit_path(catalog_node) , data: { turbo_frame: element, 'protocol-elements-target': "button" }, class: 'hidden' do%>
  21. <% end %>
  22. <% end %>
  23. <div class="element__number hidden!"><%= presenter.position %></div>
  24. <div class="z-10 absolute top-1/2 left-0 transform -translate-y-1/2 w-[24px] h-[24px] rounded items-center! flex! justify-center! cursor-move handle" style="border-radius: 8px;
  25. border: 1px solid var(--Colors-General-Grays-300, #DEE2E6);
  26. background: var(--Colors-General-Grays-100, #F8F9FA);">
  27. <i class="fa-sharp fa-regular text-xs fa-grip-dots-vertical"></i>
  28. </div>
  29. <% end %>
  30. then: 0 else: 0 <hr class="absolute <%= @edit ? "border-t-2" : "border-t" %> border-gray-900 border-dashed" style="width: calc(100% + 122px); margin-left: -66px;">
  31. <div class="absolute right-0 top-1/2 transform -translate-y-1/2 flex flex-row gap-2">
  32. then: 0 else: 0 <% if @edit %>
  33. then: 0 else: 0 <% if presenter.input? %>
  34. <div class="element__toggle-wrapper">
  35. <span class="copy-12 text-black">Mark as required</span>
  36. <%= turbo_frame_tag element, :required do %>
  37. then: 0 else: 0 <% if current_user.can_edit_protocols? %>
  38. <%= form_with model: [:admin_v2, element.becomes(::Protocol::Element)], data: { controller: "autosubmit", autosubmit_target: "form" } do |form| %>
  39. <input name="protocol_element[required]" type="hidden" value="0" autocomplete="off">
  40. then: 0 else: 0 <%= react_component("Toggle", { id: "protocol_element_required", name: "protocol_element[required]", checked: presenter.required ? "checked" : "false", small: true, inputDataAttributes: { action: "change->autosubmit#submit", turbo_frame: "_top" } }) %>
  41. then: 0 else: 0 <%= form.hidden_field :catalog_node_id, value: @catalog_node&.id %>
  42. <% end %>
  43. <% end %>
  44. <% end %>
  45. </div>
  46. <% end %>
  47. <%= render LooposUi::Modal.for_duplicate(model_name: "Protocol Element") do |modal| %>
  48. <% modal.with_trigger do %>
  49. <button type="button"
  50. class="bg-neutral_bg w-[24px] h-[24px] rounded items-center flex justify-center text-[10px] fa-kit fa-regular-copy-circle-plus text-neutral_text"
  51. data-toggle="tooltip"
  52. title="Duplicate protocol element"
  53. ></button>
  54. <% end %>
  55. <% modal.with_primary_action(
  56. text: t(".confirm"),
  57. href: duplicate_element_admin_v2_protocol_element_path(element),
  58. kind: :app,
  59. tag_options: { data: { "turbo-method": :post, type: "ignore", action: "click->lui--button#startLoading click->actions_loading#run" } }
  60. ) %>
  61. <% end %>
  62. <%= render LooposUi::Modal.for_delete(model_name: "Protocol Element") do |modal| %>
  63. <% modal.with_trigger do %>
  64. <button type="button" class="flex items-center justify-center w-[24px] h-[24px] bg-danger_bg text-danger-fg rounded text-[10px] fa-regular fa-trash-can"></button>
  65. <% end %>
  66. <% modal.with_custom_content do %>
  67. <%= form_with(
  68. model: [:admin_v2, element.becomes(::Protocol::Element)],
  69. id: "#{element.id}",
  70. class: "flex items-center justify-center w-fit h-fit",
  71. method: "delete",
  72. data: { turbo_frame: "#{dom_id(protocol)}_protocol_elements" }) do |form| %>
  73. then: 0 else: 0 <%= form.hidden_field :catalog_node_id, name: :catalog_node_id, value: catalog_node&.id %>
  74. <% end %>
  75. <% end %>
  76. <% modal.with_primary_action(
  77. text: t(".confirm"),
  78. kind: :app,
  79. tag_options: {
  80. form: "#{element.id}",
  81. type: "submit",
  82. data: {
  83. action: "click->lui--button#startLoading click->actions_loading#run"
  84. }
  85. }
  86. ) %>
  87. <% end %>
  88. <% end %>
  89. </div>
  90. </div>
  91. </div>
  92. <% end %>

app/components/loopos_ui/protocol/elements/structure/divider.rb

58.82% lines covered

100.0% branches covered

17 relevant lines. 10 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 module Elements
  4. 1 module Structure
  5. 1 class Divider < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 include Turbo::StreamsHelper
  8. 1 include LooposUi::InlineEditComponent
  9. 1 renders_one :editable_content
  10. 1 def initialize(element:, catalog_node: nil, current_user:, edit: false, title:, export_url: nil,
  11. broadcast_url: nil)
  12. @element= element
  13. @current_user = current_user
  14. @catalog_node = catalog_node
  15. @edit = edit
  16. @title = title
  17. @export_url = export_url
  18. @broadcast_url = broadcast_url
  19. end
  20. end
  21. end
  22. end
  23. end
  24. end

app/components/loopos_ui/protocol/protocol_builder_component.erb

0.0% lines covered

0.0% branches covered

7 relevant lines. 0 lines covered and 7 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. then: 0 else: 0 <% if defined?(@filter) %>
  2. <%= render LooposUi::Protocol::ProtocolFilterComponent.new(filter: @filter, disabled_protocols: @disabled_protocols, catalog_node: @catalog_node, protocol_show: @protocol_show, protocol: @protocol)%>
  3. <% end %>
  4. <div class="protocol-builder">
  5. <%= elements_section %>
  6. then: 0 else: 0 <% if settings_section? %>
  7. <%= settings_section %>
  8. <% end %>
  9. </div>

app/components/loopos_ui/protocol/protocol_builder_component.rb

65.0% lines covered

100.0% branches covered

20 relevant lines. 13 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 class ProtocolBuilderComponent < ViewComponent::Base
  4. 1 include Turbo::FramesHelper
  5. 1 renders_one :elements_section, "LooposUi::Protocol::ProtocolElementsSectionComponent"
  6. 1 renders_one :settings_section, "LooposUi::Protocol::ProtocolSettingsSectionComponent"
  7. 1 def initialize(filter: nil, disabled_protocols: "checked", catalog_node: nil, protocol_show: false, protocol: nil)
  8. @filter = filter
  9. @disabled_protocols = disabled_protocols
  10. @catalog_node = catalog_node
  11. @protocol_show = protocol_show
  12. @protocol = protocol
  13. end
  14. end
  15. 1 class ProtocolElementsSectionComponent < ViewComponent::Base
  16. 1 def initialize
  17. end
  18. 1 def call
  19. content
  20. end
  21. end
  22. 1 class ProtocolSettingsSectionComponent < ViewComponent::Base
  23. 1 def initialize
  24. end
  25. 1 def call
  26. content
  27. end
  28. end
  29. end
  30. end

app/components/loopos_ui/protocol/protocol_component.html.erb

0.0% lines covered

0.0% branches covered

30 relevant lines. 0 lines covered and 30 lines missed.
20 total branches, 0 branches covered and 20 branches missed.
    
  1. <%
  2. protocol = @protocol
  3. current_user = @current_user
  4. presenter = protocol.presenter
  5. elements ||= presenter.elements
  6. readonly||= @readonly ||= false
  7. current_catalog_node ||= @current_catalog_node ||= nil
  8. show_opened ||= params[:show_opened] == "true"
  9. apn = presenter.applicable_catalog_node_protocol(current_catalog_node)
  10. then: 0 else: 0 show_inheritance_path = current_catalog_node.present? && protocol.catalog_node_ids.exclude?(current_catalog_node&.id)
  11. ai_locked = presenter.ai_locked?
  12. %>
  13. <%= turbo_frame_tag "#{turbo_id(protocol)}", data: {id: protocol.id } do %>
  14. <%= turbo_stream_from(turbo_stream_id(protocol)) %>
  15. <div class="flex">
  16. then: 0 else: 0 <% if !show_inheritance_path %>
  17. <div class="protocol-component__grip-wrapper">
  18. <div class="protocol-component__grip">
  19. <i class="protocol-component__grip-icon text-base fa-regular fa-grip-dots"></i>
  20. </div>
  21. </div>
  22. <% end %>
  23. then: 0 else: 0 <div class="protocol-component <%= show_inheritance_path.present? ? "": "protocol-component--open" %>"
  24. data-accordion="open"
  25. data-sortable-protocol-target="applicableCatalogNodeProtocol"
  26. then: 0 else: 0 data-id="<%= apn&.protocol_id %>"
  27. data-controller="protocol-accordion"
  28. data-protocol-accordion-target="wrapper"
  29. then: 0 else: 0 data-position="<%= apn&.position %>"
  30. >
  31. <%= render LooposUi::Protocol::ProtocolHeader.new( protocol: protocol, readonly: readonly, current_catalog_node: current_catalog_node, show_opened: show_opened, current_user: current_user ) %>
  32. then: 0 else: 0 <div class="protocol-component__elements-wrapper <%= current_catalog_node.present? && !show_opened ? "hidden" : "" %>" id="accordion-collapse-<%= dom_id(protocol) %>"
  33. aria-labelledby="accordion-collapse-<%= dom_id(protocol) %>-header">
  34. <div id="<%= dom_id(protocol) %>_protocol_elements" class="protocol-component__elements-container" data-controller="sortable" data-sortable-target="main"
  35. then: 0 else: 0 data-id="<%= protocol.id %>" data-catalog-node-id="<%= current_catalog_node&.id %>">
  36. <%= elements_list %>
  37. </div>
  38. then: 0 else: 0 <% if current_user.can_edit_protocols? && !ai_locked %>
  39. <%= render LooposUi::Protocol::AddElement::AddElementComponent.new(
  40. filter_path: filter_element_options_admin_v2_protocol_path(protocol.id),
  41. assign_path: assign_element_admin_v2_protocol_path(protocol.id),
  42. import_path: import_admin_v2_protocol_elements_path,
  43. filter_wrapper_id: "protocol_elements_add_#{dom_id(protocol)}",
  44. param_key: "element_type",
  45. data_attributes: {
  46. assign_method: "POST",
  47. payload_data: {
  48. # element_position: elements.size + 1,
  49. protocol_id: protocol.id,
  50. then: 0 else: 0 catalog_node_id: current_catalog_node&.id,
  51. }
  52. },
  53. ) %>
  54. <% end %>
  55. then: 0 else: 0 <% if readonly || ai_locked %>
  56. <div class="absolute top-0 left-0 w-full h-full cursor-not-allowed z-50">
  57. </div>
  58. <% end %>
  59. </div>
  60. </div>
  61. </div>
  62. <% end %>

app/components/loopos_ui/protocol/protocol_component.rb

55.0% lines covered

100.0% branches covered

20 relevant lines. 11 lines covered and 9 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 class ProtocolComponent < ViewComponent::Base
  4. 1 include Turbo::FramesHelper
  5. 1 include Turbo::StreamsHelper
  6. 1 renders_one :elements_list
  7. 1 def initialize(protocol:, readonly:, current_catalog_node:, current_user:)
  8. @protocol= protocol
  9. @readonly= readonly
  10. @current_catalog_node = current_catalog_node
  11. @current_user = current_user
  12. end
  13. end
  14. 1 class ProtocolHeader < ViewComponent::Base
  15. 1 include Turbo::FramesHelper
  16. 1 include Turbo::StreamsHelper
  17. 1 def initialize(protocol:, readonly:, current_catalog_node:, show_opened:, current_user:)
  18. @protocol= protocol
  19. @readonly= readonly
  20. @current_catalog_node= current_catalog_node
  21. @current_user = current_user
  22. @show_opened = show_opened
  23. end
  24. end
  25. end
  26. end

app/components/loopos_ui/protocol/protocol_element_component.html.erb

0.0% lines covered

0.0% branches covered

102 relevant lines. 0 lines covered and 102 lines missed.
58 total branches, 0 branches covered and 58 branches missed.
    
  1. <%
  2. element = @element
  3. then: 0 else: 0 presenter = element&.presenter
  4. then: 0 else: 0 protocol = element&.protocol
  5. catalog_node ||= @catalog_node || nil
  6. current_user = @current_user
  7. params = @params
  8. locale = I18n.locale
  9. %>
  10. then: 0 <% if @loading %>
  11. <div class="h-[120px] rounded-xl bg-gray-300 bg-linear-to-r from-gray-300 to-gray-500 animate-gradient border border-gray-300 mb-4"></div>
  12. else: 0 <% else %>
  13. <%= turbo_frame_tag element do %>
  14. <div
  15. data-controller="protocol-elements"
  16. data-protocol-elements-element-value="<%= dom_id(element)%>"
  17. data-protocol-elements-id-value="<%= element.id %>"
  18. then: 0 else: 0 data-protocol-elements-node-value="<%= catalog_node&.id %>">
  19. then: 0 else: 0 then: 0 else: 0 <div class="protocol-element group <%= @edit ? "protocol-element--selected": "" %> <%= presenter.input? ? "protocol-element--input" : "protocol-element--info" %>"
  20. then: 0 else: 0 data-action="<%= !@edit ? "click->protocol-elements#edit" : ""%>">
  21. then: 0 else: 0 <% if current_user.can_edit_protocols? %>
  22. then: 0 <% if @edit %>
  23. <span class="hidden" aria-hidden="true" data-protocol-elements-target="preview"></span>
  24. <%= link_to presenter.show_path(catalog_node) , data: { turbo_method: :get,turbo_frame: element, 'protocol-elements-target': "preview show" }, class: 'hidden' do%>
  25. <% end %>
  26. else: 0 <% else %>
  27. <%= link_to presenter.settings_path(catalog_node) , data: { turbo_method: :get,turbo_frame: "protocol_sidebar", 'protocol-elements-target': "button" }, class: 'hidden' do%>
  28. <% end %>
  29. <%= link_to presenter.edit_path(catalog_node) , data: { turbo_method: :get,turbo_frame: element, 'protocol-elements-target': "button" }, class: 'hidden' do%>
  30. <% end %>
  31. <% end %>
  32. <div class="element__number hidden!"><%= presenter.position %></div>
  33. then: 0 else: 0 <% if !@edit %>
  34. <div class="absolute top-1/2 left-0 transform -translate-y-1/2 w-[24px] h-[24px] rounded items-center flex justify-center opacity-0 group-hover:opacity-100 cursor-move handle">
  35. <i class="fa-sharp fa-regular text-xs fa-grip-dots-vertical"></i>
  36. </div>
  37. <% end %>
  38. <% end %>
  39. <div class="protocol-element__wrapper">
  40. <div class="protocol-element__header">
  41. <div class="protocol-element__header-left">
  42. then: 0 <% if @edit %>
  43. <div class="
  44. then: 0 else: 0 <% if presenter.input? %> protocol-element__input
  45. <% else %> protocol-element__info
  46. <% end %>">
  47. <i class="copy-12 <%= presenter.icon%>"></i>
  48. </div>
  49. <span class="copy-14-medium text-black!"><%= presenter.type %></span>
  50. else: 0 <% else %>
  51. then: 0 else: 0 <% if !presenter.structure? %>
  52. <div class="relative w-full">
  53. <div class="absolute top-0 left-0 h-full w-full z-2 cursor-pointer"></div>
  54. <div class="opacity-70">
  55. <%= react_component("ProtocolPreview", { jsonData: element.presenter.json_data}) %>
  56. </div>
  57. </div>
  58. <% end %>
  59. <% end %>
  60. </div>
  61. <div class="protocol-element__header-right">
  62. then: 0 <% if @edit %>
  63. then: 0 else: 0 <% if presenter.input? %>
  64. <div class="element__toggle-wrapper">
  65. <span class="copy-12-medium text-black! w-max"><%= t(".mark_as_required") %></span>
  66. <%= turbo_frame_tag element, :required do %>
  67. then: 0 else: 0 <% if current_user.can_edit_protocols? %>
  68. <%= form_with model: [:admin_v2, element.becomes(::Protocol::Element)], data: { controller: "autosubmit", autosubmit_target: "form" } do |form| %>
  69. <input name="protocol_element[required]" type="hidden" value="0" autocomplete="off">
  70. then: 0 else: 0 <%= react_component("Toggle", { id: "protocol_element_required", name: "protocol_element[required]", checked: presenter.required ? "checked" : "false", small: true, inputDataAttributes: { action: "change->autosubmit#submit", turbo_frame: "_top" } }) %>
  71. then: 0 else: 0 <%= form.hidden_field :catalog_node_id, value: @catalog_node&.id %>
  72. <% end %>
  73. <% end %>
  74. <% end %>
  75. </div>
  76. <% end %>
  77. <div data-controller="clipboard">
  78. <button data-action="click->clipboard#copy" data-clipboard-export-param="<%= @export_url %>" data-clipboard-broadcast-param="<%= @broadcast_url %>"
  79. class="bg-neutral_bg w-[24px] h-[24px] rounded items-center flex justify-center text-[10px] fa-regular fa-copy text-neutral_text"
  80. data-toggle="tooltip"
  81. title="<%= t('.copy_to_clipboard') %>"
  82. ></button>
  83. </div>
  84. <div data-controller="modal">
  85. <%= render LooposUi::Modal.for_duplicate(model_name: "Element") do |modal| %>
  86. <% modal.with_trigger do %>
  87. <button data-action="click->modal#open"
  88. class="bg-neutral_bg w-[24px] h-[24px] rounded items-center flex justify-center text-[10px] fa-kit fa-regular-copy-circle-plus text-neutral_text"
  89. data-toggle="tooltip"
  90. title="<%= t('.duplicate_protocol_element') %>"
  91. ></button>
  92. <% end %>
  93. then: 0 else: 0 <% modal.with_primary_action(text: t(".confirm"), href: duplicate_element_admin_v2_protocol_element_path(element, catalog_node_id: @catalog_node&.id), tag_options: { type: "submit", "data-turbo-method": :post, turbo_frame: "_top", action: "click->protocol-elements#resetWindowLocks" }) %>
  94. <% end %>
  95. </div>
  96. <%= render LooposUi::Modal.for_delete(model_name: "Protocol Element") do |modal| %>
  97. <% modal.with_trigger do %>
  98. <button type="button" class="flex items-center justify-center w-[24px] h-[24px] bg-danger_bg text-danger-fg rounded text-[10px] fa-regular fa-trash-can"></button>
  99. <% end %>
  100. <% modal.with_custom_content do %>
  101. <%= form_with(
  102. model: [:admin_v2, element.becomes(::Protocol::Element)],
  103. id: "#{element.id}",
  104. class: "flex items-center justify-center w-fit h-fit",
  105. method: "delete",
  106. data: { turbo_frame: "#{dom_id(protocol)}_protocol_elements", "protocol-elements-target": "deleteForm" }) do |form| %>
  107. then: 0 else: 0 <%= form.hidden_field :catalog_node_id, name: :catalog_node_id, value: catalog_node&.id %>
  108. <% end %>
  109. <% end %>
  110. <% modal.with_primary_action(
  111. text: t(".confirm"),
  112. kind: :app,
  113. tag_options: {
  114. form: "#{element.id}",
  115. type: "submit",
  116. data: {
  117. action: "click->lui--button#startLoading click->actions_loading#run"
  118. }
  119. }
  120. ) %>
  121. <% end %>
  122. else: 0 <% else %>
  123. <% presenter.enabled_apps.each do |app, enabled| %>
  124. then: 0 <% if app.to_s == "submission_extra" && enabled %>
  125. <div class="relative inline-block">
  126. <%= react_component("LogoApp", {
  127. app: "submission",
  128. size: "small",
  129. icon: true
  130. }) %>
  131. <div class="absolute bottom-[15px] left-[15px] w-3 h-3">
  132. <%= image_tag('submission-extra-arrow-core.svg', width: '12', height: '12') %>
  133. </div>
  134. else: 0 </div>
  135. then: 0 else: 0 <% elsif enabled %>
  136. <%= react_component("LogoApp", {
  137. app: app,
  138. size: "small",
  139. icon: true
  140. }) %>
  141. <% end %>
  142. <% end %>
  143. <% end %>
  144. </div>
  145. </div>
  146. then: 0 else: 0 <% if @edit %>
  147. then: 0 else: 0 <div class="protocol-element__content <%= presenter.input? ? "protocol-element__content--input" : "protocol-element__content--info" %>">
  148. <%= form_with(model: [:admin_v2, @element.becomes(::Protocol::Element)], id:"#{dom_id(element)}" , class: "w-full", data: { controller: "autosubmit", "autosubmit-target": "form", turbo_frame: dom_id(element) }) do |form| %>
  149. <%
  150. element = @element
  151. presenter = element.presenter
  152. schema = presenter.left_settings_schema
  153. protocol = element.protocol
  154. %>
  155. <%# label %>
  156. <% edit_value = element.untranslated_label_html(locale: locale).presence || element.untranslated_label(locale: locale).to_s %>
  157. <div class='w-full'>
  158. then: 0 <% if @element.is_a?(Protocol::Element::Info::Html) %>
  159. <%= render LooposUi::Inputs::TextArea.new(
  160. value: edit_value,
  161. name: "protocol_element[untranslated_label_html]",
  162. rows: 5
  163. ) %>
  164. else: 0 <% else %>
  165. <%= render(LooposUi::Wysiwyg.new(initial_value: edit_value, input_name: "protocol_element[untranslated_label_html]", small: true, app: "neutral")) %>
  166. <% end %>
  167. <%#= form.text_field :label, class: "protocol-input", data: { controller: "#dropkiq", "preview-id": "left-setting-label-#{form.object.id}-preview" }, value: edit_value %>
  168. then: 0 else: 0 <%= form.hidden_field "catalog_node_id", value: @catalog_node&.id %>
  169. <div id="left-setting-label-<%= form.object.id %>-preview" class='dropkiq-preview'></div>
  170. </div>
  171. <% schema[:properties].each do |attribute, properties| %>
  172. <%
  173. specific_component = "LooposUi::Protocol::Elements::Fields::#{properties[:type].camelize}".constantize
  174. then: 0 else: 0 default_value = element.respond_to?(attribute) ? element.send(attribute) : nil
  175. translatable_setting = element.class.respond_to?(:translatable_settings) && element.class.translatable_settings.include?(attribute.to_s)
  176. then: 0 value = if translatable_setting && element.respond_to?("#{attribute}_#{locale}")
  177. element.send("#{attribute}_#{locale}")
  178. else: 0 else
  179. default_value
  180. end
  181. then: 0 else: 0 settings_key = translatable_setting ? "#{attribute}_#{locale}" : attribute
  182. edit_value = element.settings_hash.dig(settings_key.to_s) || value
  183. properties[:value] = value
  184. %>
  185. <%= render specific_component.new(
  186. attribute: attribute,
  187. properties: properties,
  188. value: value,
  189. element: element,
  190. protocol: protocol,
  191. edit_value: edit_value,
  192. form: form) %>
  193. <% end %>
  194. <% end %>
  195. </div>
  196. <% end %>
  197. </div>
  198. </div>
  199. </div>
  200. <% end %>
  201. <% end %>

app/components/loopos_ui/protocol/protocol_element_component.rb

50.0% lines covered

100.0% branches covered

16 relevant lines. 8 lines covered and 8 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 class ProtocolElementComponent < ViewComponent::Base
  4. 1 include Turbo::FramesHelper
  5. 1 include Turbo::StreamsHelper
  6. 1 include LooposUi::InlineEditComponent
  7. 1 renders_one :editable_content
  8. 1 def initialize(loading: false, element: nil, catalog_node: nil, current_user: nil, edit: false, title: nil,
  9. export_url: nil, broadcast_url: nil)
  10. @loading= loading
  11. @element= element
  12. @current_user = current_user
  13. @catalog_node = catalog_node
  14. @edit = edit
  15. @title = title
  16. @export_url = export_url
  17. @broadcast_url = broadcast_url
  18. end
  19. end
  20. end
  21. end

app/components/loopos_ui/protocol/protocol_filter_component.html.erb

0.0% lines covered

0.0% branches covered

54 relevant lines. 0 lines covered and 54 lines missed.
18 total branches, 0 branches covered and 18 branches missed.
    
  1. <%
  2. # Extract the keys from `filter` that have `true` values
  3. filter = @filter.map(&:to_s)
  4. # Add additional keys to check
  5. apps = ::Protocol::Element.enabled_apps.keys + [:link]
  6. apps = apps.map(&:to_s)
  7. then: 0 if @protocol_show
  8. highlight_url = highlight_admin_v2_protocol_path(@protocol)
  9. form_url = filter_elements_admin_v2_protocol_path(@protocol)
  10. else: 0 else
  11. disabled_protocols = @disabled_protocols
  12. then: 0 else: 0 disabled_protocols_url = @catalog_node.catalogable_type == "Catalog::Product" ? toggle_protocols_admin_v2_catalog_product_path(@catalog_node.catalogable_id) : toggle_protocols_admin_v2_catalog_category_path(@catalog_node.catalogable_id)
  13. then: 0 else: 0 form_url = @catalog_node.catalogable_type == "Catalog::Product" ? filter_protocols_admin_v2_catalog_product_path(@catalog_node.catalogable_id) : filter_protocols_admin_v2_catalog_category_path(@catalog_node.catalogable_id)
  14. paste_configuration_copy = t(".paste_config_details", element: @catalog_node.catalogable_type.split("::").second)
  15. end
  16. %>
  17. <div class="protocol-filter mb-8">
  18. <div class="flex flex-row items-center justify-between">
  19. <div class="flex flex-row items-center justify-center gap-2">
  20. then: 0 <% if @protocol_show %>
  21. <p class="copy-14-medium text-app-800-primary"><%= t(".catalog_section") %></p>
  22. <%= form_tag(highlight_url, method: :put, remote: false, class: "contents", id: 'protocol_highlight', data: { turbo_frame: '_top', controller: "autosubmit", autosubmit_target: "form" }, multipart: true) do |form| %>
  23. <%= react_component(
  24. "Toggle",
  25. {
  26. name: "protocol[highlight]",
  27. then: 0 else: 0 checked: @protocol.highlight == true ? 'checked' : false,
  28. dataAttributes:{ controller: "loading", "loading-target": "toggle"},
  29. inputDataAttributes: {
  30. action: "change->autosubmit#submit change->loading#setToggleLoading"
  31. },
  32. }
  33. )
  34. %>
  35. <% end %>
  36. else: 0 <% else %>
  37. <p class="copy-14-medium text-app-800-primary"><%= t(".show_disabled") %></p>
  38. <%= form_tag(disabled_protocols_url, method: :get, remote: false, class: "contents", id: 'applicable_catalog_node_protocol_form', data: { turbo_stream: true, controller: "autosubmit", autosubmit_target: "form" }, multipart: true) do |form| %>
  39. <%= react_component(
  40. "Toggle",
  41. {
  42. id: "disabled_protocols",
  43. name: "disabled_protocols",
  44. then: 0 else: 0 checked: disabled_protocols == 'true' ? 'checked' : 'false',
  45. dataAttributes:{ controller: "loading", "loading-target": "toggle"},
  46. inputDataAttributes: {
  47. action: "change->autosubmit#submit change->loading#setToggleLoading"
  48. },
  49. }
  50. )
  51. %>
  52. <% end %>
  53. <% end %>
  54. </div>
  55. <div class="flex flex-row items-center gap-4">
  56. <p class="copy-14-medium text-app-800-primary"><%= t(".view_by") %></p>
  57. <div class="flex flex-row items-center gap-2">
  58. <% apps.each do |app| %>
  59. <%= form_with(url: form_url) do |form| %>
  60. <% values = filter.dup %>
  61. then: 0 else: 0 <% values.include?(app) ? values.delete(app) : values.append(app) %>
  62. <%= form.button nil, value: false, name: :turbo_stream do %>
  63. <% values.each do |v| %>
  64. <%= form.hidden_field :enabled_apps, multiple: true, value: v %>
  65. <% end %>
  66. <div class="relative inline-block">
  67. then: 0 <% if app == "submission_extra" %>
  68. <%= react_component("AppButton", {
  69. app: "submission",
  70. size: "small",
  71. active: !filter.include?(app)
  72. }) %>
  73. <div class="absolute top-px right-px">
  74. <%= image_tag('submission-extra-arrow-core.svg', width: '12', height: '12') %>
  75. </div>
  76. else: 0 <% else %>
  77. <%= react_component("AppButton", {
  78. app: app,
  79. size: "small",
  80. active: !filter.include?(app)
  81. }) %>
  82. <% end %>
  83. </div>
  84. <% end %>
  85. <% end %>
  86. <% end %>
  87. </div>
  88. </div>
  89. </div>
  90. then: 0 else: 0 <% if @protocol_show == false %>
  91. <div data-controller="modal clipboard" class="flex flex-row gap-1">
  92. <% copy_configuration = capture do %>
  93. <%= react_component("Button", { text: "", app: "core", icon: "fa-kit fa-regular-copy-gear", variant: "secondary", size: "large", iconPosition: "left", dataAttributes: { 'action': "click->clipboard#copy", 'clipboard-export-param': export_protocols_configuration_admin_v2_catalog_node_path(@catalog_node), 'clipboard-broadcast-param': broadcast_copied_protocols_configuration_admin_v2_catalog_nodes_path } }) %>
  94. <% end %>
  95. <%= react_component("Tooltip", { text: t(".copy_configuration"), children: copy_configuration, placement: "top" }) %>
  96. <%= render LooposUi::Modal.new(
  97. title: t(".confirm_paste_configuration"),
  98. description: raw(paste_configuration_copy)
  99. ) do |modal| %>
  100. <% modal.with_trigger do %>
  101. <% paste_configuration = capture do %>
  102. <%= react_component("Button", { text: "", app: "core", icon: "fa-kit fa-regular-paste-gear", variant: "secondary", size: "large", iconPosition: "left", dataAttributes: { "loading-target": "toggle" } }) %>
  103. <% end %>
  104. <%= react_component("Tooltip", { text: t(".paste_configuration"), children: paste_configuration, placement: "top" }) %>
  105. <% end %>
  106. <% modal.with_primary_action(
  107. text: t('.paste'),
  108. kind: :app,
  109. tag_options: {
  110. data: {
  111. action: "click->clipboard#paste click->lui--button#startLoading click->actions_loading#run",
  112. "clipboard-import-param": import_protocols_configuration_admin_v2_catalog_node_path(@catalog_node),
  113. "clipboard-node-param": @catalog_node.id,
  114. "clipboard-refresh-param": "true"
  115. }
  116. }
  117. ) %>
  118. <% end %>
  119. </div>
  120. <% end %>
  121. </div>

app/components/loopos_ui/protocol/protocol_filter_component.rb

50.0% lines covered

100.0% branches covered

10 relevant lines. 5 lines covered and 5 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 class ProtocolFilterComponent < ViewComponent::Base
  4. 1 include Turbo::FramesHelper
  5. 1 def initialize(filter: nil, disabled_protocols: nil, catalog_node: nil, protocol_show: false, protocol: nil)
  6. @filter = filter
  7. @disabled_protocols = disabled_protocols
  8. @catalog_node = catalog_node
  9. @protocol_show = protocol_show
  10. @protocol = protocol
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/protocol/protocol_header.html.erb

0.0% lines covered

0.0% branches covered

105 relevant lines. 0 lines covered and 105 lines missed.
46 total branches, 0 branches covered and 46 branches missed.
    
  1. <%
  2. protocol= @protocol
  3. readonly||= @readonly ||= false
  4. current_catalog_node ||= @current_catalog_node ||= nil
  5. current_user = @current_user
  6. protocol_presenter = protocol.presenter
  7. show_inheritance_path = current_catalog_node.present? && protocol.catalog_node_ids.exclude?(current_catalog_node.id)
  8. inherited_protocol_path ||= show_inheritance_path && protocol.inheritance_path(current_catalog_node)
  9. applicable_catalog_node_protocol ||= protocol_presenter.applicable_catalog_node_protocol(current_catalog_node)
  10. show_opened ||= @show_opened ||= false
  11. export_modal_protocol_id = "export_protocol_#{dom_id(protocol)}"
  12. duplicate_modal_protocol_id = "duplicate_protocol_#{dom_id(protocol)}"
  13. ai_locked = protocol_presenter.ai_locked?
  14. lock_tooltip = t(".ai_action_disabled_while_ai_running")
  15. %>
  16. then: 0 else: 0 <div class="protocol-component__header <%= show_inheritance_path.present? ? "": "protocol-component__header--open" %>" data-protocol-accordion-target="header">
  17. <div class="protocol-component__header-content">
  18. <div class="protocol-component__header-left">
  19. then: 0 else: 0 <div class="protocol-component__trigger--close <%= show_inheritance_path.present? ? "rotate-[0]!": "" %>" id="#accordion-collapse-<%= dom_id(protocol)%>-header"
  20. data-accordion-target="#accordion-collapse-<%= dom_id(protocol)%>"
  21. data-action="click->protocol-accordion#toggleClass"
  22. then: 0 else: 0 aria-expanded="<%= show_inheritance_path.present? ? "false": "true" %>"
  23. aria-controls="accordion-collapse-<%= dom_id(protocol)%>">
  24. <svg data-accordion-icon class="w-3 h-3 shrink-0" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
  25. then: 0 else: 0 <path xmlns="http://www.w3.org/2000/svg" d="M10.375 1.42188L5.89844 5.71094C5.75781 5.82812 5.61719 5.875 5.5 5.875C5.35938 5.875 5.21875 5.82812 5.10156 5.73438L0.601562 1.42188C0.367188 1.21094 0.367188 0.835938 0.578125 0.625C0.789062 0.390625 1.16406 0.390625 1.375 0.601562L5.5 4.53906L9.60156 0.601562C9.8125 0.390625 10.1875 0.390625 10.3984 0.625C10.6094 0.835938 10.6094 1.21094 10.375 1.42188Z" fill="<%= show_inheritance_path.present? ? "#212529": "#B53C00" %>"/>
  26. </svg>
  27. </div>
  28. <span class="protocol-component__name copy-14-medium" id="<%= dom_id(protocol)%>_name">
  29. <%= protocol.name %>
  30. </span>
  31. </div>
  32. then: 0 <% if applicable_catalog_node_protocol.present? %>
  33. <div class="protocol-component__header-right">
  34. then: 0 else: 0 <% if show_inheritance_path %>
  35. <% icon = capture do %>
  36. <%= link_to protocol_presenter.link_to_attached_node(current_catalog_node), remote: false, data: { turbo_frame: '_top' } do %>
  37. <div class="protocol-component__header-inheritance copy-12 tooltip-wrapper">
  38. <span class="flex">
  39. <i class="fa-regular fa-diagram-nested text-app-800-primary"></i>
  40. </span>
  41. <span class="protocol-component__header-inheritance-title copy-12 font-bold underline text-app-800-primary"><%= inherited_protocol_path %></span>
  42. </div>
  43. <% end %>
  44. <% end %>
  45. <%= react_component("Tooltip", { text: "#{t(".inherited_from")} <a href='#{protocol_presenter.link_to_attached_node(current_catalog_node)}'>#{inherited_protocol_path}</a>" , children: icon, placement: "top" }) %>
  46. <% end %>
  47. then: 0 else: 0 <% if readonly && current_user.can_edit_protocols? %>
  48. <div class="form-layout__status">
  49. <%# FIXME: This should not reload the page %>
  50. <%= form_tag(active_for_catalog_node_admin_v2_protocol_path(protocol.id), method: :put, remote: false, class: "contents", id: 'applicable_catalog_node_protocol_form', data: { turbo_frame: '_top', controller: "autosubmit", autosubmit_target: "form" }, multipart: true) do |form| %>
  51. <input name="referrer" type="hidden" value="<%= request.original_url %>" autocomplete="off">
  52. <input name="applicable_catalog_node_protocol[id]" type="hidden" value="<%= applicable_catalog_node_protocol.id %>" autocomplete="off">
  53. <input name="applicable_catalog_node_protocol[active]" type="hidden" value="0" autocomplete="off">
  54. <%= react_component(
  55. "Toggle",
  56. {
  57. id: "applicable_catalog_node_protocol_active",
  58. name: "applicable_catalog_node_protocol[active]",
  59. then: 0 else: 0 checked: applicable_catalog_node_protocol.active ? "checked" : "false",
  60. dataAttributes:{ controller: "loading", "loading-target": "toggle"},
  61. inputDataAttributes: {
  62. then: 0 else: 0 action: current_user.can_edit_protocol_inheritance? && !ai_locked ? "change->autosubmit#submit change->loading#setToggleLoading" : "",
  63. turbo_frame: "_top"
  64. },
  65. blocked: !current_user.can_edit_protocol_inheritance? || ai_locked,
  66. }
  67. )
  68. %>
  69. <% end %>
  70. </div>
  71. <% end %>
  72. then: 0 else: 0 <% if !show_inheritance_path %>
  73. then: 0 <% if ai_locked %>
  74. <% move_locked = capture do %>
  75. <span class="inline-flex pointer-events-none" aria-disabled="true" role="presentation">
  76. then: 0 else: 0 <i class="fa-regular <%= protocol.highlight ? "fa-arrow-down-from-bracket": "fa-arrow-up-from-bracket" %> text-gray-400"></i>
  77. </span>
  78. <% end %>
  79. <%= react_component("Tooltip", { text: lock_tooltip, children: move_locked, placement: "top" }) %>
  80. else: 0 <% else %>
  81. <%= form_tag(highlight_admin_v2_protocol_path(protocol), method: :put, remote: false, class: "contents", id: 'protocol_highlight', data: { turbo_frame: '_top', controller: "autosubmit", autosubmit_target: "form" }, multipart: true) do |form| %>
  82. <input name="protocol[highlight]" type="hidden" value="<%= !protocol.highlight %>" autocomplete="off">
  83. then: 0 else: 0 <input name="protocol[catalog_node_id]" type="hidden" value="<%= current_catalog_node&.id %>" autocomplete="off">
  84. <% move = capture do %>
  85. then: 0 else: 0 <button type="submit" class=""><i class="fa-regular <%= protocol.highlight ? "fa-arrow-down-from-bracket": "fa-arrow-up-from-bracket" %> text-app-800-primary cursor-pointer"></i></button>
  86. <% end %>
  87. then: 0 else: 0 <%= react_component("Tooltip", { text: protocol.highlight ? t(".move_to_main") : t(".move_to_catalog") , children: move, placement: "top" }) %>
  88. <% end %>
  89. <% end %>
  90. then: 0 <% if readonly && current_user.can_edit_protocols? %>
  91. <i class="fa-regular fa-clone text-gray-400"></i>
  92. <i class="fa-regular fa-trash-alt text-gray-400"></i>
  93. else: 0 <% else %>
  94. then: 0 <% if ai_locked %>
  95. <% settings_locked = capture do %>
  96. <i class="fa-regular fa-gear text-gray-400 pointer-events-none cursor-not-allowed" aria-hidden="true"></i>
  97. <% end %>
  98. <%= react_component("Tooltip", { text: lock_tooltip, children: settings_locked, placement: "top" }) %>
  99. else: 0 <% else %>
  100. <% settings = capture do %>
  101. then: 0 else: 0 <%= link_to settings_admin_v2_protocol_path(protocol, catalog_node_id: current_catalog_node.present? ? current_catalog_node.id : nil) , data: { turbo_frame: 'lui-main-layout-drawer_bar', action: "click->protocol-accordion#moveSidebar click->protocol-accordion#selectedProtocol" }, class: 'btn btn-primary' do %>
  102. <i class="fa-regular fa-gear text-app-800-primary cursor-pointer"></i>
  103. <% end %>
  104. <% end %>
  105. <%= react_component("Tooltip", { text: t(".open_settings") , children: settings, placement: "top" }) %>
  106. <% end %>
  107. <%= render LooposUi::Modal.for_export(model_name: "Protocol", id: export_modal_protocol_id) do |modal| %>
  108. <% export = capture do %>
  109. <i class="fa-regular fa-download text-app-800-primary cursor-pointer"></i>
  110. <% end %>
  111. <% modal.with_trigger do %>
  112. <%= react_component("Tooltip", { text: t(".export_protocol") , children: export, placement: "top" }) %>
  113. <% end %>
  114. <% modal.with_primary_action(
  115. text: t(".confirm"),
  116. href: export_admin_v2_protocol_path(protocol, format: :json),
  117. kind: :app,
  118. tag_options: { data: { action: "click->lui--button#startLoading click->actions_loading#run" } }
  119. ) %>
  120. <% end %>
  121. <div data-controller="clipboard">
  122. <% copy = capture do %>
  123. <i class="fa-regular fa-copy text-app-800-primary cursor-pointer" data-action="click->clipboard#copy" data-clipboard-export-param="<%= export_admin_v2_protocol_path(protocol) %>" data-clipboard-broadcast-param="<%= broadcast_copied_protocol_admin_v2_protocols_path %>"></i>
  124. <% end %>
  125. <%= react_component("Tooltip", { text: t(".copy_protocol") , children: copy, placement: "top" }) %>
  126. </div>
  127. <%= render LooposUi::Modal.for_duplicate(model_name: "Protocol", id: duplicate_modal_protocol_id) do |modal| %>
  128. <% clone = capture do %>
  129. <i class="fa-kit fa-regular-copy-circle-plus text-app-800-primary cursor-pointer"></i>
  130. <% end %>
  131. <% modal.with_trigger do %>
  132. <%= react_component("Tooltip", { text: t(".duplicate_protocol") , children: clone, placement: "top" }) %>
  133. <% end %>
  134. <% modal.with_primary_action(
  135. text: t(".confirm"),
  136. href: duplicate_admin_v2_protocol_path(protocol, catalog_node_id: current_catalog_node.id),
  137. kind: :app,
  138. tag_options: { data: { "turbo-method": :post, action: "click->lui--button#startLoading click->actions_loading#run" } }
  139. ) %>
  140. <% end %>
  141. then: 0 <% if ai_locked %>
  142. <% remove_locked = capture do %>
  143. <i class="fa-regular fa-trash-alt text-gray-400 cursor-not-allowed pointer-events-none" aria-hidden="true"></i>
  144. <% end %>
  145. <%= react_component("Tooltip", { text: lock_tooltip, children: remove_locked, placement: "top" }) %>
  146. else: 0 <% else %>
  147. <%= render LooposUi::Modal.new(title: t(".delete_protocol")) do |modal| %>
  148. <% remove = capture do %>
  149. <i class="fa-regular fa-trash-alt text-app-800-primary cursor-pointer"></i>
  150. <% end %>
  151. <% modal.with_trigger do %>
  152. <%= react_component("Tooltip", { text: t(".delete_protocol") , children: remove, placement: "top" }) %>
  153. <% end %>
  154. <% modal.with_primary_action(
  155. text: t(".confirm"),
  156. href: admin_v2_protocol_path(protocol),
  157. kind: :app,
  158. tag_options: { data: { "turbo-method": :delete, action: "click->lui--button#startLoading click->actions_loading#run" } }
  159. ) %>
  160. <%= tag.span(t(".delete_protocol_text")) %>
  161. <% end %>
  162. <% end %>
  163. <% end %>
  164. <% end %>
  165. <div class="protocol-component__trigger" id="#accordion-collapse-<%= dom_id(protocol)%>-header"
  166. data-accordion-target="#accordion-collapse-<%= dom_id(protocol)%>"
  167. data-action="click->protocol-accordion#toggleClass"
  168. aria-controls="accordion-collapse-<%= dom_id(protocol)%>">
  169. <svg data-accordion-icon class="w-3 h-3 shrink-0" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
  170. then: 0 else: 0 <path xmlns="http://www.w3.org/2000/svg" d="M10.375 1.42188L5.89844 5.71094C5.75781 5.82812 5.61719 5.875 5.5 5.875C5.35938 5.875 5.21875 5.82812 5.10156 5.73438L0.601562 1.42188C0.367188 1.21094 0.367188 0.835938 0.578125 0.625C0.789062 0.390625 1.16406 0.390625 1.375 0.601562L5.5 4.53906L9.60156 0.601562C9.8125 0.390625 10.1875 0.390625 10.3984 0.625C10.6094 0.835938 10.6094 1.21094 10.375 1.42188Z" fill="<%= show_inheritance_path.present? ? "#212529": "#B53C00" %>"/>
  171. </svg>
  172. </div>
  173. </div>
  174. else: 0 <% else %>
  175. <div class="protocol-component__header-right">
  176. then: 0 else: 0 <% if !show_inheritance_path %>
  177. then: 0 else: 0 <% if readonly && current_user.can_edit_protocols? %>
  178. <i class="fa-regular fa-clone text-gray-400"></i>
  179. <i class="fa-regular fa-trash-alt text-gray-400"></i>
  180. <% end %>
  181. <% end %>
  182. <div class="protocol-component__trigger" id="#accordion-collapse-<%= dom_id(protocol)%>-header"
  183. data-accordion-target="#accordion-collapse-<%= dom_id(protocol)%>"
  184. data-action="click->protocol-accordion#toggleClass"
  185. aria-controls="accordion-collapse-<%= dom_id(protocol)%>">
  186. <svg data-accordion-icon class="w-3 h-3 shrink-0" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
  187. then: 0 else: 0 <path xmlns="http://www.w3.org/2000/svg" d="M10.375 1.42188L5.89844 5.71094C5.75781 5.82812 5.61719 5.875 5.5 5.875C5.35938 5.875 5.21875 5.82812 5.10156 5.73438L0.601562 1.42188C0.367188 1.21094 0.367188 0.835938 0.578125 0.625C0.789062 0.390625 1.16406 0.390625 1.375 0.601562L5.5 4.53906L9.60156 0.601562C9.8125 0.390625 10.1875 0.390625 10.3984 0.625C10.6094 0.835938 10.6094 1.21094 10.375 1.42188Z" fill="<%= show_inheritance_path.present? ? "#212529": "#B53C00" %>"/>
  188. </svg>
  189. </div>
  190. </div>
  191. <% end %>
  192. </div>
  193. </div>

app/components/loopos_ui/protocol/protocol_sidebar_component.erb

0.0% lines covered

0.0% branches covered

7 relevant lines. 0 lines covered and 7 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <turbo-frame id='protocol_sidebar_inner' class='h-full block'>
  2. then: 0 else: 0 <div class="protocol-sidebar <%= !@app_block ? "protocol-sidebar--fit" : "h-full"%>">
  3. <%# <div class="protocol-sidebar__section protocol-sidebar__section--header copy-16-medium"> %>
  4. <%# header %>
  5. <%# </div> %>
  6. then: 0 else: 0 <div class="protocol-sidebar__content-wrapper <%= !@app_block ? "protocol-sidebar__content-wrapper--full" : ""%>">
  7. <% content_sections.each do |section| %>
  8. <div class="protocol-sidebar__section">
  9. <%= section %>
  10. </div>
  11. <% end %>
  12. </div>
  13. </div>
  14. </turbo-frame>

app/components/loopos_ui/protocol/protocol_sidebar_component.rb

87.5% lines covered

100.0% branches covered

8 relevant lines. 7 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 class ProtocolSidebarComponent < ViewComponent::Base
  4. 1 include Turbo::FramesHelper
  5. 1 renders_one :header
  6. 1 renders_many :content_sections
  7. 1 def initialize(app_block: false)
  8. @app_block = app_block
  9. end
  10. end
  11. end
  12. end

app/components/loopos_ui/protocol/protocols_area_component.erb

0.0% lines covered

0.0% branches covered

25 relevant lines. 0 lines covered and 25 lines missed.
8 total branches, 0 branches covered and 8 branches missed.
    
  1. <div class="protocol-area flex protocol-sidebar">
  2. <div class="protocol-area__section">
  3. <%= add_protocol(highlighted: true) %>
  4. then: 0 else: 0 <% if inherited_highlighted_protocol_section.present? %>
  5. <div class="protocol-area__inherited bg-gray-200" >
  6. <div class="protocol-area__inherited-title">
  7. <% icon = capture do %>
  8. <i class="copy-12 font-bold text-app-800-primary fa-solid fa-lock"></i>
  9. <% end %>
  10. <%= react_component("Tooltip", { text: "Inherited Protocol" , children: icon, placement: "right" }) %>
  11. <p class="copy-12 font-bold ">Inherited protocols</p>
  12. </div>
  13. <%= inherited_highlighted_protocol_section %>
  14. </div>
  15. <% end %>
  16. <div class="protocol-area__highlighted"
  17. data-controller="sortable-protocol"
  18. data-sortable-protocol-catalog-node-patch-url-value="<%= @update_path %>">
  19. then: 0 else: 0 <% if highlighted_protocol_section.present? %>
  20. <%= highlighted_protocol_section%>
  21. <% end %>
  22. </div>
  23. </div>
  24. <hr class="protocol-area__separator">
  25. <div class="protocol-area__section">
  26. <%= add_protocol %>
  27. then: 0 else: 0 <% if inherited_protocol_section.present? %>
  28. <div class="protocol-area__inherited bg-gray-200" >
  29. <div class="protocol-area__inherited-title">
  30. <% icon = capture do %>
  31. <i class="copy-12 font-bold text-app-800-primary fa-solid fa-lock"></i>
  32. <% end %>
  33. <%= react_component("Tooltip", { text: "Inherited Protocol" , children: icon, placement: "right" }) %>
  34. <p class="copy-12 font-bold ">Inherited protocols</p>
  35. </div>
  36. <%= inherited_protocol_section%>
  37. </div>
  38. <% end %>
  39. <div class="protocol-area__not-highlighted"
  40. data-controller="sortable-protocol"
  41. data-sortable-protocol-catalog-node-patch-url-value="<%= @update_path %>">
  42. then: 0 else: 0 <% if protocol_section.present? %>
  43. <%= protocol_section%>
  44. <% end %>
  45. </div>
  46. </div>
  47. </div>

app/components/loopos_ui/protocol/protocols_area_component.rb

46.67% lines covered

100.0% branches covered

30 relevant lines. 14 lines covered and 16 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Protocol
  3. 1 class ProtocolsAreaComponent < ViewComponent::Base
  4. 1 include Turbo::FramesHelper
  5. 1 renders_one :inherited_highlighted_protocol_section
  6. 1 renders_one :highlighted_protocol_section
  7. 1 renders_one :inherited_protocol_section
  8. 1 renders_one :protocol_section
  9. 1 def initialize(update_path: nil, current_user:, turbo_frame_id:, catalog_node:, model:)
  10. @update_path = update_path
  11. @current_user = current_user
  12. @turbo_frame_id = turbo_frame_id
  13. @catalog_node = catalog_node
  14. @model = model
  15. end
  16. 1 def add_protocol(highlighted: false)
  17. render(LooposUi::Protocol::AddProtocol.new(
  18. highlighted: highlighted,
  19. current_user: @current_user,
  20. turbo_frame_id: @turbo_frame_id,
  21. catalog_node: @catalog_node,
  22. model: @model,
  23. ))
  24. end
  25. 1 def render_protocols(protocols, attached_protocols, filter)
  26. protocols.map do |protocol|
  27. is_readonly = attached_protocols.exclude?(protocol)
  28. render(LooposUi::Protocol::ProtocolComponent.new(
  29. protocol: protocol,
  30. readonly: is_readonly,
  31. current_catalog_node: @catalog_node,
  32. current_user: @current_user,
  33. )) do |component|
  34. component.with_elements_list do
  35. render(
  36. "admin/v2/protocols/protocol_elements_list",
  37. protocol: protocol,
  38. readonly: is_readonly,
  39. current_catalog_node: @catalog_node,
  40. show_opened: true,
  41. filter: filter,
  42. )
  43. end
  44. end
  45. end.join.html_safe
  46. end
  47. end
  48. 1 class AddProtocol < ViewComponent::Base
  49. 1 include Turbo::FramesHelper
  50. 1 def initialize(highlighted: false, current_user:, turbo_frame_id:, catalog_node:, model:)
  51. @highlighted = highlighted
  52. @current_user = current_user
  53. @turbo_frame_id = turbo_frame_id
  54. @catalog_node = catalog_node
  55. @model = model
  56. end
  57. end
  58. end
  59. end

app/components/loopos_ui/protocol/settings.html.erb

0.0% lines covered

0.0% branches covered

57 relevant lines. 0 lines covered and 57 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%= turbo_frame_tag 'protocol_sidebar' do %>
  2. <%= turbo_stream_from "protocol_sidebar_stream" %>
  3. <%= render LooposUi::Protocol::ProtocolSidebarComponent.new do |sidebar| %>
  4. <%
  5. =begin%>
  6. <% sidebar.with_header do %>
  7. <%= turbo_stream_from "protocol_sidebar_stream" %>
  8. <div class="flex flex-row items-center justify-between" data-controller="protocol-accordion">
  9. <div class="flex flex-row items-center gap-2">
  10. <i class="fa-regular fa-bars-staggered"></i>
  11. <h1>Protocol Settings</h1>
  12. </div>
  13. <%= link_to preview_protocol_path, data: { turbo_frame: 'protocol_sidebar', action: "click->protocol-accordion#closeSidebar click->protocol-accordion#selectedProtocol" }, class: 'btn btn-primary' do %>
  14. <i class="fa-solid fa-xmark text-gray-600 cursor-pointer"></i>
  15. <% end %>
  16. </div>
  17. <% end %>
  18. <%
  19. =end %>
  20. <% sidebar.with_content_section do %>
  21. <div class="protocol_sidebar__section--padded">
  22. <%= turbo_frame_tag "show_protocol_name", class: "flex flex-row items-center justify-between gap-4" do %>
  23. <div class="flex flex-col gap-1">
  24. <%= turbo_frame_tag "edit_protocol_name", class: "editComponents flex flex-row items-center gap-base" do %>
  25. <p class="text-foreground copy-14 font-bold"><%= protocol_name %></p>
  26. <% end %>
  27. <p class="copy-12 text-content-secondary">
  28. <%= t(".protocol_info.update_date") %>: <%= formatted_updated_at %>
  29. </p>
  30. <p class="copy-12 text-content-secondary">
  31. <%= t(".protocol_info.created_date") %>: <%= formatted_created_at %>
  32. </p>
  33. </div>
  34. <%= link_to edit_protocol_path, class: "min-w-fit", data: { turbo: true, turbo_frame: "edit_protocol_name" } do %>
  35. <%= react_component("Button", { text: t(".edit_protocol_name.button"), app: "neutral", icon: "fa-regular fa-pencil", variant: "secondary", size: "medium", iconPosition: "left", dataAttributes: { 'action': "click->modal#open" } }) %>
  36. <% end %>
  37. <% end %>
  38. </div>
  39. <% end %>
  40. <% sidebar.with_content_section do %>
  41. <div class="protocol_sidebar__section--padded">
  42. <div class="flex flex-row items-center justify-between gap-4">
  43. <div class="flex flex-col gap-1">
  44. <p class="text-foreground copy-14 font-bold"><%= t(".duplicate_protocol.action") %></p>
  45. <p class="copy-12 text-content-secondary"><%= t(".duplicate_protocol.description") %></p>
  46. </div>
  47. <%= render LooposUi::Modal.for_duplicate(model_name: "Protocol") do |modal| %>
  48. <% modal.with_trigger do %>
  49. <%= react_component("Button", { text: t(".duplicate_protocol.button"), app: "neutral", icon: "fa-kit fa-regular-copy-circle-plus", variant: "secondary", size: "medium", iconPosition: "left" }) %>
  50. <% end %>
  51. <% modal.with_primary_action(
  52. text: t(".confirm_button"),
  53. href: duplicate_protocol_path,
  54. kind: :app,
  55. tag_options: { data: { "turbo-method": :post, action: "click->lui--button#startLoading click->actions_loading#run" } }
  56. ) %>
  57. <% end %>
  58. </div>
  59. </div>
  60. <% end %>
  61. <% sidebar.with_content_section do %>
  62. <div class="protocol_sidebar__section--padded">
  63. <div class="flex flex-row items-center justify-between gap-4">
  64. <div class="flex flex-col gap-1">
  65. <p class="text-foreground copy-14 font-bold"><%= t(".copy_protocol.action") %></p>
  66. <p class="copy-12 text-content-secondary"><%= t(".copy_protocol.description") %></p>
  67. </div>
  68. <div data-controller="clipboard">
  69. <%= react_component("Button", { text: t(".copy_protocol.button"), app: "neutral", icon: "fa-regular fa-copy", variant: "secondary", size: "medium", iconPosition: "left", dataAttributes: { 'action': "click->clipboard#copy", 'clipboard-export-param': copy_protocol_path, 'clipboard-broadcast-param': broadcast_copied_protocol_admin_v2_protocols_path } }) %>
  70. </div>
  71. </div>
  72. </div>
  73. <% end %>
  74. <% sidebar.with_content_section do %>
  75. <div class="protocol_sidebar__section--padded">
  76. <div class="flex flex-row items-center justify-between gap-4">
  77. <div class="flex flex-col gap-1">
  78. <p class="text-foreground copy-14 font-bold"><%= t(".export_protocol.action") %></p>
  79. <p class="copy-14 text-content-secondary"><%= t(".export_protocol.description") %></p>
  80. </div>
  81. <%= render LooposUi::Modal.for_export(model_name: "Protocol") do |modal| %>
  82. <% modal.with_trigger do %>
  83. <%= react_component("Button", { text: t(".export_protocol.button"), app: "neutral", icon: "fa-regular fa-download", variant: "secondary", size: "medium", iconPosition: "left" }) %>
  84. <% end %>
  85. <% modal.with_primary_action(
  86. text: t(".confirm_button"),
  87. href: export_protocol_path,
  88. kind: :app,
  89. tag_options: { data: { action: "click->lui--button#startLoading click->actions_loading#run" } }
  90. ) %>
  91. <% end %>
  92. </div>
  93. </div>
  94. <% end %>
  95. <%
  96. =begin%>
  97. <% sidebar.with_content_section do %>
  98. <div class="protocol_sidebar__section--padded">
  99. <p class="text-foreground copy-14 font-bold">Tags</p>
  100. <div class="flex justify-start mt-6">
  101. <%= turbo_frame_tag protocol, 'tags' do %>
  102. <%= render 'admin/v2/tags/tags', taggable: protocol %>
  103. <% end %>
  104. </div>
  105. </div>
  106. <% end %>
  107. <%
  108. =end %>
  109. then: 0 else: 0 <% if catalog_node.present? %>
  110. <% sidebar.with_content_section do %>
  111. <div class="protocol_sidebar__section--padded">
  112. <div class="flex flex-row items-center justify-between gap-4">
  113. <div class="flex flex-col gap-1">
  114. <p class="text-foreground copy-14 font-bold"><%= t(".detach_protocol.action") %></p>
  115. <p class="copy-12 text-content-secondary"><%= t(".detach_protocol.description") %></p>
  116. </div>
  117. <%= render LooposUi::Modal.for_detach(model_name: "Protocol") do |modal| %>
  118. <% modal.with_trigger do %>
  119. <%= react_component("Button", { text: t(".detach_protocol.button"), app: "neutral", icon: "fa-kit fa-regular-paperclip-slash", variant: "secondary", size: "medium", iconPosition: "left" }) %>
  120. <% end %>
  121. <% modal.with_primary_action(
  122. text: t(".confirm_button"),
  123. href: detach_protocol_path,
  124. kind: :app,
  125. tag_options: { data: { "turbo-method": :post, action: "click->lui--button#startLoading click->actions_loading#run" } }
  126. ) %>
  127. <% end %>
  128. </div>
  129. </div>
  130. <% end %>
  131. <% end %>
  132. <% sidebar.with_content_section do %>
  133. <div class="protocol_sidebar__section--padded">
  134. <div class="flex flex-row items-center justify-between gap-4">
  135. <div class="flex flex-col gap-1">
  136. <p class="text-foreground copy-14 font-bold"><%= t(".delete_protocol.action") %></p>
  137. <p class="copy-12 text-content-secondary"><%= t(".delete_protocol.description") %></p>
  138. </div>
  139. <%= render LooposUi::Modal.new(title: t(".delete_protocol.action")) do |modal| %>
  140. <% modal.with_trigger do %>
  141. <%= react_component("Button", { text: t(".delete_protocol.button"), app: "danger", icon: "fa-regular fa-trash-can", variant: "secondary", size: "medium", iconPosition: "left" }) %>
  142. <% end %>
  143. <% modal.with_primary_action(
  144. text: t(".confirm_button"),
  145. href: delete_protocol_path,
  146. kind: :app,
  147. tag_options: { data: { "turbo-method": :delete, turbo_frame: "_top", action: "click->lui--button#startLoading click->actions_loading#run" } }
  148. ) %>
  149. <%= tag.span(t(".delete_protocol.modal_text")) %>
  150. <% end %>
  151. </div>
  152. </div>
  153. <% end %>
  154. <% end %>
  155. <% end %>

app/components/loopos_ui/protocol/settings.rb

53.85% lines covered

0.0% branches covered

26 relevant lines. 14 lines covered and 12 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. 1 class LooposUi::Protocol::Settings < ViewComponent::Base
  2. 1 include Turbo::FramesHelper
  3. 1 include Turbo::StreamsHelper
  4. 1 attr_reader :protocol, :presenter, :catalog_node
  5. 1 def initialize(protocol:, presenter:, catalog_node: nil)
  6. @protocol = protocol
  7. @presenter = presenter
  8. @catalog_node = catalog_node
  9. end
  10. 1 def protocol_name
  11. protocol.name
  12. end
  13. 1 def formatted_created_at
  14. protocol.created_at.strftime("%d-%m-%Y")
  15. end
  16. 1 def formatted_updated_at
  17. protocol.updated_at.strftime("%d-%m-%Y")
  18. end
  19. 1 def edit_protocol_path
  20. presenter.edit_path
  21. end
  22. 1 def duplicate_protocol_path
  23. then: 0 else: 0 duplicate_admin_v2_protocol_path(protocol, catalog_node_id: catalog_node&.id)
  24. end
  25. 1 def delete_protocol_path
  26. admin_v2_protocol_path(protocol)
  27. end
  28. 1 def export_protocol_path
  29. export_admin_v2_protocol_path(protocol, format: :json)
  30. end
  31. 1 def copy_protocol_path
  32. export_admin_v2_protocol_path(protocol)
  33. end
  34. 1 def detach_protocol_path
  35. then: 0 else: 0 detach_protocol_admin_v2_catalog_node_path(catalog_node, protocol_id: protocol&.id)
  36. end
  37. end

app/components/loopos_ui/protocol_answer_value.rb

100.0% lines covered

100.0% branches covered

2 relevant lines. 2 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class ProtocolAnswerValue < LoopComponent
  3. end
  4. end

app/components/loopos_ui/protocol_answer_value/protocol_answer_value.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. Hello from ProtocolAnswerValue component

app/components/loopos_ui/protocol_element/drawer_bar.rb

90.91% lines covered

100.0% branches covered

11 relevant lines. 10 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module ProtocolElement
  3. 1 class DrawerBar < LoopComponent
  4. 1 option :element
  5. 1 option :oauth_core_token
  6. 1 option :form_authenticity_token
  7. 1 option :catalog_node
  8. 1 option :current_user
  9. 1 private
  10. 1 def presenter
  11. @presenter ||= element.presenter
  12. end
  13. end
  14. end
  15. end

app/components/loopos_ui/protocol_element/drawer_bar/drawer_bar.html.erb

0.0% lines covered

0.0% branches covered

26 relevant lines. 0 lines covered and 26 lines missed.
8 total branches, 0 branches covered and 8 branches missed.
    
  1. <%= render LooposUi::DrawerBar.new(close_on_outside_click: false) do |drawer| %>
  2. <% drawer.with_header do %>
  3. <%= render LooposUi::DrawerBar::Header.new do |header| %>
  4. <% header.with_corner_actions do %>
  5. then: 0 else: 0 <% if current_user.can_manage_translations? %>
  6. <%= render("shared/translations_for_model", model: element) %> <%# Making changes to leave a conflict here %>
  7. <% end %>
  8. <% end %>
  9. <%= render LooposUi::DrawerBar::Entity.new(
  10. icon: element.class.icon,
  11. title: element.label,
  12. description: element.displayed_name
  13. )%>
  14. <span class="inline-flex">
  15. <%= tag.turbo_frame id: dom_id(element, 'tags') do %>
  16. <%= render LooposUi::ModelAssociationList.new(
  17. model: element,
  18. association: :tags,
  19. policy: helpers.policy(:catalog_management))
  20. %>
  21. <% end %>
  22. </span>
  23. <% end %>
  24. <% end %>
  25. <% drawer.with_header do %>
  26. <%= render LooposUi::DrawerBar::ContentSection.new(
  27. title: t(".associate_apps"),
  28. description: t(".associate_apps_description")) do %>
  29. <%= render "admin/v2/protocol_elements/apps", enabled_apps: presenter.enabled_apps, element: element %>
  30. <% end %>
  31. <% end %>
  32. <%= render LooposUi::TabsLayout.new(keep_tab_in_url: false) do |layout| %>
  33. <% layout.with_tab(title: t(".settings")) do %>
  34. <div class="px-4">
  35. <%= react_component("JsonForm", {
  36. app: "core",
  37. defaultUiSchema: presenter.settings_ui_schema,
  38. schema: presenter.settings_schema,
  39. settings: element.untranslated_settings || {},
  40. resourceId: element.id,
  41. resourceClass: element.type,
  42. token: oauth_core_token,
  43. apiBaseUrl: '/api/v1',
  44. fetchSchemaUrl: '/protocol_elements/:resourceId/settings_schema/:scriptId',
  45. updateUrl: "/protocol_elements/:id/update_settings?catalog_node_id=#{catalog_node&.id}",
  46. then: 0 else: 0 csrfToken: form_authenticity_token,
  47. canToggleForm: false,
  48. language: I18n.locale,
  49. # TODO: Until protocol can support adding or importing scripts...
  50. userScopes: (current_user&.scopes&.split(" ") || []) - ["can_manage_scripts"]
  51. then: 0 else: 0 then: 0 else: 0 }, class: "font-sans") %>
  52. </div>
  53. <% end %>
  54. <% layout.with_tab(title: "Slug") do %>
  55. <div class="flex flex-col gap-4 protocol_sidebar__section px-4">
  56. <%= render "admin/v2/protocol_elements/slug_edit", element: element, protocol: element.protocol %>
  57. </div>
  58. <% end %>
  59. <% end %>
  60. <% end %>

app/components/loopos_ui/protocol_form.rb

30.41% lines covered

0.0% branches covered

388 relevant lines. 118 lines covered and 270 lines missed.
214 total branches, 0 branches covered and 214 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "cgi"
  3. 1 module LooposUi
  4. 1 class ProtocolForm < LoopComponent
  5. 1 include LooposUi::Concerns::ProtocolAnswerValueFormatter
  6. 1 include LooposUi::Concerns::ProtocolAnswerNormalizer
  7. 1 ACTIVE_STORAGE_SIGNED_ID_PATTERN = %r{
  8. eyJ[A-Za-z0-9+\/=_-]+
  9. --[a-f0-9]+
  10. }x
  11. 1 option :protocol, default: -> { {} }
  12. 1 option :field_name_prefix, default: -> { "protocol_answers" }
  13. 1 option :value_for, optional: true
  14. 1 option :upload_form_id, optional: true
  15. 1 option :core_base_url, optional: true
  16. 1 option :core_api_token, optional: true
  17. # Override Core direct upload target (e.g. dummy app Active Storage in Lookbook previews).
  18. 1 option :direct_upload_url, optional: true
  19. 1 option :direct_upload_authorization, optional: true
  20. 1 option :orientation,
  21. default: -> { :vertical },
  22. type: Types::Coercible::Symbol.enum(:vertical, :horizontal)
  23. 1 option :label_width, optional: true
  24. 1 option :input_mode,
  25. default: -> { :form },
  26. type: Types::Coercible::Symbol.enum(:inline, :form, :autosubmit)
  27. # Visual modifier for inputs; defaults to `input_mode` when omitted.
  28. 1 option :input_appearance, optional: true, type: Types::Coercible::Symbol.enum(
  29. :inline, :form, :autosubmit
  30. )
  31. # Optional HTML form attribute when inputs are rendered outside a <form> (e.g. Lookbook).
  32. 1 option :input_form, optional: true
  33. # Protocol validation payload: { errors: [{ protocol_element_id:, errors: [], is_deal_breaker: }] }
  34. 1 option :errors, default: -> { {} }
  35. # When true, nil values from value_for stay empty instead of falling back to element defaults.
  36. 1 option :skip_element_defaults, default: -> { false }, type: Types::Bool
  37. 1 TEXT_INPUT_TYPES = [
  38. "input:text",
  39. "input:email",
  40. "input:iban",
  41. "input:postalcode",
  42. "input:identifier",
  43. ].freeze
  44. 1 MONEY_INPUT_TYPE = "input:money"
  45. 1 IMAGES_INPUT_TYPE = "input:images"
  46. 1 MULTISELECT_INPUT_TYPE = "input:multiselect"
  47. 1 FILES_INPUT_TYPE = "input:files"
  48. 1 DATE_INPUT_TYPE = "input:date"
  49. 1 DATE_RANGE_INPUT_TYPE = "input:daterange"
  50. 1 PHONE_NUMBER_INPUT_TYPE = "input:phonenumber"
  51. 1 CUSTOM_INPUT_TYPE = "input:custom"
  52. 1 INFO_HTML_TYPE = "info:html"
  53. 1 INFO_IMAGE_TYPE = "info:image"
  54. # TODO
  55. # Core sets the default placeholder to "Question" and the translations modal does not expose
  56. # placeholder for editing or translation. Until Core fixes that default and translation
  57. # support, ignore API placeholders in the UI so inputs render without the hardcoded string.
  58. 1 IGNORE_CORE_PLACEHOLDERS = true
  59. 1 def sorted_items
  60. items.sort_by { |it| it[:position].to_f }
  61. end
  62. 1 def input_item?(item)
  63. type = item[:type].to_s
  64. then: 0 else: 0 return true if TEXT_INPUT_TYPES.include?(type)
  65. then: 0 else: 0 return true if type == "input:number" || type == "input:bool" || type == MONEY_INPUT_TYPE
  66. then: 0 else: 0 return true if render_select?(item)
  67. then: 0 else: 0 return true if render_images?(item)
  68. then: 0 else: 0 return true if render_multiselect?(item)
  69. then: 0 else: 0 return true if render_files?(item)
  70. then: 0 else: 0 return true if type == DATE_INPUT_TYPE || type == DATE_RANGE_INPUT_TYPE
  71. then: 0 else: 0 return true if type == PHONE_NUMBER_INPUT_TYPE
  72. then: 0 else: 0 return true if type == CUSTOM_INPUT_TYPE
  73. false
  74. end
  75. 1 def answer_index_for(item)
  76. input_items_with_index[item[:id].to_i]
  77. end
  78. 1 def protocol_element_id_field_name(index)
  79. "#{field_name_prefix}[#{index}][protocol_element_id]"
  80. end
  81. 1 def value_field_name(index)
  82. "#{field_name_prefix}[#{index}][value]"
  83. end
  84. 1 def values_field_name(index)
  85. "#{field_name_prefix}[#{index}][values][]"
  86. end
  87. 1 def entry_label(item)
  88. helpers.strip_tags((item[:label_html].presence || item[:label]).to_s)
  89. end
  90. 1 def resolved_value(item)
  91. then: 0 else: 0 raw = value_for.call(item) if value_for.respond_to?(:call)
  92. entry = protocol_answer_entry(item)
  93. value = protocol_answer_value(raw)
  94. then: 0 else: 0 if entry.present?
  95. return cleared_value_for_item(item, value)
  96. end
  97. then: 0 else: 0 return value if skip_element_defaults && value.nil?
  98. then: 0 else: 0 value.nil? ? element_default(item) : value
  99. end
  100. 1 def cleared_value_for_item(item, value)
  101. case item[:type].to_s
  102. when: 0 when MULTISELECT_INPUT_TYPE
  103. then: 0 else: 0 return [] if value.nil?
  104. multiselect_value_list(value)
  105. when: 0 when "input:bool"
  106. then: 0 else: 0 value.nil? ? "false" : value
  107. else: 0 else
  108. then: 0 else: 0 value.nil? ? "" : value
  109. end
  110. end
  111. 1 def protocol_answer_entry(item)
  112. then: 0 else: 0 raw = value_for.call(item) if value_for.respond_to?(:call)
  113. else: 0 then: 0 return unless raw.respond_to?(:to_hash) || raw.is_a?(Hash)
  114. then: 0 else: 0 hash = raw.respond_to?(:to_hash) ? raw.to_hash : raw
  115. then: 0 else: 0 hash = hash.with_indifferent_access if hash.respond_to?(:with_indifferent_access)
  116. then: 0 else: 0 return if hash[:protocol_element_id].blank?
  117. hash
  118. end
  119. 1 def protocol_answer_value(raw)
  120. else: 0 then: 0 return raw unless raw.respond_to?(:to_hash) || raw.is_a?(Hash)
  121. then: 0 else: 0 hash = raw.respond_to?(:to_hash) ? raw.to_hash : raw
  122. then: 0 else: 0 hash = hash.with_indifferent_access if hash.respond_to?(:with_indifferent_access)
  123. then: 0 else: 0 return hash[:value] if hash[:protocol_element_id].present?
  124. raw
  125. end
  126. 1 def resolved_input_value(item)
  127. value = resolved_value(item)
  128. else: 0 then: 0 return value unless item[:type].to_s == MONEY_INPUT_TYPE
  129. then: 0 else: 0 return value if value.nil?
  130. normalize_money_value_for_input(value)
  131. end
  132. 1 def resolved_select_value(item)
  133. resolved_value(item).to_s
  134. end
  135. 1 def resolved_multiselect_value(item)
  136. normalize_multiselect_option_ids(item, resolved_value(item))
  137. end
  138. 1 def normalize_multiselect_option_ids(item, value)
  139. map_multiselect_labels_to_option_ids(item, value)
  140. end
  141. 1 def resolved_files_display_value(item)
  142. entry = protocol_answer_entry(item)
  143. then: 0 else: 0 raw = entry.present? ? entry[:value] : resolved_value(item)
  144. then: 0 else: 0 return [] if raw.blank?
  145. then: 0 else: 0 names_from_entry = Array(entry&.[](:file_names)).map(&:to_s)
  146. files_value_entries(raw).filter_map.with_index do |file_entry, index|
  147. signed_id, filename, url = extract_file_entry_metadata(file_entry, names_from_entry[index])
  148. then: 0 else: 0 next if signed_id.blank?
  149. filename ||= filename_from_blob_url(url)
  150. {
  151. signed_id: signed_id,
  152. filename: filename,
  153. url: url.presence || core_blob_url_for(signed_id, filename),
  154. }
  155. end.uniq { |file| file[:signed_id] }
  156. end
  157. 1 def select2_options_for(item)
  158. select_options_for_input(item).map do |opt|
  159. Inputs::Select2::Option.new(
  160. value: opt[:value],
  161. text: opt[:text],
  162. )
  163. end
  164. end
  165. 1 def resolved_images_signed_ids(item)
  166. raw = resolved_value(item)
  167. then: 0 else: 0 return [] if raw.nil?
  168. Array(raw).filter_map do |entry|
  169. then: 0 if entry.respond_to?(:to_hash)
  170. h = entry.to_hash.with_indifferent_access
  171. then: 0 else: 0 (h[:signed_id].presence || h[:value].presence)&.to_s
  172. else: 0 else
  173. entry.to_s.presence
  174. end
  175. end
  176. end
  177. 1 def image_preloaded_images(item)
  178. Array(resolved_value(item)).filter_map do |entry|
  179. then: 0 if entry.respond_to?(:to_hash)
  180. h = entry.to_hash.with_indifferent_access
  181. then: 0 else: 0 signed_id = (h[:signed_id].presence || h[:value].presence)&.to_s
  182. then: 0 else: 0 next if signed_id.blank?
  183. {
  184. signed_id: signed_id,
  185. url: h[:url].presence || core_blob_url_for(signed_id, h[:filename]),
  186. }
  187. else: 0 else
  188. signed_id = entry.to_s.presence
  189. then: 0 else: 0 next if signed_id.blank?
  190. { signed_id: signed_id, url: core_blob_url_for(signed_id) }
  191. end
  192. end
  193. end
  194. 1 def image_accept_formats(item)
  195. raw = item[:formats].presence || item_settings(item)[:formats].presence
  196. then: 0 else: 0 return LooposUi.config.image_default_accept_formats if raw.blank?
  197. Array(raw).flat_map { |formats| formats.to_s.split(",") }.map { |fmt| fmt.strip.downcase.to_sym }.compact_blank
  198. end
  199. 1 def image_max_size(item)
  200. image_size_setting(item, :max_size, :max_size_mb)
  201. end
  202. 1 def image_max_size_batch(item)
  203. image_size_setting(item, :max_size_batch, :max_size_batch_mb)
  204. end
  205. 1 def core_direct_upload_url
  206. then: 0 else: 0 return direct_upload_url if direct_upload_url.present?
  207. then: 0 else: 0 return if core_base_url.blank?
  208. "#{core_base_url.to_s.chomp("/")}/rails/active_storage/direct_uploads"
  209. end
  210. 1 def core_direct_upload_authorization
  211. then: 0 else: 0 return direct_upload_authorization if direct_upload_authorization.present?
  212. then: 0 else: 0 return if core_api_token.blank?
  213. "Bearer #{core_api_token}"
  214. end
  215. 1 def element_default(item)
  216. then: 0 else: 0 return item[:default] if item.key?(:default) && !item[:default].nil?
  217. default = item_settings(item)[:default]
  218. else: 0 then: 0 default unless default.nil?
  219. end
  220. 1 def element_placeholder(item)
  221. then: 0 else: 0 return if IGNORE_CORE_PLACEHOLDERS
  222. item[:placeholder].presence
  223. end
  224. 1 def select_options_for_input(item)
  225. Array(item[:options]).filter_map do |opt|
  226. else: 0 then: 0 next unless opt.is_a?(Hash) || opt.respond_to?(:to_hash)
  227. then: 0 else: 0 opt = opt.to_hash if opt.respond_to?(:to_hash) && !opt.is_a?(Hash)
  228. opt = opt.with_indifferent_access
  229. option_value = opt[:value].presence || opt[:id]
  230. then: 0 else: 0 next if option_value.blank?
  231. {
  232. value: option_value.to_s,
  233. text: (opt[:label].presence || opt[:text]).to_s,
  234. }
  235. end
  236. end
  237. 1 def number_bounds(item)
  238. [numeric_setting(item, :min), numeric_setting(item, :max)]
  239. end
  240. 1 def number_step(item)
  241. numeric_setting(item, :step) || 1
  242. end
  243. 1 def text_minlength(item)
  244. numeric_setting(item, :min)
  245. end
  246. 1 def text_maxlength(item)
  247. numeric_setting(item, :max)
  248. end
  249. 1 def autocomplete_type_for(item)
  250. item[:autocomplete_type].presence
  251. end
  252. 1 def text_area?(item)
  253. truthy?(item[:text_area]) || truthy?(item_settings(item)[:text_area])
  254. end
  255. 1 def text_area_rows(item)
  256. size = item[:text_area_size].presence || item_settings(item)[:text_area_size]
  257. then: 0 else: 0 size.to_s == "small" ? 3 : nil
  258. end
  259. 1 def money_currency(item)
  260. raw = item[:currency].presence || item_settings(item)[:currency]
  261. then: 0 else: 0 return ::Money.default_currency.iso_code if raw.blank?
  262. ::Money::Currency.new(raw)
  263. raw
  264. rescue ::Money::Currency::UnknownCurrency
  265. match = ::Money::Currency.all.find { |c| c.symbol == raw.to_s }
  266. then: 0 else: 0 match&.iso_code || ::Money.default_currency.iso_code
  267. end
  268. 1 def money_bounds(item)
  269. [numeric_setting(item, :min), numeric_setting(item, :max)]
  270. end
  271. 1 def money_step(item)
  272. numeric_setting(item, :step) || 0.01
  273. end
  274. 1 def date_granularity(item)
  275. item[:granularity].presence || item_settings(item)[:granularity].presence || "date"
  276. end
  277. 1 def date_min_date(item)
  278. parse_date_limit(item[:min_date]) ||
  279. parse_date_limit(item_settings(item)[:min_date]) ||
  280. compute_date_from_relative(item, :start_date_absolute, :start_date_relative)
  281. end
  282. 1 def date_max_date(item)
  283. parse_date_limit(item[:max_date]) ||
  284. parse_date_limit(item_settings(item)[:max_date]) ||
  285. compute_date_from_relative(item, :end_date_absolute, :end_date_relative)
  286. end
  287. 1 def date_blocked_days(item)
  288. item[:blocked_days].presence || item_settings(item)[:blocked_days].presence
  289. end
  290. 1 def date_initial_date(item)
  291. raw = resolved_value(item)
  292. then: 0 else: 0 return if raw.blank?
  293. granularity = date_granularity(item)
  294. case granularity
  295. when "date_time"
  296. when: 0 # Stored as "DD/MM/YYYY HH:MM:SS" → JS _parseDate needs "YYYY-MM-DD HH:MM"
  297. date_str, time_str = raw.to_s.split(" ")
  298. then: 0 else: 0 return if date_str.blank? || time_str.blank?
  299. date_parts = date_str.split("/")
  300. else: 0 then: 0 return unless date_parts.length == 3
  301. iso_date = "#{date_parts[2]}-#{date_parts[1]}-#{date_parts[0]}"
  302. hhmm = time_str.split(":").first(2).join(":")
  303. "#{iso_date} #{hhmm}"
  304. when "time"
  305. when: 0 # Stored as "HH:MM:SS" → JS _parseDate needs "HH:MM"
  306. raw.to_s.split(":").first(2).join(":")
  307. when "year"
  308. when: 0 # Stored as "YYYY" → JS _parseDate needs "YYYY-MM-DD"
  309. year = raw.to_s.strip.to_i
  310. then: 0 else: 0 return if year.zero?
  311. "#{year}-01-01"
  312. when "month_year"
  313. when: 0 # Stored as "MM/YYYY" → JS _parseDate needs "YYYY-MM-DD"
  314. parts = raw.to_s.split("/")
  315. else: 0 then: 0 return unless parts.length == 2
  316. month = parts[0].to_i
  317. year = parts[1].to_i
  318. then: 0 else: 0 return if month.zero? || year.zero?
  319. format("%04d-%02d-01", year, month)
  320. else: 0 else
  321. Date.parse(raw.to_s)
  322. end
  323. rescue ArgumentError, TypeError
  324. nil
  325. end
  326. 1 def date_range_initial_date(item)
  327. raw = resolved_value(item)
  328. then: 0 else: 0 return if raw.blank?
  329. parts = raw.to_s.split("~").map(&:strip)
  330. else: 0 then: 0 return unless parts.length == 2
  331. iso_dates = parts.map do |date_str|
  332. date_parts = date_str.split("/")
  333. else: 0 then: 0 next nil unless date_parts.length == 3
  334. "#{date_parts[2]}-#{date_parts[1]}-#{date_parts[0]}"
  335. end
  336. then: 0 else: 0 return if iso_dates.any?(&:nil?)
  337. iso_dates.to_json
  338. end
  339. 1 def date_picker_value(item)
  340. then: 0 else: 0 resolved_value(item).presence&.to_s
  341. end
  342. 1 def phone_initial_country(item)
  343. item[:default_country].presence || item_settings(item)[:default_country].presence || "pt"
  344. end
  345. 1 def dropdown_setting_enabled?(item)
  346. val = item[:dropdown]
  347. then: 0 else: 0 val = item_settings(item)[:dropdown] if val.nil?
  348. then: 0 else: 0 return false if val.nil?
  349. truthy?(val)
  350. end
  351. 1 def normalize_money_value_for_input(value)
  352. format_money_for_protocol(value) || value
  353. end
  354. 1 def info_text_inner_html(item)
  355. helpers.sanitize(
  356. (item[:label_html].presence || item[:label]).to_s,
  357. tags: ["p", "br", "strong", "em", "b", "i", "u", "a", "ul", "ol", "li", "span"],
  358. attributes: ["href", "target", "rel", "class"],
  359. )
  360. end
  361. 1 def info_body(item)
  362. helpers.sanitize(
  363. item[:body].to_s,
  364. tags: ["p", "br", "strong", "em", "b", "i", "u", "a", "ul", "ol", "li", "span"],
  365. attributes: ["href", "target", "rel", "class"],
  366. )
  367. end
  368. 1 def info_html_content(item)
  369. (item[:label_html].presence || item[:label]).to_s
  370. end
  371. 1 def custom_html_for(item)
  372. item[:custom_html].presence || item[:custom].presence || item_settings(item)[:custom].presence
  373. end
  374. 1 def info_image_url(item)
  375. item[:url].presence
  376. end
  377. 1 def hint_present?(item)
  378. item[:hint].present? || item[:hint_url].present?
  379. end
  380. 1 def hint_icon(item)
  381. item[:hint_icon].to_s.presence || "fa-regular fa-circle-question"
  382. end
  383. 1 def element_hint_locals(item)
  384. {
  385. icon: hint_icon(item),
  386. hint_text: item[:hint].presence,
  387. link_href: item[:hint_url].presence,
  388. link_name: (item[:hint_url_text].presence || item[:hint_url]).to_s,
  389. }
  390. end
  391. 1 def hint_form_entry_options(item)
  392. else: 0 then: 0 return {} unless hint_present?(item)
  393. {
  394. tooltip: item[:hint].presence,
  395. tooltip_icon: hint_icon(item),
  396. tooltip_link_href: item[:hint_url].presence,
  397. tooltip_link_name: (item[:hint_url_text].presence || item[:hint_url]).to_s,
  398. }
  399. end
  400. 1 def form_entry_options(item)
  401. {
  402. label: entry_label(item),
  403. required: item[:required].present?,
  404. orientation: orientation,
  405. label_width: label_width,
  406. **hint_form_entry_options(item),
  407. }
  408. end
  409. 1 def bool_form_entry_options(item)
  410. form_entry_options(item).merge(
  411. orientation: :horizontal_reverse,
  412. error: field_error_message(item),
  413. )
  414. end
  415. 1 def render_select?(item)
  416. item[:type].to_s == "input:select"
  417. end
  418. 1 def render_images?(item)
  419. item[:type].to_s == IMAGES_INPUT_TYPE
  420. end
  421. 1 def render_multiselect?(item)
  422. item[:type].to_s == MULTISELECT_INPUT_TYPE
  423. end
  424. 1 def render_files?(item)
  425. item[:type].to_s == FILES_INPUT_TYPE
  426. end
  427. 1 def file_accept_formats(item)
  428. raw = item[:formats].presence || item_settings(item)[:formats].presence
  429. then: 0 else: 0 return ["pdf", "jpg", "jpeg", "png", "webp"] if raw.blank?
  430. Array(raw).flat_map { |formats| formats.to_s.split(",") }.map { |fmt| fmt.strip.downcase }.compact_blank
  431. end
  432. 1 def truthy?(val)
  433. val == true || val.to_s == "true"
  434. end
  435. 1 def bool_checked?(item)
  436. cast_protocol_bool(resolved_value(item))
  437. end
  438. 1 def bool_field_value(item)
  439. then: 0 else: 0 bool_checked?(item) ? "true" : "false"
  440. end
  441. 1 def cast_protocol_bool(value)
  442. then: 0 else: 0 return false if value.nil?
  443. then: 0 else: 0 normalized = value.is_a?(Array) ? value.last : value
  444. then: 0 else: 0 return false if normalized.nil?
  445. ActiveModel::Type::Boolean.new.cast(normalized)
  446. end
  447. 1 def field_form_attribute
  448. input_form.presence || upload_form_id.presence
  449. end
  450. # CSS appearance for inputs; follows `input_mode` unless `input_appearance` is set explicitly.
  451. 1 def resolved_input_appearance
  452. then: 0 else: 0 input_appearance.nil? ? input_mode : input_appearance
  453. end
  454. 1 def field_error_messages(item)
  455. then: 0 else: 0 entry = normalized_errors[:errors]&.find { |e| e[:protocol_element_id].to_i == item[:id].to_i }
  456. then: 0 else: 0 Array(entry&.dig(:errors)).compact_blank
  457. end
  458. 1 def field_error_message(item)
  459. field_error_messages(item).first
  460. end
  461. 1 def normalized_errors
  462. then: 0 if errors.is_a?(Hash)
  463. errors.with_indifferent_access
  464. else: 0 else
  465. {}
  466. end
  467. end
  468. 1 def checkbox_input_data
  469. else: 0 then: 0 return unless input_mode == :autosubmit
  470. { action: "change->checkbox#toggle change->checkbox#submitForm" }
  471. end
  472. 1 private
  473. 1 def files_value_entries(raw)
  474. then: 0 if raw.is_a?(Hash) || raw.respond_to?(:to_hash)
  475. then: 0 else: 0 hash = raw.respond_to?(:to_hash) ? raw.to_hash : raw
  476. then: 0 else: 0 hash = hash.with_indifferent_access if hash.respond_to?(:with_indifferent_access)
  477. then: 0 else: 0 return hash.values if hash.respond_to?(:values) && hash[:signed_id].blank? && hash[:value].blank?
  478. [hash]
  479. else: 0 else
  480. Array(raw)
  481. end
  482. end
  483. 1 def extract_file_entry_metadata(file_entry, fallback_name = nil)
  484. then: 0 if file_entry.respond_to?(:to_hash)
  485. h = file_entry.to_hash
  486. then: 0 else: 0 h = h.with_indifferent_access if h.respond_to?(:with_indifferent_access)
  487. candidate = (h[:signed_id].presence || h[:value].presence).to_s
  488. signed_id =
  489. then: 0 if candidate.match?(ACTIVE_STORAGE_SIGNED_ID_PATTERN)
  490. candidate
  491. else: 0 else
  492. candidate.scan(ACTIVE_STORAGE_SIGNED_ID_PATTERN).first
  493. end
  494. filename = h[:filename].presence || h[:name].presence || fallback_name.presence
  495. url = h[:url].presence
  496. filename ||= filename_from_blob_url(url)
  497. else: 0 else
  498. text = file_entry.to_s
  499. signed_id = text.scan(ACTIVE_STORAGE_SIGNED_ID_PATTERN).first
  500. then: 0 else: 0 url = text.match?(%r{\Ahttps?://}i) ? text : nil
  501. filename = fallback_name.presence || filename_from_blob_url(url)
  502. end
  503. [signed_id, filename, url]
  504. end
  505. 1 def item_settings(item)
  506. settings = item[:settings]
  507. then: 0 else: 0 return settings.with_indifferent_access if settings.respond_to?(:with_indifferent_access)
  508. then: 0 else: 0 return settings.with_indifferent_access if settings.is_a?(Hash)
  509. {}.with_indifferent_access
  510. end
  511. 1 def numeric_setting(item, key)
  512. validation = item[:validation]
  513. then: 0 else: 0 if validation.respond_to?(:[])
  514. then: 0 else: 0 validation = validation.with_indifferent_access if validation.respond_to?(:with_indifferent_access)
  515. val = validation[key]
  516. else: 0 then: 0 return val unless val.nil?
  517. end
  518. val = item[key]
  519. else: 0 then: 0 return val unless val.nil?
  520. item_settings(item)[key]
  521. end
  522. 1 def image_size_setting(item, key, mb_key)
  523. value = item[key].presence ||
  524. item[mb_key].presence ||
  525. item_settings(item)[key].presence ||
  526. item_settings(item)[mb_key].presence
  527. then: 0 else: 0 return if value.blank?
  528. value.to_f
  529. end
  530. 1 def input_items_with_index
  531. @input_items_with_index ||= begin
  532. index_by_id = {}
  533. idx = -1
  534. sorted_items.each do |item|
  535. else: 0 then: 0 next unless input_item?(item)
  536. idx += 1
  537. index_by_id[item[:id].to_i] = idx
  538. end
  539. index_by_id
  540. end
  541. end
  542. 1 def core_blob_url_for(signed_id, filename = nil)
  543. then: 0 else: 0 return if core_base_url.blank? || signed_id.blank?
  544. safe_filename = filename.presence || "image"
  545. "#{core_base_url.to_s.chomp("/")}/rails/active_storage/blobs/redirect/#{signed_id}/#{CGI.escape(safe_filename)}"
  546. end
  547. 1 def items
  548. raw =
  549. then: 0 if protocol.is_a?(ActionController::Parameters)
  550. else: 0 protocol.to_unsafe_h.with_indifferent_access[:items]
  551. then: 0 elsif protocol.respond_to?(:with_indifferent_access)
  552. else: 0 protocol.with_indifferent_access[:items]
  553. then: 0 elsif protocol.is_a?(Hash) || protocol.respond_to?(:to_hash)
  554. then: 0 else: 0 h = protocol.is_a?(Hash) ? protocol : protocol.to_hash
  555. h.with_indifferent_access[:items]
  556. else: 0 else
  557. []
  558. end
  559. Array(raw).map { |row| indifferent(row) }
  560. end
  561. 1 def indifferent(row)
  562. then: 0 if row.is_a?(Hash)
  563. else: 0 row.with_indifferent_access
  564. then: 0 elsif row.respond_to?(:to_hash)
  565. row.to_hash.with_indifferent_access
  566. else: 0 else
  567. row
  568. end
  569. end
  570. 1 def parse_date_limit(value)
  571. then: 0 else: 0 return if value.blank?
  572. then: 0 else: 0 return value if value.is_a?(Date)
  573. Date.parse(value.to_s)
  574. rescue ArgumentError, TypeError
  575. nil
  576. end
  577. 1 def compute_date_from_relative(item, absolute_key, relative_key)
  578. absolute = item[absolute_key].presence || item_settings(item)[absolute_key].presence
  579. relative = item[relative_key].presence || item_settings(item)[relative_key].presence
  580. then: 0 else: 0 return parse_date_limit(absolute) if absolute.present?
  581. then: 0 else: 0 return if relative.blank?
  582. Time.zone.today + relative.to_i.days
  583. end
  584. end
  585. end

app/components/loopos_ui/protocol_form/protocol_form.html.erb

0.0% lines covered

0.0% branches covered

151 relevant lines. 0 lines covered and 151 lines missed.
81 total branches, 0 branches covered and 81 branches missed.
    
  1. <div class="protocol_form__container">
  2. <% sorted_items.each do |item| %>
  3. <% type = item[:type].to_s %>
  4. <% answer_index = answer_index_for(item) %>
  5. <div
  6. class="protocol_form__item"
  7. id="<%= item[:id].to_s %>"
  8. data-protocol-element-id="<%= item[:id] %>"
  9. >
  10. else: 0 <% case type %>
  11. when: 0 <% when *LooposUi::ProtocolForm::TEXT_INPUT_TYPES %>
  12. else: 0 then: 0 <% unless answer_index.nil? %>
  13. <%= render LooposUi::FormEntry.new(**form_entry_options(item)) do |entry| %>
  14. <% entry.with_input do %>
  15. <%= hidden_field_tag protocol_element_id_field_name(answer_index), item[:id], form: field_form_attribute %>
  16. <% text_common = {
  17. name: value_field_name(answer_index),
  18. value: resolved_input_value(item).to_s,
  19. placeholder: element_placeholder(item),
  20. mode: input_mode,
  21. appearance: resolved_input_appearance,
  22. open_actions: true,
  23. form: field_form_attribute,
  24. error: field_error_message(item),
  25. minlength: text_minlength(item),
  26. maxlength: text_maxlength(item),
  27. autocomplete_type: autocomplete_type_for(item),
  28. } %>
  29. then: 0 <% if text_area?(item) %>
  30. <%= render LooposUi::Inputs::TextArea.new(
  31. **text_common,
  32. rows: text_area_rows(item),
  33. ) %>
  34. else: 0 <% else %>
  35. <%= render LooposUi::Inputs::Text.new(**text_common) %>
  36. <% end %>
  37. <% end %>
  38. <% end %>
  39. <% end %>
  40. when: 0 <% when LooposUi::ProtocolForm::MONEY_INPUT_TYPE %>
  41. else: 0 then: 0 <% unless answer_index.nil? %>
  42. <% money_min, money_max = money_bounds(item) %>
  43. <%= render LooposUi::FormEntry.new(**form_entry_options(item)) do |entry| %>
  44. <% entry.with_input do %>
  45. <%= hidden_field_tag protocol_element_id_field_name(answer_index), item[:id], form: field_form_attribute %>
  46. <%= render LooposUi::Inputs::Money.new(
  47. name: value_field_name(answer_index),
  48. value: resolved_input_value(item).to_s,
  49. placeholder: element_placeholder(item),
  50. currency: money_currency(item),
  51. min: money_min,
  52. max: money_max,
  53. step: money_step(item),
  54. mode: input_mode,
  55. appearance: resolved_input_appearance,
  56. open_actions: true,
  57. form: field_form_attribute,
  58. error: field_error_message(item),
  59. autocomplete_type: autocomplete_type_for(item),
  60. ) %>
  61. <% end %>
  62. <% end %>
  63. <% end %>
  64. when: 0 <% when "input:number" %>
  65. else: 0 then: 0 <% unless answer_index.nil? %>
  66. <% min_v, max_v = number_bounds(item) %>
  67. <%= render LooposUi::FormEntry.new(**form_entry_options(item)) do |entry| %>
  68. <% entry.with_input do %>
  69. <%= hidden_field_tag protocol_element_id_field_name(answer_index), item[:id], form: field_form_attribute %>
  70. <%= render LooposUi::Inputs::Number.new(
  71. name: value_field_name(answer_index),
  72. value: resolved_input_value(item).to_s,
  73. placeholder: element_placeholder(item),
  74. min: min_v,
  75. max: max_v,
  76. step: number_step(item),
  77. mode: input_mode,
  78. appearance: resolved_input_appearance,
  79. open_actions: true,
  80. form: field_form_attribute,
  81. error: field_error_message(item),
  82. autocomplete_type: autocomplete_type_for(item),
  83. ) %>
  84. <% end %>
  85. <% end %>
  86. <% end %>
  87. when: 0 <% when "input:bool" %>
  88. else: 0 then: 0 <% unless answer_index.nil? %>
  89. <div class="protocol_form__bool-entry">
  90. <%= render LooposUi::FormEntry.new(**bool_form_entry_options(item)) do |entry| %>
  91. <% entry.with_input do %>
  92. <%= hidden_field_tag protocol_element_id_field_name(answer_index), item[:id], form: field_form_attribute %>
  93. <div class="protocol_form__bool-field">
  94. <%= render LooposUi::Inputs::Checkbox.new(
  95. name: value_field_name(answer_index),
  96. value: "true",
  97. submit_via_hidden: true,
  98. submitted_value: bool_field_value(item),
  99. then: 0 else: 0 state: bool_checked?(item) ? :checked : :unchecked,
  100. readonly: item[:isDisabled].present?,
  101. form: field_form_attribute,
  102. error: field_error_message(item),
  103. data: checkbox_input_data,
  104. ) %>
  105. </div>
  106. <% end %>
  107. <% end %>
  108. </div>
  109. <% end %>
  110. when: 0 <% when "input:select" %>
  111. else: 0 then: 0 <% unless answer_index.nil? %>
  112. then: 0 else: 0 <% if render_select?(item) && select_options_for_input(item).any? %>
  113. <%= render LooposUi::FormEntry.new(**form_entry_options(item)) do |entry| %>
  114. <% entry.with_input do %>
  115. <%= hidden_field_tag protocol_element_id_field_name(answer_index), item[:id], form: field_form_attribute %>
  116. <% opts = select_options_for_input(item) %>
  117. <% list_mode = dropdown_setting_enabled?(item) %>
  118. <%= render LooposUi::Inputs::Select.new(
  119. name: value_field_name(answer_index),
  120. value: resolved_select_value(item),
  121. options: opts,
  122. then: 0 else: 0 placeholder: list_mode ? element_placeholder(item) : nil,
  123. include_blank: list_mode,
  124. then: 0 else: 0 variant: list_mode ? :list : :grid,
  125. mode: input_mode,
  126. appearance: resolved_input_appearance,
  127. open_actions: true,
  128. form: field_form_attribute,
  129. error: field_error_message(item),
  130. ) %>
  131. <% end %>
  132. <% end %>
  133. <% end %>
  134. <% end %>
  135. when: 0 <% when LooposUi::ProtocolForm::IMAGES_INPUT_TYPE %>
  136. else: 0 then: 0 <% unless answer_index.nil? %>
  137. then: 0 else: 0 <% if render_images?(item) %>
  138. <%= render LooposUi::FormEntry.new(**form_entry_options(item)) do |entry| %>
  139. <% entry.with_input do %>
  140. <%= hidden_field_tag protocol_element_id_field_name(answer_index), item[:id], form: field_form_attribute %>
  141. <%= render LooposUi::V2::Image.new(
  142. editable: true,
  143. list_view: true,
  144. multiple: true,
  145. size: :full,
  146. rounded: false,
  147. name: values_field_name(answer_index),
  148. form_id: upload_form_id,
  149. direct_upload_url: core_direct_upload_url,
  150. direct_upload_authorization: core_direct_upload_authorization,
  151. submit_on_upload: false,
  152. preloaded_images: image_preloaded_images(item),
  153. accept: image_accept_formats(item),
  154. max_size: image_max_size(item),
  155. max_size_batch: image_max_size_batch(item),
  156. error: field_error_message(item),
  157. ) %>
  158. <% end %>
  159. <% end %>
  160. <% end %>
  161. <% end %>
  162. when: 0 <% when LooposUi::ProtocolForm::FILES_INPUT_TYPE %>
  163. else: 0 then: 0 <% unless answer_index.nil? %>
  164. then: 0 else: 0 <% if render_files?(item) %>
  165. <%= render LooposUi::FormEntry.new(**form_entry_options(item)) do |entry| %>
  166. <% entry.with_input do %>
  167. <%= hidden_field_tag protocol_element_id_field_name(answer_index), item[:id], form: upload_form_id %>
  168. <%= render LooposUi::Inputs::File.new(
  169. multiple: true,
  170. name: values_field_name(answer_index),
  171. form_id: upload_form_id,
  172. direct_upload_url: core_direct_upload_url,
  173. direct_upload_authorization: core_direct_upload_authorization,
  174. submit_on_upload: false,
  175. validate_on_upload: true,
  176. value: resolved_files_display_value(item),
  177. accept: file_accept_formats(item),
  178. error: field_error_message(item),
  179. ) %>
  180. <% end %>
  181. <% end %>
  182. <% end %>
  183. <% end %>
  184. when: 0 <% when LooposUi::ProtocolForm::DATE_INPUT_TYPE %>
  185. else: 0 then: 0 <% unless answer_index.nil? %>
  186. <%= render LooposUi::FormEntry.new(**form_entry_options(item)) do |entry| %>
  187. <% entry.with_input do %>
  188. <%= hidden_field_tag protocol_element_id_field_name(answer_index), item[:id], form: field_form_attribute %>
  189. <%= render LooposUi::DatePicker.new(
  190. name: value_field_name(answer_index),
  191. granularity: date_granularity(item),
  192. initial_date: date_initial_date(item),
  193. value: date_picker_value(item),
  194. min_date: date_min_date(item),
  195. max_date: date_max_date(item),
  196. blocked_days: date_blocked_days(item),
  197. submit_on_select: input_mode == :autosubmit,
  198. error: field_error_message(item),
  199. ) %>
  200. <% end %>
  201. <% end %>
  202. <% end %>
  203. when: 0 <% when LooposUi::ProtocolForm::DATE_RANGE_INPUT_TYPE %>
  204. else: 0 then: 0 <% unless answer_index.nil? %>
  205. <%= render LooposUi::FormEntry.new(**form_entry_options(item)) do |entry| %>
  206. <% entry.with_input do %>
  207. <%= hidden_field_tag protocol_element_id_field_name(answer_index), item[:id], form: field_form_attribute %>
  208. <%= render LooposUi::DatePicker.new(
  209. name: value_field_name(answer_index),
  210. range: true,
  211. granularity: date_granularity(item),
  212. initial_date: date_range_initial_date(item),
  213. value: date_picker_value(item),
  214. min_date: date_min_date(item),
  215. max_date: date_max_date(item),
  216. blocked_days: date_blocked_days(item),
  217. submit_on_select: input_mode == :autosubmit,
  218. error: field_error_message(item),
  219. ) %>
  220. <% end %>
  221. <% end %>
  222. <% end %>
  223. when: 0 <% when LooposUi::ProtocolForm::PHONE_NUMBER_INPUT_TYPE %>
  224. else: 0 then: 0 <% unless answer_index.nil? %>
  225. <%= render LooposUi::FormEntry.new(**form_entry_options(item)) do |entry| %>
  226. <% entry.with_input do %>
  227. <%= hidden_field_tag protocol_element_id_field_name(answer_index), item[:id], form: field_form_attribute %>
  228. <%= render LooposUi::Inputs::PhoneNumber.new(
  229. name: value_field_name(answer_index),
  230. value: resolved_value(item).to_s,
  231. placeholder: element_placeholder(item),
  232. mode: input_mode,
  233. appearance: resolved_input_appearance,
  234. open_actions: true,
  235. form: field_form_attribute,
  236. error: field_error_message(item),
  237. autocomplete_type: autocomplete_type_for(item) || "off",
  238. initial_country: phone_initial_country(item),
  239. ) %>
  240. <% end %>
  241. <% end %>
  242. <% end %>
  243. when: 0 <% when LooposUi::ProtocolForm::CUSTOM_INPUT_TYPE %>
  244. else: 0 then: 0 <% unless answer_index.nil? %>
  245. <%= render LooposUi::FormEntry.new(**form_entry_options(item)) do |entry| %>
  246. <% entry.with_input do %>
  247. <%= hidden_field_tag protocol_element_id_field_name(answer_index), item[:id], form: field_form_attribute %>
  248. then: 0 else: 0 <% if custom_html_for(item).present? %>
  249. <div
  250. class="protocol_form__custom-input"
  251. data-controller="protocol-custom-input"
  252. data-protocol-custom-input-element-id-value="<%= item[:id] %>"
  253. data-protocol-custom-input-initial-value-value="<%= resolved_value(item).to_s %>"
  254. data-protocol-custom-input-autosubmit-value="<%= input_mode == :autosubmit %>"
  255. data-protocol-custom-input-form-id-value="<%= upload_form_id %>"
  256. >
  257. <%= hidden_field_tag value_field_name(answer_index),
  258. resolved_value(item).to_s,
  259. form: field_form_attribute,
  260. data: { protocol_custom_input_value_field: true } %>
  261. <script type="text/template" data-protocol-custom-input-source>
  262. <%= custom_html_for(item).html_safe %>
  263. </script>
  264. <div data-protocol-custom-input-target="container"></div>
  265. </div>
  266. <% end %>
  267. then: 0 else: 0 <% if field_error_message(item).present? %>
  268. <span class="protocol_form__field-error"><%= field_error_message(item) %></span>
  269. <% end %>
  270. <% end %>
  271. <% end %>
  272. <% end %>
  273. when: 0 <% when LooposUi::ProtocolForm::INFO_HTML_TYPE %>
  274. then: 0 else: 0 <% if info_html_content(item).present? %>
  275. <div class="protocol_form__info-html">
  276. <%= tag.div info_html_content(item).html_safe, class: "protocol_form__info-html-content" %>
  277. </div>
  278. <% end %>
  279. when: 0 <% when LooposUi::ProtocolForm::INFO_IMAGE_TYPE %>
  280. <div class="protocol_form__info-image">
  281. then: 0 else: 0 <% if entry_label(item).present? || hint_present?(item) %>
  282. <div class="protocol_form__info-image-header">
  283. then: 0 <% if entry_label(item).present? %>
  284. <div class="protocol_form__info-image-label">
  285. <div class="protocol_form__label-text">
  286. <%= tag.div info_text_inner_html(item) %>
  287. </div>
  288. then: 0 else: 0 <% if hint_present?(item) %>
  289. <%= render "loopos_ui/protocol_form/element_hint", **element_hint_locals(item) %>
  290. <% end %>
  291. else: 0 </div>
  292. then: 0 else: 0 <% elsif hint_present?(item) %>
  293. <%= render "loopos_ui/protocol_form/element_hint", **element_hint_locals(item) %>
  294. <% end %>
  295. </div>
  296. <% end %>
  297. then: 0 else: 0 <% if info_image_url(item).present? %>
  298. <%= image_tag info_image_url(item), alt: "", class: "protocol_form__info-image-img", loading: "lazy" %>
  299. <% end %>
  300. </div>
  301. when: 0 <% when LooposUi::ProtocolForm::MULTISELECT_INPUT_TYPE %>
  302. else: 0 then: 0 <% unless answer_index.nil? %>
  303. then: 0 else: 0 <% if render_multiselect?(item) && select2_options_for(item).any? %>
  304. <%= render LooposUi::FormEntry.new(**form_entry_options(item)) do |entry| %>
  305. <% entry.with_input do %>
  306. <%= hidden_field_tag protocol_element_id_field_name(answer_index), item[:id], form: field_form_attribute %>
  307. <%= render LooposUi::Inputs::Select2.new(
  308. name: value_field_name(answer_index),
  309. value: resolved_multiselect_value(item),
  310. options: select2_options_for(item),
  311. placeholder: element_placeholder(item),
  312. then: 0 else: 0 variant: dropdown_setting_enabled?(item) ? :list : :grid,
  313. multiple: true,
  314. show_actions_in_menu: true,
  315. submit_selected_value: true,
  316. mode: input_mode,
  317. appearance: resolved_input_appearance,
  318. open_actions: true,
  319. form: field_form_attribute,
  320. error: field_error_message(item),
  321. ) %>
  322. <% end %>
  323. <% end %>
  324. <% end %>
  325. <% end %>
  326. when: 0 <% when "info:title" %>
  327. <div class="protocol_form__info-title">
  328. <p class="protocol_form__label-text protocol_form__label-text--title">
  329. <%= tag.div info_text_inner_html(item) %>
  330. </p>
  331. </div>
  332. when: 0 <% when "info:text" %>
  333. <div class="protocol_form__info-text">
  334. <div class="protocol_form__label-text">
  335. <%= tag.div info_text_inner_html(item) %>
  336. </div>
  337. then: 0 else: 0 <% if item[:body].present? %>
  338. <span class="protocol_form__label-text-body"><%= info_body(item) %></span>
  339. <% end %>
  340. </div>
  341. when: 0 <% when "info:break" %>
  342. <div class="protocol_form__info-break">
  343. <hr>
  344. </div>
  345. when: 0 <% when "info:url" %>
  346. <div class="protocol_form__info-url">
  347. <span class="protocol_form__label-text protocol_form__label-text--url">
  348. then: 0 else: 0 <% if item[:url].present? %>
  349. <%= link_to item[:url_text].presence || item[:url], item[:url], target: "_blank", rel: "noopener noreferrer" %>
  350. <% end %>
  351. </span>
  352. </div>
  353. <% end %>
  354. </div>
  355. <% end %>
  356. </div>

app/components/loopos_ui/radio_card.rb

81.82% lines covered

0.0% branches covered

22 relevant lines. 18 lines covered and 4 lines missed.
3 total branches, 0 branches covered and 3 branches missed.
    
  1. 1 module LooposUi
  2. 1 class RadioCard < LoopComponent
  3. 1 option :value, Types::Coercible::String
  4. 1 option :name, Types::Coercible::String
  5. 1 option :checked, Types::Bool, default: -> { false }
  6. 1 option :disabled, Types::Bool, default: -> { false }
  7. 1 option :form, Types::Coercible::String, optional: true
  8. 1 option :data, Types::Hash, optional: true
  9. 1 private
  10. 1 def option_id
  11. "#{name}-#{value}"
  12. end
  13. 1 class Content < LoopComponent
  14. 1 option :kind, Types::Symbol.enum(:compact, :full), default: -> { :compact }
  15. # FIXME: Should only be Money, but review_extra_value sends strings
  16. 1 option :price, Types::String | Types::Instance(Money), optional: true
  17. 1 renders_one :header, LooposUi::Header
  18. 1 renders_one :money_input
  19. 1 renders_one :text_area, LooposUi::Inputs::TextArea
  20. 1 renders_one :label_tag, LooposUi::StateLabel
  21. 1 def formated_price
  22. else: 0 case price
  23. when: 0 when String
  24. price
  25. when: 0 when Money
  26. price.format
  27. end
  28. end
  29. end
  30. end
  31. end

app/components/loopos_ui/radio_card/content.html.erb

0.0% lines covered

0.0% branches covered

12 relevant lines. 0 lines covered and 12 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. then: 0 <% if kind == :full %>
  2. <div class="lui-radio_card__content__full">
  3. <%= header %>
  4. <div class="w-full">
  5. <%= money_input %>
  6. </div>
  7. <div class="w-full">
  8. <%= text_area %>
  9. </div>
  10. </div>
  11. <div class="lui-radio_card__content__compact">
  12. <%= header %>
  13. <div class="lui-radio_card__content__compact__price">
  14. <%= label_tag %>
  15. <%= tag.span(formated_price, class: "heading-20 text-content whitespace-nowrap") %>
  16. </div>
  17. </div>
  18. else: 0 <% else %>
  19. <div class="lui-radio_card__content__compact-only">
  20. <%= header %>
  21. <div class="lui-radio_card__content__compact-only__price">
  22. <%= label_tag %>
  23. <%= tag.span(formated_price, class: "heading-20 text-content whitespace-nowrap") %>
  24. </div>
  25. </div>
  26. <% end %>

app/components/loopos_ui/radio_card/radio_card.html.erb

0.0% lines covered

100.0% branches covered

4 relevant lines. 0 lines covered and 4 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.label for: option_id, class: classes, data: data do %>
  2. <%= tag.input type: "radio", id: option_id, name: name, value: value, checked: checked, disabled: disabled, class: "lui-radio_card__input", form: form %>
  3. <%= tag.div class: "flex-1 ml-8" do %>
  4. <%= content %>
  5. <% end %>
  6. <% end %>

app/components/loopos_ui/script_editor.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class ScriptEditor < LoopComponent
  3. 1 include LooposUi::InlineEditComponent
  4. 1 option :type
  5. 1 option :can_upload_code, default: -> { false }
  6. 1 option :loop_os_script, optional: true
  7. 1 option :download_url, optional: true
  8. 1 option :attribute
  9. 1 option :title, default: -> { "" }
  10. 1 option :turbo_id
  11. 1 option :default_value
  12. 1 option :read_only, default: -> { true }
  13. 1 option :delete_url
  14. 1 option :upload_url
  15. 1 option :new_edit_params
  16. 1 option :upload_validation_file_url, optional: true
  17. 1 option :delete_validation_file_url, optional: true
  18. # new_edit_params can have
  19. # [:element, :form_url, :submitable, :show_path, :edit_path, :show_edit_button]
  20. end
  21. end

app/components/loopos_ui/script_editor/script_editor.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render "loopos_ui/inline/script_editor",
  2. type: type,
  3. action_buttons: can_upload_code,
  4. loop_os_script: loop_os_script,
  5. download_url: download_url,
  6. attribute: attribute,
  7. title: title,
  8. turbo_id: turbo_id,
  9. default_value: default_value,
  10. read_only: read_only,
  11. upload_url: upload_url,
  12. delete_url: delete_url,
  13. upload_validation_file_url: upload_validation_file_url,
  14. delete_validation_file_url: delete_validation_file_url,
  15. view_component: self,
  16. **new_edit_params
  17. %>

app/components/loopos_ui/script_input_form.rb

42.86% lines covered

0.0% branches covered

28 relevant lines. 12 lines covered and 16 lines missed.
10 total branches, 0 branches covered and 10 branches missed.
    
  1. 1 module LooposUi
  2. 1 class ScriptInputForm < LoopComponent
  3. 1 option :selected_script_id, Types::Coercible::Integer, optional: true
  4. 1 option :input_fields, Types::Array.of(Types::Hash), default: -> { [] }
  5. 1 option :form_id, Types::Coercible::String
  6. 1 option :frame_id, Types::Coercible::String
  7. 1 def normalized_input_fields
  8. Array(input_fields).filter_map do |input_field|
  9. else: 0 then: 0 next unless input_field.is_a?(Hash)
  10. raw_field_name = input_field["name"].to_s
  11. then: 0 else: 0 next if raw_field_name.blank?
  12. {
  13. raw_field_name: raw_field_name,
  14. field_label: field_label_for(raw_field_name),
  15. field_type: input_field["type"].to_s,
  16. required: required_field?(input_field),
  17. input_name: input_name_for(raw_field_name),
  18. options: select_options_for(input_field),
  19. }
  20. end
  21. end
  22. 1 private
  23. 1 def field_label_for(raw_field_name)
  24. then: 0 else: 0 return I18n.t("script.item_id") if raw_field_name == "item_id"
  25. raw_field_name.split("_").map(&:titleize).join(" ")
  26. end
  27. 1 def required_field?(input_field)
  28. input_field["required"].nil? ||
  29. input_field["required"] == true ||
  30. input_field["required"].to_s == "true"
  31. end
  32. 1 def input_name_for(raw_field_name)
  33. "script_inputs[#{raw_field_name}]"
  34. end
  35. 1 def select_options_for(input_field)
  36. suggestions = input_field["suggestions"]
  37. Array(suggestions).filter_map do |suggestion|
  38. else: 0 then: 0 next unless suggestion.is_a?(Hash)
  39. value = suggestion["value"]
  40. label = suggestion["label"] || value
  41. then: 0 else: 0 next if value.blank?
  42. { value: value, text: label }
  43. end
  44. end
  45. end
  46. end

app/components/loopos_ui/script_input_form/script_input_form.html.erb

0.0% lines covered

0.0% branches covered

31 relevant lines. 0 lines covered and 31 lines missed.
13 total branches, 0 branches covered and 13 branches missed.
    
  1. <turbo-frame id="<%= frame_id %>">
  2. <%= hidden_field_tag :loop_os_script_id, selected_script_id %>
  3. then: 0 else: 0 <% if normalized_input_fields.empty? %>
  4. <div class="lui-script-input-form--loading">
  5. <%= render LooposUi::Spinner.new %>
  6. </div>
  7. <% end %>
  8. <div class="lui-script-input-form">
  9. <% normalized_input_fields.each do |field| %>
  10. then: 0 else: 0 <% if field[:required] %>
  11. <%= tag.input(
  12. type: "hidden",
  13. value: field[:raw_field_name],
  14. data: {
  15. custom_action_form_target: "requiredField",
  16. input_name: field[:input_name],
  17. },
  18. ) %>
  19. <% end %>
  20. <%= render LooposUi::FormEntry.new(
  21. label: field[:field_label],
  22. required: field[:required],
  23. orientation: "vertical",
  24. ) do |entry| %>
  25. <%= entry.with_input do %>
  26. <% case field[:field_type] %>
  27. when: 0 <% when "date" %>
  28. <%= render LooposUi::Inputs::Date.new(
  29. name: field[:input_name],
  30. lang: I18n.locale,
  31. mode: :form,
  32. form: form_id,
  33. ) %>
  34. when: 0 <% when "number" %>
  35. <%= render LooposUi::Inputs::Number.new(
  36. name: field[:input_name],
  37. mode: :form,
  38. with_actions: false,
  39. form: form_id,
  40. ) %>
  41. when: 0 <% when "string", "text" %>
  42. <%= render LooposUi::Inputs::Text.new(
  43. name: field[:input_name],
  44. mode: :form,
  45. form: form_id,
  46. ) %>
  47. when: 0 <% when "file" %>
  48. <%= render LooposUi::Inputs::File.new(
  49. name: field[:input_name],
  50. form_id: form_id,
  51. mode: :form,
  52. ) %>
  53. when: 0 <% when "select", "tags" %>
  54. <%= render LooposUi::Inputs::Select.new(
  55. name: field[:input_name],
  56. mode: :form,
  57. options: field[:options],
  58. placeholder: field[:field_label],
  59. then: 0 else: 0 value: field[:options].first&.dig(:value),
  60. readonly: false,
  61. form: form_id,
  62. ) %>
  63. when: 0 <% when "boolean" %>
  64. <%= hidden_field_tag field[:input_name], false %>
  65. <%= render LooposUi::Inputs::Checkbox.new(
  66. label: field[:field_label],
  67. name: field[:input_name],
  68. value: true,
  69. ) %>
  70. else: 0 <% else %>
  71. <%= render LooposUi::Inputs::Text.new(
  72. name: field[:input_name],
  73. mode: :form,
  74. form: form_id,
  75. ) %>
  76. <% end %>
  77. <% end %>
  78. <% end %>
  79. <% end %>
  80. </div>
  81. </turbo-frame>

app/components/loopos_ui/services/base_concern.rb

73.68% lines covered

0.0% branches covered

19 relevant lines. 14 lines covered and 5 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Services
  3. 1 module BaseConcern
  4. 1 extend ActiveSupport::Concern
  5. 1 PERMISSIONS = [
  6. :can_view_errors,
  7. :can_view_providers,
  8. ]
  9. 1 included do
  10. 2 option :data, type: ->(m) { LooposUi::Resources::InvoiceResource.new(model: m) }
  11. 2 option :permissions, default: -> { PERMISSIONS.to_h { |p| [p, false] } }
  12. 2 option :partials_sub_path
  13. 2 option :tabs_to_render,
  14. default: -> {
  15. [
  16. LooposUi::Services::Invoices::Tabs::Info,
  17. { "extra_data"=> "Extra Data" },
  18. { "preview"=> "Preview" },
  19. ]
  20. },
  21. optional: true
  22. end
  23. 1 def initialize(...)
  24. super
  25. @model = data.model
  26. end
  27. 1 PERMISSIONS.each do |permission_name|
  28. 2 define_method(:"#{permission_name}?") do
  29. then: 0 else: 0 @permissions&.dig(permission_name)
  30. end
  31. end
  32. 1 def show_catalog?
  33. LooposUi.config.app_type?(:core)
  34. end
  35. end
  36. end
  37. end

app/components/loopos_ui/services/email_messages/table.rb

38.0% lines covered

0.0% branches covered

50 relevant lines. 19 lines covered and 31 lines missed.
22 total branches, 0 branches covered and 22 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Services
  3. 1 module EmailMessages
  4. 1 class Table < LooposUi::V2::Table
  5. 1 include Turbo::FramesHelper
  6. 1 PERMISSIONS = [
  7. :can_view_errors,
  8. :can_view_providers,
  9. :can_view_templates,
  10. ]
  11. 1 option :data, type: [->(m) { LooposUi::Resources::EmailMessageResource.new(model: m) }]
  12. 1 option :columns_extra, default: -> { {} } # Format: { column_key: { hidden: boolean, filters: array } }
  13. 1 option :permissions, default: -> { PERMISSIONS.to_h { |p| [p, false] } }
  14. 1 def before_render
  15. @columns = table_columns
  16. end
  17. 1 def initialize(**kwargs)
  18. then: 0 else: 0 kwargs[:sortable] = true if kwargs[:sortable].nil?
  19. super(columns: [], **kwargs)
  20. @kwargs = kwargs.except(:columns_extra, :permissions)
  21. end
  22. 1 private
  23. 1 def table_columns
  24. table_columns = HashWithIndifferentAccess.new
  25. table_columns[:id] = { title: t(".email_id"), dataIndex: "id", key: "id", sortable: true }
  26. table_columns[:status] = { title: t(".state"), dataIndex: "status", key: "status", sortable: true }
  27. table_columns[:item] = { title: t(".item_id"), dataIndex: "item", key: "item", sortable: true }
  28. then: 0 else: 0 table_columns[:partnable] =
  29. {
  30. title: t(".partner"),
  31. dataIndex: "partnable",
  32. key: "partnable",
  33. sortable: true,
  34. } if show_partner?
  35. then: 0 else: 0 table_columns[:provider] =
  36. {
  37. title: t(".service_provider"),
  38. dataIndex: "provider",
  39. key: "provider",
  40. sortable: true,
  41. } if can_view_providers?
  42. then: 0 else: 0 table_columns[:template] =
  43. {
  44. title: t(".template"),
  45. dataIndex: "template",
  46. key: "template",
  47. sortable: true,
  48. } if can_view_templates?
  49. table_columns[:to] = { title: t(".to"), dataIndex: "to", key: "to", sortable: true }
  50. then: 0 else: 0 table_columns[:categories] =
  51. {
  52. title: t(".categories"),
  53. dataIndex: "categories",
  54. key: "categories",
  55. sortable: false,
  56. } if show_catalog?
  57. then: 0 else: 0 table_columns[:product] =
  58. { title: t(".product"), dataIndex: "product", key: "product", sortable: false } if show_catalog?
  59. then: 0 else: 0 table_columns[:brands] =
  60. { title: t(".brands"), dataIndex: "brands", key: "brands", sortable: false } if show_catalog?
  61. table_columns[:created_at] =
  62. { title: t(".created_at"), dataIndex: "created_at", key: "created_at", sortable: true }
  63. table_columns.deep_merge!(columns_extra.slice(*table_columns.keys.map(&:to_sym)))
  64. else: 0 then: 0 disable_sorting!(table_columns) unless sortable
  65. table_columns.values
  66. end
  67. 1 PERMISSIONS.each do |permission_name|
  68. 3 define_method(:"#{permission_name}?") do
  69. then: 0 else: 0 @permissions&.dig(permission_name)
  70. end
  71. end
  72. 1 def show_partner?
  73. LooposUi.config.app_type?(:manager)
  74. end
  75. 1 def show_catalog?
  76. !LooposUi.config.app_type?(:manager)
  77. end
  78. 1 def disable_sorting!(columns)
  79. columns.each_value do |column|
  80. column.delete(:sortable)
  81. column.delete(:default_sort)
  82. column.delete(:sort_key)
  83. end
  84. end
  85. 1 def services_status(status)
  86. case status
  87. when: 0 when "created", "pending"
  88. :informative
  89. when: 0 when "failed"
  90. :danger
  91. when: 0 when "sent"
  92. :success
  93. else: 0 else
  94. :neutral
  95. end
  96. end
  97. end
  98. end
  99. end
  100. end

app/components/loopos_ui/services/email_messages/table/table.html.erb

0.0% lines covered

0.0% branches covered

47 relevant lines. 0 lines covered and 47 lines missed.
22 total branches, 0 branches covered and 22 branches missed.
    
  1. <%= render LooposUi::V2::Table.new(columns: @columns, **@kwargs) do |table| %>
  2. <% @data.each do |data_object| %>
  3. <%= service = data_object.model %>
  4. <% table.with_row(key: service.id) do |row| %>
  5. then: 0 else: 0 <% row.with_cell(property: :id, **(service.latest_error_message.present? && can_view_errors? ? { error_messages: [service.latest_error_message] } : {})) do %>
  6. then: 0 <% if service.show_url.present? %>
  7. <%= link_to service.show_url, data: { turbo: false } do %>
  8. <%= tag.div(service.id, class: "copy-14 text-content" )%>
  9. <% end %>
  10. else: 0 <% else %>
  11. <%= tag.div(service.id, class: "copy-14 text-content" )%>
  12. <% end %>
  13. <% end %>
  14. <% row.with_cell(property: :status) do %>
  15. <span>
  16. <%= render LooposUi::StateLabel.new(text: service.status.titleize, color: services_status(service.status)) %>
  17. </span>
  18. <% end %>
  19. <% row.with_cell(property: :item) do %>
  20. then: 0 <% if service.item.present? %>
  21. <%= render LooposUi::Entities::Item.new(item: service.item, url: service.item.show_url) %>
  22. else: 0 <% else %>
  23. <%= tag.div('-', class: "copy-14 text-content" )%>
  24. <% end %>
  25. <% end %>
  26. then: 0 else: 0 <% row.with_cell(property: :partnable) do %>
  27. then: 0 <% if service.partnable.present? %>
  28. <%= render LooposUi::EntityToken.new(text: service.partnable.name, url: service.partnable.show_url) %>
  29. else: 0 <% else %>
  30. <%= tag.div('-', class: "copy-14 text-content" )%>
  31. <% end %>
  32. <% end if show_partner?%>
  33. then: 0 else: 0 <% row.with_cell(property: :categories) do %>
  34. <%= render LooposUi::TokenList.new(max_tokens: 2) do |token_list| %>
  35. <% service.categories.each do |category| %>
  36. <% token_list.with_token_manual do %>
  37. <%= render LooposUi::Entities::Category.new(category: category, url: category.show_url) %>
  38. <% end %>
  39. <% end %>
  40. <% end %>
  41. <% end if show_catalog? %>
  42. then: 0 else: 0 <% row.with_cell(property: :product) do %>
  43. then: 0 <% if service.product.present? %>
  44. <%= render(LooposUi::Entities::Product.new(product: service.product, url: service.product.show_url )) %>
  45. else: 0 <% else %>
  46. <%= tag.div('-', class: "copy-14 text-content" )%>
  47. <% end %>
  48. <% end if show_catalog? %>
  49. then: 0 else: 0 <% row.with_cell(property: :brands) do %>
  50. then: 0 <% if service.brands.present? %>
  51. <%= render LooposUi::TokenList.new(max_tokens: 2) do |token_list| %>
  52. <% service.brands.each do |brand| %>
  53. <% token_list.with_token_manual do %>
  54. <%= render LooposUi::Entities::Brand.new(brand: brand, url: brand.show_url ) %>
  55. <% end %>
  56. <% end %>
  57. <% end %>
  58. else: 0 <% else %>
  59. <%= render LooposUi::Inputs::Text.new( name: "brands", value: "-", readonly: true) %>
  60. <% end %>
  61. <% end if show_catalog? %>
  62. <% row.with_cell(property: :provider) do %>
  63. <%= render LooposUi::EntityToken.new(text: service.provider.name, url: service.provider.show_url) %>
  64. <% end %>
  65. <% row.with_cell(property: :template) do %>
  66. <%= render LooposUi::EntityToken.new(text: service.template.name, url: service.template.show_url) %>
  67. <% end %>
  68. <% row.with_cell(property: :to) do %>
  69. <%= tag.div(service.to || "-", class: "copy-14 text-content" )%>
  70. <% end %>
  71. <% row.with_cell(property: :created_at) do %>
  72. <%= render LooposUi::DateShow.new(date: service.created_at) %>
  73. <% end %>
  74. then: 0 else: 0 <% row.with_action do %>
  75. <%= render LooposUi::Button.for_row_action(
  76. icon: "arrow-up-right-and-arrow-down-left-from-center",
  77. tooltip_text: t(".open_email_message"),
  78. href: service.show_url,
  79. tag_options: { data: { turbo: false } })%>
  80. <% end if service.show_url.present? %>
  81. <% end %>
  82. <% end %>
  83. <% end %>

app/components/loopos_ui/services/email_messages/tabs/info.rb

84.62% lines covered

0.0% branches covered

13 relevant lines. 11 lines covered and 2 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Services
  3. 1 module EmailMessages
  4. 1 module Tabs
  5. 1 class Info < LoopComponent
  6. 1 PERMISSIONS = [
  7. :can_view_providers,
  8. ]
  9. 1 option :data, type: ->(m) { LooposUi::Resources::EmailMessageResource.new(model: m) }
  10. 1 option :permissions, default: -> { PERMISSIONS.to_h { |p| [p, false] } }
  11. 1 PERMISSIONS.each do |permission_name|
  12. 1 define_method(:"#{permission_name}?") do
  13. then: 0 else: 0 @permissions&.dig(permission_name)
  14. end
  15. end
  16. 1 def show_catalog?
  17. LooposUi.config.app_type?(:core)
  18. end
  19. end
  20. end
  21. end
  22. end
  23. end

app/components/loopos_ui/services/email_messages/tabs/info/info.html.erb

0.0% lines covered

0.0% branches covered

57 relevant lines. 0 lines covered and 57 lines missed.
18 total branches, 0 branches covered and 18 branches missed.
    
  1. <% email = data.model %>
  2. <%= render LooposUi::TabsContent.new do |tab| %>
  3. <% tab.with_row do |row| %>
  4. <% row.with_column do %>
  5. <%= render LooposUi::TitleDescription.new( title: t(".title"), description: t(".description"), size: "normal") %>
  6. <% end %>
  7. <% end %>
  8. <% tab.with_row do |row| %>
  9. <% row.with_column do %>
  10. <%= render LooposUi::TabsSection.new(title: t(".email_details.title"),size: "small", underline: true) do %>
  11. then: 0 else: 0 <%= render LooposUi::FormEntry.new(label: t(".email_details.provider"), orientation: "horizontal", label_width: 140) do |entry| %>
  12. <%= entry.with_input do %>
  13. then: 0 <% if email.provider.name.present? %>
  14. <%= render LooposUi::Token.new(text: email.provider.name) %>
  15. else: 0 <% else %>
  16. <%= render LooposUi::Inputs::Text.new(name: "provider", value: "-", readonly: true) %>
  17. <% end %>
  18. <% end %>
  19. <% end if can_view_providers? %>
  20. <%= render LooposUi::FormEntry.new(label: t(".email_details.to"), orientation: "horizontal", label_width: 140) do |entry| %>
  21. <%= entry.with_input do %>
  22. <%= render LooposUi::Inputs::Text.new( name: "to", value: email.to, placeholder: "-", readonly: true) %>
  23. <% end %>
  24. <% end %>
  25. <%= render LooposUi::FormEntry.new(label: t(".email_details.template"), orientation: "horizontal", label_width: 140) do |entry| %>
  26. <%= entry.with_input do %>
  27. <%= render LooposUi::EntityToken.new(text: email.template.name, url: email.template.show_url) %>
  28. <% end %>
  29. <% end %>
  30. <%= render LooposUi::FormEntry.new(label: t(".email_details.created_at"), orientation: "horizontal", label_width: 140) do |entry| %>
  31. <%= entry.with_input do %>
  32. <%= render LooposUi::DateShow.new(date: email.created_at) %>
  33. <% end %>
  34. <% end %>
  35. <%= render LooposUi::FormEntry.new(label: t(".email_details.updated_at"), orientation: "horizontal", label_width: 140) do |entry| %>
  36. <%= entry.with_input do %>
  37. <%= render LooposUi::DateShow.new(date: email.updated_at) %>
  38. <% end %>
  39. <% end %>
  40. <% end %>
  41. <% end %>
  42. <% row.with_column do %>
  43. <%= render LooposUi::TabsSection.new(title: t(".item_info.title"),size: "small", underline: true) do %>
  44. <%= render LooposUi::FormEntry.new(label:"Item ID", orientation: "horizontal", label_width: 140) do |entry| %>
  45. <%= entry.with_input do %>
  46. then: 0 <% if email.item.present? %>
  47. <%= render LooposUi::Entities::Item.new(item: email.item, url: email.item.show_url) %>
  48. else: 0 <% else %>
  49. <%= render LooposUi::Inputs::Text.new( name: "item_full_id", value: "-", readonly: true) %>
  50. <% end %>
  51. <% end %>
  52. <% end %>
  53. then: 0 else: 0 <%= render LooposUi::FormEntry.new(label: t(".item_info.categories"), orientation: "horizontal", label_width: 140) do |entry| %>
  54. <%= entry.with_input do %>
  55. then: 0 <% if email.categories.present? %>
  56. <%= render LooposUi::TokenList.new(max_tokens: 3) do |token_list| %>
  57. <% email.categories.each do |category| %>
  58. <% token_list.with_token_manual do %>
  59. <%= render LooposUi::Entities::Category.new(category: category, url: category.show_url) %>
  60. <% end %>
  61. <% end %>
  62. <% end %>
  63. else: 0 <% else %>
  64. <%= render LooposUi::Inputs::Text.new( name: "categories", value: "-", readonly: true) %>
  65. <% end %>
  66. <% end %>
  67. <% end if show_catalog? %>
  68. then: 0 else: 0 <%= render LooposUi::FormEntry.new(label: t(".item_info.product"), orientation: "horizontal", label_width: 140) do |entry| %>
  69. <%= entry.with_input do %>
  70. then: 0 <% if email.product.present? %>
  71. <%= render(LooposUi::Entities::Product.new(product: email.product, url: email.product.show_url )) %>
  72. else: 0 <% else %>
  73. <%= render LooposUi::Inputs::Text.new( name: "product", value: "-", readonly: true) %>
  74. <% end %>
  75. <% end %>
  76. <% end if show_catalog? %>
  77. then: 0 else: 0 <%= render LooposUi::FormEntry.new(label: t(".item_info.brands"), orientation: "horizontal", label_width: 140) do |entry| %>
  78. <%= entry.with_input do %>
  79. then: 0 <% if email.brands.present? %>
  80. <%= render LooposUi::TokenList.new(max_tokens: 3) do |token_list| %>
  81. <% email.brands.each do |brand| %>
  82. <% token_list.with_token_manual do %>
  83. <%= render LooposUi::Entities::Brand.new(brand: brand, url: brand.show_url) %>
  84. <% end %>
  85. <% end %>
  86. <% end %>
  87. else: 0 <% else %>
  88. <%= render LooposUi::Inputs::Text.new( name: "brands", value: "-", readonly: true) %>
  89. <% end %>
  90. <% end %>
  91. <% end if show_catalog? %>
  92. <% end %>
  93. <% end %>
  94. <% end %>
  95. <% end %>

app/components/loopos_ui/services/email_templates/table.rb

45.95% lines covered

0.0% branches covered

37 relevant lines. 17 lines covered and 20 lines missed.
10 total branches, 0 branches covered and 10 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Services
  3. 1 module EmailTemplates
  4. 1 class Table < LooposUi::V2::Table
  5. 1 include Turbo::FramesHelper
  6. 1 PERMISSIONS = [
  7. :can_view_providers,
  8. ]
  9. 1 option :data, type: [->(m) { LooposUi::Resources::EmailTemplateResource.new(model: m) }]
  10. 1 option :columns_extra, default: -> { {} } # Format: { column_key: { hidden: boolean, filters: array } }
  11. 1 option :permissions, default: -> { PERMISSIONS.to_h { |p| [p, false] } }
  12. 1 option :float_bar_actions_params, default: -> {
  13. {
  14. duplicate: { url: nil },
  15. export_csv: { url: nil },
  16. delete: { url: nil },
  17. }
  18. }
  19. 1 def before_render
  20. @columns = table_columns
  21. end
  22. 1 def initialize(**kwargs)
  23. super(columns: [], **kwargs)
  24. @kwargs = kwargs.except(:columns_extra, :permissions, :float_bar_actions_params)
  25. end
  26. 1 private
  27. 1 def table_columns
  28. table_columns = HashWithIndifferentAccess.new
  29. table_columns[:id] = { title: t(".id"), dataIndex: "id", key: "id", sortable: true }
  30. table_columns[:name] = { title: t(".name"), dataIndex: "name", key: "name", sortable: true }
  31. table_columns[:subject] = { title: t(".subject"), dataIndex: "subject", key: "subject", sortable: false }
  32. table_columns[:template_type] = { title: t(".template_type"), dataIndex: "template_type", key: "template_type", sortable: true }
  33. then: 0 else: 0 table_columns[:external_template_id] =
  34. {
  35. title: t(".external_template_id"),
  36. dataIndex: "external_template_id",
  37. key: "external_template_id",
  38. sortable: true,
  39. } if can_view_providers?
  40. then: 0 else: 0 table_columns[:partnable] =
  41. {
  42. title: t(".partner"),
  43. dataIndex: "partnable",
  44. key: "partnable",
  45. sortable: true,
  46. sort_key: :partnable_name,
  47. } if show_partner?
  48. table_columns[:updated_at] =
  49. {
  50. title: t(".updated_at"),
  51. dataIndex: "updated_at",
  52. key: "updated_at",
  53. sortable: true,
  54. }
  55. table_columns[:created_at] =
  56. {
  57. title: t(".created_at"),
  58. dataIndex: "created_at",
  59. key: "created_at",
  60. sortable: true,
  61. default_sort: :desc,
  62. }
  63. [:template_type, :external_template_id, :partnable, :updated_at, :created_at].each do |column_key|
  64. then: 0 else: 0 then: 0 else: 0 if table_columns[column_key] && columns_extra&.dig(column_key, :filter_key).present?
  65. table_columns[column_key][:filters] = columns_extra.dig(column_key, :filters) || []
  66. end
  67. end
  68. table_columns.deep_merge!(columns_extra.slice(*table_columns.keys.map(&:to_sym)))
  69. table_columns.values
  70. end
  71. 1 PERMISSIONS.each do |permission_name|
  72. 1 define_method(:"#{permission_name}?") do
  73. then: 0 else: 0 @permissions&.dig(permission_name)
  74. end
  75. end
  76. 1 def show_partner?
  77. #[LooposUi::Resources::EmailTemplateResource::EmailTemplate.new] :template is missing in Hash input
  78. true
  79. end
  80. end
  81. end
  82. end
  83. end

app/components/loopos_ui/services/email_templates/table/table.html.erb

0.0% lines covered

0.0% branches covered

49 relevant lines. 0 lines covered and 49 lines missed.
22 total branches, 0 branches covered and 22 branches missed.
    
  1. <%= render LooposUi::V2::Table.new(columns: @columns, **@kwargs) do |table| %>
  2. <% @data.each do |data_object| %>
  3. <% service = data_object.model %>
  4. <% table.with_row(key: service.id) do |row| %>
  5. <% row.with_cell(property: :id) do %>
  6. then: 0 <% if service.show_url.present? %>
  7. <%= link_to service.show_url, data: { turbo: false } do %>
  8. <%= tag.div(service.id, class: "copy-14 text-content" )%>
  9. <% end %>
  10. else: 0 <% else %>
  11. <%= tag.div(service.id, class: "copy-14 text-content" )%>
  12. <% end %>
  13. <% end %>
  14. <% row.with_cell(property: :name) do %>
  15. then: 0 <% if service.show_url.present? %>
  16. <%= link_to service.show_url, data: { turbo: false } do %>
  17. <%= tag.div(service.name, class: "copy-14 text-content" )%>
  18. <% end %>
  19. else: 0 <% else %>
  20. <%= tag.div(service.name, class: "copy-14 text-content" )%>
  21. <% end %>
  22. <% end %>
  23. <% row.with_cell(property: :subject) do %>
  24. <%= tag.div(service.subject, class: "copy-14 text-content" )%>
  25. <% end %>
  26. <% row.with_cell(property: :template_type) do %>
  27. then: 0 else: 0 <%= render LooposUi::EntityToken.new(text: service.template_type&.titleize) %>
  28. <% end %>
  29. then: 0 else: 0 <% row.with_cell(property: :external_template_id) do %>
  30. <%= tag.div(service.external_template_id || "-", class: "copy-14 text-content" )%>
  31. <% end if can_view_providers? %>
  32. then: 0 else: 0 <% row.with_cell(property: :partnable) do %>
  33. then: 0 <% if service.partnable.present? %>
  34. <%= render LooposUi::EntityToken.new(text: service.partnable.name, leading_icon: "fa-regular fa-handshake", url: service.partnable.show_url) %>
  35. else: 0 <% else %>
  36. <%= tag.div('-', class: "copy-14 text-content" )%>
  37. <% end %>
  38. <% end if show_partner? %>
  39. <% row.with_cell(property: :updated_at) do %>
  40. <%= render LooposUi::DateShow.new(date: service.updated_at) %>
  41. <% end %>
  42. <% row.with_cell(property: :created_at) do %>
  43. <%= render LooposUi::DateShow.new(date: service.created_at) %>
  44. <% end %>
  45. then: 0 else: 0 <% if service.try(:duplicate_url).present? %>
  46. <% row.with_action do %>
  47. <%= render LooposUi::Button.for_row_action(
  48. icon: "fa-kit fa-regular-copy-circle-plus",
  49. tooltip_text: t(".duplicate_email_template"),
  50. href: service.duplicate_url,
  51. tag_options: { data: { method: :post, turbo: false } }) %>
  52. <% end %>
  53. <% end %>
  54. then: 0 else: 0 <% if service.try(:delete_url).present? %>
  55. <% row.with_action do %>
  56. <%= render LooposUi::Modal.new(title: t("admin.loop_os_services.email_templates.delete_modal.title"), description: t("admin.loop_os_services.email_templates.delete_modal.description")) do |modal| %>
  57. <% modal.with_trigger do %>
  58. <%= render LooposUi::Button.new(
  59. leading_icon: :trash,
  60. type: :tertiary,
  61. size: :tiny,
  62. kind: :neutral,
  63. tooltip_text: t(".delete_email_template")
  64. ) %>
  65. <% end %>
  66. <% modal.with_primary_action(
  67. text: t("admin.loop_os_services.email_templates.delete_modal.confirm_button"),
  68. tag_options: { type: "submit", "data-turbo-method": :delete },
  69. href: service.delete_url,
  70. type: :primary
  71. ) %>
  72. <% end %>
  73. <% end %>
  74. <% end %>
  75. then: 0 else: 0 <% row.with_action do %>
  76. <%= render LooposUi::Button.for_row_action(
  77. icon: "arrow-up-right-and-arrow-down-left-from-center",
  78. tooltip_text: t(".open_email_template"),
  79. href: service.show_url,
  80. tag_options: { data: { turbo: false } })%>
  81. <% end if service.show_url.present? %>
  82. <% end %>
  83. <% end %>
  84. <%= table.with_action_bar do |float_bar| %>
  85. then: 0 else: 0 <% if float_bar_actions_params.dig(:duplicate, :url).present? %>
  86. <% float_bar.with_action_duplicate(href: float_bar_actions_params.dig(:duplicate, :url), text: t(".duplicate"), type: :secondary, tooltip_text: t(".duplicate_email_templates")) %>
  87. <% end %>
  88. then: 0 else: 0 <% if float_bar_actions_params.dig(:delete, :url).present? %>
  89. <% float_bar.with_modal_action(
  90. title: t("admin.loop_os_services.email_templates.delete_modal.title"),
  91. description: t("admin.loop_os_services.email_templates.delete_modal.description")
  92. ) do |modal| %>
  93. <% modal.with_trigger_button(
  94. text: t(".delete"),
  95. leading_icon: :trash,
  96. size: :tiny,
  97. type: :tertiary,
  98. kind: :neutral,
  99. tooltip_text: t(".delete_email_templates")
  100. ) %>
  101. <% modal.with_primary_action(
  102. text: t("admin.loop_os_services.email_templates.delete_modal.confirm_button"),
  103. tag_options: { data: { turbo_method: :delete } },
  104. href: float_bar_actions_params.dig(:delete, :url),
  105. type: :primary
  106. ) %>
  107. <% end %>
  108. <% end %>
  109. <% end %>
  110. <% end %>

app/components/loopos_ui/services/email_templates/tabs/info.rb

86.67% lines covered

0.0% branches covered

15 relevant lines. 13 lines covered and 2 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Services
  3. 1 module EmailTemplates
  4. 1 module Tabs
  5. 1 class Info < LoopComponent
  6. 1 PERMISSIONS = [
  7. :can_edit,
  8. ]
  9. 1 option :data, type: ->(m) { LooposUi::Resources::EmailTemplateResource.new(model: m) }
  10. 1 option :form_data, default: -> { {} }
  11. 1 option :available_partnables, default: -> { [] }
  12. 1 option :permissions, default: -> { PERMISSIONS.to_h { |p| [p, false] } }
  13. 1 PERMISSIONS.each do |permission_name|
  14. 1 define_method(:"#{permission_name}?") do
  15. then: 0 else: 0 @permissions&.dig(permission_name)
  16. end
  17. end
  18. 1 def partnables_list
  19. (available_partnables.presence || [data.model.partnable]).compact.map { |partnable| { value: partnable.full_id, text: partnable.name } }
  20. end
  21. end
  22. end
  23. end
  24. end
  25. end

app/components/loopos_ui/services/email_templates/tabs/info/info.html.erb

0.0% lines covered

0.0% branches covered

33 relevant lines. 0 lines covered and 33 lines missed.
8 total branches, 0 branches covered and 8 branches missed.
    
  1. <%
  2. email_template = data.model
  3. then: 0 else: 0 form_url = form_data&.dig(:url)
  4. then: 0 else: 0 turbo_frame = form_data&.dig(:turbo_frame)
  5. form_attributes = {
  6. url: form_url.presence,
  7. method: :put,
  8. data: { turbo: true, turbo_frame: turbo_frame },
  9. }
  10. readonly = form_url.blank? || !can_edit?
  11. %>
  12. <%= render LooposUi::TabsContent.new do |tab| %>
  13. <% tab.with_row do |row| %>
  14. <% row.with_column(half: true) do %>
  15. <%= form_with(**form_attributes) do |f| %>
  16. <%= render LooposUi::FormEntry.new(label: t(".id"), orientation: "horizontal", label_width: 140) do |entry| %>
  17. <%= entry.with_input do %>
  18. <%= render LooposUi::Inputs::Text.new(name: "id", value: email_template.id, placeholder: "-", readonly: true) %>
  19. <% end %>
  20. <% end %>
  21. <% end %>
  22. <%= form_with(**form_attributes) do |f| %>
  23. <%= render LooposUi::FormEntry.new(label: t(".kind"), orientation: "horizontal", label_width: 140) do |entry| %>
  24. <%= entry.with_input do %>
  25. <%= render LooposUi::Inputs::Select.new(
  26. name: "template_type",
  27. options: LooposUi::Resources::EmailTemplateResource::TEMPLATE_TYPES.map { |type| { value: type, text: type.titleize } },
  28. value: email_template.template_type,
  29. placeholder: "-",
  30. readonly: readonly,
  31. extra_input_attributes: {
  32. id: "template_type_input",
  33. },
  34. clearable: false,
  35. )
  36. %>
  37. <% end %>
  38. <% end %>
  39. <% end %>
  40. then: 0 else: 0 <div class="<%= email_template.template_type == "send_in_blue" ? "" : "hidden" %>">
  41. <%= form_with(**form_attributes) do |f| %>
  42. <%= render LooposUi::FormEntry.new(label: t(".external_template_id"), orientation: "horizontal", label_width: 140, required: true) do |entry| %>
  43. <%= entry.with_input do %>
  44. <%= render LooposUi::Inputs::Text.new(name: "external_template_id", value: email_template.external_template_id, placeholder: "-", readonly: readonly) %>
  45. <% end %>
  46. <% end %>
  47. <% end %>
  48. </div>
  49. <%= form_with(**form_attributes) do |f| %>
  50. <%= render LooposUi::FormEntry.new(label: t(".subject"), orientation: "horizontal", label_width: 140) do |entry| %>
  51. <%= entry.with_input do %>
  52. <%= render LooposUi::Inputs::Text.new(name: "subject", value: email_template.subject, placeholder: "-", readonly: readonly) %>
  53. <% end %>
  54. <% end %>
  55. <% end %>
  56. <%= form_with(**form_attributes) do |f| %>
  57. <%= render LooposUi::FormEntry.new(label: t(".partnable"), orientation: "horizontal", label_width: 140) do |entry| %>
  58. <%= entry.with_input do %>
  59. <%= render LooposUi::Inputs::Select2.new(
  60. name: "partnable",
  61. options: partnables_list,
  62. then: 0 else: 0 value: email_template.partnable&.full_id,
  63. placeholder: "-",
  64. readonly: readonly,
  65. extra_input_attributes: {
  66. id: "partnable_input",
  67. },
  68. clearable: false,
  69. )
  70. %>
  71. <% end %>
  72. <% end %>
  73. <% end %>
  74. <% end %>
  75. <% end %>
  76. <% end %>

app/components/loopos_ui/services/extra_data.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 module Services
  4. 1 class ExtraData < LoopComponent
  5. 1 option :data, type: ->(m) { LooposUi::Resource.new(model: m) }
  6. end
  7. end
  8. end

app/components/loopos_ui/services/extra_data/extra_data.html.erb

0.0% lines covered

0.0% branches covered

12 relevant lines. 0 lines covered and 12 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%
  2. columns = [
  3. { title: 'Key', dataIndex: 'key', key: :key },
  4. { title: 'Value', dataIndex: 'value', key: :value }
  5. ]
  6. then: 0 else: 0 service_extra_data = data.model.extra_data&.each_with_object({}) do |(key, value), hash|
  7. hash[key] = value
  8. end || {}
  9. %>
  10. <%= render LooposUi::TabsContent.new do |tab| %>
  11. <% tab.with_row do |row| %>
  12. <% row.with_column do %>
  13. <%= render LooposUi::TitleDescription.new(title: "Extra Data", size: "normal") %>
  14. <% end %>
  15. <% end %>
  16. <% tab.with_row do |row| %>
  17. <% row.with_column do %>
  18. <%= render LooposUi::ExtraDataViewer.new(
  19. data: service_extra_data,
  20. readonly: true
  21. ) %>
  22. <% end %>
  23. <% end %>
  24. <% end %>

app/components/loopos_ui/services/incoming_payments/table.rb

41.3% lines covered

0.0% branches covered

46 relevant lines. 19 lines covered and 27 lines missed.
16 total branches, 0 branches covered and 16 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Services
  3. 1 module IncomingPayments
  4. 1 class Table < LooposUi::V2::Table
  5. 1 include Turbo::FramesHelper
  6. 1 PERMISSIONS = [
  7. :can_view_errors,
  8. :can_view_providers,
  9. ]
  10. 1 option :data, type: [->(m) { LooposUi::Resources::IncomingPaymentResource.new(model: m) }]
  11. 1 option :columns_extra, default: -> { {} } # Format: { column_key: { hidden: boolean, filters: array } }
  12. 1 option :permissions, default: -> { PERMISSIONS.to_h { |p| [p, false] } }
  13. 1 option :float_bar_actions_params, default: -> {
  14. {
  15. export_csv: { url: nil },
  16. }
  17. } # Format: { action_key: { url: string } }
  18. 1 def before_render
  19. @columns = table_columns
  20. end
  21. 1 def initialize(**kwargs)
  22. then: 0 else: 0 kwargs[:sortable] = true if kwargs[:sortable].nil?
  23. super(columns: [], **kwargs)
  24. @kwargs = kwargs.except(:columns_extra, :permissions, :float_bar_actions_params)
  25. end
  26. 1 def table_columns
  27. table_columns = HashWithIndifferentAccess.new
  28. table_columns[:id] =
  29. { title: t(".incoming_payment_id"), dataIndex: "id", key: "id", sortable: true }
  30. table_columns[:status] = { title: t(".state"), dataIndex: "status", key: "status", sortable: true }
  31. table_columns[:item] = { title: t(".item_id"), dataIndex: "item", key: "item", sortable: true }
  32. table_columns[:amount] = { title: t(".amount"), dataIndex: "amount", key: "amount", sortable: false }
  33. then: 0 else: 0 table_columns[:partnable] =
  34. { title: t(".partnable"), dataIndex: "partnable", key: "partnable", sortable: true } if show_partner?
  35. then: 0 else: 0 table_columns[:provider] =
  36. { title: t(".provider"), dataIndex: "provider", key: "provider", sortable: true } if can_view_providers?
  37. then: 0 else: 0 table_columns[:categories] =
  38. { title: t(".categories"), dataIndex: "categories", key: "categories", sortable: false } if show_catalog?
  39. then: 0 else: 0 table_columns[:product] =
  40. { title: t(".product"), dataIndex: "product", key: "product", sortable: true } if show_catalog?
  41. then: 0 else: 0 table_columns[:brands] =
  42. { title: t(".brands"), dataIndex: "brands", key: "brands", sortable: false } if show_catalog?
  43. table_columns[:created_at] =
  44. { title: t(".created_at"), dataIndex: "created_at", key: "created_at", sortable: true }
  45. table_columns.deep_merge!(columns_extra.slice(*table_columns.keys.map(&:to_sym)))
  46. else: 0 then: 0 disable_sorting!(table_columns) unless sortable
  47. table_columns.values
  48. end
  49. 1 PERMISSIONS.each do |permission_name|
  50. 2 define_method(:"#{permission_name}?") do
  51. then: 0 else: 0 @permissions&.dig(permission_name)
  52. end
  53. end
  54. 1 def show_partner?
  55. LooposUi.config.app_type?(:manager)
  56. end
  57. 1 def show_catalog?
  58. !LooposUi.config.app_type?(:manager)
  59. end
  60. 1 def disable_sorting!(columns)
  61. columns.each_value do |column|
  62. column.delete(:sortable)
  63. column.delete(:default_sort)
  64. column.delete(:sort_key)
  65. end
  66. end
  67. 1 def services_status(status)
  68. LooposUi::Resources::IncomingPaymentResource::STATUS_LABEL_MAPPING[status.to_sym]
  69. end
  70. end
  71. end
  72. end
  73. end

app/components/loopos_ui/services/incoming_payments/table/table.html.erb

0.0% lines covered

0.0% branches covered

67 relevant lines. 0 lines covered and 67 lines missed.
26 total branches, 0 branches covered and 26 branches missed.
    
  1. <%= render LooposUi::V2::Table.new(columns: @columns, **@kwargs) do |table| %>
  2. <% data.each do |resource| %>
  3. <% service = resource.model %>
  4. <% table.with_row(key: service.id) do |row| %>
  5. <% row.with_cell(property: :id) do %>
  6. then: 0 <% if service.show_url.present? %>
  7. <%= link_to service.show_url, data: { turbo: false } do %>
  8. <%= tag.div(service.id, class: "copy-14 text-content") %>
  9. <% end %>
  10. else: 0 <% else %>
  11. <%= tag.div(service.id, class: "copy-14 text-content") %>
  12. <% end %>
  13. <% end %>
  14. <% row.with_cell(property: :reference) do %>
  15. then: 0 <% if service.reference.present? %>
  16. <%= tag.div(service.reference, class: "copy-14 text-content") %>
  17. else: 0 <% else %>
  18. <%= tag.div('-', class: "copy-14 text-content" )%>
  19. <% end %>
  20. <% end %>
  21. <% row.with_cell(property: :status) do %>
  22. <span>
  23. <%= render LooposUi::StateLabel.new(text: service.status.titleize, color: services_status(service.status)) %>
  24. then: 0 else: 0 <% if can_view_errors? && service.error_messages.present? %>
  25. <%= render LooposUi::Tooltip.new(title: "Issues", position: :bottom) do |tooltip| %>
  26. <% service.error_messages.each do |error_message| %>
  27. <%= tag.div(error_message) %>
  28. <% end %>
  29. <% end %>
  30. <% end %>
  31. </span>
  32. <% end %>
  33. <% row.with_cell(property: :item) do %>
  34. then: 0 <% if service.item.present? %>
  35. <%= render LooposUi::Entities::Item.new(item: service.item, url: service.item.show_url) %>
  36. else: 0 <% else %>
  37. <%= tag.div('-', class: "copy-14 text-content" )%>
  38. <% end %>
  39. <% end %>
  40. <% row.with_cell(property: :amount) do %>
  41. <%= tag.div(service.amount, class: "copy-14 text-content") %>
  42. <% end %>
  43. then: 0 else: 0 <% row.with_cell(property: :partnable) do %>
  44. <%= render LooposUi::Entities::Partnable.new(partnable: service.partnable, url: service.partnable.show_url) %>
  45. <% end if show_partner? %>
  46. then: 0 else: 0 <% row.with_cell(property: :provider) do %>
  47. <%= render LooposUi::Entities::Provider.new(provider: service.provider, url: service.provider.show_url) %>
  48. <% end if can_view_providers? %>
  49. then: 0 else: 0 <% row.with_cell(property: :categories) do %>
  50. <%= render LooposUi::TokenList.new(max_tokens: 2) do |token_list| %>
  51. <%# Make sure to include :catalog_node %>
  52. <% service.categories.each do |category| %>
  53. <% token_list.with_token_manual do %>
  54. <%= render LooposUi::Entities::Category.new(category: category, url: category.show_url) %>
  55. <% end %>
  56. <% end %>
  57. <% end %>
  58. <% end if show_catalog? %>
  59. then: 0 else: 0 <% row.with_cell(property: :product) do %>
  60. then: 0 <% if service.product.present? %>
  61. <%= render(LooposUi::Entities::Product.new(product: service.product, url: service.product.show_url )) %>
  62. else: 0 <% else %>
  63. <%= tag.div('-', class: "copy-14 text-content" )%>
  64. <% end %>
  65. <% end if show_catalog? %>
  66. then: 0 else: 0 <% row.with_cell(property: :brands) do %>
  67. then: 0 <% if service.brands.present? %>
  68. <%= render LooposUi::TokenList.new(max_tokens: 2) do |token_list| %>
  69. <% service.brands.each do |brand| %>
  70. <% token_list.with_token_manual do %>
  71. <%= render LooposUi::Entities::Brand.new(brand: brand, url: brand.show_url ) %>
  72. <% end %>
  73. <% end %>
  74. <% end %>
  75. else: 0 <% else %>
  76. <%= render LooposUi::Inputs::Text.new( name: "brands", value: "-", readonly: true) %>
  77. <% end %>
  78. <% end if show_catalog? %>
  79. <% row.with_cell(property: :created_at) do %>
  80. <%= render LooposUi::DateShow.new(date: service.created_at) %>
  81. <% end %>
  82. <%# Actions %>
  83. then: 0 else: 0 <% row.with_action do %>
  84. <%= render LooposUi::Button.for_row_action(
  85. icon: "arrow-up-right-and-arrow-down-left-from-center",
  86. tooltip_text: t(".open_incoming_payment"),
  87. href: service.show_url,
  88. tag_options: { data: { turbo: false } })%>
  89. <% end if service.show_url.present? %>
  90. <% end %>
  91. <%= table.with_action_bar do |float_bar| %>
  92. then: 0 else: 0 <% if float_bar_actions_params.dig(:export_csv, :url).present? %>
  93. <% float_bar.with_action_export(text: t(".export_csv"), href: float_bar_actions_params.dig(:export_csv, :url)) %>
  94. <% end %>
  95. <% end %>
  96. <% end %>
  97. <% end %>

app/components/loopos_ui/services/incoming_payments/tabs/info.rb

84.62% lines covered

0.0% branches covered

13 relevant lines. 11 lines covered and 2 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Services
  3. 1 module IncomingPayments
  4. 1 module Tabs
  5. 1 class Info < LoopComponent
  6. 1 PERMISSIONS = [
  7. :can_view_providers,
  8. ]
  9. 1 option :data, type: ->(m) { LooposUi::Resources::IncomingPaymentResource.new(model: m) }
  10. 1 option :permissions, default: -> { PERMISSIONS.to_h { |p| [p, false] } }
  11. 1 PERMISSIONS.each do |permission_name|
  12. 1 define_method(:"#{permission_name}?") do
  13. then: 0 else: 0 @permissions&.dig(permission_name)
  14. end
  15. end
  16. 1 def show_catalog?
  17. LooposUi.config.app_type?(:core)
  18. end
  19. end
  20. end
  21. end
  22. end
  23. end

app/components/loopos_ui/services/incoming_payments/tabs/info/info.html.erb

0.0% lines covered

0.0% branches covered

69 relevant lines. 0 lines covered and 69 lines missed.
18 total branches, 0 branches covered and 18 branches missed.
    
  1. <% incoming_payment = data.model %>
  2. <%= render LooposUi::TabsContent.new do |tab| %>
  3. <% tab.with_row do |row| %>
  4. <% row.with_column do %>
  5. <%= render LooposUi::TitleDescription.new( title: t(".title"), description: t(".description"), size: "normal") %>
  6. <% end %>
  7. <% end %>
  8. <% tab.with_row do |row| %>
  9. <% row.with_column do %>
  10. <%= render LooposUi::TabsSection.new(title: t(".incoming_payment_details.title"),size: "small", underline: true) do %>
  11. then: 0 else: 0 <%= render LooposUi::FormEntry.new(label: t(".incoming_payment_details.provider"), orientation: "horizontal", label_width: 140) do |entry| %>
  12. <%= entry.with_input do %>
  13. then: 0 <% if incoming_payment.provider.name.present? %>
  14. <%= render LooposUi::Token.new(text: incoming_payment.provider.name) %>
  15. else: 0 <% else %>
  16. <%= render LooposUi::Inputs::Text.new(name: "provider", value: "-", readonly: true) %>
  17. <% end %>
  18. <% end %>
  19. <% end if can_view_providers? %>
  20. <%= render LooposUi::FormEntry.new(label: t(".incoming_payment_details.amount"), orientation: "horizontal", label_width: 140) do |entry| %>
  21. <%= entry.with_input do %>
  22. <%= render LooposUi::Inputs::Text.new( name: "amount", value: incoming_payment.amount, placeholder: "-", readonly: true) %>
  23. <% end %>
  24. <% end %>
  25. <%= render LooposUi::FormEntry.new(label: t(".incoming_payment_details.paid_date"), orientation: "horizontal", label_width: 140) do |entry| %>
  26. <%= entry.with_input do %>
  27. <%= render LooposUi::DateShow.new(date: incoming_payment.paid_date) %>
  28. <% end %>
  29. <% end %>
  30. <%= render LooposUi::FormEntry.new(label: t(".incoming_payment_details.created_at"), orientation: "horizontal", label_width: 140) do |entry| %>
  31. <%= entry.with_input do %>
  32. <%= render LooposUi::DateShow.new(date: incoming_payment.created_at) %>
  33. <% end %>
  34. <% end %>
  35. <%= render LooposUi::FormEntry.new(label: t(".incoming_payment_details.updated_at"), orientation: "horizontal", label_width: 140) do |entry| %>
  36. <%= entry.with_input do %>
  37. <%= render LooposUi::DateShow.new(date: incoming_payment.updated_at) %>
  38. <% end %>
  39. <% end %>
  40. <% end %>
  41. <% end %>
  42. <% row.with_column do %>
  43. <%= render LooposUi::TabsSection.new(title: t(".item_info.title"),size: "small", underline: true) do %>
  44. <%= render LooposUi::FormEntry.new(label: t(".item_info.item_id"), orientation: "horizontal", label_width: 140) do |entry| %>
  45. <%= entry.with_input do %>
  46. then: 0 <% if incoming_payment.item.present? %>
  47. <%= render LooposUi::Entities::Item.new(item: incoming_payment.item, url: incoming_payment.item.show_url) %>
  48. else: 0 <% else %>
  49. <%= render LooposUi::Inputs::Text.new( name: "item_full_id", value: "-", readonly: true) %>
  50. <% end %>
  51. <% end %>
  52. <% end %>
  53. then: 0 else: 0 <%= render LooposUi::FormEntry.new(label: t(".item_info.categories"), orientation: "horizontal", label_width: 140) do |entry| %>
  54. <%= entry.with_input do %>
  55. then: 0 <% if incoming_payment.categories.present? %>
  56. <%= render LooposUi::TokenList.new(max_tokens: 3) do |token_list| %>
  57. <% incoming_payment.categories.each do |category| %>
  58. <% token_list.with_token_manual do %>
  59. <%= render LooposUi::Entities::Category.new(category: category, url: category.show_url) %>
  60. <% end %>
  61. <% end %>
  62. <% end %>
  63. else: 0 <% else %>
  64. <%= render LooposUi::Inputs::Text.new( name: "categories", value: "-", readonly: true) %>
  65. <% end %>
  66. <% end %>
  67. <% end if show_catalog? %>
  68. then: 0 else: 0 <%= render LooposUi::FormEntry.new(label: t(".item_info.product"), orientation: "horizontal", label_width: 140) do |entry| %>
  69. <%= entry.with_input do %>
  70. then: 0 <% if incoming_payment.product.present? %>
  71. <%= render(LooposUi::Entities::Product.new(product: incoming_payment.product, url: incoming_payment.product.show_url )) %>
  72. else: 0 <% else %>
  73. <%= render LooposUi::Inputs::Text.new( name: "product", value: "-", readonly: true) %>
  74. <% end %>
  75. <% end %>
  76. <% end if show_catalog? %>
  77. then: 0 else: 0 <%= render LooposUi::FormEntry.new(label: t(".item_info.brands"), orientation: "horizontal", label_width: 140) do |entry| %>
  78. <%= entry.with_input do %>
  79. then: 0 <% if incoming_payment.brands.present? %>
  80. <%= render LooposUi::TokenList.new(max_tokens: 3) do |token_list| %>
  81. <% incoming_payment.brands.each do |brand| %>
  82. <% token_list.with_token_manual do %>
  83. <%= render LooposUi::Entities::Brand.new(brand: brand, url: brand.show_url) %>
  84. <% end %>
  85. <% end %>
  86. <% end %>
  87. else: 0 <% else %>
  88. <%= render LooposUi::Inputs::Text.new( name: "brands", value: "-", readonly: true) %>
  89. <% end %>
  90. <% end %>
  91. <% end if show_catalog? %>
  92. <% end %>
  93. <% end %>
  94. <% end %>
  95. <% end %>

app/components/loopos_ui/services/invoices/show.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Services
  3. 1 module Invoices
  4. 1 class Show < LoopComponent
  5. 1 include LooposUi::Services::BaseConcern
  6. 1 option :index_services_path
  7. end
  8. end
  9. end
  10. end

app/components/loopos_ui/services/invoices/show/show.html.erb

0.0% lines covered

0.0% branches covered

16 relevant lines. 0 lines covered and 16 lines missed.
8 total branches, 0 branches covered and 8 branches missed.
    
  1. <%= render LooposUi::ShowLayout.new do |layout| %>
  2. <% layout.with_action_bar do |ab| %>
  3. <% ab.with_breadcrumbs_list do |breadcrumbs| %>
  4. <% breadcrumbs.with_breadcrumb(href: "#" ) { "Services" } %>
  5. <% breadcrumbs.with_breadcrumb(href: index_services_path ) { "Invoices" } %>
  6. then: 0 else: 0 <% breadcrumbs.with_breadcrumb(href: "#" ) { @model.item&.full_id } %>
  7. <% end %>
  8. <% ab.with_action_buttons do |buttons| %>
  9. <% buttons.with_button_group do |g| %>
  10. <% g.with_button_back(href: index_services_path, type: :tertiary) %>
  11. <% end %>
  12. <% end %>
  13. <% end %>
  14. then: 0 else: 0 <% layout.with_header(title: @model.item&.full_id) do |header| %>
  15. <% header.with_title_label_state(**data.status_label_args) %>
  16. <% header.with_right_side do %>
  17. then: 0 else: 0 then: 0 else: 0 <% if can_view_errors? && @model.error_messages&.any? %>
  18. <%= render LooposUi::Services::IssuesArea.new(issues: issues) %>
  19. <% end %>
  20. <% end %>
  21. <% end %>
  22. <%= render LooposUi::Services::TabsLayout.new(
  23. data: @model,
  24. permissions: permissions,
  25. tabs_to_render: tabs_to_render,
  26. partials_sub_path: partials_sub_path
  27. ) %>
  28. <% end %>

app/components/loopos_ui/services/invoices/table.rb

37.74% lines covered

0.0% branches covered

53 relevant lines. 20 lines covered and 33 lines missed.
18 total branches, 0 branches covered and 18 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Services
  3. 1 module Invoices
  4. 1 class Table < LooposUi::V2::Table
  5. 1 include Turbo::FramesHelper
  6. 1 PERMISSIONS = [
  7. :can_view_errors,
  8. :can_view_providers,
  9. ]
  10. 1 option :data, type: [->(m) { LooposUi::Resources::InvoiceResource.new(model: m) }]
  11. # TODO: Set type for the columns
  12. 1 option :columns_extra, default: -> { {} } # Format: { column_key: { hidden: boolean, filters: array } }
  13. 1 option :permissions, default: -> { PERMISSIONS.to_h { |p| [p, false] } }
  14. 1 option :float_bar_actions_params, default: -> {
  15. {
  16. export_csv: { url: nil },
  17. }
  18. } # Format: { action_key: { url: string } }
  19. 1 def before_render(**kwargs)
  20. @kwargs[:columns] = table_columns
  21. end
  22. 1 def initialize(**kwargs)
  23. then: 0 else: 0 kwargs[:sortable] = true if kwargs[:sortable].nil?
  24. super(columns: [], **kwargs)
  25. @kwargs = kwargs.except(:columns_extra, :permissions, :float_bar_actions_params)
  26. end
  27. 1 private
  28. 1 def table_columns
  29. table_columns = HashWithIndifferentAccess.new
  30. table_columns[:id] = { title: t(".invoice_id"), dataIndex: "id", key: "id", sortable: true }
  31. table_columns[:status] = { title: t(".state"), dataIndex: "status", key: "status", sortable: true }
  32. table_columns[:item] = { title: t(".item_id"), dataIndex: "item", key: "item", sortable: true }
  33. table_columns[:invoice_kind] = { title: t(".invoice_kind"), dataIndex: "kind", key: "kind", sortable: true }
  34. table_columns[:amount] = { title: t(".amount"), dataIndex: "amount", key: "amount", sortable: true }
  35. table_columns[:vat] = { title: t(".vat"), dataIndex: "vat", key: "vat", sortable: true }
  36. then: 0 else: 0 table_columns[:partnable] =
  37. { title: t(".partnable"), dataIndex: "partnable", key: "partnable", sortable: true } if show_partner?
  38. then: 0 else: 0 table_columns[:provider] =
  39. { title: t(".provider"), dataIndex: "provider", key: "provider", sortable: true } if can_view_providers?
  40. then: 0 else: 0 table_columns[:categories] =
  41. { title: t(".categories"), dataIndex: "categories", key: "categories", sortable: false } if show_catalog?
  42. then: 0 else: 0 table_columns[:product] =
  43. { title: t(".product"), dataIndex: "product", key: "product", sortable: true } if show_catalog?
  44. then: 0 else: 0 table_columns[:brands] =
  45. { title: t(".brands"), dataIndex: "brands", key: "brands", sortable: false } if show_catalog?
  46. table_columns[:created_at] =
  47. { title: t(".created_at"), dataIndex: "created_at", key: "created_at", sortable: true }
  48. table_columns.deep_merge!(columns_extra.slice(*table_columns.keys.map(&:to_sym)))
  49. else: 0 then: 0 disable_sorting!(table_columns) unless sortable
  50. table_columns.values
  51. end
  52. 1 PERMISSIONS.each do |permission_name|
  53. 2 define_method(:"#{permission_name}?") do
  54. permissions.dig(permission_name)
  55. end
  56. end
  57. 1 def show_partner?
  58. LooposUi.config.app_type?(:manager)
  59. end
  60. 1 def show_catalog?
  61. !LooposUi.config.app_type?(:manager)
  62. end
  63. 1 def disable_sorting!(columns)
  64. columns.each_value do |column|
  65. column.delete(:sortable)
  66. column.delete(:default_sort)
  67. column.delete(:sort_key)
  68. end
  69. end
  70. 1 def services_status(status)
  71. case status
  72. when: 0 when "created", "pending"
  73. :informative
  74. when: 0 when "failed"
  75. :danger
  76. when: 0 when "finalized", "sent"
  77. :success
  78. else: 0 else
  79. :neutral
  80. end
  81. end
  82. end
  83. end
  84. end
  85. end

app/components/loopos_ui/services/invoices/table/table.html.erb

0.0% lines covered

0.0% branches covered

70 relevant lines. 0 lines covered and 70 lines missed.
26 total branches, 0 branches covered and 26 branches missed.
    
  1. <%= render LooposUi::V2::Table.new(**@kwargs) do |table| %>
  2. <% data.each do |resource| %>
  3. <% service = resource.model %>
  4. <% table.with_row(key: service.id) do |row| %>
  5. then: 0 else: 0 <% row.with_cell(property: :id, **(service.latest_error_message.present? && can_view_errors? ? { error_messages: [service.latest_error_message] } : {})) do %>
  6. then: 0 <% if service.show_url.present? %>
  7. <%= link_to service.show_url, data: { turbo: false } do %>
  8. <%= tag.div(service.id, class: "copy-14 text-content") %>
  9. <% end %>
  10. else: 0 <% else %>
  11. <%= tag.div(service.id, class: "copy-14 text-content") %>
  12. <% end %>
  13. <% end %>
  14. <% row.with_cell(property: :status) do %>
  15. <span>
  16. <%= render LooposUi::StateLabel.new(text: service.status.titleize, color: services_status(service.status)) %>
  17. </span>
  18. <% end %>
  19. <% row.with_cell(property: :item) do %>
  20. then: 0 <% if service.item.present? %>
  21. <%= render LooposUi::Entities::Item.new(item: service.item, url: service.item.show_url) %>
  22. else: 0 <% else %>
  23. <%= tag.div('-', class: "copy-14 text-content" )%>
  24. <% end %>
  25. <% end %>
  26. <% row.with_cell(property: :kind) do %>
  27. <%= render LooposUi::EntityToken.new(text: service.kind.titleize) %>
  28. <% end %>
  29. then: 0 else: 0 <% row.with_cell(property: :categories) do %>
  30. <%= render LooposUi::TokenList.new(max_tokens: 2) do |token_list| %>
  31. <%# Make sure to include :catalog_node %>
  32. <% service.categories.each do |category| %>
  33. <% token_list.with_token_manual do %>
  34. <%= render LooposUi::Entities::Category.new(category: category, url: category.show_url) %>
  35. <% end %>
  36. <% end %>
  37. <% end %>
  38. <% end if show_catalog? %>
  39. then: 0 else: 0 <% row.with_cell(property: :product) do %>
  40. then: 0 <% if service.product.present? %>
  41. <%= render(LooposUi::Entities::Product.new(product: service.product, url: service.product.show_url )) %>
  42. else: 0 <% else %>
  43. <%= tag.div('-', class: "copy-14 text-content" )%>
  44. <% end %>
  45. <% end if show_catalog? %>
  46. then: 0 else: 0 <% row.with_cell(property: :brands) do %>
  47. then: 0 <% if service.brands.present? %>
  48. <%= render LooposUi::TokenList.new(max_tokens: 2) do |token_list| %>
  49. <% service.brands.each do |brand| %>
  50. <% token_list.with_token_manual do %>
  51. <%= render LooposUi::Entities::Brand.new(brand: brand, url: brand.show_url ) %>
  52. <% end %>
  53. <% end %>
  54. <% end %>
  55. else: 0 <% else %>
  56. <%= render LooposUi::Inputs::Text.new( name: "brands", value: "-", readonly: true) %>
  57. <% end %>
  58. <% end if show_catalog? %>
  59. <% row.with_cell(property: :amount) do %>
  60. <%= tag.div(service.amount || "-", class: "copy-14 text-content" )%>
  61. <% end %>
  62. <% row.with_cell(property: :vat) do %>
  63. <%= tag.div(service.vat || "-", class: "copy-14 text-content" )%>
  64. <% end %>
  65. then: 0 else: 0 <% row.with_cell(property: :partnable) do %>
  66. then: 0 <% if service.partnable.present? %>
  67. <%= render LooposUi::Entities::Partnable.new(partnable: service.partnable, url: service.partnable.show_url) %>
  68. else: 0 <% else %>
  69. <%= tag.div('-', class: "copy-14 text-content" )%>
  70. <% end %>
  71. <% end if show_partner? %>
  72. then: 0 else: 0 <% row.with_cell(property: :provider) do %>
  73. <%= render LooposUi::Entities::Provider.new(provider: service.provider, url: service.provider.show_url) %>
  74. <% end if can_view_providers? %>
  75. <% row.with_cell(property: :amount) do %>
  76. <%= tag.div(service.amount || "-", class: "copy-14 text-content" )%>
  77. <% end %>
  78. <% row.with_cell(property: :created_at) do %>
  79. <%= render LooposUi::DateShow.new(date: service.created_at) %>
  80. <% end %>
  81. <%# Not needed for now %>
  82. <% row.with_cell(property: :updated_at) do %>
  83. <%= render LooposUi::DateShow.new(date: service.updated_at) %>
  84. <% end if false %>
  85. <% row.with_cell(property: :description) do %>
  86. <%= tag.div(class: "copy-14 text-content text-ellipsis whitespace-nowrap overflow-hidden max-w-[250px]") do %>
  87. <%= service.description || "-" %>
  88. <%= render LooposUi::Tooltip.new(title: "", position: :bottom) do |tooltip| %>
  89. <%= service.description %>
  90. <% end %>
  91. <% end %>
  92. <% end if false %>
  93. <%# Actions %>
  94. then: 0 else: 0 <% row.with_action do %>
  95. <%= render LooposUi::Button.for_row_action(
  96. icon: "arrow-up-right-and-arrow-down-left-from-center",
  97. tooltip_text: t(".open_invoice"),
  98. href: service.show_url,
  99. tag_options: { data: { turbo: false } })%>
  100. </span>
  101. <% end if service.show_url.present? %>
  102. <% end %>
  103. <% end %>
  104. <%= table.with_action_bar do |float_bar| %>
  105. then: 0 else: 0 <% if float_bar_actions_params.dig(:export_csv, :url).present? %>
  106. <% float_bar.with_action_export(text: "Export CSV", href: float_bar_actions_params.dig(:export_csv, :url)) %>
  107. <% end %>
  108. <% end %>
  109. <% end %>

app/components/loopos_ui/services/invoices/tabs/info.rb

73.08% lines covered

0.0% branches covered

26 relevant lines. 19 lines covered and 7 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Services
  3. 1 module Invoices
  4. 1 module Tabs
  5. 1 class Info < LoopComponent
  6. 1 PERMISSIONS = [
  7. :can_view_providers,
  8. ]
  9. 1 option :data, type: ->(m) { LooposUi::Resources::InvoiceResource.new(model: m) }
  10. 1 option :icon, default: -> { self.class.icon }
  11. 1 option :tab_name, default: -> { self.class.tab_name }
  12. 1 option :tab_title, default: -> { self.class.tab_title }
  13. 1 option :permissions, default: -> { PERMISSIONS.to_h { |p| [p, false] } }
  14. 1 def initialize(...)
  15. super
  16. @model = data.model
  17. end
  18. 1 class << self
  19. 1 def icon
  20. "fa-regular fa-memo-circle-info"
  21. end
  22. 1 def tab_name
  23. "info"
  24. end
  25. 1 def tab_title
  26. "Invoice Info"
  27. end
  28. end
  29. 1 PERMISSIONS.each do |permission_name|
  30. 1 define_method(:"#{permission_name}?") do
  31. then: 0 else: 0 @permissions&.dig(permission_name)
  32. end
  33. end
  34. 1 def show_catalog?
  35. LooposUi.config.app_type?(:core)
  36. end
  37. end
  38. end
  39. end
  40. end
  41. end

app/components/loopos_ui/services/invoices/tabs/info/info.html.erb

0.0% lines covered

0.0% branches covered

60 relevant lines. 0 lines covered and 60 lines missed.
14 total branches, 0 branches covered and 14 branches missed.
    
  1. <%= render LooposUi::TabsContent.new do |tab| %>
  2. <% tab.with_row do |row| %>
  3. <% row.with_column do %>
  4. <%= render LooposUi::TitleDescription.new(title: t(".title"), size: "normal" ) %>
  5. <% end %>
  6. <% end %>
  7. <% tab.with_row do |row| %>
  8. <% row.with_column do %>
  9. <%= render LooposUi::TabsSection.new(title: t(".invoice_details.title"), underline: "yes", size: "small") do %>
  10. then: 0 else: 0 <%= render LooposUi::FormEntry.new(label: t(".invoice_details.provider"), orientation: "horizontal", label_width: 140) do |entry| %>
  11. <%= entry.with_input do %>
  12. then: 0 <% if @model.provider.name.present? %>
  13. <%= render LooposUi::EntityToken.new(text: @model.provider.name, url: @model.provider.show_url) %>
  14. else: 0 <% else %>
  15. <%= render LooposUi::Inputs::Text.new(name: "provider", value: "-", readonly: true) %>
  16. <% end %>
  17. <% end %>
  18. <% end if can_view_providers? %>
  19. <%= render LooposUi::FormEntry.new(label: t(".invoice_details.kind"), orientation: "horizontal", label_width: 140) do |entry| %>
  20. <%= entry.with_input do %>
  21. <%= render LooposUi::Token.new(text: @model.kind) %>
  22. <% end %>
  23. <% end %>
  24. <%= render LooposUi::FormEntry.new(label: t(".invoice_details.description"), orientation: "horizontal", label_width: 140) do |entry| %>
  25. <%= entry.with_input do %>
  26. <%= render LooposUi::Inputs::Text.new(name: "description", value: @model.description, placeholder: "-", readonly: true) %>
  27. <% end %>
  28. <% end %>
  29. <%= render LooposUi::FormEntry.new(label: t(".invoice_details.amount"), orientation: "horizontal", label_width: 140) do |entry| %>
  30. <%= entry.with_input do %>
  31. <%= render LooposUi::Inputs::Text.new(name: "amount", value: @model.amount, placeholder: "-", readonly: true) %>
  32. <% end %>
  33. <% end %>
  34. <%= render LooposUi::FormEntry.new(label: t(".invoice_details.vat"), orientation: "horizontal", label_width: 140) do |entry| %>
  35. <%= entry.with_input do %>
  36. <%= render LooposUi::Inputs::Text.new(name: "vat", value: @model.vat, placeholder: "-", readonly: true) %>
  37. <% end %>
  38. <% end %>
  39. <%= render LooposUi::FormEntry.new(label: t(".invoice_details.created_at"), orientation: "horizontal", label_width: 140) do |entry| %>
  40. <%= entry.with_input do %>
  41. <%= render LooposUi::DateShow.new(date: @model.created_at) %>
  42. <% end %>
  43. <% end %>
  44. <%= render LooposUi::FormEntry.new(label: t(".invoice_details.updated_at"), orientation: "horizontal", label_width: 140) do |entry| %>
  45. <%= entry.with_input do %>
  46. <%= render LooposUi::DateShow.new(date: @model.updated_at) %>
  47. <% end %>
  48. <% end %>
  49. <% end %>
  50. <% end %>
  51. <% row.with_column do %>
  52. <%= render LooposUi::TabsSection.new(title: t(".item_info.title"), underline: "yes", size: "small") do %>
  53. <%= render LooposUi::FormEntry.new(label: t(".item_info.item_id"), orientation: "horizontal", label_width: 140) do |entry| %>
  54. <%= entry.with_input do %>
  55. then: 0 <% if @model.item.present? %>
  56. <%= render LooposUi::Entities::Item.new(item: @model.item, url: @model.item.show_url) %>
  57. else: 0 <% else %>
  58. <%= render LooposUi::Inputs::Text.new( name: "item_full_id", value: "-", readonly: true) %>
  59. <% end %>
  60. <% end %>
  61. <% end %>
  62. then: 0 else: 0 <% if show_catalog? %>
  63. <%= render LooposUi::FormEntry.new(label: t(".item_info.categories"), orientation: "horizontal", label_width: 140) do |entry| %>
  64. <%= entry.with_input do %>
  65. then: 0 <% if @model.categories.any? %>
  66. <%= render LooposUi::TokenList.new(max_tokens: 2) do |token_list| %>
  67. <% @model.categories.each do |category| %>
  68. <% token_list.with_token_manual do %>
  69. <%= render LooposUi::Entities::Category.new(category: category, url: category.show_url ) %>
  70. <% end %>
  71. <% end %>
  72. <% end %>
  73. else: 0 <% else %>
  74. <%= render LooposUi::Inputs::Text.new(name: "categories", value: "-", readonly: true) %>
  75. <% end %>
  76. <% end %>
  77. <% end %>
  78. <%= render LooposUi::FormEntry.new(label: t(".item_info.product"), orientation: "horizontal", label_width: 140) do |entry| %>
  79. <%= entry.with_input do %>
  80. then: 0 <% if @model.product.present? %>
  81. <%= render LooposUi::Entities::Product.new(product: @model.product, url: @model.product.show_url) %>
  82. else: 0 <% else %>
  83. <%= render LooposUi::Inputs::Text.new(name: "product", value: "-", readonly: true) %>
  84. <% end %>
  85. <% end %>
  86. <% end %>
  87. <%= render LooposUi::FormEntry.new(label: t(".item_info.brands"), orientation: "horizontal", label_width: 140) do |entry| %>
  88. <%= entry.with_input do %>
  89. then: 0 <% if @model.brands.any? %>
  90. <%= render LooposUi::TokenList.new(max_tokens: 2) do |token_list| %>
  91. <% @model.brands.each do |brand| %>
  92. <% token_list.with_token_manual do %>
  93. <%= render LooposUi::Entities::Brand.new(brand: brand, url: brand.show_url ) %>
  94. <% end %>
  95. <% end %>
  96. <% end %>
  97. else: 0 <% else %>
  98. <%= render LooposUi::Inputs::Text.new(name: "brands", value: "-", readonly: true) %>
  99. <% end %>
  100. <% end %>
  101. <% end %>
  102. <% end %>
  103. <% end %>
  104. <% end %>
  105. <% end %>
  106. <% end %>

app/components/loopos_ui/services/issues_area.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Services
  3. 1 class IssuesArea < LoopComponent
  4. 1 option :issues
  5. end
  6. end
  7. end

app/components/loopos_ui/services/issues_area/issues_area.html.erb

0.0% lines covered

100.0% branches covered

5 relevant lines. 0 lines covered and 5 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <div class="form-layout__error-details">
  2. <h1 class="form-layout__error-details-title">
  3. <i class="fa-regular fa-bell-exclamation"></i>
  4. Issues
  5. </h1>
  6. <% issues.each do |issue| %>
  7. <p class="form-layout__error-details-value form-layout__error-details-value--margin-b">
  8. <%= issue %>
  9. </p>
  10. <% end %>
  11. </div>

app/components/loopos_ui/services/list.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Services
  3. 1 class List < LoopComponent
  4. 1 ORDER = [:emails, :incoming_payments, :invoices, :payments, :sms, :shippings]
  5. ICONS = {
  6. 1 emails: "fa-regular fa-envelope",
  7. incoming_payments: "fa-regular fa-envelope-open-dollar",
  8. invoices: "fa-regular fa-file-invoice",
  9. payments: "fa-regular fa-credit-card",
  10. sms: "fa-regular fa-message-sms",
  11. shippings: "fa-regular fa-truck",
  12. }
  13. 1 option :list
  14. end
  15. end
  16. end

app/components/loopos_ui/services/list/list.html.erb

0.0% lines covered

0.0% branches covered

7 relevant lines. 0 lines covered and 7 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%= render LooposUi::TokenList.new do |token_list| %>
  2. <% ORDER.each do |service| %>
  3. <% details = list[service] %>
  4. then: 0 else: 0 <% if details.present? %>
  5. <% token_list.with_token_manual do %>
  6. <%= render LooposUi::IconTooltip.new(
  7. count: details[:count],
  8. text: details[:tooltip],
  9. icon: ICONS[service]
  10. ) %>
  11. <% end %>
  12. <% end %>
  13. <% end %>
  14. <% end %>

app/components/loopos_ui/services/payments/table.rb

40.43% lines covered

0.0% branches covered

47 relevant lines. 19 lines covered and 28 lines missed.
16 total branches, 0 branches covered and 16 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Services
  3. 1 module Payments
  4. 1 class Table < LooposUi::V2::Table
  5. 1 include Turbo::FramesHelper
  6. 1 PERMISSIONS = [
  7. :can_view_errors,
  8. :can_view_providers,
  9. :can_approve_services_payments,
  10. ]
  11. 1 option :data, type: [->(m) { LooposUi::Resources::PaymentResource.new(model: m) }]
  12. 1 option :columns_extra, default: -> { {} } # Format: { column_key: { hidden: boolean, filters: array } }
  13. 1 option :permissions, default: -> { PERMISSIONS.to_h { |p| [p, false] } }
  14. 1 option :float_bar_actions_params, default: -> {
  15. {
  16. export_csv: { url: nil },
  17. }
  18. } # Format: { action_key: { url: string } }
  19. 1 def before_render
  20. @columns = table_columns
  21. end
  22. 1 def initialize(**kwargs)
  23. then: 0 else: 0 kwargs[:sortable] = true if kwargs[:sortable].nil?
  24. super(columns: [], **kwargs)
  25. @kwargs = kwargs.except(:columns_extra, :permissions, :float_bar_actions_params)
  26. end
  27. 1 def table_columns
  28. table_columns = HashWithIndifferentAccess.new
  29. table_columns[:id] = { title: t(".outgoing_payment_id"), dataIndex: "id", key: "id", sortable: true }
  30. table_columns[:reference] = { title: t(".reference"), dataIndex: "reference", key: "reference", sortable: true }
  31. table_columns[:status] = { title: t(".state"), dataIndex: "status", key: "status", sortable: true }
  32. table_columns[:item] = { title: t(".item_id"), dataIndex: "item", key: "item", sortable: true }
  33. table_columns[:amount] =
  34. { title: t(".amount"), dataIndex: "amount", key: "amount", sortable: false }
  35. then: 0 else: 0 table_columns[:partnable] =
  36. {
  37. title: t(".outgoing_payment_id"),
  38. dataIndex: "partnable",
  39. key: "partnable",
  40. sortable: true,
  41. } if show_partner?
  42. then: 0 else: 0 table_columns[:provider] =
  43. {
  44. title: t(".provider"),
  45. dataIndex: "provider",
  46. key: "provider",
  47. sortable: true,
  48. } if can_view_providers?
  49. then: 0 else: 0 table_columns[:categories] =
  50. {
  51. title: t(".categories"),
  52. dataIndex: "categories",
  53. key: "categories",
  54. sortable: false,
  55. } if show_catalog?
  56. then: 0 else: 0 table_columns[:product] =
  57. { title: t(".product"), dataIndex: "product", key: "product", sortable: true } if show_catalog?
  58. then: 0 else: 0 table_columns[:brands] =
  59. { title: t(".brands"), dataIndex: "brands", key: "brands", sortable: false } if show_catalog?
  60. table_columns[:created_at] =
  61. { title: t(".created_at"), dataIndex: "created_at", key: "created_at", sortable: true }
  62. table_columns.deep_merge!(columns_extra.slice(*table_columns.keys.map(&:to_sym)))
  63. else: 0 then: 0 disable_sorting!(table_columns) unless sortable
  64. table_columns.values
  65. end
  66. 1 PERMISSIONS.each do |permission_name|
  67. 3 define_method(:"#{permission_name}?") do
  68. then: 0 else: 0 @permissions&.dig(permission_name)
  69. end
  70. end
  71. 1 def show_partner?
  72. LooposUi.config.app_type?(:manager)
  73. end
  74. 1 def show_catalog?
  75. !LooposUi.config.app_type?(:manager)
  76. end
  77. 1 def disable_sorting!(columns)
  78. columns.each_value do |column|
  79. column.delete(:sortable)
  80. column.delete(:default_sort)
  81. column.delete(:sort_key)
  82. end
  83. end
  84. 1 def services_status(status)
  85. LooposUi::Resources::PaymentResource::STATUS_LABEL_MAPPING[status.to_sym]
  86. end
  87. end
  88. end
  89. end
  90. end

app/components/loopos_ui/services/payments/table/table.html.erb

0.0% lines covered

0.0% branches covered

78 relevant lines. 0 lines covered and 78 lines missed.
30 total branches, 0 branches covered and 30 branches missed.
    
  1. <%= render LooposUi::V2::Table.new(columns: @columns, **@kwargs) do |table| %>
  2. <% data.each do |resource| %>
  3. <% service = resource.model %>
  4. <% table.with_row(key: service.id) do |row| %>
  5. then: 0 else: 0 <% row.with_cell(property: :id, **(service.latest_error_message.present? && can_view_errors? ? { error_messages: [service.latest_error_message] } : {})) do %>
  6. then: 0 <% if service.show_url.present? %>
  7. <%= link_to service.show_url, data: { turbo: false } do %>
  8. <%= tag.div(service.id, class: "copy-14 text-content") %>
  9. <% end %>
  10. else: 0 <% else %>
  11. <%= tag.div(service.id, class: "copy-14 text-content") %>
  12. <% end %>
  13. <% end %>
  14. <% row.with_cell(property: :reference) do %>
  15. then: 0 <% if service.reference.present? %>
  16. <%= tag.div(service.reference, class: "copy-14 text-content") %>
  17. else: 0 <% else %>
  18. <%= tag.div('-', class: "copy-14 text-content" )%>
  19. <% end %>
  20. <% end %>
  21. <% row.with_cell(property: :status) do %>
  22. <span>
  23. <%= render LooposUi::StateLabel.new(text: service.status.titleize, color: services_status(service.status)) %>
  24. </span>
  25. <% end %>
  26. <% row.with_cell(property: :item) do %>
  27. then: 0 <% if service.item.present? %>
  28. <%= render LooposUi::Entities::Item.new(item: service.item, url: service.item.show_url) %>
  29. else: 0 <% else %>
  30. <%= tag.div('-', class: "copy-14 text-content" )%>
  31. <% end %>
  32. <% end %>
  33. <% row.with_cell(property: :amount) do %>
  34. <%= tag.div(service.amount, class: "copy-14 text-content") %>
  35. <% end %>
  36. then: 0 else: 0 <% row.with_cell(property: :partnable) do %>
  37. <%= render LooposUi::Entities::Partnable.new(partnable: service.partnable, url: service.partnable.show_url) %>
  38. <% end if show_partner? %>
  39. then: 0 else: 0 <% row.with_cell(property: :provider) do %>
  40. <%= render LooposUi::Entities::Provider.new(provider: service.provider, url: service.provider.show_url) %>
  41. <% end if can_view_providers?%>
  42. then: 0 else: 0 <% row.with_cell(property: :categories) do %>
  43. <%= render LooposUi::TokenList.new(max_tokens: 2) do |token_list| %>
  44. <%# Make sure to include :catalog_node %>
  45. <% service.categories.each do |category| %>
  46. <% token_list.with_token_manual do %>
  47. <%= render LooposUi::Entities::Category.new(category: category, url: category.show_url) %>
  48. <% end %>
  49. <% end %>
  50. <% end %>
  51. <% end if show_catalog? %>
  52. then: 0 else: 0 <% row.with_cell(property: :product) do %>
  53. then: 0 <% if service.product.present? %>
  54. <%= render(LooposUi::Entities::Product.new(product: service.product, url: service.product.show_url )) %>
  55. else: 0 <% else %>
  56. <%= tag.div('-', class: "copy-14 text-content" )%>
  57. <% end %>
  58. <% end if show_catalog? %>
  59. then: 0 else: 0 <% row.with_cell(property: :brands) do %>
  60. then: 0 <% if service.brands.present? %>
  61. <%= render LooposUi::TokenList.new(max_tokens: 2) do |token_list| %>
  62. <% service.brands.each do |brand| %>
  63. <% token_list.with_token_manual do %>
  64. <%= render LooposUi::Entities::Brand.new(brand: brand, url: brand.show_url ) %>
  65. <% end %>
  66. <% end %>
  67. <% end %>
  68. else: 0 <% else %>
  69. <%= render LooposUi::Inputs::Text.new( name: "brands", value: "-", readonly: true) %>
  70. <% end %>
  71. <% end if show_catalog? %>
  72. <% row.with_cell(property: :created_at) do %>
  73. <%= render LooposUi::DateShow.new(date: service.created_at) %>
  74. <% end %>
  75. <%# Actions %>
  76. then: 0 else: 0 <% row.with_action do %>
  77. <%= render LooposUi::Button.for_row_action(
  78. icon: "arrow-up-right-and-arrow-down-left-from-center",
  79. tooltip_text: t(".open_payment"),
  80. href: service.show_url,
  81. tag_options: { data: { turbo: false } })%>
  82. </span>
  83. <% end if service.show_url.present? %>
  84. then: 0 else: 0 <% row.with_action do %>
  85. <%= render LooposUi::Modal.new(
  86. title: t(".pay_retry_modal_title"),
  87. description: t(".confirm_payment"),
  88. ) do |modal| %>
  89. <% modal.with_trigger do %>
  90. <%= render LooposUi::Button.for_row_action(
  91. tooltip_text: t(".pay_retry"),
  92. icon: "credit-card"
  93. ) %>
  94. <% end %>
  95. <% modal.with_primary_action(
  96. text: t(".pay"),
  97. href: service.payment_retry_url,
  98. kind: :app,
  99. tag_options: { data: { "mass-assign-target": "action", action: "click->lui--button#startLoading click->actions_loading#run" } },
  100. )%>
  101. <% end %>
  102. <% end if service.payment_retry_url.present? && can_approve_services_payments?%>
  103. <% end %>
  104. <% end %>
  105. <%= table.with_action_bar do |float_bar| %>
  106. then: 0 else: 0 <% if float_bar_actions_params.dig(:export_csv, :url).present? %>
  107. <% float_bar.with_action_export(text: t(".export_csv"), href: float_bar_actions_params.dig(:export_csv, :url)) %>
  108. <% end %>
  109. then: 0 else: 0 <% if can_approve_services_payments? && float_bar_actions_params.dig(:payment_retry, :url).present? %>
  110. <% float_bar.with_action_manual do %>
  111. <%= render LooposUi::Modal.new(
  112. title: t(".pay_retry_modal_title"),
  113. description: t(".confirm_payment"),
  114. ) do |modal| %>
  115. <% modal.with_trigger do %>
  116. <%= render LooposUi::Button.new(
  117. text: t(".pay_retry"),
  118. type: :secondary,
  119. kind: :app,
  120. size: :tiny,
  121. icon: "credit-card"
  122. ) %>
  123. <% end %>
  124. <% modal.with_primary_action(
  125. text: t(".pay"),
  126. href: float_bar_actions_params.dig(:payment_retry, :url),
  127. kind: :app,
  128. tag_options: { data: { "float-bar-target": "actionLink", action: "click->lui--button#startLoading click->actions_loading#run" } },
  129. )%>
  130. <% end %>
  131. <% end %>
  132. <% end %>
  133. <% end %>
  134. <% end %>

app/components/loopos_ui/services/payments/tabs/info.rb

84.62% lines covered

0.0% branches covered

13 relevant lines. 11 lines covered and 2 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Services
  3. 1 module Payments
  4. 1 module Tabs
  5. 1 class Info < LoopComponent
  6. 1 PERMISSIONS = [
  7. :can_view_providers,
  8. ]
  9. 1 option :data, type: ->(m) { LooposUi::Resources::PaymentResource.new(model: m) }
  10. 1 option :permissions, default: -> { PERMISSIONS.to_h { |p| [p, false] } }
  11. 1 PERMISSIONS.each do |permission_name|
  12. 1 define_method(:"#{permission_name}?") do
  13. then: 0 else: 0 @permissions&.dig(permission_name)
  14. end
  15. end
  16. 1 def show_catalog?
  17. LooposUi.config.app_type?(:core)
  18. end
  19. end
  20. end
  21. end
  22. end
  23. end

app/components/loopos_ui/services/payments/tabs/info/info.html.erb

0.0% lines covered

0.0% branches covered

64 relevant lines. 0 lines covered and 64 lines missed.
20 total branches, 0 branches covered and 20 branches missed.
    
  1. <% payment = data.model %>
  2. <%= render LooposUi::TabsContent.new do |tab| %>
  3. <% tab.with_row do |row| %>
  4. <% row.with_column do %>
  5. <%= render LooposUi::TitleDescription.new( title: t(".title"), description: t(".description"), size: "normal") %>
  6. <% end %>
  7. <% end %>
  8. <% tab.with_row do |row| %>
  9. <% row.with_column do %>
  10. <%= render LooposUi::TabsSection.new(title: t(".outgoing_payment_details.title"),size: "small", underline: true) do %>
  11. then: 0 else: 0 <%= render LooposUi::FormEntry.new(label: t(".outgoing_payment_details.provider"), orientation: "horizontal", label_width: 140) do |entry| %>
  12. <%= entry.with_input do %>
  13. then: 0 <% if payment.provider.name.present? %>
  14. <%= render LooposUi::Token.new(text: payment.provider.name) %>
  15. else: 0 <% else %>
  16. <%= render LooposUi::Inputs::Text.new(name: "provider", value: "-", readonly: true) %>
  17. <% end %>
  18. <% end %>
  19. <% end if can_view_providers? %>
  20. <%= render LooposUi::FormEntry.new(label: t(".outgoing_payment_details.reference"), orientation: "horizontal", label_width: 140) do |entry| %>
  21. <%= entry.with_input do %>
  22. <%= render LooposUi::Inputs::Text.new( name: "reference", value: payment.reference, placeholder: "-", readonly: true) %>
  23. <% end %>
  24. <% end %>
  25. <%= render LooposUi::FormEntry.new(label: t(".outgoing_payment_details.amount"), orientation: "horizontal", label_width: 140) do |entry| %>
  26. <%= entry.with_input do %>
  27. <%= render LooposUi::Inputs::Text.new( name: "amount", value: payment.amount, placeholder: "-", readonly: true) %>
  28. <% end %>
  29. <% end %>
  30. <%= render LooposUi::FormEntry.new(label: t(".outgoing_payment_details.paid_date"), orientation: "horizontal", label_width: 140) do |entry| %>
  31. <%= entry.with_input do %>
  32. <%= render LooposUi::Inputs::Text.new( name: "paid_date", value: payment.paid_date, placeholder: "-", readonly: true) %>
  33. <% end %>
  34. <% end %>
  35. then: 0 else: 0 <%= render LooposUi::FormEntry.new(label: t(".outgoing_payment_details.error_messages"), orientation: "horizontal", label_width: 140) do |entry| %>
  36. <%= entry.with_input do %>
  37. <%= render LooposUi::Inputs::Text.new( name: "error_messages", value: payment.error_messages.join(', '), placeholder: "-", readonly: true) %>
  38. <% end %>
  39. <% end if payment.error_messages.present? && payment.status.to_s == "failed" %>
  40. <%= render LooposUi::FormEntry.new(label: t(".outgoing_payment_details.created_at"), orientation: "horizontal", label_width: 140) do |entry| %>
  41. <%= entry.with_input do %>
  42. <%= render LooposUi::DateShow.new(date: payment.created_at) %>
  43. <% end %>
  44. <% end %>
  45. <%= render LooposUi::FormEntry.new(label: t(".outgoing_payment_details.updated_at"), orientation: "horizontal", label_width: 140) do |entry| %>
  46. <%= entry.with_input do %>
  47. <%= render LooposUi::DateShow.new(date: payment.updated_at) %>
  48. <% end %>
  49. <% end %>
  50. <% end %>
  51. <% end %>
  52. <% row.with_column do %>
  53. <%= render LooposUi::TabsSection.new(title: t(".item_info.title"),size: "small", underline: true) do %>
  54. <%= render LooposUi::FormEntry.new(label: t(".item_info.item_id"), orientation: "horizontal", label_width: 140) do |entry| %>
  55. <%= entry.with_input do %>
  56. then: 0 <% if payment.item.present? %>
  57. <%= render LooposUi::Entities::Item.new(item: payment.item, url: payment.item.show_url) %>
  58. else: 0 <% else %>
  59. <%= render LooposUi::Inputs::Text.new( name: "item_full_id", value: "-", readonly: true) %>
  60. <% end %>
  61. <% end %>
  62. <% end %>
  63. then: 0 else: 0 <%= render LooposUi::FormEntry.new(label: t(".item_info.categories"), orientation: "horizontal", label_width: 140) do |entry| %>
  64. <%= entry.with_input do %>
  65. then: 0 <% if payment.categories.present? %>
  66. <%= render LooposUi::TokenList.new(max_tokens: 3) do |token_list| %>
  67. <% payment.categories.each do |category| %>
  68. <% token_list.with_token_manual do %>
  69. <%= render LooposUi::Entities::Category.new(category: category, url: category.show_url) %>
  70. <% end %>
  71. <% end %>
  72. <% end %>
  73. else: 0 <% else %>
  74. <%= render LooposUi::Inputs::Text.new( name: "categories", value: "-", readonly: true) %>
  75. <% end %>
  76. <% end %>
  77. <% end if show_catalog? %>
  78. then: 0 else: 0 <%= render LooposUi::FormEntry.new(label: t(".item_info.product"), orientation: "horizontal", label_width: 140) do |entry| %>
  79. <%= entry.with_input do %>
  80. then: 0 <% if payment.product.present? %>
  81. <%= render(LooposUi::Entities::Product.new(product: payment.product, url: payment.product.show_url )) %>
  82. else: 0 <% else %>
  83. <%= render LooposUi::Inputs::Text.new( name: "product", value: "-", readonly: true) %>
  84. <% end %>
  85. <% end %>
  86. <% end if show_catalog? %>
  87. then: 0 else: 0 <%= render LooposUi::FormEntry.new(label: t(".item_info.brands"), orientation: "horizontal", label_width: 140) do |entry| %>
  88. <%= entry.with_input do %>
  89. then: 0 <% if payment.brands.present? %>
  90. <%= render LooposUi::TokenList.new(max_tokens: 3) do |token_list| %>
  91. <% payment.brands.each do |brand| %>
  92. <% token_list.with_token_manual do %>
  93. <%= render LooposUi::Entities::Brand.new(brand: brand, url: brand.show_url) %>
  94. <% end %>
  95. <% end %>
  96. <% end %>
  97. else: 0 <% else %>
  98. <%= render LooposUi::Inputs::Text.new( name: "brands", value: "-", readonly: true) %>
  99. <% end %>
  100. <% end %>
  101. <% end if show_catalog? %>
  102. <% end %>
  103. <% end %>
  104. <% end %>
  105. <% end %>

app/components/loopos_ui/services/shipping_pickups/table.rb

45.45% lines covered

0.0% branches covered

33 relevant lines. 15 lines covered and 18 lines missed.
10 total branches, 0 branches covered and 10 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Services
  3. 1 module ShippingPickups
  4. 1 class Table < LooposUi::V2::Table
  5. 1 include Turbo::FramesHelper
  6. 1 PERMISSIONS = [
  7. :can_view_errors,
  8. ]
  9. 1 PICKUP_TYPES = [:automatic, :manual]
  10. 1 option :data, type: [->(m) { LooposUi::Resources::ShippingPickupResource.new(model: m) }]
  11. 1 option :columns_extra, default: -> { {} } # Format: { column_key: { hidden: boolean, filters: array } }
  12. 1 option :permissions, default: -> { PERMISSIONS.to_h { |p| [p, false] } }
  13. 1 def initialize(**kwargs)
  14. super(columns: [], **kwargs)
  15. @kwargs = kwargs.except(:columns_extra)
  16. end
  17. 1 def table_columns
  18. table_columns = HashWithIndifferentAccess.new
  19. table_columns[:external_id] =
  20. { title: t(".external_id"), dataIndex: "external_id", key: "external_id", sortable: true }
  21. table_columns[:status] = { title: t(".status"), dataIndex: "status", key: "status", sortable: true }
  22. table_columns[:pickup_type] =
  23. { title: t(".type"), dataIndex: "pickup_type", key: "pickup_type", sortable: true }
  24. table_columns[:pickup_date] =
  25. { title: t(".pickup_date"), dataIndex: "pickup_date", key: "pickup_date", sortable: true }
  26. table_columns[:created_at] =
  27. { title: t(".created_at"), dataIndex: "created_at", key: "created_at", sortable: true }
  28. then: 0 else: 0 then: 0 else: 0 if columns_extra&.dig(:status, :filter_key).present?
  29. table_columns[:status][:filters] = LooposUi::Services::Shippings::Table::STATUSES.map do |status|
  30. {
  31. text: t(".states.#{status}"),
  32. value: status,
  33. }
  34. end
  35. end
  36. then: 0 else: 0 then: 0 else: 0 if columns_extra&.dig(:pickup_type, :filter_key).present?
  37. table_columns[:pickup_type][:filters] = PICKUP_TYPES.map do |type|
  38. {
  39. text: t(".pickup_types.#{type}"),
  40. value: type,
  41. }
  42. end
  43. end
  44. table_columns.deep_merge!(columns_extra.slice(*table_columns.keys.map(&:to_sym)))
  45. table_columns.values
  46. end
  47. 1 PERMISSIONS.each do |permission_name|
  48. 1 define_method(:"#{permission_name}?") do
  49. then: 0 else: 0 @permissions&.dig(permission_name)
  50. end
  51. end
  52. 1 def services_status(status)
  53. LooposUi::Resources::ShippingResource::STATUS_LABEL_MAPPING.fetch(status.to_sym, :neutral)
  54. end
  55. end
  56. end
  57. end
  58. end

app/components/loopos_ui/services/shipping_pickups/table/table.html.erb

0.0% lines covered

0.0% branches covered

31 relevant lines. 0 lines covered and 31 lines missed.
10 total branches, 0 branches covered and 10 branches missed.
    
  1. <%= render LooposUi::V2::Table.new(columns: table_columns, **@kwargs) do |table| %>
  2. <% data.each do |resource| %>
  3. <% service = resource.model %>
  4. <% table.with_row(key: service.external_id) do |row| %>
  5. then: 0 else: 0 <% row.with_cell(property: :external_id, **(service.latest_error_message.present? && can_view_errors? ? { error_messages: [service.latest_error_message] } : {})) do %>
  6. then: 0 <% if service.external_id.blank? %>
  7. else: 0 <%= tag.div('-', class: "copy-14 text-content" )%>
  8. then: 0 <% elsif service.show_url.present? %>
  9. <%= link_to service.show_url, data: { turbo: false } do %>
  10. <%= tag.div(service.external_id, class: "copy-14 text-content") %>
  11. <% end %>
  12. else: 0 <% else %>
  13. <%= tag.div(service.external_id, class: "copy-14 text-content") %>
  14. <% end %>
  15. <% end %>
  16. <% row.with_cell(property: :status) do %>
  17. <span>
  18. <%= render LooposUi::StateLabel.new(text: service.try(:status_label) || t(".states.#{service.status}"), color: services_status(service.status)) %>
  19. </span>
  20. <% end %>
  21. <% row.with_cell(property: :pickup_type) do %>
  22. <%= tag.div(t(".pickup_types.#{service.pickup_type}"), class: "copy-14 text-content") %>
  23. <% end %>
  24. <% row.with_cell(property: :pickup_date) do %>
  25. <%= render LooposUi::DateShow.new(date: service.pickup_date) %>
  26. <% end %>
  27. <% row.with_cell(property: :created_at) do %>
  28. <%= render LooposUi::DateShow.new(date: service.created_at) %>
  29. <% end %>
  30. <%# Actions %>
  31. then: 0 else: 0 <% row.with_action do %>
  32. <%= render LooposUi::Button.for_row_action(
  33. icon: "fa-regular fa-file-arrow-down",
  34. tooltip_text: t(".export_btn"),
  35. href: service.export_pdf, tag_options: { data: { turbo: false } }) %>
  36. <% end if service.export_pdf.present? %>
  37. then: 0 else: 0 <% row.with_action do %>
  38. <%= render LooposUi::Button.for_row_action(
  39. icon: "arrow-up-right-and-arrow-down-left-from-center",
  40. tooltip_text: t(".open_btn"),
  41. href: service.show_url, tag_options: { data: { turbo: false } }) %>
  42. <% end if service.show_url.present? %>
  43. <% end %>
  44. <% end %>
  45. <% end %>

app/components/loopos_ui/services/shipping_pickups/tabs/info.rb

88.89% lines covered

100.0% branches covered

9 relevant lines. 8 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Services
  3. 1 module ShippingPickups
  4. 1 module Tabs
  5. 1 class Info < LoopComponent
  6. 1 option :data, type: ->(m) { LooposUi::Resources::ShippingPickupResource.new(model: m) }
  7. 1 option :shipping_table
  8. 1 def show_catalog?
  9. LooposUi.config.app_type?(:core)
  10. end
  11. end
  12. end
  13. end
  14. end
  15. end

app/components/loopos_ui/services/shipping_pickups/tabs/info/info.html.erb

0.0% lines covered

100.0% branches covered

29 relevant lines. 0 lines covered and 29 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <% shipping_pickup = data.model %>
  2. <%= render LooposUi::TabsContent.new do |tab| %>
  3. <% tab.with_row do |row| %>
  4. <% row.with_column do %>
  5. <%= render LooposUi::TitleDescription.new(title: t(".title"), description: t(".description"), size: "normal") %>
  6. <% end %>
  7. <% end %>
  8. <% tab.with_row do |row| %>
  9. <% row.with_column(half: true) do %>
  10. <%= render LooposUi::TabsSection.new(title: t(".details_title"),size: "small", underline: true) do %>
  11. <%= render LooposUi::FormEntry.new(label: t(".provider"), orientation: "horizontal", label_width: 140) do |entry| %>
  12. <%= entry.with_input do %>
  13. <%= render LooposUi::Inputs::Text.new(name: "provider", value: shipping_pickup.provider.name, readonly: true) %>
  14. <% end %>
  15. <% end %>
  16. <%= render LooposUi::FormEntry.new(label: t(".type"), orientation: "horizontal", label_width: 140) do |entry| %>
  17. <%= entry.with_input do %>
  18. <%= render LooposUi::Inputs::Text.new( name: "type", value: t(".types.#{shipping_pickup.pickup_type}"), readonly: true) %>
  19. <% end %>
  20. <% end %>
  21. <%= render LooposUi::FormEntry.new(label: t(".created_at"), orientation: "horizontal", label_width: 140) do |entry| %>
  22. <%= entry.with_input do %>
  23. <%= render LooposUi::DateShow.new(date: shipping_pickup.created_at) %>
  24. <% end %>
  25. <% end %>
  26. <%= render LooposUi::FormEntry.new(label: t(".pickup_date"), orientation: "horizontal", label_width: 140) do |entry| %>
  27. <%= entry.with_input do %>
  28. <%= render LooposUi::DateShow.new(date: shipping_pickup.pickup_date) %>
  29. <% end %>
  30. <% end %>
  31. <% end %>
  32. <% end %>
  33. <% end %>
  34. <% tab.with_row do |row| %>
  35. <% row.with_column do %>
  36. <%= render LooposUi::TabsSection.new(title: t(".shippings_list_title"), size: "small", underline: true) do %>
  37. <%= render shipping_table %>
  38. <% end %>
  39. <% end %>
  40. <% end %>
  41. <% end %>

app/components/loopos_ui/services/shippings/table.rb

38.46% lines covered

0.0% branches covered

52 relevant lines. 20 lines covered and 32 lines missed.
22 total branches, 0 branches covered and 22 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Services
  3. 1 module Shippings
  4. 1 class Table < LooposUi::V2::Table
  5. 1 include Turbo::FramesHelper
  6. 1 PERMISSIONS = [
  7. :can_view_errors,
  8. :can_view_providers,
  9. ]
  10. 1 STATUSES = [
  11. :created,
  12. :pending,
  13. :waiting_close,
  14. :waiting_receive,
  15. :waiting_collection,
  16. :received,
  17. :collected,
  18. :awaiting_update,
  19. :sent,
  20. :delivered,
  21. :delivery_failed,
  22. :failed,
  23. :canceled,
  24. :complex,
  25. :collection_failed,
  26. :error,
  27. :expired,
  28. ]
  29. 1 option :data, type: [->(m) { LooposUi::Resources::ShippingResource.new(model: m) }]
  30. 1 option :columns_extra, default: -> { {} } # Format: { column_key: { hidden: boolean, filters: array } }
  31. 1 option :permissions, default: -> { PERMISSIONS.to_h { |p| [p, false] } }
  32. 1 option :show_shipping_guide, default: -> { false }
  33. 1 option :float_bar_actions_params, default: -> {
  34. {
  35. export_csv: { url: nil },
  36. }
  37. } # Format: { action_key: { url: string } }
  38. 1 def initialize(**kwargs)
  39. then: 0 else: 0 kwargs[:sortable] = true if kwargs[:sortable].nil?
  40. super(columns: [], **kwargs)
  41. @kwargs = kwargs.except(:columns_extra, :permissions, :float_bar_actions_params, :show_shipping_guide)
  42. end
  43. 1 def table_columns
  44. table_columns = HashWithIndifferentAccess.new
  45. table_columns[:id] = { title: t(".id"), dataIndex: "id", key: "id", sortable: true, default_sort: :desc }
  46. table_columns[:reference] =
  47. { title: t(".reference"), dataIndex: "reference", key: "reference", sortable: true }
  48. table_columns[:status] = { title: t(".status"), dataIndex: "status", key: "status", sortable: true }
  49. table_columns[:items] = { title: t(".items"), dataIndex: "items", key: "items" }
  50. then: 0 else: 0 table_columns[:partnable] =
  51. { title: t(".partnable"), dataIndex: "partnable", key: "partnable", sortable: true } if show_partner?
  52. then: 0 else: 0 table_columns[:provider] =
  53. { title: t(".provider"), dataIndex: "provider", key: "provider", sortable: true } if can_view_providers?
  54. table_columns[:tracking_code] =
  55. { title: t(".tracking_code"), dataIndex: "tracking_code", key: "tracking_code", sortable: false }
  56. table_columns[:pickup_id] =
  57. { title: t(".pickup_id"), dataIndex: "pickup_id", key: "pickup_id", sortable: false }
  58. then: 0 else: 0 table_columns[:categories] =
  59. { title: t(".categories"), dataIndex: "categories", key: "categories", sortable: false } if show_catalog?
  60. then: 0 else: 0 table_columns[:product] =
  61. { title: t(".product"), dataIndex: "product", key: "product", sortable: true } if show_catalog?
  62. then: 0 else: 0 table_columns[:brands] =
  63. { title: t(".brands"), dataIndex: "brands", key: "brands", sortable: false } if show_catalog?
  64. then: 0 else: 0 table_columns[:shipping_guide] =
  65. { title: t(".shipping_guide"), dataIndex: "shipping_guide", key: "shipping_guide", sortable: false } if show_shipping_guide
  66. table_columns[:created_at] =
  67. { title: t(".created_at"), dataIndex: "created_at", key: "created_at", sortable: true }
  68. then: 0 else: 0 then: 0 else: 0 if columns_extra&.dig(:status, :filter_key).present?
  69. table_columns[:status][:filters] = STATUSES.map do |status|
  70. {
  71. text: t(".states.#{status}"),
  72. value: status,
  73. }
  74. end
  75. end
  76. table_columns.deep_merge!(columns_extra.slice(*table_columns.keys.map(&:to_sym)))
  77. else: 0 then: 0 disable_sorting!(table_columns) unless sortable
  78. table_columns.values
  79. end
  80. 1 PERMISSIONS.each do |permission_name|
  81. 2 define_method(:"#{permission_name}?") do
  82. then: 0 else: 0 @permissions&.dig(permission_name)
  83. end
  84. end
  85. 1 def show_partner?
  86. LooposUi.config.app_type?(:manager)
  87. end
  88. 1 def show_catalog?
  89. LooposUi.config.app_type?(:core)
  90. end
  91. 1 def disable_sorting!(columns)
  92. columns.each_value do |column|
  93. column.delete(:sortable)
  94. column.delete(:default_sort)
  95. column.delete(:sort_key)
  96. end
  97. end
  98. 1 def services_status(status)
  99. LooposUi::Resources::ShippingResource::STATUS_LABEL_MAPPING.fetch(status.to_sym, :neutral)
  100. end
  101. end
  102. end
  103. end
  104. end

app/components/loopos_ui/services/shippings/table/table.html.erb

0.0% lines covered

0.0% branches covered

120 relevant lines. 0 lines covered and 120 lines missed.
40 total branches, 0 branches covered and 40 branches missed.
    
  1. <%= render LooposUi::V2::Table.new(columns: table_columns, **@kwargs) do |table| %>
  2. <% data.each do |resource| %>
  3. <% service = resource.model %>
  4. <% table.with_row(key: service.id) do |row| %>
  5. then: 0 else: 0 <% row.with_cell(property: :id, **(service.latest_error_message.present? && can_view_errors? ? { error_messages: [service.latest_error_message] } : {})) do %>
  6. then: 0 <% if service.show_url.present? %>
  7. <%= link_to service.show_url, data: { turbo: false } do %>
  8. <%= tag.div(service.id, class: "copy-14 text-content") %>
  9. <% end %>
  10. else: 0 <% else %>
  11. <%= tag.div(service.id, class: "copy-14 text-content") %>
  12. <% end %>
  13. <% end %>
  14. <% row.with_cell(property: :reference) do %>
  15. <%= tag.div(service.reference, class: "copy-14 text-content") %>
  16. <% end %>
  17. <% row.with_cell(property: :status) do %>
  18. <span>
  19. <%= render LooposUi::StateLabel.new(text: service.try(:status_label) || t(".states.#{service.status}"), color: services_status(service.status)) %>
  20. </span>
  21. <% end %>
  22. <% row.with_cell(property: :items) do %>
  23. then: 0 <% if service.items.present? || service.item.present? %>
  24. <% items_list = (service.items.presence || [service.item]).sort_by(&:full_id) %>
  25. <%= render LooposUi::TokenList.new(max_tokens: 1) do |token_list| %>
  26. <% items_list.each do |item| %>
  27. <% token_list.with_token_manual do %>
  28. <%= render LooposUi::Entities::Item.new(item: item, url: item.show_url) %>
  29. <% end %>
  30. <% end %>
  31. <% end %>
  32. else: 0 <% else %>
  33. <%= tag.div('-', class: "copy-14 text-content" )%>
  34. <% end %>
  35. <% end %>
  36. then: 0 else: 0 <% row.with_cell(property: :partnable) do %>
  37. <%= render LooposUi::Entities::Partnable.new(partnable: service.partnable, url: service.partnable.show_url) %>
  38. <% end if show_partner? %>
  39. then: 0 else: 0 <% row.with_cell(property: :provider) do %>
  40. <%= render LooposUi::Entities::Provider.new(provider: service.provider, url: service.provider.show_url) %>
  41. <% end if can_view_providers? %>
  42. <% row.with_cell(property: :tracking_code) do %>
  43. then: 0 <% if service.tracking_url.present? %>
  44. <%= link_to service.tracking_url, data: { turbo: false } do %>
  45. <%= tag.div(service.tracking_code, class: "copy-14 text-content") %>
  46. <% end %>
  47. else: 0 <% else %>
  48. <%= tag.div(service.tracking_code || '-', class: "copy-14 text-content") %>
  49. <% end %>
  50. <% end %>
  51. <% row.with_cell(property: :pickup_id) do %>
  52. then: 0 <% if service.pickup_show_url.present? %>
  53. <%= link_to service.pickup_show_url, data: { turbo: false } do %>
  54. <%= tag.div(service.pickup_id, class: "copy-14 text-content") %>
  55. <% end %>
  56. else: 0 <% else %>
  57. <%= tag.div(service.pickup_id || '-', class: "copy-14 text-content") %>
  58. <% end %>
  59. <% end %>
  60. then: 0 else: 0 <% row.with_cell(property: :categories) do %>
  61. then: 0 <% if service.categories.present? %>
  62. <%= render LooposUi::TokenList.new(max_tokens: 2) do |token_list| %>
  63. <%# Make sure to include :catalog_node %>
  64. <% service.categories.each do |category| %>
  65. <% token_list.with_token_manual do %>
  66. <%= render LooposUi::Entities::Category.new(category: category, url: category.show_url) %>
  67. <% end %>
  68. <% end %>
  69. <% end %>
  70. else: 0 <% else %>
  71. <%= render LooposUi::Inputs::Text.new( name: "categories", value: "-", readonly: true) %>
  72. <% end %>
  73. <% end if show_catalog? %>
  74. then: 0 else: 0 <% row.with_cell(property: :product) do %>
  75. then: 0 <% if service.product.present? %>
  76. <%= render(LooposUi::Entities::Product.new(product: service.product, url: service.product.show_url )) %>
  77. else: 0 <% else %>
  78. <%= tag.div('-', class: "copy-14 text-content" )%>
  79. <% end %>
  80. <% end if show_catalog? %>
  81. then: 0 else: 0 <% row.with_cell(property: :brands) do %>
  82. then: 0 <% if service.brands.present? %>
  83. <%= render LooposUi::TokenList.new(max_tokens: 2) do |token_list| %>
  84. <% service.brands.each do |brand| %>
  85. <% token_list.with_token_manual do %>
  86. <%= render LooposUi::Entities::Brand.new(brand: brand, url: brand.show_url ) %>
  87. <% end %>
  88. <% end %>
  89. <% end %>
  90. else: 0 <% else %>
  91. <%= render LooposUi::Inputs::Text.new( name: "brands", value: "-", readonly: true) %>
  92. <% end %>
  93. <% end if show_catalog? %>
  94. then: 0 else: 0 <% row.with_cell(property: :shipping_guide) do %>
  95. then: 0 <% if service.shipping_guide_url.present? %>
  96. <%= render LooposUi::Button.new(
  97. text: t(".shipping_guide"),
  98. leading_icon: "file-pdf",
  99. href: service.shipping_guide_url,
  100. tag_options: { target: "_blank" },
  101. size: :tiny,
  102. type: :tertiary,
  103. kind: :neutral,
  104. load_on_click: false,
  105. )
  106. %>
  107. else: 0 <% else %>
  108. <%= tag.div('-', class: "copy-14 text-content") %>
  109. <% end %>
  110. <% end if show_shipping_guide %>
  111. <% row.with_cell(property: :created_at) do %>
  112. <%= render LooposUi::DateShow.new(date: service.created_at) %>
  113. <% end %>
  114. <%# Actions %>
  115. then: 0 else: 0 <% row.with_action do %>
  116. <%= render LooposUi::Button.for_row_action(
  117. icon: "arrow-up-right-and-arrow-down-left-from-center",
  118. tooltip_text: t(".open_btn"),
  119. href: service.show_url, tag_options: { data: { turbo: false } }) %>
  120. <% end if service.show_url.present? %>
  121. then: 0 else: 0 <% row.with_action do %>
  122. <%= render LooposUi::Modal.new(
  123. title: "Update Shipping Status",
  124. description: "Are you sure you want to perform this action?",
  125. form_id: "update_shipping_status_form_#{service.id}",
  126. ) do |modal| %>
  127. <% modal.with_trigger do %>
  128. <%= render LooposUi::Button.for_row_action(
  129. leading_icon: "arrows-rotate",
  130. tooltip_text: t(".update_status_btn")
  131. ) %>
  132. <% end %>
  133. <% modal.with_custom_content do %>
  134. <div class="modal-container">
  135. <div data-controller="mass-assign">
  136. <%= form_tag service.update_status_url, method: :get, id: "update_shipping_status_form_#{service.id}", data: { "mass-assign-target": "action" } do %>
  137. <div class="modal-form" data-mass-assign-target="modal">
  138. <%= hidden_field_tag 'id', service.id %>
  139. <div class="modal__subtitle copy-14 modal-container__select-container">
  140. Statuses
  141. <%= render LooposUi::Inputs::Select2.new(
  142. mode: :form,
  143. form: "update_shipping_status_form_#{service.id}",
  144. bind_hidden_input_to_form: true,
  145. name: "status",
  146. multiple: false,
  147. options: float_bar_actions_params.dig(:update_status, :options).map { |status| { value: status, text: status } },
  148. placeholder: "Statuses",
  149. ) %>
  150. </div>
  151. </div>
  152. <% end %>
  153. </div>
  154. </div>
  155. <% end %>
  156. <% modal.with_primary_action(
  157. text: "Update",
  158. kind: :app,
  159. tag_options: { data: { action: "click->lui--button#startLoading click->actions_loading#run" } },
  160. )%>
  161. <% end %>
  162. <% end if service.update_status_url.present? %>
  163. <% end %>
  164. <% end %>
  165. <%= table.with_action_bar do |float_bar| %>
  166. then: 0 else: 0 <% if float_bar_actions_params.dig(:request_manual_pickup, :url).present? %>
  167. <% float_bar.with_action(text: t(".request_manual_pickup_btn"), leading_icon: "fa-regular fa-plus-large", href: float_bar_actions_params.dig(:request_manual_pickup, :url)) %>
  168. <% end %>
  169. then: 0 else: 0 <% if float_bar_actions_params.dig(:export_csv, :url).present? %>
  170. <% float_bar.with_action_export(text: "Export CSV", href: float_bar_actions_params.dig(:export_csv, :url)) %>
  171. <% end %>
  172. then: 0 else: 0 <% if float_bar_actions_params.dig(:update_status, :url).present? %>
  173. <% float_bar.with_action_manual do %>
  174. <span>
  175. <%= render LooposUi::Tooltip.new(title: "Open", position: :top) %>
  176. <%= render LooposUi::Modal.new(
  177. title: t(".update_status_btn"),
  178. description: "Are you sure you want to perform this action?",
  179. form_id: "update_shipping_status_form_bulk",
  180. ) do |modal| %>
  181. <% modal.with_trigger do %>
  182. <%= render LooposUi::Button.new(
  183. leading_icon: "arrows-rotate", text: t(".update_status_btn"),
  184. size: :tiny,
  185. type: :tertiary, kind: :neutral) %>
  186. <% end %>
  187. <% modal.with_custom_content do %>
  188. <div class="modal-container">
  189. <div data-controller="mass-assign">
  190. <%= form_tag float_bar_actions_params.dig(:update_status, :url), method: :get, id: "update_shipping_status_form_bulk", data: { "mass-assign-target": "action" } do %>
  191. <div class="modal-form" data-mass-assign-target="modal">
  192. <div class="modal__subtitle copy-14 modal-container__select-container">
  193. Statuses
  194. <%= hidden_field_tag :status, value: nil %>
  195. <% status_options = float_bar_actions_params.dig(:update_status, :options).map do |status| %>
  196. <% {
  197. value: status,
  198. text: status,
  199. attributes: {
  200. id: status,
  201. "data-action": "click->mass-assign#updateInputValue",
  202. "data-value": status,
  203. "data-attribute": "status"
  204. }
  205. } %>
  206. <% end %>
  207. <%= render LooposUi::Inputs::Select2.new(
  208. mode: :form,
  209. name: "status",
  210. multiple: false,
  211. options: status_options,
  212. placeholder: "Statuses",
  213. ) %>
  214. <div class="select-form" id="shipping-status-select-form-bulk"></div>
  215. </div>
  216. </div>
  217. <% end %>
  218. </div>
  219. </div>
  220. <% end %>
  221. <% modal.with_primary_action(
  222. text: "Update",
  223. kind: :app,
  224. tag_options: { data: { action: "click->lui--button#startLoading click->actions_loading#run" } },
  225. )%>
  226. <% end %>
  227. </span>
  228. <% end %>
  229. <% end %>
  230. <% end %>
  231. <% end %>

app/components/loopos_ui/services/shippings/tabs/info.rb

90.91% lines covered

0.0% branches covered

11 relevant lines. 10 lines covered and 1 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Services
  3. 1 module Shippings
  4. 1 module Tabs
  5. 1 class Info < LoopComponent
  6. 1 PERMISSIONS = [
  7. :can_view_providers,
  8. ]
  9. 1 option :data, type: ->(m) { LooposUi::Resources::ShippingResource.new(model: m) }
  10. 1 option :permissions, default: -> { PERMISSIONS.to_h { |p| [p, false] } }
  11. 1 PERMISSIONS.each do |permission_name|
  12. 1 define_method(:"#{permission_name}?") do
  13. then: 0 else: 0 @permissions&.dig(permission_name)
  14. end
  15. end
  16. end
  17. end
  18. end
  19. end
  20. end

app/components/loopos_ui/services/shippings/tabs/info/info.html.erb

0.0% lines covered

0.0% branches covered

87 relevant lines. 0 lines covered and 87 lines missed.
16 total branches, 0 branches covered and 16 branches missed.
    
  1. <% shipping = data.model %>
  2. <%= render LooposUi::TabsContent.new do |tab| %>
  3. <% tab.with_row do |row| %>
  4. <% row.with_column do %>
  5. <%= render LooposUi::TitleDescription.new(title: t(".title"), description: t(".description"), size: "normal") %>
  6. <% end %>
  7. <% end %>
  8. <% tab.with_row do |row| %>
  9. <% row.with_column do %>
  10. <%= render LooposUi::TabsSection.new(title: t(".details_title"), size: "small", underline: true) do %>
  11. <% end %>
  12. <% end %>
  13. <% end %>
  14. <% tab.with_row do |row| %>
  15. <% row.with_column do %>
  16. then: 0 else: 0 <%= render LooposUi::FormEntry.new(label: t(".provider"), orientation: "horizontal", label_width: 140) do |entry| %>
  17. <%= entry.with_input do %>
  18. then: 0 <% if shipping.provider.name.present? %>
  19. <%= render LooposUi::Token.new(text: shipping.provider.name) %>
  20. else: 0 <% else %>
  21. <%= render LooposUi::Inputs::Text.new(name: "provider", value: "-", readonly: true) %>
  22. <% end %>
  23. <% end %>
  24. <% end if can_view_providers? %>
  25. <%= render LooposUi::FormEntry.new(label: t(".service_method_name"), orientation: "horizontal", label_width: 140) do |entry| %>
  26. <%= entry.with_input do %>
  27. then: 0 <% if shipping.service_method_name.present? %>
  28. <%= render LooposUi::Token.new(text: shipping.service_method_name) %>
  29. else: 0 <% else %>
  30. <%= render LooposUi::Inputs::Text.new(name: "service_method_name", value: "-", readonly: true) %>
  31. <% end %>
  32. <% end %>
  33. <% end %>
  34. <%= render LooposUi::FormEntry.new(label: t(".package_count"), orientation: "horizontal", label_width: 140) do |entry| %>
  35. <%= entry.with_input do %>
  36. then: 0 else: 0 <%= render LooposUi::Inputs::Text.new(name: "package_count", value: shipping.packages&.size || 0, placeholder: "-", readonly: true) %>
  37. <% end %>
  38. <% end %>
  39. <%= render LooposUi::FormEntry.new(label: t(".items_count"), orientation: "horizontal", label_width: 140) do |entry| %>
  40. <%= entry.with_input do %>
  41. then: 0 else: 0 <%= render LooposUi::Inputs::Text.new(name: "items_count", value: shipping.items&.size || 0, placeholder: "-", readonly: true) %>
  42. <% end %>
  43. <% end %>
  44. <%= render LooposUi::FormEntry.new(label: t(".pickup_number"), orientation: "horizontal", label_width: 140) do |entry| %>
  45. <%= entry.with_input do %>
  46. then: 0 <% if shipping.pickup_id.present? %>
  47. <% link_to shipping.pickup_show_url, data: { turbo: false } do %>
  48. <%= render LooposUi::Inputs::Text.new(name: "close_pickup_id", value: shipping.pickup_id, readonly: true) %>
  49. <% end %>
  50. else: 0 <% else %>
  51. <%= render LooposUi::Inputs::Text.new(name: "close_pickup_id", value: "-", readonly: true) %>
  52. <% end %>
  53. <% end %>
  54. <% end %>
  55. <% end %>
  56. <% row.with_column do %>
  57. <%= render LooposUi::FormEntry.new(label: t(".created_at"), orientation: "horizontal", label_width: 140) do |entry| %>
  58. <%= entry.with_input do %>
  59. <%= render LooposUi::DateShow.new(date: shipping.created_at) %>
  60. <% end %>
  61. <% end %>
  62. <%= render LooposUi::FormEntry.new(label: t(".updated_at"), orientation: "horizontal", label_width: 140) do |entry| %>
  63. <%= entry.with_input do %>
  64. <%= render LooposUi::DateShow.new(date: shipping.updated_at) %>
  65. <% end %>
  66. <% end %>
  67. <% end %>
  68. <% end %>
  69. <% tab.with_row do |row| %>
  70. <% row.with_column do %>
  71. <%= render LooposUi::TabsSection.new(title: t(".sender_info_title"), size: "small", underline: true) do %>
  72. then: 0 else: 0 <%= render LooposUi::FormEntry.new(label: t(".sender_name"), orientation: "horizontal", label_width: 140) do |entry| %>
  73. <%= entry.with_input do %>
  74. <%= render LooposUi::Inputs::Text.new( name: "sender_name", value: shipping.sender_info.name, readonly: true) %>
  75. <% end %>
  76. <% end if shipping.sender_info.name.present? %>
  77. <%= render LooposUi::FormEntry.new(label: t(".sender_street"), orientation: "horizontal", label_width: 140) do |entry| %>
  78. <%= entry.with_input do %>
  79. <%= render LooposUi::Inputs::Text.new( name: "sender_street", value: shipping.sender_info.street, readonly: true) %>
  80. <% end %>
  81. <% end %>
  82. <%= render LooposUi::FormEntry.new(label: t(".sender_zipcode"), orientation: "horizontal", label_width: 140) do |entry| %>
  83. <%= entry.with_input do %>
  84. <%= render LooposUi::Inputs::Text.new( name: "zipcode", value: shipping.sender_info.zipcode, readonly: true) %>
  85. <% end %>
  86. <% end %>
  87. <%= render LooposUi::FormEntry.new(label: t(".sender_city"), orientation: "horizontal", label_width: 140) do |entry| %>
  88. <%= entry.with_input do %>
  89. <%= render LooposUi::Inputs::Text.new( name: "sender_city", value: shipping.sender_info.city, readonly: true) %>
  90. <% end %>
  91. <% end %>
  92. <% end %>
  93. <% end %>
  94. <% row.with_column do %>
  95. <%= render LooposUi::TabsSection.new(title: t(".receiver_info_title"),size: "small", underline: true) do %>
  96. then: 0 else: 0 <%= render LooposUi::FormEntry.new(label: t(".receiver_name"), orientation: "horizontal", label_width: 140) do |entry| %>
  97. <%= entry.with_input do %>
  98. <%= render LooposUi::Inputs::Text.new( name: "receiver_name", value: shipping.receiver_info.name, readonly: true) %>
  99. <% end %>
  100. <% end if shipping.receiver_info.name.present? %>
  101. <%= render LooposUi::FormEntry.new(label: t(".receiver_street"), orientation: "horizontal", label_width: 140) do |entry| %>
  102. <%= entry.with_input do %>
  103. <%= render LooposUi::Inputs::Text.new( name: "receiver_street", value: shipping.receiver_info.street, readonly: true) %>
  104. <% end %>
  105. <% end %>
  106. <%= render LooposUi::FormEntry.new(label: t(".receiver_zipcode"), orientation: "horizontal", label_width: 140) do |entry| %>
  107. <%= entry.with_input do %>
  108. <%= render LooposUi::Inputs::Text.new( name: "zipcode", value: shipping.receiver_info.zipcode, readonly: true) %>
  109. <% end %>
  110. <% end %>
  111. <%= render LooposUi::FormEntry.new(label: t(".receiver_city"), orientation: "horizontal", label_width: 140) do |entry| %>
  112. <%= entry.with_input do %>
  113. <%= render LooposUi::Inputs::Text.new( name: "receiver_city", value: shipping.receiver_info.city, readonly: true) %>
  114. <% end %>
  115. <% end %>
  116. <% end %>
  117. <% end %>
  118. <% end %>
  119. <% end %>

app/components/loopos_ui/services/shippings/tabs/packages.rb

90.0% lines covered

100.0% branches covered

10 relevant lines. 9 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Services
  3. 1 module Shippings
  4. 1 module Tabs
  5. 1 class Packages < LoopComponent
  6. 1 option :data, type: ->(m) { LooposUi::Resources::ShippingResource.new(model: m) }
  7. 1 option :turbo_frame_id, optional: true
  8. 1 [:core, :manager, :hubs].each do |app_type|
  9. 3 define_method(:"#{app_type}?") do
  10. LooposUi.config.app_type?(app_type)
  11. end
  12. end
  13. end
  14. end
  15. end
  16. end
  17. end

app/components/loopos_ui/services/shippings/tabs/packages/packages.html.erb

0.0% lines covered

0.0% branches covered

83 relevant lines. 0 lines covered and 83 lines missed.
20 total branches, 0 branches covered and 20 branches missed.
    
  1. <%
  2. columns = [
  3. { title: t(".item"), dataIndex: 'id', key: :id, sortable: false },
  4. core? && { title: t(".identifiers"), dataIndex: 'identifiers', key: :state, sortable: false },
  5. core? && { title: t(".category"), dataIndex: 'category', key: :state, sortable: false },
  6. core? && { title: t(".product"), dataIndex: 'product', key: :state, sortable: false },
  7. core? && { title: t(".brand"), dataIndex: 'brand', key: :state, sortable: false },
  8. (hubs? || core?) && { title: t(".updated_at"), dataIndex: 'updated_at', key: :state, sortable: false },
  9. (hubs? || core?) && { title: t(".created_at"), dataIndex: 'created_at', key: :state, sortable: false },
  10. ].compact_blank
  11. packages = data.model.packages
  12. %>
  13. <%= render LooposUi::TabsContent.new do |tab| %>
  14. <% tab.with_row do |row| %>
  15. <% row.with_column do %>
  16. <%= render LooposUi::TitleDescription.new(title: t(".title"), description: t(".subtitle"), size: "normal") %>
  17. <% end %>
  18. <% end %>
  19. <% packages.each_with_index do |package, index| %>
  20. <% tab.with_row do |row| %>
  21. <% row.with_column do %>
  22. <% title = capture do %>
  23. <%= t(".package", id: package.id) %>
  24. <% end %>
  25. <%= render LooposUi::Accordion.new(title: title, open: index == 0) do %>
  26. <%= render LooposUi::TabsContent.new do |tab| %>
  27. <% tab.with_row do |row| %>
  28. <% row.with_column do %>
  29. <%= render LooposUi::FormEntry.new(label: t(".package_id"), orientation: "horizontal", label_width: 140) do |entry| %>
  30. <%= entry.with_input do %>
  31. <%= render LooposUi::Inputs::Text.new(name: "package_id", value: package.id, placeholder: "-", readonly: true) %>
  32. <% end %>
  33. <% end %>
  34. <%= render LooposUi::FormEntry.new(label: t(".tracking_code"), orientation: "horizontal", label_width: 140) do |entry| %>
  35. <%= entry.with_input do %>
  36. <% input = capture do %>
  37. <div class="flex flex-row">
  38. then: 0 else: 0 <div class="<%= package.tracking_url.present? && package.tracking_code.present? ? "underline" : "" %>">
  39. <%= render LooposUi::Inputs::Text.new(name: "tracking_code", value: package.tracking_code, placeholder: "-", readonly: true) %>
  40. </div>
  41. </div>
  42. <% end %>
  43. then: 0 <% if package.tracking_url.present? && package.tracking_code.present? %>
  44. <%= link_to package.tracking_url, target: "_blank" do %>
  45. <%= input %>
  46. <% end %>
  47. else: 0 <% else %>
  48. <%= input %>
  49. <% end %>
  50. <% end %>
  51. <% end %>
  52. <%= render LooposUi::FormEntry.new(label: t(".weight"), orientation: "horizontal", label_width: 140) do |entry| %>
  53. <%= entry.with_input do %>
  54. <%= render LooposUi::Inputs::Text.new(name: "weight", value: package.weight, placeholder: "-", readonly: true) %>
  55. <% end %>
  56. <% end %>
  57. <% end %>
  58. <% row.with_column do %>
  59. <%= render LooposUi::FormEntry.new(label: t(".created_at"), orientation: "horizontal", label_width: 140) do |entry| %>
  60. <%= entry.with_input do %>
  61. <%= render LooposUi::DateShow.new(date: package.created_at) %>
  62. <% end %>
  63. <% end %>
  64. <%= render LooposUi::FormEntry.new(label: t(".updated_at"), orientation: "horizontal", label_width: 140) do |entry| %>
  65. <%= entry.with_input do %>
  66. <%= render LooposUi::DateShow.new(date: package.updated_at) %>
  67. <% end %>
  68. <% end %>
  69. <% end %>
  70. <% end %>
  71. then: 0 else: 0 <% if package.items.present? %>
  72. <% tab.with_row do |row| %>
  73. <% row.with_column do %>
  74. <%= render LooposUi::V2::Table.new(id: "items_table", data: package.items, columns: columns, show_pagination: false, show_result_count: false, searchable: false) do |table| %>
  75. <% package.items.each do |item| %>
  76. <% table.with_row(key: item.full_id) do |table_row| %>
  77. <% table_row.with_cell(property: :id) do %>
  78. then: 0 <% if item.show_url.present? %>
  79. <%= link_to item.show_url, data: { "turbo-frame": turbo_frame_id } do %>
  80. <%= item.full_id %>
  81. <% end %>
  82. else: 0 <% else %>
  83. <%= item.full_id %>
  84. <% end %>
  85. <% end %>
  86. <% table_row.with_cell(property: :identifiers) do %>
  87. <div class="flex gap-2">
  88. then: 0 <% if item.identifiers.present? %>
  89. <%= render LooposUi::TokenList.new(max_tokens: 3) do |token_list| %>
  90. <% item.identifiers.each do |identifier| %>
  91. <% token_list.with_token_manual do %>
  92. <% identifier_display = OpenStruct.new(key_text: t("admin.items.identifier_kinds.#{identifier.kind}"), text: identifier.reference) %>
  93. <%= render LooposUi::Entities::Identifier.new(identifier: identifier_display) %>
  94. <% end %>
  95. <% end %>
  96. <% end %>
  97. else: 0 <% else %>
  98. -
  99. <% end %>
  100. </div>
  101. <% end %>
  102. <% table_row.with_cell(property: :category) do %>
  103. then: 0 <% if item.categories.present? %>
  104. <%= render LooposUi::TokenList.new(max_tokens: 3) do |token_list| %>
  105. <% item.categories.each do |category| %>
  106. <% token_list.with_token_manual do %>
  107. <%= render LooposUi::Entities::Category.new(category: category, url: category.show_url.presence) %>
  108. <% end %>
  109. <% end %>
  110. <% end %>
  111. else: 0 <% else %>
  112. -
  113. <% end %>
  114. <% end %>
  115. <% table_row.with_cell(property: :product) do %>
  116. then: 0 <% if item.product.present? %>
  117. <%= render LooposUi::Entities::Product.new(product: item.product, url: item.product.show_url.presence) %>
  118. else: 0 <% else %>
  119. -
  120. <% end %>
  121. <% end %>
  122. <% table_row.with_cell(property: :brand) do %>
  123. then: 0 <% if item.brands.present? %>
  124. <%= render LooposUi::TokenList.new(max_tokens: 3) do |token_list| %>
  125. <% item.brands.each do |brand| %>
  126. <% token_list.with_token_manual do %>
  127. <%= render LooposUi::Entities::Brand.new(brand: brand, url: brand.show_url.presence) %>
  128. <% end %>
  129. <% end %>
  130. <% end %>
  131. else: 0 <% else %>
  132. -
  133. <% end %>
  134. <% end %>
  135. then: 0 else: 0 <% table_row.with_cell(property: :created_at) do %>
  136. <%= render LooposUi::DateShow.new(date: item.created_at) %>
  137. <% end if item.created_at.present? %>
  138. then: 0 else: 0 <% table_row.with_cell(property: :updated_at) do %>
  139. <%= render LooposUi::DateShow.new(date: item.updated_at) %>
  140. <% end if item.updated_at.present? %>
  141. <% end %>
  142. <% end %>
  143. <% end %>
  144. <% end %>
  145. <% end %>
  146. <% end %>
  147. <% end %>
  148. <% end %>
  149. <% end %>
  150. <% end %>
  151. <% end %>
  152. <% end %>

app/components/loopos_ui/services/sms_messages/table.rb

38.0% lines covered

0.0% branches covered

50 relevant lines. 19 lines covered and 31 lines missed.
20 total branches, 0 branches covered and 20 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Services
  3. 1 module SmsMessages
  4. 1 class Table < LooposUi::V2::Table
  5. 1 include Turbo::FramesHelper
  6. 1 PERMISSIONS = [
  7. :can_view_errors,
  8. :can_view_providers,
  9. ]
  10. 1 option :data, type: [->(m) { LooposUi::Resources::SmsMessageResource.new(model: m) }]
  11. 1 option :columns_extra, default: -> { {} } # Format: { column_key: { hidden: boolean, filters: array } }
  12. 1 option :permissions, default: -> { PERMISSIONS.to_h { |p| [p, false] } }
  13. 1 option :float_bar_actions_params, default: -> {
  14. {
  15. export_csv: { url: nil },
  16. }
  17. } # Format: { action_key: { url: string } }
  18. 1 def before_render
  19. @columns = table_columns
  20. end
  21. 1 def initialize(**kwargs)
  22. then: 0 else: 0 kwargs[:sortable] = true if kwargs[:sortable].nil?
  23. super(columns: [], **kwargs)
  24. @kwargs = kwargs.except(:columns_extra, :permissions, :float_bar_actions_params)
  25. end
  26. 1 def table_columns
  27. table_columns = HashWithIndifferentAccess.new
  28. table_columns[:id] = { title: t(".sms_id"), dataIndex: "id", key: "id", sortable: true }
  29. table_columns[:status] = { title: t(".state"), dataIndex: "status", key: "status", sortable: true }
  30. table_columns[:item] = { title: t(".item_id"), dataIndex: "item", key: "item", sortable: true }
  31. then: 0 else: 0 table_columns[:partnable] =
  32. {
  33. title: t(".partnable"),
  34. dataIndex: "partnable",
  35. key: "partnable",
  36. sortable: true,
  37. } if show_partner?
  38. then: 0 else: 0 table_columns[:provider] =
  39. {
  40. title: t(".provider"),
  41. dataIndex: "provider",
  42. key: "provider",
  43. sortable: true,
  44. } if can_view_providers?
  45. table_columns[:to] = { title: t(".to"), dataIndex: "to", key: "to", sortable: true }
  46. then: 0 else: 0 table_columns[:categories] =
  47. { title: t(".categories"), dataIndex: "categories", key: "categories", sortable: false } if show_catalog?
  48. then: 0 else: 0 table_columns[:product] =
  49. { title: t(".product"), dataIndex: "product", key: "product", sortable: true } if show_catalog?
  50. then: 0 else: 0 table_columns[:brands] =
  51. { title: t(".brands"), dataIndex: "brands", key: "brands", sortable: false } if show_catalog?
  52. table_columns[:created_at] =
  53. { title: t(".created_at"), dataIndex: "created_at", key: "created_at", sortable: true }
  54. table_columns.deep_merge!(columns_extra.slice(*table_columns.keys.map(&:to_sym)))
  55. else: 0 then: 0 disable_sorting!(table_columns) unless sortable
  56. table_columns.values
  57. end
  58. 1 PERMISSIONS.each do |permission_name|
  59. 2 define_method(:"#{permission_name}?") do
  60. then: 0 else: 0 @permissions&.dig(permission_name)
  61. end
  62. end
  63. 1 def show_partner?
  64. LooposUi.config.app_type?(:manager)
  65. end
  66. 1 def show_catalog?
  67. !LooposUi.config.app_type?(:manager)
  68. end
  69. 1 def disable_sorting!(columns)
  70. columns.each_value do |column|
  71. column.delete(:sortable)
  72. column.delete(:default_sort)
  73. column.delete(:sort_key)
  74. end
  75. end
  76. 1 def services_status(status)
  77. case status
  78. when: 0 when "created", "sent"
  79. :success
  80. when: 0 when "pending"
  81. :informative
  82. when: 0 when "failed"
  83. :danger
  84. else: 0 else
  85. :neutral
  86. end
  87. end
  88. end
  89. end
  90. end
  91. end

app/components/loopos_ui/services/sms_messages/table/table.html.erb

0.0% lines covered

0.0% branches covered

46 relevant lines. 0 lines covered and 46 lines missed.
24 total branches, 0 branches covered and 24 branches missed.
    
  1. <%= render LooposUi::V2::Table.new(columns: @columns, **@kwargs) do |table| %>
  2. <% data.each do |resource| %>
  3. <% service = resource.model %>
  4. <% table.with_row(key: service.id) do |row| %>
  5. then: 0 else: 0 <% row.with_cell(property: :id, **(service.latest_error_message.present? && can_view_errors? ? { error_messages: [service.latest_error_message] } : {})) do %>
  6. then: 0 <% if service.show_url.present? %>
  7. <%= link_to service.show_url, data: { turbo: false } do %>
  8. <%= tag.div(service.id, class: "copy-14 text-content") %>
  9. <% end %>
  10. else: 0 <% else %>
  11. <%= tag.div(service.id, class: "copy-14 text-content") %>
  12. <% end %>
  13. <% end %>
  14. <% row.with_cell(property: :status) do %>
  15. <span>
  16. <%= render LooposUi::StateLabel.new(text: service.status.titleize, color: services_status(service.status)) %>
  17. </span>
  18. <% end %>
  19. <% row.with_cell(property: :item) do %>
  20. then: 0 <% if service.item.present? %>
  21. <%= render LooposUi::Entities::Item.new(item: service.item, url: service.item.show_url) %>
  22. else: 0 <% else %>
  23. <%= tag.div('-', class: "copy-14 text-content" )%>
  24. <% end %>
  25. <% end %>
  26. then: 0 else: 0 <% row.with_cell(property: :partnable) do %>
  27. <%= render LooposUi::Entities::Partnable.new(partnable: service.partnable, url: service.partnable.show_url) %>
  28. <% end if show_partner? %>
  29. then: 0 else: 0 <% row.with_cell(property: :provider) do %>
  30. <%= render LooposUi::Entities::Provider.new(provider: service.provider, url: service.provider.show_url) %>
  31. <% end if can_view_providers? %>
  32. <% row.with_cell(property: :to) do %>
  33. <%= tag.div(service.to, class: "copy-14 text-content") %>
  34. <% end %>
  35. then: 0 else: 0 <% row.with_cell(property: :categories) do %>
  36. <%= render LooposUi::TokenList.new(max_tokens: 2) do |token_list| %>
  37. <%# Make sure to include :catalog_node %>
  38. <% service.categories.each do |category| %>
  39. <% token_list.with_token_manual do %>
  40. <%= render LooposUi::Entities::Category.new(category: category, url: category.show_url) %>
  41. <% end %>
  42. <% end %>
  43. <% end %>
  44. <% end if show_catalog? %>
  45. then: 0 else: 0 <% row.with_cell(property: :product) do %>
  46. then: 0 <% if service.product.present? %>
  47. <%= render(LooposUi::Entities::Product.new(product: service.product, url: service.product.show_url )) %>
  48. else: 0 <% else %>
  49. <%= tag.div('-', class: "copy-14 text-content" )%>
  50. <% end %>
  51. <% end if show_catalog? %>
  52. then: 0 else: 0 <% row.with_cell(property: :brands) do %>
  53. then: 0 <% if service.brands.present? %>
  54. <%= render LooposUi::TokenList.new(max_tokens: 2) do |token_list| %>
  55. <% service.brands.each do |brand| %>
  56. <% token_list.with_token_manual do %>
  57. <%= render LooposUi::Entities::Brand.new(brand: brand, url: brand.show_url ) %>
  58. <% end %>
  59. <% end %>
  60. <% end %>
  61. else: 0 <% else %>
  62. <%= render LooposUi::Inputs::Text.new( name: "brands", value: "-", readonly: true) %>
  63. <% end %>
  64. <% end if show_catalog? %>
  65. <% row.with_cell(property: :created_at) do %>
  66. <%= render LooposUi::DateShow.new(date: service.created_at) %>
  67. <% end %>
  68. <%# Actions %>
  69. then: 0 else: 0 <% row.with_action do %>
  70. <%= render LooposUi::Button.for_row_action(
  71. icon: "arrow-up-right-and-arrow-down-left-from-center",
  72. tooltip_text: t(".open_sms_message"),
  73. href: service.show_url, tag_options: { data: { turbo: false } })%>
  74. <% end if service.show_url.present? %>
  75. <% end %>
  76. <% end %>
  77. <%= table.with_action_bar do |float_bar| %>
  78. then: 0 else: 0 <% if float_bar_actions_params.dig(:export_csv, :url).present? %>
  79. <% float_bar.with_action_export(text: t(".export_csv"), href: float_bar_actions_params.dig(:export_csv, :url)) %>
  80. <% end %>
  81. <% end %>
  82. <% end %>

app/components/loopos_ui/services/sms_messages/tabs/info.rb

84.62% lines covered

0.0% branches covered

13 relevant lines. 11 lines covered and 2 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Services
  3. 1 module SmsMessages
  4. 1 module Tabs
  5. 1 class Info < LoopComponent
  6. 1 PERMISSIONS = [
  7. :can_view_providers,
  8. ]
  9. 1 option :data, type: ->(m) { LooposUi::Resources::SmsMessageResource.new(model: m) }
  10. 1 option :permissions, default: -> { PERMISSIONS.to_h { |p| [p, false] } }
  11. 1 PERMISSIONS.each do |permission_name|
  12. 1 define_method(:"#{permission_name}?") do
  13. then: 0 else: 0 @permissions&.dig(permission_name)
  14. end
  15. end
  16. 1 def show_catalog?
  17. LooposUi.config.app_type?(:core)
  18. end
  19. end
  20. end
  21. end
  22. end
  23. end

app/components/loopos_ui/services/sms_messages/tabs/info/info.html.erb

0.0% lines covered

0.0% branches covered

69 relevant lines. 0 lines covered and 69 lines missed.
18 total branches, 0 branches covered and 18 branches missed.
    
  1. <% sms = data.model %>
  2. <%= render LooposUi::TabsContent.new do |tab| %>
  3. <% tab.with_row do |row| %>
  4. <% row.with_column do %>
  5. <%= render LooposUi::TitleDescription.new( title: t(".title"), description: t(".description"), size: "normal") %>
  6. <% end %>
  7. <% end %>
  8. <% tab.with_row do |row| %>
  9. <% row.with_column do %>
  10. <%= render LooposUi::TabsSection.new(title: t(".sms_details.title"),size: "small", underline: true) do %>
  11. then: 0 else: 0 <%= render LooposUi::FormEntry.new(label: t(".sms_details.provider"), orientation: "horizontal", label_width: 140) do |entry| %>
  12. <%= entry.with_input do %>
  13. then: 0 <% if sms.provider.name.present? %>
  14. <%= render LooposUi::Token.new(text: sms.provider.name) %>
  15. else: 0 <% else %>
  16. <%= render LooposUi::Inputs::Text.new(name: "provider", value: "-", readonly: true) %>
  17. <% end %>
  18. <% end %>
  19. <% end if can_view_providers? %>
  20. <%= render LooposUi::FormEntry.new(label: t(".sms_details.to"), orientation: "horizontal", label_width: 140) do |entry| %>
  21. <%= entry.with_input do %>
  22. <%= render LooposUi::Inputs::Text.new( name: "to", value: sms.to, placeholder: "-", readonly: true) %>
  23. <% end %>
  24. <% end %>
  25. <%= render LooposUi::FormEntry.new(label: t(".sms_details.template"), orientation: "horizontal", label_width: 140) do |entry| %>
  26. <%= entry.with_input do %>
  27. <%= render LooposUi::EntityToken.new(text: sms.template.name, url: sms.template.show_url, data: {turbo: false}) %>
  28. <% end %>
  29. <% end %>
  30. <%= render LooposUi::FormEntry.new(label: t(".sms_details.created_at"), orientation: "horizontal", label_width: 140) do |entry| %>
  31. <%= entry.with_input do %>
  32. <%= render LooposUi::DateShow.new(date: sms.created_at) %>
  33. <% end %>
  34. <% end %>
  35. <%= render LooposUi::FormEntry.new(label: t(".sms_details.updated_at"), orientation: "horizontal", label_width: 140) do |entry| %>
  36. <%= entry.with_input do %>
  37. <%= render LooposUi::DateShow.new(date: sms.updated_at) %>
  38. <% end %>
  39. <% end %>
  40. <% end %>
  41. <% end %>
  42. <% row.with_column do %>
  43. <%= render LooposUi::TabsSection.new(title: t(".item_info.title"),size: "small", underline: true) do %>
  44. <%= render LooposUi::FormEntry.new(label: t(".item_info.item_id"), orientation: "horizontal", label_width: 140) do |entry| %>
  45. <%= entry.with_input do %>
  46. then: 0 <% if sms.item.present? %>
  47. <%= render LooposUi::Entities::Item.new(item: sms.item, url: sms.item.show_url) %>
  48. else: 0 <% else %>
  49. <%= render LooposUi::Inputs::Text.new( name: "item_full_id", value: "-", readonly: true) %>
  50. <% end %>
  51. <% end %>
  52. <% end %>
  53. then: 0 else: 0 <%= render LooposUi::FormEntry.new(label: t(".item_info.categories"), orientation: "horizontal", label_width: 140) do |entry| %>
  54. <%= entry.with_input do %>
  55. then: 0 <% if sms.categories.present? %>
  56. <%= render LooposUi::TokenList.new(max_tokens: 3) do |token_list| %>
  57. <% sms.categories.each do |category| %>
  58. <% token_list.with_token_manual do %>
  59. <%= render LooposUi::Entities::Category.new(category: category, url: category.show_url) %>
  60. <% end %>
  61. <% end %>
  62. <% end %>
  63. else: 0 <% else %>
  64. <%= render LooposUi::Inputs::Text.new( name: "categories", value: "-", readonly: true) %>
  65. <% end %>
  66. <% end %>
  67. <% end if show_catalog? %>
  68. then: 0 else: 0 <%= render LooposUi::FormEntry.new(label: t(".item_info.product"), orientation: "horizontal", label_width: 140) do |entry| %>
  69. <%= entry.with_input do %>
  70. then: 0 <% if sms.product.present? %>
  71. <%= render(LooposUi::Entities::Product.new(product: sms.product, url: sms.product.show_url )) %>
  72. else: 0 <% else %>
  73. <%= render LooposUi::Inputs::Text.new( name: "product", value: "-", readonly: true) %>
  74. <% end %>
  75. <% end %>
  76. <% end if show_catalog? %>
  77. then: 0 else: 0 <%= render LooposUi::FormEntry.new(label: t(".item_info.brands"), orientation: "horizontal", label_width: 140) do |entry| %>
  78. <%= entry.with_input do %>
  79. then: 0 <% if sms.brands.present? %>
  80. <%= render LooposUi::TokenList.new(max_tokens: 3) do |token_list| %>
  81. <% sms.brands.each do |brand| %>
  82. <% token_list.with_token_manual do %>
  83. <%= render LooposUi::Entities::Brand.new(brand: brand, url: brand.show_url) %>
  84. <% end %>
  85. <% end %>
  86. <% end %>
  87. else: 0 <% else %>
  88. <%= render LooposUi::Inputs::Text.new( name: "brands", value: "-", readonly: true) %>
  89. <% end %>
  90. <% end %>
  91. <% end if show_catalog? %>
  92. <% end %>
  93. <% end %>
  94. <% end %>
  95. <% end %>

app/components/loopos_ui/services/tabs_layout.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Services
  3. 1 class TabsLayout < LoopComponent
  4. 1 include LooposUi::Services::BaseConcern
  5. end
  6. end
  7. end

app/components/loopos_ui/services/tabs_layout/tabs_layout.htm.erb

0.0% lines covered

0.0% branches covered

20 relevant lines. 0 lines covered and 20 lines missed.
17 total branches, 0 branches covered and 17 branches missed.
    
  1. <%
  2. icons = {
  3. "info" => "fa-regular fa-memo-circle-info",
  4. "packages" => "fa-regular fa-box-taped",
  5. "extra_data" => "fa-regular fa-folders",
  6. "preview" => "fa-regular fa-eye"
  7. }
  8. then: 0 else: 0 active_tab = params[:tab]&.parameterize(separator: "_") || tabs_to_render.first
  9. %>
  10. <%= render LooposUi::TabsLayout.new do |layout| %>
  11. <% tabs_to_render.each do |tab_object| %>
  12. <%
  13. then: 0 else: 0 key = tab_object.is_a?(Hash) ? tab_object.keys.first : tab_object.tab_name
  14. then: 0 else: 0 title = tab_object.is_a?(Hash) ? tab_object.values.first : tab_object.tab_title
  15. then: 0 else: 0 is_active = active_tab.is_a?(Hash) ? key == active_tab.keys.first : key == active_tab
  16. then: 0 else: 0 icon = tab_object.is_a?(Hash) ? icons[key] : tab_object.icon
  17. %>
  18. then: 0 else: 0 <% next if key == "preview" %>
  19. <% layout.with_tab(title: title, icon: icon, active: is_active, key: key) do %>
  20. <% case tab_object %>
  21. when: 0 <% when Hash %>
  22. then: 0 <% if key == "extra_data" %>
  23. <%= render LooposUi::TabsContent.new do |tab| %>
  24. <% tab.with_row do |row| %>
  25. <% row.with_column do %>
  26. <div class="tabs">
  27. <div class="flex-1">
  28. <div class="info-tab__section pt-6">
  29. <%= render LooposUi::Services::ExtraData.new(data: @model) %>
  30. </div>
  31. </div>
  32. </div>
  33. <% end %>
  34. <% end %>
  35. <% end %>
  36. else: 0 <% else %>
  37. <%= render "#{partials_sub_path}/#{key}", service: @model %>
  38. <% end %>
  39. when: 0 <% when String %>
  40. <%= render "#{partials_sub_path}/#{key}", service: @model %>
  41. else: 0 <% else %>
  42. <%= render tab_object.new(data: @model, permissions: permissions) %>
  43. <% end %>
  44. <% end %>
  45. <% end %>
  46. <% end %>

app/components/loopos_ui/show_header.rb

56.25% lines covered

0.0% branches covered

32 relevant lines. 18 lines covered and 14 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class ShowHeader < LoopComponent
  4. 1 include Turbo::FramesHelper
  5. 1 include LooposUi::ResourceAware
  6. 1 renders_one :title_zone
  7. 1 renders_many :tokens # "LooposUi::Token"
  8. 1 renders_many :title_labels, types: {
  9. manual: ->(&block) { capture(&block) },
  10. counter: LooposUi::CounterLabel,
  11. state: LooposUi::StateLabel,
  12. double_state: LooposUi::DoubleStateLabel,
  13. }
  14. 1 renders_one :details
  15. 1 attr_reader :title
  16. # BACKCOMPATIBILITY - may be removed
  17. 1 renders_one :right_side
  18. 1 renders_one :bottom_side
  19. 1 attr_reader :top_page_args
  20. # New APi
  21. 1 renders_one :image, LooposUi::V2::Image
  22. 1 renders_one :token_zone
  23. # Do not use directly, use with_detail_zone
  24. 1 renders_many :_detail_zones
  25. # TODO: replace with DRY::initializer
  26. 1 def initialize(title: nil, model: nil, **kwargs)
  27. @title = title
  28. @model = model
  29. # BACKCOMPATIBILITY - may be removed
  30. @kwargs = {
  31. top_page_args: {
  32. rounded: false,
  33. image: false,
  34. },
  35. }.merge!(kwargs)
  36. @top_page_args = @kwargs[:top_page_args]
  37. @detail_zones_count = 0
  38. end
  39. 1 def with_detail_zone(...)
  40. @detail_zones_count += 1
  41. then: 0 else: 0 raise ArgumentError, "You can't have more than 3 detail zones" if @detail_zones_count > 3
  42. with__detail_zone(...)
  43. end
  44. 1 def before_render
  45. then: 0 else: 0 if resource.present? && @title.blank?
  46. @title = resource.model_title
  47. end
  48. else: 0 if !title_zone? && @title.present?
  49. then: 0
  50. with_title_zone do
  51. tag.h1(@title, class: "lui-show-header__title")
  52. end
  53. end
  54. end
  55. end
  56. end

app/components/loopos_ui/show_header/show_header.html.erb

0.0% lines covered

0.0% branches covered

35 relevant lines. 0 lines covered and 35 lines missed.
16 total branches, 0 branches covered and 16 branches missed.
    
  1. <turbo-frame id="lui-show-header" class="lui-show-header">
  2. <div class="loopui-form-layout__main-info">
  3. <div class="loopui-form-layout__details">
  4. <div class="loopui-form-layout__details-section">
  5. then: 0 else: 0 <% if image? %>
  6. <%= image %>
  7. <% end %>
  8. <div class="loopui-form-layout__info-section">
  9. then: 0 else: 0 <% if token_zone? || tokens.present? %>
  10. <div class="loopui-form-layout__line-wrapper">
  11. then: 0 else: 0 <%= token_zone if token_zone? %>
  12. <% tokens.each do |token| %>
  13. <%= token %>
  14. <% end %>
  15. </div>
  16. <% end %>
  17. <div class="loopui-form-layout__middle-wrapper--small">
  18. then: 0 else: 0 <% if title_zone? %>
  19. <div class="flex">
  20. <%= title_zone %>
  21. <div class="flex gap-2 ml-2">
  22. <% title_labels.each do |label| %>
  23. <%= label %>
  24. <% end %>
  25. </div>
  26. </div>
  27. <% end %>
  28. then: 0 else: 0 <% if details.present? %>
  29. <div class="loopui-form-layout__line-wrapper">
  30. <div class="pl-1">
  31. <%= details %>
  32. </div>
  33. </div>
  34. <% end %>
  35. </div>
  36. then: 0 else: 0 <% if bottom_side.present? %>
  37. <div class="loopui-form-layout__middle-wrapper">
  38. <div class="loopui-form-layout__line-wrapper">
  39. <div class="pl-1">
  40. <%= bottom_side %>
  41. </div>
  42. </div>
  43. </div>
  44. <% end %>
  45. then: 0 else: 0 <% if _detail_zones.present? %>
  46. <div class="loopui-form-layout__middle-wrapper">
  47. <div class="loopui-form-layout__line-wrapper">
  48. <div class="lui-show-header__detail_zones flex flex-col gap-2">
  49. <% _detail_zones.each do |detail| %>
  50. <div>
  51. <%= detail %>
  52. </div>
  53. <% end %>
  54. </div>
  55. </div>
  56. </div>
  57. <% end %>
  58. </div>
  59. then: 0 else: 0 <% if right_side.present? %>
  60. <div class="loopui-form-layout__right">
  61. <%= right_side %>
  62. </div>
  63. <% end %>
  64. </div>
  65. </div>
  66. </div>
  67. </turbo-frame>

app/components/loopos_ui/show_layout.rb

75.0% lines covered

100.0% branches covered

8 relevant lines. 6 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class ShowLayout < LoopComponent
  4. 1 option :model, optional: true
  5. 1 renders_one :action_bar, ->(**args, &block) {
  6. LooposUi::ActionBar.new(**args, model: model, current_action: :show, &block)
  7. }
  8. # renders_one :header, ->(**args) {
  9. # case header_type
  10. # when :show
  11. # LooposUi::ShowHeader.new(**args, model: model)
  12. # else
  13. # LooposUi::PageHeader.new(**args, model: model)
  14. # end
  15. # }
  16. #
  17. #
  18. 1 renders_one :header, types: {
  19. page: { renders: LooposUi::PageHeader, as: :page_header },
  20. show: { renders: LooposUi::ShowHeader, as: :header },
  21. custom: { renders: ->(&block) { capture(&block) }, as: :header_custom },
  22. }
  23. 1 renders_one :highlight
  24. end
  25. end

app/components/loopos_ui/show_layout/show_layout.html.erb

0.0% lines covered

0.0% branches covered

8 relevant lines. 0 lines covered and 8 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <div class="lui-show-layout ">
  2. <%= action_bar %>
  3. <div class="lui-show-layout__container">
  4. <div class="lui-show-layout__content">
  5. <%= header %>
  6. then: 0 else: 0 <% if highlight? %>
  7. <div class="lui-show-layout__highlight">
  8. <%= highlight %>
  9. </div>
  10. <% end %>
  11. <main class="lui-show-layout__main" style="height: calc(100% - var(--show-layout-header-height));">
  12. <%= content %>
  13. </main>
  14. <turbo-frame class="lui-show-layout__drawer_wrapper" id="lui-main-layout-drawer_bar" class="fixed top-[106px] right-0" data-controller="drawer-bar">
  15. </turbo-frame>
  16. </div>
  17. </div>
  18. </div>

app/components/loopos_ui/show_layout_component/show_layout_component.html.erb

0.0% lines covered

100.0% branches covered

5 relevant lines. 0 lines covered and 5 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <div id="app_layout_frame" class="loopui-form-layout">
  2. <%= top_page_actions %>
  3. <%= top_page_info %>
  4. <%= tabs %>
  5. <%= table_section %>
  6. </div>

app/components/loopos_ui/sidebar.rb

80.0% lines covered

0.0% branches covered

15 relevant lines. 12 lines covered and 3 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class Sidebar < LoopComponent
  4. 1 include LooposUi::SidebarItemPartitioning
  5. 1 option :config, default: -> { LooposUi.config.sidebar }
  6. 1 USE_UI_2 = false
  7. 1 def call
  8. then: 0 if USE_UI_2
  9. render(LooposUi::Sidebar::V2::Sidebar.new(config: config))
  10. else: 0 else
  11. render(LooposUi::Sidebar::V1::Sidebar.new(config: config))
  12. end
  13. end
  14. 1 class << self
  15. 1 attr_writer :configuration
  16. 1 def configuration
  17. 1 @configuration ||= Configuration.new
  18. end
  19. 1 def configure
  20. 1 yield configuration
  21. end
  22. end
  23. end
  24. end

app/components/loopos_ui/sidebar/configuration.rb

100.0% lines covered

100.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class Sidebar
  3. 1 class Configuration
  4. 1 attr_accessor :groups
  5. 1 alias_method :items, :groups
  6. 1 alias_method :items=, :groups=
  7. 1 attr_accessor :foo
  8. 1 attr_accessor :logout_path
  9. # V1 DEPRECATED, but supported
  10. 1 attr_accessor :top_items, :bottom_items
  11. 1 def initialize
  12. 1 @top_items ||= []
  13. 1 @bottom_items ||= []
  14. 1 @groups ||= []
  15. 1 @foo ||= {}
  16. end
  17. end
  18. end
  19. end

app/components/loopos_ui/sidebar/v1/divider.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <div class="w-[64px] h-px bg-gray-500">
  2. </div>

app/components/loopos_ui/sidebar/v1/divider.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class Sidebar
  4. 1 module V1
  5. 1 class Divider < LoopComponent
  6. end
  7. end
  8. end
  9. end

app/components/loopos_ui/sidebar/v1/drawer_divider.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div(class: "lui-sidebar__drawer__divider", role: "separator") %>

app/components/loopos_ui/sidebar/v1/drawer_divider.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class Sidebar
  4. 1 module V1
  5. 1 class DrawerDivider < LoopComponent
  6. end
  7. end
  8. end
  9. end

app/components/loopos_ui/sidebar/v1/drawer_entry.html.erb

0.0% lines covered

100.0% branches covered

2 relevant lines. 0 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= link_to path, class: "lui-sidebar__drawer__entry #{classes}" , data: { turbo_action: "advance" } do %>
  2. <%= title %>
  3. <% end %>

app/components/loopos_ui/sidebar/v1/drawer_item.html.erb

0.0% lines covered

0.0% branches covered

25 relevant lines. 0 lines covered and 25 lines missed.
12 total branches, 0 branches covered and 12 branches missed.
    
  1. <div class="w-full"
  2. then: 0 else: 0 data-action="<%= !disabled? ? "mouseleave->sidebar#hide mouseenter->sidebar#show" : "" %>"
  3. data-id="<%= data_target_modal %>"
  4. >
  5. <%= link_to path, data: { turbo_action: "advance" } do %>
  6. <div id=<%= id %> class="lui-sidebar__item <%= sidebar_item_classes %>"
  7. style="<%= sidebar_item_styles %>">
  8. then: 0 else: 0 <%= render LooposUi::Tooltip.new(title: tooltip_title, position: :right) if tooltip_title.present? %>
  9. then: 0 else: 0 <% if highlight %>
  10. <div class="lui-sidebar__highlight">
  11. <span class="lui-sidebar__highlight--animation"></span>
  12. <span class="lui-sidebar__highlight--circle"></span>
  13. </div>
  14. <% end %>
  15. then: 0 <% if image.present? %>
  16. <%= image_tag(image, class: "h-4") %>
  17. else: 0 <% else%>
  18. <%= content_tag(:icon, "", class: icon_classes) %>
  19. <% end %>
  20. <div class="lui-sidebar__item__short-title">
  21. <%= tag.p(short_title, class: "lui-sidebar__item__label") %>
  22. then: 0 else: 0 <%= render LooposUi::Counter.new(count: counter, kind: counter_kind, size: :tinny) if render_counter? %>
  23. </div>
  24. </div>
  25. <% end %>
  26. <%# FIXME: data-drawer-taget is also used by flowbite, causing errors %>
  27. <div class="hidden lui-sidebar__drawer">
  28. <div class="lui-sidebar__drawer__header">
  29. <div class="lui-sidebar__drawer__header-title">
  30. <div>
  31. then: 0 <% if image.present? %>
  32. <%= image_tag(image, class: "h-4") %>
  33. else: 0 <% else%>
  34. <%= content_tag(:icon, "", class: "lui-sidebar__drawer__icon #{icon_classes}") %>
  35. <% end %>
  36. </div>
  37. <div>
  38. <%= title %>
  39. </div>
  40. </div>
  41. <div class="lui-sidebar__drawer__header-description">
  42. <%= description %>
  43. </div>
  44. </div>
  45. <div class="lui-sidebar__drawer__items">
  46. <% menu_items_config.each do |menu_entry| %>
  47. <%= render LooposUi::Sidebar::V1::DrawerItem.menu_entry_from(menu_entry) %>
  48. <% end %>
  49. </div>
  50. </div>
  51. </div>

app/components/loopos_ui/sidebar/v1/drawer_item.rb

37.97% lines covered

0.0% branches covered

79 relevant lines. 30 lines covered and 49 lines missed.
47 total branches, 0 branches covered and 47 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class Sidebar
  4. 1 module V1
  5. 1 class DrawerItem < Item
  6. 1 include ActiveLinkTo
  7. 1 attr_reader :description
  8. 1 class << self
  9. 1 def drawer_menu_divider?(attrs)
  10. attrs == :divider || (attrs.is_a?(Hash) && attrs[:type].to_s == "divider")
  11. end
  12. 1 def menu_entry_from(attrs)
  13. case attrs
  14. when: 0 when :divider
  15. DrawerDivider.new
  16. when: 0 when Hash
  17. then: 0 else: 0 drawer_menu_divider?(attrs) ? DrawerDivider.new : DrawerEntry.new(attrs: attrs)
  18. else: 0 else
  19. raise ArgumentError, "Invalid drawer menu item: #{attrs.inspect}"
  20. end
  21. end
  22. end
  23. 1 def initialize(attrs:)
  24. else: 0 then: 0 raise ArgumentError, "DrawerItem requires a :menu_items attribute" unless attrs[:menu_items].present?
  25. super(attrs: attrs)
  26. @path = attrs[:path]
  27. @description = attrs[:description]
  28. end
  29. 1 def path
  30. then: 0 @_path ||= if @path.nil? || @disabled
  31. "#"
  32. else: 0 # TODO: document this, after ui-34 merged
  33. then: 0 elsif @path == :first_item
  34. then: 0 else: 0 first_item_path = first_link_menu_item&.dig(:path)
  35. then: 0 else: 0 first_item_path.respond_to?(:call) ? instance_exec(&first_item_path) : first_item_path
  36. else: 0 else
  37. then: 0 else: 0 @path.respond_to?(:call) ? instance_exec(&@path) : @path
  38. end
  39. end
  40. 1 def active?
  41. # FIXME: document active states / possible values. We should always forward @active, but i think this
  42. # may break some existing code. is_active_link? supports boolean
  43. then: 0 else: 0 active_value = @active.respond_to?(:call) ? instance_exec(&@active) : @active
  44. then: 0 else: 0 return active_value if active_value == true || active_value == false
  45. is_active_link?(path, :exclusive) || submenu_item_active?
  46. end
  47. 1 def submenu_item_active?
  48. link_menu_paths.any? do |path|
  49. then: 0 else: 0 path = path.respond_to?(:call) ? instance_exec(&path) : path
  50. is_active_link?(path || "#", :inclusive)
  51. end
  52. end
  53. 1 def menu_items_config
  54. @data[:menu_items]
  55. end
  56. 1 def link_menu_paths
  57. menu_items_config.filter_map do |entry|
  58. then: 0 else: 0 next if self.class.drawer_menu_divider?(entry)
  59. else: 0 then: 0 next unless entry.is_a?(Hash)
  60. entry[:path]
  61. end
  62. end
  63. 1 def first_link_menu_item
  64. menu_items_config.find do |entry|
  65. entry.is_a?(Hash) && entry[:path].present? && !self.class.drawer_menu_divider?(entry)
  66. end
  67. end
  68. 1 def data_target_modal
  69. "#{id}-data-target-modal"
  70. end
  71. end
  72. 1 class DrawerEntry < ViewComponent::Base
  73. 1 with_collection_parameter :attrs
  74. 1 include ActiveLinkTo
  75. 1 attr_reader :title, :path
  76. 1 def initialize(attrs:)
  77. else: 0 then: 0 raise ArgumentError,
  78. "DrawerItemComponent requires a :path attribute. Attrs: #{attrs}" unless attrs[:path].present?
  79. @title = attrs[:title]
  80. @path = attrs[:path]
  81. @visible = attrs.fetch(:visible, true)
  82. @disabled = attrs.fetch(:disabled, false)
  83. @active = attrs.fetch(:active, :inclusive)
  84. end
  85. 1 def title
  86. then: 0 else: 0 @_title ||= @title.respond_to?(:call) ? instance_exec(&@title) : @title
  87. end
  88. 1 def classes
  89. [active_classes, disabled_classes].compact.join(" ")
  90. end
  91. 1 def active?
  92. # FIXME: document active states / possible values. We should always forward @active, but i think this
  93. # may break some existing code. is_active_link? supports boolean
  94. then: 0 else: 0 active_value = @active.respond_to?(:call) ? instance_exec(&@active) : @active
  95. then: 0 else: 0 return active_value if active_value == true || active_value == false
  96. is_active_link?(path, :inclusive)
  97. end
  98. 1 def active_classes
  99. then: 0 else: 0 active? ? "lui-sidebar__drawer__entry--active" : ""
  100. end
  101. 1 def disabled_classes
  102. then: 0 else: 0 @disabled ? "lui-sidebar__drawer__entry--disabled" : ""
  103. end
  104. 1 def path
  105. then: 0 @_path ||= if @path.nil? || @disabled
  106. "#"
  107. else: 0 else
  108. then: 0 else: 0 @path.respond_to?(:call) ? instance_exec(&@path) : @path
  109. end
  110. end
  111. 1 def visible?
  112. # If @visible is callable, execute it in the context of this instance
  113. then: 0 @_visible ||= if @visible.nil? # default visible
  114. true
  115. else: 0 else
  116. then: 0 else: 0 @visible.respond_to?(:call) ? instance_exec(&@visible) : @visible
  117. end
  118. end
  119. 1 def render?
  120. visible?
  121. end
  122. end
  123. end
  124. end
  125. end

app/components/loopos_ui/sidebar/v1/item.rb

34.69% lines covered

0.0% branches covered

98 relevant lines. 34 lines covered and 64 lines missed.
52 total branches, 0 branches covered and 52 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class Sidebar # Will be class
  4. 1 module V1
  5. 1 class Item < ViewComponent::Base
  6. 1 include ActionView::Helpers::UrlHelper
  7. 1 include Turbo::FramesHelper
  8. 1 include ActiveLinkTo
  9. 1 include FaviconAware
  10. 1 COLOR_MAPPING = {
  11. impact: "#40A77F",
  12. }
  13. 1 VALID_COUNTER_KINDS = [
  14. :success,
  15. :danger,
  16. :warning,
  17. :neutral,
  18. :informative,
  19. ].freeze
  20. 1 def self.from_hash(attrs = {})
  21. then: 0 else: 0 if attrs.is_a?(Hash) && attrs.key?(:type)
  22. return "LooposUi::Sidebar::#{attrs[:type].to_s.camelize}".constantize.new(attrs)
  23. end
  24. then: 0 else: 0 return Divider.new if attrs == :divider
  25. then: 0 if attrs.key?(:menu_items)
  26. DrawerItem.new(attrs: attrs)
  27. else: 0 else
  28. SingleItem.new(attrs: attrs)
  29. end
  30. end
  31. 1 attr_reader :icon, :disabled, :image, :tooltip_title, :coming_soon, :highlight
  32. # TODO: change to LoopComponent
  33. 1 def initialize(attrs:)
  34. @data = attrs
  35. @title = attrs[:title]
  36. @short_title = attrs[:short_title]
  37. @tooltip_title = attrs[:tooltip_title]
  38. @icon = faviconize(attrs[:icon])
  39. @image = attrs[:image]
  40. @coming_soon = attrs[:coming_soon]
  41. @counter = attrs[:counter]
  42. @counter_kind = attrs.fetch(:counter_kind, :warning)
  43. @increment = attrs.fetch(:increment, false)
  44. @kind_styles = attrs.fetch(:kind_styles, false)
  45. # TODO: review docs, this is not documented
  46. @active = attrs.fetch(:active, :inclusive)
  47. @visible = attrs.fetch(:visible, true)
  48. @disabled = attrs.fetch(:disabled, false)
  49. then: 0 else: 0 if @disabled && @tooltip_title.blank?
  50. @tooltip_title = "Disabled"
  51. end
  52. @color = case attrs[:color]
  53. when: 0 when Symbol
  54. COLOR_MAPPING[attrs[:color]]
  55. else: 0 else
  56. attrs[:color]
  57. end
  58. @bg_color = attrs[:bg_color]
  59. @highlight = attrs[:highlight]
  60. end
  61. 1 def id
  62. then: 0 else: 0 @_id ||= "#{title&.parameterize}_#{self.class.name.parameterize}"
  63. end
  64. 1 def visible?
  65. # If @visible is callable, execute it in the context of this instance
  66. then: 0 @_visible ||= if @visible.nil? # default visible
  67. true
  68. else: 0 else
  69. then: 0 else: 0 @visible.respond_to?(:call) ? instance_exec(&@visible) : @visible
  70. end
  71. end
  72. 1 def render?
  73. visible?
  74. end
  75. 1 def disabled?
  76. disabled == true
  77. end
  78. 1 def active?
  79. raise NotImplementedError.new("Implement in subclass")
  80. end
  81. 1 def sidebar_item_classes
  82. "#{active_classes} #{disabled_classes}"
  83. end
  84. 1 def color
  85. then: 0 else: 0 @_color ||= @color.respond_to?(:call) ? instance_exec(&@color) : @color
  86. end
  87. 1 def bg_color
  88. then: 0 else: 0 @_bg_color ||= @bg_color.respond_to?(:call) ? instance_exec(&@bg_color) : @bg_color
  89. end
  90. 1 def highlight
  91. then: 0 else: 0 @_highlight ||= @highlight.respond_to?(:call) ? instance_exec(&@highlight) : @highlight
  92. end
  93. 1 def title
  94. then: 0 else: 0 @_title ||= @title.respond_to?(:call) ? instance_exec(&@title) : @title
  95. end
  96. 1 def sidebar_item_styles
  97. s = StringIO.new
  98. then: 0 else: 0 s << "color: #{color}; border-color: #{color};" if color.present?
  99. then: 0 else: 0 s << "pointer-events: none;" if coming_soon
  100. then: 0 else: 0 s << "background-color: #{bg_color};" if bg_color.present?
  101. s.string
  102. end
  103. 1 def active_classes
  104. then: 0 else: 0 active? ? "lui-sidebar__item--active" : ""
  105. end
  106. 1 def disabled_classes
  107. then: 0 else: 0 disabled? ? "lui-sidebar__item--disabled" : ""
  108. end
  109. 1 def icon_classes
  110. "lui-sidebar-item__icon #{icon}"
  111. end
  112. 1 def short_title
  113. then: 0 @_short_title ||= if @short_title.respond_to?(:call)
  114. else: 0 instance_exec(&@short_title)
  115. then: 0 elsif @title.respond_to?(:call)
  116. instance_exec(&@title)
  117. else: 0 else
  118. @short_title || @title
  119. end
  120. end
  121. 1 def tooltip_title
  122. then: 0 else: 0 @_tooltip_title ||= @tooltip_title.respond_to?(:call) ? instance_exec(&@tooltip_title) : @tooltip_title
  123. end
  124. 1 def render_counter?
  125. then: 0 else: 0 counter&.positive?
  126. end
  127. 1 def counter
  128. then: 0 @_counter ||= if @counter.respond_to?(:call)
  129. instance_exec(&@counter)
  130. else: 0 else
  131. @counter
  132. end
  133. end
  134. 1 def counter_kind
  135. @_counter_kind ||= begin
  136. then: 0 kind = if @counter_kind.respond_to?(:call)
  137. instance_exec(&@counter_kind)
  138. else: 0 else
  139. @counter_kind
  140. end
  141. then: 0 else: 0 VALID_COUNTER_KINDS.include?(kind) ? kind : :warning
  142. end
  143. end
  144. 1 def increment
  145. then: 0 else: 0 @_increment ||= @increment.respond_to?(:call) ? instance_exec(&@increment) : @increment
  146. end
  147. 1 def kind_styles
  148. then: 0 else: 0 @_kind_styles ||= @kind_styles.respond_to?(:call) ? instance_exec(&@kind_styles) : @kind_styles
  149. end
  150. end
  151. end
  152. end
  153. end

app/components/loopos_ui/sidebar/v1/sidebar.html.erb

0.0% lines covered

0.0% branches covered

10 relevant lines. 0 lines covered and 10 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <turbo-frame id="lui-sidebar" target="lui-main-layout" data-controller="sidebar" class="h-full">
  2. <aside class="lui-sidebar">
  3. <div class="lui-sidebar__item-list lui-sidebar__item-list--top">
  4. <% top_items.each do |item| %>
  5. <%= render(item) %>
  6. <% end %>
  7. </div>
  8. then: 0 else: 0 <% if bottom_items.any? %>
  9. <div class="lui-sidebar__item-list lui-sidebar__item-list--bottom">
  10. <% bottom_items.each do |item| %>
  11. <%= render(item) %>
  12. <% end %>
  13. </div>
  14. <% end %>
  15. </aside>
  16. </turbo-frame>

app/components/loopos_ui/sidebar/v1/sidebar.rb

83.33% lines covered

100.0% branches covered

12 relevant lines. 10 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class Sidebar
  4. 1 module V1
  5. 1 class Sidebar < LoopComponent
  6. 1 include LooposUi::SidebarItemPartitioning
  7. 1 option :config, default: -> { LooposUi.config.sidebar }
  8. 1 class << self
  9. 1 attr_writer :configuration
  10. 1 def configuration
  11. @configuration ||= ::LooposUi::Sidebar::Configuration.new
  12. end
  13. 1 def configure
  14. yield configuration
  15. end
  16. end
  17. end
  18. end
  19. end
  20. end

app/components/loopos_ui/sidebar/v1/single_item.html.erb

0.0% lines covered

0.0% branches covered

12 relevant lines. 0 lines covered and 12 lines missed.
8 total branches, 0 branches covered and 8 branches missed.
    
  1. <%= tag.div(id: id, class: "w-full", data: { action: "mouseleave->sidebar#hide mouseenter->sidebar#show" }) do %>
  2. then: 0 else: 0 <%= render LooposUi::Tooltip.new(title: tooltip_title, position: :right) if tooltip_title.present? %>
  3. <%= link_to path, data: { action: "mouseover->drawer#hide", turbo_action: "advance" } do %>
  4. <div class="lui-sidebar__item <%= sidebar_item_classes %>" style="<%= sidebar_item_styles %>">
  5. then: 0 else: 0 <% if highlight %>
  6. <div class="lui-sidebar__highlight">
  7. <span class="lui-sidebar__highlight lui-sidebar__highlight--animation"></span>
  8. <span class="lui-sidebar__highlight lui-sidebar__highlight--circle"></span>
  9. </div>
  10. <% end %>
  11. then: 0 <% if image.present? %>
  12. <%= image_tag(image, class: "h-4") %>
  13. else: 0 <% else%>
  14. <%= content_tag(:icon, "", class: icon_classes) %>
  15. <% end %>
  16. <div class="lui-sidebar__item__short-title">
  17. <%= tag.p(short_title, class: "lui-sidebar__item__label") %>
  18. then: 0 else: 0 <%= render LooposUi::Counter.new(count: counter, kind: counter_kind, size: :tinny, increment: increment, kind_styles: kind_styles) if render_counter? %>
  19. </div>
  20. </div>
  21. <% end %>
  22. <% end %>

app/components/loopos_ui/sidebar/v1/single_item.rb

36.84% lines covered

0.0% branches covered

19 relevant lines. 7 lines covered and 12 lines missed.
12 total branches, 0 branches covered and 12 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class Sidebar
  4. 1 module V1
  5. 1 class SingleItem < Item
  6. 1 def initialize(attrs:)
  7. super(attrs: attrs)
  8. @path = attrs[:path]
  9. end
  10. 1 def active?
  11. then: 0 @is_active ||= if disabled?
  12. false
  13. else: 0 else
  14. then: 0 else: 0 active_value = @active.respond_to?(:call) ? instance_exec(&@active) : @active
  15. then: 0 else: 0 return active_value if active_value == true || active_value == false
  16. is_active_link?(path, active_value)
  17. end
  18. end
  19. 1 def path
  20. then: 0 @_path ||= if @path.nil? || disabled?
  21. "#"
  22. else: 0 else
  23. then: 0 else: 0 @path.respond_to?(:call) ? instance_exec(&@path) : @path
  24. end
  25. then: 0 else: 0 raise ArgumentError, <<~ERROR if @_path.nil?
  26. Path is nil, check your sidebar configuration. Bad data at item with config: #{@data}"
  27. ERROR
  28. @_path
  29. end
  30. end
  31. end
  32. end
  33. end

app/components/loopos_ui/sidebar/v2/group.html.erb

0.0% lines covered

100.0% branches covered

5 relevant lines. 0 lines covered and 5 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div(class: classes) do %>
  2. <%= tag.span(title, class: bem_element_class(:title)) %>
  3. <%= tag.div(class: bem_element_class(:items)) do %>
  4. <% items.each do |item| %>
  5. <%= render Item.new(item: item) %>
  6. <%# render LooposUi::MIcon.new(item.icon, size: 16) %>
  7. <% end %>
  8. <% end %>
  9. <% end %>

app/components/loopos_ui/sidebar/v2/item.html.erb

0.0% lines covered

0.0% branches covered

17 relevant lines. 0 lines covered and 17 lines missed.
8 total branches, 0 branches covered and 8 branches missed.
    
  1. <%= tag.a(
  2. id: id,
  3. then: 0 else: 0 href: drawer? ? nil : path, # drawer items are not links
  4. class: classes,
  5. data: {
  6. controller: "lui--sidebar-item",
  7. "lui--sidebar-target": :item,
  8. then: 0 **( if !drawer?
  9. { action: "click->lui--sidebar#resetActiveItems click->lui--sidebar-item#activate" }
  10. else: 0 else
  11. { action: "click->lui--sidebar#expand click->lui--sidebar#resetExpandedDrawers click->lui--sidebar-item#toggleDrawer" }
  12. end),
  13. turbo_frame: "lui-main-layout",
  14. turbo_action: "advance"
  15. }
  16. ) do %>
  17. <%= render LooposUi::Tooltip.new(title: item.title) %>
  18. <div class="flex gap-1">
  19. <%= tag.span(class: bem_element_class(:icon_container)) do %>
  20. then: 0 else: 0 <%= render LooposUi::MIcon.new(
  21. :arrow_right,
  22. tag: :button,
  23. class: bem_element_class(:drawer_icon)
  24. ) if drawer? %>
  25. <%= render LooposUi::MIcon.new(
  26. #item.icon,
  27. :home,
  28. class: bem_element_class(:icon)) %>
  29. <% end %>
  30. <%= tag.span(item.title, class: bem_element_class(:title)) %>
  31. </div>
  32. <% end %>
  33. then: 0 else: 0 <%= tag.div(id: drawer_id, class: bem_element_class(:sub_items)) do %>
  34. <% item.items.each do |item| %>
  35. <%= render self.class.new(item: item) %>
  36. <% end %>
  37. <% end if item.items.any? %>

app/components/loopos_ui/sidebar/v2/sidebar.html.erb

0.0% lines covered

100.0% branches covered

22 relevant lines. 0 lines covered and 22 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.aside(
  2. id: "lui-sidebar",
  3. class: classes,
  4. data: {
  5. controller: "lui--sidebar",
  6. turbo_frame: "lui-main-layout"
  7. }) do %>
  8. <%= tag.div(id: "lui-sidebar-active-item-id", class: "hidden", data: { "lui--sidebar-target": "activeItemId" }) %>
  9. <%= tag.div class: bem_element_class(:app_selector) do %>
  10. <%= tag.span(
  11. render(LooposUi::AppLogo.new(app: LooposUi.config.app_type),
  12. ), class: "lui-sidebar__app_logo_condensed") %>
  13. <%= tag.span(
  14. render(LooposUi::AppLogo.new(app: LooposUi.config.app_type, expanded: true),
  15. ), class: "lui-sidebar__app_logo_expanded") %>
  16. <%= tag.div(
  17. class: bem_element_class(:toggle_button),
  18. data: { action: "click->lui--sidebar#toggleExpand" }) do %>
  19. <%= render(LooposUi::MIcon.new(:left_panel_close, tag: :button)) %>
  20. <%= render(LooposUi::Tooltip.new(title: t(".toggle_sidebar"))) %>
  21. <%end %>
  22. <% end %>
  23. <%= tag.main(class: bem_element_class(:groups)) do %>
  24. <% groups.each do |group| %>
  25. <%= render group %>
  26. <% end %>
  27. <% end %>
  28. <div>
  29. <%= render LooposUi::DummySlot.new(text: "Business Slot", classes: "h-[200px]") %>
  30. </div>
  31. <footer class="flex items-center justify-between">
  32. <%= render LooposUi::UserMenu.sidebar %>
  33. <%= tag.div(class: "lui-sidebar__language_selector") do %>
  34. <%= render LooposUi::Button.new(text: current_locale, type: :tertiary, size: :small, kind: :neutral) do |button| %>
  35. <%= button.with_leading_icon(icon: :language) %>
  36. <% end %>
  37. <% end %>
  38. </footer>
  39. <% end %>

app/components/loopos_ui/sidebar/v2/sidebar.rb

61.11% lines covered

0.0% branches covered

54 relevant lines. 33 lines covered and 21 lines missed.
11 total branches, 0 branches covered and 11 branches missed.
    
  1. 1 module LooposUi
  2. 1 class Sidebar
  3. 1 module V2
  4. 1 class Sidebar < LoopComponent
  5. 1 option :config, default: -> { LooposUi.config.sidebar }
  6. 1 option :expanded, Types::Bool, default: -> { false } # FIXME: does not work
  7. 1 mod :expanded
  8. 1 def expanded?
  9. then: 0 else: 0 @expanded ? :expanded : :condensed
  10. end
  11. 1 def groups
  12. config.groups.map do |group|
  13. Group.new(**group)
  14. end
  15. end
  16. 1 def current_locale
  17. then: 0 else: 0 ::LooposUi.config.sidebar.foo.dig("language", "current_locale")&.upcase || "EN"
  18. end
  19. 1 def active_item
  20. flat_items = groups.flat_map do |group|
  21. group.items.flat_map do |item|
  22. [
  23. ::LooposUi::Sidebar::V2::Sidebar::Group::Item.new(item: item),
  24. *item.items.map { |i| ::LooposUi::Sidebar::V2::Sidebar::Group::Item.new(item: i) },
  25. ]
  26. end.each do |item|
  27. item.instance_variable_set(:@view_context, view_context)
  28. end
  29. end
  30. then: 0 else: 0 then: 0 else: 0 flat_items&.find(&:active?)&.id
  31. end
  32. 1 class Group < LoopComponent
  33. 1 class TItem < Dry::Struct
  34. 1 attribute :title, Types::Coercible::String
  35. 1 attribute :icon, Types::Coercible::Symbol.default(:home)
  36. 1 attribute? :path, Types::String | Types::Interface(:call)
  37. 1 attribute :items, Types::Array.of(TItem).default([])
  38. 1 attribute? :active, Types::Bool |
  39. Types::Symbol.enum(:exclusive, :inclusive, :exact) |
  40. Types::Instance(Regexp) |
  41. Types::Array.of(Types::Array.of(Types::Symbol)) | Types::Hash
  42. end
  43. 1 option :title, Types::Coercible::String
  44. 1 option :items, Types::Array.of(TItem), default: -> { [] } # TODO: add type
  45. 1 def render?
  46. items.any?
  47. end
  48. 1 class Item < LoopComponent
  49. 1 include ActiveLinkTo
  50. 1 option :item, TItem
  51. 1 option :active, Types::Bool, default: -> { false }
  52. 1 mod :active
  53. 1 def id
  54. @id = "lui-sidebar-item-#{item.title}" # FIXME: title is not unique enough
  55. end
  56. 1 def additional_classes
  57. [
  58. bem_element_class(:drawer, condition: drawer?),
  59. ]
  60. end
  61. 1 def drawer?
  62. item.items.any?
  63. end
  64. 1 def drawer_id
  65. "#{random_id}-drawer"
  66. end
  67. 1 def path
  68. else: 0 @path ||= case item.path
  69. when: 0 when String
  70. item.path
  71. when: 0 when Proc
  72. instance_exec(&item.path)
  73. end
  74. end
  75. 1 def active?
  76. path.present? && is_active_link?(path, item.active)
  77. end
  78. end
  79. end
  80. 1 def before_render
  81. return
  82. config.items = [
  83. {
  84. title: "Overview",
  85. items: [
  86. {
  87. title: "Home",
  88. icon: :home,
  89. },
  90. {
  91. title: "Submenu",
  92. icon: :face,
  93. items: [
  94. {
  95. title: "Submenu Item 1",
  96. icon: :face,
  97. },
  98. {
  99. title: "Submenu Item 1",
  100. icon: :face,
  101. },
  102. ],
  103. },
  104. ],
  105. },
  106. {
  107. title: "Overview 2",
  108. items: [
  109. {
  110. title: "Foo",
  111. icon: :home,
  112. },
  113. {
  114. title: "Submenu bar",
  115. icon: :face,
  116. items: [
  117. {
  118. title: "Submenu Item 2",
  119. icon: :face,
  120. },
  121. {
  122. title: "Submenu Item 2",
  123. icon: :face,
  124. },
  125. ],
  126. },
  127. ],
  128. },
  129. ]
  130. end
  131. end
  132. end
  133. end
  134. end

app/components/loopos_ui/sidebar_layout_component.rb

58.82% lines covered

0.0% branches covered

17 relevant lines. 10 lines covered and 7 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. 1 module LooposUi
  2. 1 class SidebarLayoutComponent < ViewComponent::Base
  3. 1 renders_one :top_navigation, "SidebarOldComponent"
  4. 1 renders_one :bottom_navigation, "SidebarOldComponent"
  5. 1 def initialize(app_logo: nil, app_url: nil)
  6. @app_logo= app_logo
  7. @app_url= app_url
  8. end
  9. 1 class SidebarOldComponent < ViewComponent::Base
  10. 1 def initialize(menu: [])
  11. else: 0 then: 0 @menu = map_menu_items(menu) unless menu.empty?
  12. end
  13. 1 private
  14. 1 def map_menu_items(menu)
  15. menu.map { |item_hash| create_menu_item(item_hash) }
  16. end
  17. 1 def create_menu_item(item_hash)
  18. then: 0 if item_hash[:subitems].present?
  19. LooposUi::MenuItem::DrawerItem.new(item_hash: item_hash)
  20. else: 0 else
  21. LooposUi::MenuItem::SingleItem.new(item_hash: item_hash)
  22. end
  23. end
  24. end
  25. end
  26. end

app/components/loopos_ui/sidebar_layout_component/sidebar_old_component.html.erb

0.0% lines covered

0.0% branches covered

2 relevant lines. 0 lines covered and 2 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <% @menu.each do |item| %>
  2. then: 0 else: 0 <%= render item if item.can_view? %>
  3. <% end %>

app/components/loopos_ui/sidebar_old_component.rb

53.85% lines covered

0.0% branches covered

13 relevant lines. 7 lines covered and 6 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. # TODO: move to Sidebar namespace. Should become LooposUi::Sidebar
  4. 1 class SidebarOldComponent < ViewComponent::Base
  5. 1 attr_reader :config
  6. 1 def initialize(config: LooposUi.config.sidebar)
  7. @config = config
  8. end
  9. # TODO: test check visible?
  10. # TODO: update documentation, active no longer necessary, explain :exact, :icluded, :exculed
  11. 1 def items
  12. item_config_list.map do |attrs|
  13. LooposUi::Sidebar::Item.from_hash(attrs)
  14. end
  15. end
  16. 1 private
  17. 1 def item_config_list
  18. then: 0 else: 0 then: 0 if config.items&.any?
  19. config.items
  20. else: 0 else # backward compatibility to old initializer
  21. (config.top_items || []) + (config.bottom_items || [])
  22. end || []
  23. end
  24. end
  25. end

app/components/loopos_ui/spinner.rb

46.67% lines covered

0.0% branches covered

15 relevant lines. 7 lines covered and 8 lines missed.
5 total branches, 0 branches covered and 5 branches missed.
    
  1. 1 module LooposUi
  2. 1 class Spinner < LoopComponent
  3. 1 option :theme, Types::Coercible::Symbol.enum(:light, :dark), default: -> { :dark }
  4. 1 option :size, Types::Coercible::Symbol.enum(:tiny, :small, :default), default: -> { :default }
  5. 1 def spinner_json
  6. then: 0 if @theme == :dark
  7. <<~JSON.squish
  8. {"nm":"Composição 1","ddd":0,"h":200,"w":200,"meta":{"g":"@lottiefiles/toolkit-js 0.67.2","tc":"#ffffff"},"layers":[{"ty":4,"nm":"4_layer_ring","sr":1,"st":17,"op":2267,"ip":17,"ln":"24","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"p":{"a":0,"k":[97.5,98,0]},"r":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":6},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[178.72],"t":31},{"s":[357.4],"t":56}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","nm":"Retângulo 1","it":[{"ty":"rc","nm":"Caminho do retângulo 1","d":1,"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":999},"s":{"a":0,"k":[121.283,121.283]}},{"ty":"st","nm":"Traçado 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":20},"c":{"a":0,"k":[0.9647,0.9647,0.9647]}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[124.669,124.669]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":-89},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"tm","nm":"Aparar caminhos 1","e":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[4],"t":17},{"s":[100],"t":31}]},"o":{"a":0,"k":0},"s":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":17},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":31},{"s":[100],"t":42}],"x":"var $bm_rt;\n$bm_rt = content('Aparar caminhos 1').start;"},"m":1}],"ind":1},{"ty":4,"nm":"3_layer_ring","sr":1,"st":13,"op":2263,"ip":13,"ln":"22","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"p":{"a":0,"k":[97.5,98,0]},"r":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":6},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[178.72],"t":31},{"s":[357.4],"t":56}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","nm":"Retângulo 1","it":[{"ty":"rc","nm":"Caminho do retângulo 1","d":1,"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":999},"s":{"a":0,"k":[121.283,121.283]}},{"ty":"st","nm":"Traçado 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":20},"c":{"a":0,"k":[0.9059,0.9059,0.9059]}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[124.669,124.669]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":-89},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"tm","nm":"Aparar caminhos 1","e":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[4],"t":13},{"s":[100],"t":31}]},"o":{"a":0,"k":0},"s":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":13},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":31},{"s":[100],"t":47}],"x":"var $bm_rt;\n$bm_rt = content('Aparar caminhos 1').start;"},"m":1}],"ind":2},{"ty":4,"nm":"2_layer_ring","sr":1,"st":9,"op":2259,"ip":9,"ln":"20","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"p":{"a":0,"k":[97.5,98,0]},"r":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":6},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[178.72],"t":31},{"s":[357.4],"t":56}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","nm":"Retângulo 1","it":[{"ty":"rc","nm":"Caminho do retângulo 1","d":1,"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":999},"s":{"a":0,"k":[121.283,121.283]}},{"ty":"st","nm":"Traçado 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":20},"c":{"a":0,"k":[0.5333,0.5333,0.5333]}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[124.669,124.669]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":-89},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"tm","nm":"Aparar caminhos 1","e":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[4],"t":9},{"s":[100],"t":31}]},"o":{"a":0,"k":0},"s":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":9},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":31},{"s":[100],"t":52}],"x":"var $bm_rt;\n$bm_rt = content('Aparar caminhos 1').start;"},"m":1}],"ind":3},{"ty":4,"nm":"1_layer_ring","sr":1,"st":6,"op":2256,"ip":6,"ln":"16","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"p":{"a":0,"k":[97.5,98,0]},"r":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":6},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[178.72],"t":31},{"s":[357.4],"t":56}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","nm":"Retângulo 1","it":[{"ty":"rc","nm":"Caminho do retângulo 1","d":1,"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":999},"s":{"a":0,"k":[121.283,121.283]}},{"ty":"st","nm":"Traçado 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":20},"c":{"a":0,"k":[0.4275,0.4275,0.4275]}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[124.669,124.669]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":-89},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"tm","nm":"Aparar caminhos 1","e":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[4],"t":6},{"s":[100],"t":31}]},"o":{"a":0,"k":0},"s":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":6},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":31},{"s":[100],"t":56}],"x":"var $bm_rt;\n$bm_rt = content('Aparar caminhos 1').start;"},"m":1}],"ind":4},{"ty":4,"nm":"bg_ring","sr":1,"st":0,"op":2250,"ip":0,"ln":"14","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"p":{"a":0,"k":[97.5,98,0]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","nm":"Retângulo 1","it":[{"ty":"rc","nm":"Caminho do retângulo 1","d":1,"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":999},"s":{"a":0,"k":[121.283,121.283]}},{"ty":"st","nm":"Traçado 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":20},"c":{"a":0,"k":[0.2392,0.2392,0.2392]}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[124.669,124.669]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]}],"ind":5}],"v":"5.7.0","fr":25,"op":57,"ip":0,"assets":[]}
  9. JSON
  10. else: 0 else
  11. <<~JSON.squish
  12. {"nm":"Composição 1","ddd":0,"h":200,"w":200,"meta":{"g":"@lottiefiles/toolkit-js 0.66.4","tc":"#ffffff"},"layers":[{"ty":4,"nm":"4_layer_ring","sr":1,"st":17,"op":2267,"ip":17,"ln":"24","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"p":{"a":0,"k":[97.5,98,0]},"r":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":6},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[178.72],"t":31},{"s":[357.4],"t":56}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","nm":"Retângulo 1","it":[{"ty":"rc","nm":"Caminho do retângulo 1","d":1,"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":999},"s":{"a":0,"k":[121.283,121.283]}},{"ty":"st","bm":1,"nm":"Traçado 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":20},"c":{"a":0,"k":[0.2706,0.2706,0.2706]}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[124.669,124.669]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":-89},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"tm","nm":"Aparar caminhos 1","e":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[4],"t":17},{"s":[100],"t":31}]},"o":{"a":0,"k":0},"s":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":17},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":31},{"s":[100],"t":42}],"x":"var $bm_rt;\n$bm_rt = content('Aparar caminhos 1').start;"},"m":1}],"ind":1},{"ty":4,"nm":"3_layer_ring","sr":1,"st":13,"op":2263,"ip":13,"ln":"22","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"p":{"a":0,"k":[97.5,98,0]},"r":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":6},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[178.72],"t":31},{"s":[357.4],"t":56}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","nm":"Retângulo 1","it":[{"ty":"rc","nm":"Caminho do retângulo 1","d":1,"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":999},"s":{"a":0,"k":[121.283,121.283]}},{"ty":"st","bm":1,"nm":"Traçado 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":80},"w":{"a":0,"k":20},"c":{"a":0,"k":[0.3647,0.3647,0.3647]}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[124.669,124.669]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":-89},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"tm","nm":"Aparar caminhos 1","e":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[4],"t":13},{"s":[100],"t":31}]},"o":{"a":0,"k":0},"s":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":13},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":31},{"s":[100],"t":47}],"x":"var $bm_rt;\n$bm_rt = content('Aparar caminhos 1').start;"},"m":1}],"ind":2},{"ty":4,"nm":"2_layer_ring","sr":1,"st":9,"op":2259,"ip":9,"ln":"20","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"p":{"a":0,"k":[97.5,98,0]},"r":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":6},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[178.72],"t":31},{"s":[357.4],"t":56}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","nm":"Retângulo 1","it":[{"ty":"rc","nm":"Caminho do retângulo 1","d":1,"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":999},"s":{"a":0,"k":[121.283,121.283]}},{"ty":"st","bm":1,"nm":"Traçado 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":50},"w":{"a":0,"k":20},"c":{"a":0,"k":[0.3647,0.3647,0.3647]}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[124.669,124.669]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":-89},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"tm","nm":"Aparar caminhos 1","e":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[4],"t":9},{"s":[100],"t":31}]},"o":{"a":0,"k":0},"s":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":9},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":31},{"s":[100],"t":52}],"x":"var $bm_rt;\n$bm_rt = content('Aparar caminhos 1').start;"},"m":1}],"ind":3},{"ty":4,"nm":"1_layer_ring","sr":1,"st":6,"op":2256,"ip":6,"ln":"16","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"p":{"a":0,"k":[97.5,98,0]},"r":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":6},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[178.72],"t":31},{"s":[357.4],"t":56}]},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","nm":"Retângulo 1","it":[{"ty":"rc","nm":"Caminho do retângulo 1","d":1,"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":999},"s":{"a":0,"k":[121.283,121.283]}},{"ty":"st","bm":1,"nm":"Traçado 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":30},"w":{"a":0,"k":20},"c":{"a":0,"k":[0.3647,0.3647,0.3647]}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[124.669,124.669]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":-89},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]},{"ty":"tm","nm":"Aparar caminhos 1","e":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[4],"t":6},{"s":[100],"t":31}]},"o":{"a":0,"k":0},"s":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":6},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":31},{"s":[100],"t":56}],"x":"var $bm_rt;\n$bm_rt = content('Aparar caminhos 1').start;"},"m":1}],"ind":4},{"ty":4,"nm":"bg_ring","sr":1,"st":0,"op":2250,"ip":0,"ln":"14","hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"p":{"a":0,"k":[97.5,98,0]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}},"shapes":[{"ty":"gr","nm":"Retângulo 1","it":[{"ty":"rc","nm":"Caminho do retângulo 1","d":1,"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":999},"s":{"a":0,"k":[121.283,121.283]}},{"ty":"st","nm":"Traçado 1","lc":2,"lj":1,"ml":4,"o":{"a":0,"k":100},"w":{"a":0,"k":20},"c":{"a":0,"k":[0.9059,0.9059,0.9059]}},{"ty":"tr","a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[124.669,124.669]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":0},"sa":{"a":0,"k":0},"o":{"a":0,"k":100}}]}],"ind":5}],"v":"5.7.0","fr":25,"op":57,"ip":0,"assets":[]}
  13. JSON
  14. end
  15. end
  16. 1 def spinner_classes
  17. "lui-spinner lui-spinner--#{@size} lui-spinner--#{@theme}"
  18. end
  19. 1 def size_px
  20. when: 0 case @size
  21. when: 0 when :tiny then 12
  22. when :small then 14
  23. else: 0 else
  24. 16
  25. end
  26. end
  27. end
  28. end

app/components/loopos_ui/spinner/spinner.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%# Lottie spinner commented out becuase it breaks in production for some reason %>
  2. <%
  3. =begin%>
  4. <div class="<%= spinner_classes %>"
  5. data-controller="lui--spinner"
  6. data-lui--spinner-data-value='<%= spinner_json %>'
  7. >
  8. <canvas data-lui--spinner-target="canvas"></canvas>
  9. </div>
  10. <%
  11. =end%>
  12. <%= render LooposUi::MIcon.new(:progress_activity, class: spinner_classes, size: size_px, class: "animate-spin") %>

app/components/loopos_ui/state_label.rb

62.96% lines covered

0.0% branches covered

27 relevant lines. 17 lines covered and 10 lines missed.
12 total branches, 0 branches covered and 12 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class StateLabel < LooposUi::Label
  4. # TODO: this should be stored in a shared colors file
  5. 1 class << self
  6. 1 alias_method :c, :find_color
  7. end
  8. COLORS = {
  9. # text, background
  10. 1 success: [c("general-global-white"), c("general-success-800")],
  11. danger: [c("general-global-white"), c("general-danger-800")],
  12. warning: [c("general-global-white"), c("general-notice-800")],
  13. neutral: [c("general-global-white"), c("general-gray-900")],
  14. informative: [c("general-global-white"), c("general-informative-800")],
  15. draft: [c("general-gray-900"), c("general-gray-200")],
  16. manager: [c("apps-manager-800-primary"), c("apps-manager-200")],
  17. core: [c("apps-core-800-primary"), c("apps-core-200")],
  18. hubs: [c("apps-hubs-800-primary"), c("apps-hubs-200")],
  19. submission: [c("apps-submission-800-primary"), c("apps-submission-200")],
  20. validation: [c("apps-validation-800-primary"), c("apps-validation-200")],
  21. handling: [c("apps-handling-800-primary"), c("apps-handling-200")],
  22. default: [c("general-gray-900"), c("general-gray-200")],
  23. }
  24. LIGHT_COLORS = {
  25. # text, background
  26. 1 success: [c("general-success-800"), c("general-success-200")],
  27. danger: [c("general-danger-800"), c("general-danger-200")],
  28. warning: [c("general-notice-800"), c("general-notice-200")],
  29. neutral: [c("general-global-black"), c("general-gray-200")],
  30. informative: [c("general-informative-800"), c("general-informative-200")],
  31. draft: [c("general-gray-900"), c("general-gray-200")],
  32. white: [c("general-global-black"), c("general-global-white")],
  33. default: [c("general-gray-900"), c("general-gray-200")],
  34. }
  35. # Reference the App colors since they are using the neutral
  36. 1 LIGHT_COLORS[:manager] = LIGHT_COLORS[:neutral]
  37. 1 LIGHT_COLORS[:core] = LIGHT_COLORS[:neutral]
  38. 1 LIGHT_COLORS[:hubs] = LIGHT_COLORS[:neutral]
  39. 1 LIGHT_COLORS[:submission] = LIGHT_COLORS[:neutral]
  40. 1 LIGHT_COLORS[:validation] = LIGHT_COLORS[:neutral]
  41. 1 LIGHT_COLORS[:handling] = LIGHT_COLORS[:neutral]
  42. 1 LIGHT_COLORS[:exits] = LIGHT_COLORS[:neutral]
  43. 1 LIGHT_COLORS[:impact] = LIGHT_COLORS[:neutral]
  44. 1 attr_accessor :text, :icon, :count, :condensed
  45. 1 def initialize(text: nil, icon: nil, color: nil, light: true, count: nil, condensed: false)
  46. super(text: text, icon: icon, color: color, count: count, condensed: condensed)
  47. color = color.to_sym
  48. then: 0 else: 0 @text_color, @bg_color = light ? LIGHT_COLORS[color] : COLORS[color]
  49. # If the color is not defined, use the default color
  50. then: 0 else: 0 @text_color, @bg_color = LIGHT_COLORS[:default] if @text_color.nil? || @bg_color.nil?
  51. then: 0 else: 0 raise "No color defined for: #{color}" if @text_color.nil? || @bg_color.nil?
  52. end
  53. 1 def styles
  54. else: 0 then: 0 return super unless @color == :draft || @color == :white
  55. then: 0 if @color == :draft
  56. <<~CSS.squish
  57. #{super}
  58. border: 1px dashed var(--General-Gray-900, #495057);
  59. else: 0 CSS
  60. then: 0 else: 0 elsif @color == :white
  61. <<~CSS.squish
  62. #{super}
  63. border: 1px solid var(--General-Gray-200, #ECEFF2);
  64. CSS
  65. end
  66. end
  67. end
  68. end

app/components/loopos_ui/status_dot.rb

80.0% lines covered

100.0% branches covered

5 relevant lines. 4 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class StatusDot < LoopComponent
  3. 1 option :kind, Types::Coercible::Symbol.enum(:success, :informative, :warning, :error, :apps), default: -> { :success }
  4. 1 def status_dot_classes
  5. "lui-status_dot lui-status_dot--#{@kind}"
  6. end
  7. end
  8. end

app/components/loopos_ui/status_dot/status_dot.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div class: status_dot_classes %>

app/components/loopos_ui/stepper.rb

60.53% lines covered

0.0% branches covered

38 relevant lines. 23 lines covered and 15 lines missed.
7 total branches, 0 branches covered and 7 branches missed.
    
  1. 1 module LooposUi
  2. 1 class Stepper < LoopComponent
  3. 1 option :data, type: Types::Strict::Hash, default: -> { {} }
  4. 1 option :active_step, type: Types::Strict::Integer, default: -> { 0 }
  5. 1 renders_many :steps, ->(*args, **kwargs) {
  6. @step_count ||= 0
  7. step = LooposUi::Stepper::Step.new(*args, **kwargs, index: @step_count, stepper: self)
  8. @step_count += 1
  9. step
  10. }
  11. 1 renders_one :header, LooposUi::Header
  12. 1 class Step < LoopComponent
  13. 1 option :id, Types::Coercible::String, optional: true
  14. 1 option :index, Types::Integer
  15. 1 option :stepper, Types::Instance(LooposUi::Stepper), optional: true
  16. 1 option :state, Types::Symbol.enum(:previous, :current, :next), default: -> {
  17. else: 0 then: 0 raise "Default state only supported if stepper present, pass in the state" unless stepper.present?
  18. case index
  19. when: 0 when ...stepper.active_step
  20. :previous
  21. when: 0 when stepper.active_step
  22. :current
  23. else: 0 else
  24. :next
  25. end
  26. }
  27. 1 option :active, Types::Bool, default: -> { state == :current }
  28. 1 mod :open, condition: -> { (state == :current) && active }
  29. 1 mod :state
  30. 1 mod :active
  31. 1 renders_one :header, ->(**args) { LooposUi::Header.new(**args, size: :small) }
  32. 1 class << self
  33. 1 def previous_action_button(*_args, **kwargs)
  34. LooposUi::Button.new(
  35. **deep_merge_args(
  36. kwargs,
  37. {
  38. # tag: :button,
  39. kind: :neutral,
  40. type: :secondary,
  41. size: :tiny,
  42. full: true,
  43. text: I18n.t(".back"),
  44. # tag_options: {
  45. # type: :submit,
  46. # data: { action: "stepper#previous" },
  47. # },
  48. },
  49. ),
  50. )
  51. end
  52. 1 def next_action_button(*_args, **kwargs)
  53. LooposUi::Button.new(
  54. **deep_merge_args(
  55. kwargs,
  56. {
  57. # tag: :button,
  58. kind: :neutral,
  59. type: :primary,
  60. size: :tiny,
  61. full: true,
  62. text: I18n.t(".next"),
  63. # tag_options: {
  64. # # type: :submit,
  65. # # data: { action: "stepper#next" },
  66. # },
  67. },
  68. ),
  69. )
  70. end
  71. end
  72. 1 renders_one :previous_action, ->(*args, **kwargs) {
  73. self.class.previous_action_button(*args, **kwargs)
  74. }
  75. 1 renders_one :next_action, ->(*args, **kwargs) {
  76. self.class.next_action_button(*args, **kwargs)
  77. }
  78. 1 def before_render
  79. else: 0 then: 0 raise "Header slot is required" unless header.present?
  80. end
  81. 1 def index_data(index)
  82. {
  83. "stepper-index-value": index,
  84. }
  85. end
  86. end
  87. end
  88. end

app/components/loopos_ui/stepper/step.html.erb

0.0% lines covered

0.0% branches covered

15 relevant lines. 0 lines covered and 15 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%= tag.div id: id, class: classes, data: { stepper_target: :step, controller: "lui--step" } do %>
  2. <%= tag.div class: bem_element_class(:indicator_wrapper) do %>
  3. <%= tag.div class: bem_element_class(:indicator), data: index_data(index) do %>
  4. <%= tag.span(index + 1, class: bem_element_class(:indicator_number)) %>
  5. <%= render LooposUi::MIcon.new(:check, class: bem_element_class(:indicator_icon), size: 14)%>
  6. <% end %>
  7. <%= tag.div class: bem_element_class(:indicator_line) %>
  8. <% end %>
  9. <%= tag.div class: bem_element_class(:body) do %>
  10. <%= tag.div class: bem_element_class(:header) do %>
  11. <%= header %>
  12. <% end %>
  13. then: 0 else: 0 <%= tag.div class: bem_element_class(:content), data: {"lui--step-target": "content"} do %>
  14. <%= content %>
  15. <% end if active %>
  16. <%= tag.div class: bem_element_class(:buttons) do %>
  17. <%= previous_action %>
  18. <%= next_action %>
  19. <% end %>
  20. <% end %>
  21. <% end %>

app/components/loopos_ui/stepper/stepper.html.erb

0.0% lines covered

100.0% branches covered

5 relevant lines. 0 lines covered and 5 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.div class: classes, data: { controller: "stepper" } do %>
  2. <%= header %>
  3. <%= tag.div class: "lui-stepper__container" do %>
  4. <% steps.each do |step| %>
  5. <%= step %>
  6. <% end %>
  7. <% end %>
  8. <% end %>

app/components/loopos_ui/stepper_panel.rb

35.0% lines covered

100.0% branches covered

20 relevant lines. 7 lines covered and 13 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class StepperPanel < LoopComponent
  3. 1 renders_one :stepper, LooposUi::Stepper
  4. 1 renders_one :top
  5. 1 renders_one :bottom
  6. 1 renders_many :actions, types: {
  7. button: { renders: Button, as: :button_action },
  8. modal_button: {
  9. renders: ->(*args, **kwargs) {
  10. modal = LooposUi::Modal.new(*args, **kwargs)
  11. stepper_panel_instance = self
  12. with_trigger_button_method = ->(*args, **kwargs) {
  13. super(*args, **deep_merge_args(kwargs, stepper_panel_instance.default_trigger_args))
  14. }
  15. modal.define_singleton_method(:with_trigger_button, with_trigger_button_method)
  16. modal
  17. },
  18. as: :modal_action,
  19. },
  20. action_menu: {
  21. renders: ->(*args, **kwargs) {
  22. action_menu = LooposUi::ActionMenu.new(*args, **kwargs)
  23. stepper_panel_instance = self
  24. with_trigger_button_method = ->(*args, **kwargs) {
  25. super(*args, **deep_merge_args(kwargs, stepper_panel_instance.default_trigger_args))
  26. }
  27. action_menu.define_singleton_method(:with_trigger_button, with_trigger_button_method)
  28. action_menu
  29. },
  30. as: :action_menu_action,
  31. },
  32. }
  33. 1 def default_trigger_args
  34. {
  35. kind: :neutral,
  36. type: :secondary,
  37. size: :small,
  38. }
  39. end
  40. end
  41. end

app/components/loopos_ui/stepper_panel/stepper_panel.html.erb

0.0% lines covered

0.0% branches covered

9 relevant lines. 0 lines covered and 9 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <div class="lui-stepper_panel">
  2. <%= top %>
  3. <div class="w-full grow overflow-y-auto relative">
  4. <%= stepper %>
  5. <div class="bottom-0 left-0 right-0 h-10" style="position: sticky; background: linear-gradient(180deg, rgba(252, 252, 252, 0.00) 0%, var(--surface-secondary-soft, #FCFCFC) 100%)"></div>
  6. </div>
  7. <%= bottom %>
  8. then: 0 else: 0 <%= tag.div class: "lui-stepper_panel__actions" do %>
  9. <% actions.each do |action| %>
  10. <%= action %>
  11. <% end %>
  12. <% end if actions.any? %>
  13. </div>

app/components/loopos_ui/table/pagination_component.html.erb

0.0% lines covered

0.0% branches covered

28 relevant lines. 0 lines covered and 28 lines missed.
8 total branches, 0 branches covered and 8 branches missed.
    
  1. <%# DEPRECATED: Use LooposUi::V2::Table instead %>
  2. <%
  3. path = URI.parse(link).path
  4. per_page_parsed = Rack::Utils.parse_nested_query(URI.parse(link).query)
  5. prev_link_parsed = Rack::Utils.parse_nested_query(URI.parse(link).query)
  6. next_link_parsed = Rack::Utils.parse_nested_query(URI.parse(link).query)
  7. pagy = @pagy
  8. link = @link
  9. link_data = @link_data
  10. link_parameters = @link_parameters || {}
  11. link_remote ||= @link_remote || false
  12. %>
  13. <% link_per_page = capture do %>
  14. <%= link_to "teste!", "#{path}?#{Rack::Utils.build_nested_query(per_page_parsed.merge(link_parameters.merge(page: pagy.prev)))}", method: @method, data: link_data, remote: link_remote %>
  15. <% end %>
  16. <% link_to_back = capture do %>
  17. <%= link_to "", "#{path}?#{Rack::Utils.build_nested_query(prev_link_parsed.merge(link_parameters.merge(page: pagy.prev)))}", method: @method, data: link_data, remote: link_remote %>
  18. <% end %>
  19. <% link_to_next = capture do %>
  20. <%= link_to "", "#{path}?#{Rack::Utils.build_nested_query(next_link_parsed.merge(link_parameters.merge(page: pagy.next)))}", method: @method, data: link_data, remote: link_remote %>
  21. <% end %>
  22. <% pagy.series.each do |item| %>
  23. then: 0 else: 0 <% if item.is_a?(String) %>
  24. then: 0 <% if pagy.series.last == item && item != '1' %>
  25. else: 0 <%= react_component("Pagination",{defaultValue: params["per_page"], app: @app, totalItems: pagy.series.last , itemsRange: item, onPagePerPage: link_per_page, onPagePrevious: link_to_back, onPageNext: link_to_next, isDisabledPagePrevious: false, isDisabledPageNext: true } )%>
  26. then: 0 <% elsif pagy.series.last == item && item == '1' %>
  27. else: 0 <%= react_component("Pagination",{defaultValue: params["per_page"], app: @app, totalItems: pagy.series.last , itemsRange: item, onPagePerPage: link_per_page, onPagePrevious: link_to_back, onPageNext: link_to_next, isDisabledPagePrevious: true, isDisabledPageNext: true } )%>
  28. then: 0 <% elsif item == "1" %>
  29. <%= react_component("Pagination",{defaultValue: params["per_page"], app: @app, totalItems: pagy.series.last , itemsRange: item, onPagePerPage: link_per_page, onPagePrevious: link_to_back, onPageNext: link_to_next, isDisabledPagePrevious: true, isDisabledPageNext: false } )%>
  30. else: 0 <% else %>
  31. <%= react_component("Pagination",{defaultValue: params["per_page"], app: @app, totalItems: pagy.series.last , itemsRange: item, onPagePerPage: link_per_page, onPagePrevious: link_to_back, onPageNext: link_to_next, isDisabledPagePrevious: false, isDisabledPageNext: false } )%>
  32. <% end %>
  33. <% end %>
  34. <% end %>

app/components/loopos_ui/table/pagination_component.rb

46.15% lines covered

100.0% branches covered

13 relevant lines. 6 lines covered and 7 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # DEPRECATED: Use LooposUi::V2::Table instead
  2. 1 module LooposUi
  3. 1 module Table
  4. 1 class PaginationComponent < ViewComponent::Base
  5. 1 include Turbo::FramesHelper
  6. 1 attr_reader :pagy, :link, :link_data, :app, :method, :link_remote, :link_parameters
  7. 1 def initialize(pagy:, link:, link_data:, app: "manager", method: :get, link_remote: false, link_parameters: {})
  8. @pagy = pagy
  9. @link = link
  10. @link_data = link_data
  11. @app = app
  12. @method = method
  13. @link_remote = link_remote
  14. @link_parameters = link_parameters
  15. end
  16. end
  17. end
  18. end

app/components/loopos_ui/table/table_component.html.erb

0.0% lines covered

0.0% branches covered

35 relevant lines. 0 lines covered and 35 lines missed.
26 total branches, 0 branches covered and 26 branches missed.
    
  1. <%# DEPRECATED: Use LooposUi::V2::Table instead %>
  2. <%= turbo_frame_tag @turbo_frame do %>
  3. <% extra_params = params.to_unsafe_h.except('format', 'controller', 'action') %>
  4. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 <div class="table<%= @small_table ? '__small' : '' %> <%= @collapsible ? 'collapsible-table' : '' %> <%= !@show_footer ? 'no-footer' : '' %> <%= !@show_header ? 'no-header' : '' %>" data-controller="table-component">
  5. <table id="rules">
  6. then: 0 else: 0 <% if @show_header %>
  7. <thead class="copy-12-medium">
  8. then: 0 <% if !head? %>
  9. <tr>
  10. <% default_table_headers.each do |header| %>
  11. <td>
  12. <%= header[:label].html_safe %>
  13. then: 0 else: 0 <% if header[:sort_attribute].present? %>
  14. <%= render 'loopos_ui/sort', attribute: header[:sort_attribute], link_parameters: @pagination_parameters.merge(turbo_stream: @pagination_turbo), link_method: @pagination_method %>
  15. <% end %>
  16. </td>
  17. <% end %>
  18. </tr>
  19. else: 0 <% else %>
  20. <%= head %>
  21. <% end %>
  22. </thead>
  23. <% end %>
  24. <tbody id="rules_collection" class="copy-14-medium">
  25. then: 0 <% if body? %>
  26. else: 0 <%= body %>
  27. then: 0 else: 0 <% elsif has_partial_and_data? %>
  28. <%= render partial: @data_partial, collection: @data, as: model_element_name %>
  29. <% end %>
  30. then: 0 else: 0 <% if @data.blank? %>
  31. <tr>
  32. <td colspan="<%= default_table_headers.size %>" class="table__no-results">
  33. <%= I18n.t("pages.app_instances.no_data") %>
  34. </td>
  35. </tr>
  36. <% end %>
  37. </tbody>
  38. </table>
  39. </div>
  40. then: 0 else: 0 <% if @show_footer %>
  41. <div id="rules_pagination" class="table__pagination-wrapper">
  42. else: 0 then: 0 <% unless @add_rows.blank? %>
  43. <%= button_to @add_rows[:url], method: :post, params: {parent_element_id: @add_rows[:parent_element_id]} do %>
  44. <i class="fa-solid fa-plus"></i>
  45. <% end %>
  46. <% end %>
  47. then: 0 else: 0 <% if @show_pagination %>
  48. <%= render LooposUi::Table::PaginationComponent.new(
  49. pagy: @pagy,
  50. link: url_for(**extra_params.except('page')),
  51. link_data: { turbo_frame: @turbo_frame, turbo_action: @turbo_action, turbo_stream: @pagination_turbo },
  52. method: @pagination_method,
  53. link_remote: @pagination_remote,
  54. link_parameters: @pagination_parameters,
  55. app: @app
  56. ) %>
  57. <% end %>
  58. </div>
  59. <% end %>
  60. <% end %>

app/components/loopos_ui/table/table_component.rb

31.71% lines covered

0.0% branches covered

41 relevant lines. 13 lines covered and 28 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. # frozen_string_literal: true
  2. # DEPRECATED: Use LooposUi::V2::Table instead
  3. 1 module LooposUi
  4. 1 module Table
  5. 1 class TableComponent < ViewComponent::Base
  6. 1 include Turbo::FramesHelper
  7. 1 renders_one :head
  8. 1 renders_one :body
  9. 1 def initialize(
  10. turbo_frame:,
  11. turbo_action:,
  12. pagy:,
  13. model_class:,
  14. headers: [],
  15. data: [],
  16. data_partial: nil,
  17. namespace_prefix: "",
  18. model_element_name: nil,
  19. app: "manager",
  20. show_pagination: true,
  21. pagination_method: :get,
  22. pagination_turbo: false,
  23. pagination_parameters: {},
  24. small_table: false,
  25. show_header: true,
  26. show_footer: true,
  27. collapsible: false,
  28. add_rows: {}
  29. )
  30. super
  31. @turbo_frame = turbo_frame
  32. @turbo_action = turbo_action
  33. @pagy = pagy
  34. @model_class = model_class
  35. @headers = headers
  36. @namespace_prefix = namespace_prefix
  37. @data = data
  38. @data_partial = data_partial
  39. @model_element_name = model_element_name
  40. @app = app
  41. @show_pagination = show_pagination
  42. @pagination_method = pagination_method
  43. @pagination_turbo = pagination_turbo
  44. @pagination_parameters = pagination_parameters
  45. @small_table = small_table
  46. @show_header = show_header
  47. @show_footer = show_footer
  48. @collapsible = collapsible
  49. @add_rows = add_rows
  50. end
  51. 1 def default_table_headers
  52. @headers.map do |header|
  53. then: 0 else: 0 next { label: header.humanize } if header.is_a?(String)
  54. header
  55. end
  56. end
  57. 1 def path_helper_method
  58. "#{@namespace_prefix}#{@model_class.model_name.route_key}_path"
  59. end
  60. 1 def path_helper(*args)
  61. send(path_helper_method, *args)
  62. end
  63. 1 def model_element_name
  64. @model_element_name || @model_class.model_name.element
  65. end
  66. 1 def has_partial_and_data?
  67. @data_partial.present? && @data.present?
  68. end
  69. 1 def show_paginator?
  70. then: 0 else: 0 (body? || has_partial_and_data?) && @pagy&.pages.to_i > 1
  71. end
  72. end
  73. end
  74. end

app/components/loopos_ui/table_data_list.rb

64.0% lines covered

0.0% branches covered

25 relevant lines. 16 lines covered and 9 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. 1 module LooposUi
  2. 1 class TableDataList < LoopComponent
  3. 1 renders_many :groups, "LooposUi::TableDataList::Group"
  4. 1 private
  5. 1 def number_of_columns
  6. then: 0 else: 0 groups.map(&:number_of_columns)&.max || 0
  7. end
  8. 1 class Group < LoopComponent
  9. 1 erb_template <<-ERB
  10. <%= header %>
  11. <% rows.each do |row| %>
  12. <%= row %>
  13. <% end %>
  14. ERB
  15. 1 renders_one :header
  16. 1 renders_many :rows, "LooposUi::TableDataList::Row"
  17. 1 def number_of_columns
  18. then: 0 else: 0 then: 0 else: 0 rows.map(&:cells)&.map(&:size)&.max || 0
  19. end
  20. end
  21. 1 class Row < LoopComponent
  22. 1 erb_template <<-ERB
  23. <% cells.each do |cell| %>
  24. <%= cell %>
  25. <% end %>
  26. ERB
  27. 1 renders_many :cells
  28. end
  29. 1 class LabelHeaderRow < LoopComponent
  30. 1 renders_one :label
  31. 1 renders_one :description
  32. end
  33. end
  34. end

app/components/loopos_ui/table_data_list/label_header_row.html.erb

0.0% lines covered

100.0% branches covered

3 relevant lines. 0 lines covered and 3 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <span class="lui-table_data_list__label_header_row">
  2. <%= label %>
  3. <%= description %>
  4. </span>

app/components/loopos_ui/table_data_list/table_data_list.html.erb

0.0% lines covered

100.0% branches covered

21 relevant lines. 0 lines covered and 21 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <% groups.each do |group| %>
  2. <% capture do %>
  3. <%= group %>
  4. <% end %>
  5. <% end %>
  6. <%
  7. columns = [
  8. {
  9. title: "Fake header column",
  10. dataIndex: :row_0,
  11. },
  12. *(number_of_columns-1).times.map do |i|
  13. {
  14. title: "Fake column #{i+1}",
  15. dataIndex: "row_#{i+1}",
  16. }
  17. end
  18. ]
  19. args = {
  20. columns:,
  21. show_pagination: false,
  22. searchable: false,
  23. tree_indent_size: 0,
  24. }
  25. %>
  26. <div class="lui-table_data_list">
  27. <%= render LooposUi::V2::Table.new(**args) do |table| %>
  28. <% groups.each_with_index do |group, i| %>
  29. <% table.with_row(key: "group_#{i}") do |row| %>
  30. <% row.with_cell(property: "row_0") do %>
  31. <%= group.header %>
  32. <% end %>
  33. <% group.rows.each_with_index do |group_row, j|%>
  34. <% row.with_child(key: "group_#{i}_#{j}") do |child_row| %>
  35. <% group_row.cells.each_with_index do |cell,k| %>
  36. <% child_row.with_cell(property: "row_#{k}") do %>
  37. <%= cell %>
  38. <% end %>
  39. <% end %>
  40. <% end %>
  41. <% end %>
  42. <% end %>
  43. <% end %>
  44. <% end %>
  45. </div>

app/components/loopos_ui/table_filter.rb

72.73% lines covered

0.0% branches covered

11 relevant lines. 8 lines covered and 3 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. 1 module LooposUi
  2. 1 class TableFilter < LoopComponent
  3. 1 option :base_key, Types::Coercible::String, optional: true
  4. 1 option :key, Types::Coercible::String
  5. 1 option :text, Types::Coercible::String
  6. 1 option :visible, Types::Bool, default: -> { false }
  7. 1 def classes
  8. then: 0 else: 0 "#{super} #{visible ? "flex" : "hidden"}"
  9. end
  10. 1 def value
  11. last_key = key.split("$").last
  12. then: 0 else: 0 then: 0 else: 0 last_key == "empty" ? "#{base_key&.humanize}: -" : text
  13. end
  14. end
  15. end

app/components/loopos_ui/table_filter/table_filter.html.erb

0.0% lines covered

100.0% branches covered

4 relevant lines. 0 lines covered and 4 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <div class="<%= classes %>"
  2. data-table-filters-target="filter"
  3. data-key="<%= key %>"
  4. >
  5. <div class="lui-table_filter__text">
  6. <%= value %>
  7. </div>
  8. <div class="lui-table_filter__close" data-action="click->table-filters#delete">
  9. <%= content_tag(:icon, "", class: "fa-regular fa-xmark fa-xs") %>
  10. </div>
  11. </div>

app/components/loopos_ui/tabs_component/tabs_component.html.erb

0.0% lines covered

100.0% branches covered

8 relevant lines. 0 lines covered and 8 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= turbo_frame_tag "#{frame_id}_extra_show", data: { controller: "tab-skeleton", "turbo-action": "advance" } do %>
  2. <div class="loopui-tabs__wrapper" id="<%= frame_id %>_tab" role="tablist">
  3. <% @tabs.each_pair do |tab_link, icon_class| %>
  4. <%= link_to link_url(tab_link), class: "tab-disabled", data: { action: "click->tab-skeleton#setLoading", "tab-skeleton-target": "button", app: @app } do %>
  5. <%= react_component("Tab", { text: tab_name(@resource, tab_link), variant: @app, isActive: @tab.to_s == tab_link.to_s, icon: icon_class }) %>
  6. <% end %>
  7. <% end %>
  8. </div>
  9. <%= turbo_frame_tag "tabs_content" do %>
  10. <div class="loopui-tabs__content" id="tabs-container" data-tab-skeleton-target="container">
  11. <%= render partial: "#{@relative_path}/#{@tab}", locals: { partner: @partner } %>
  12. </div>
  13. <% end %>
  14. <% end %>

app/components/loopos_ui/tabs_component/tabs_component.rb

34.38% lines covered

0.0% branches covered

32 relevant lines. 11 lines covered and 21 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. 1 module LooposUi
  2. 1 module TabsComponent
  3. 1 class TabsComponent < ViewComponent::Base
  4. 1 include Turbo::StreamsHelper
  5. 1 include Turbo::FramesHelper
  6. 1 attr_reader :resource, :tab_param, :tabs, :url, :relative_path, :app, :extra_params
  7. 1 def initialize(resource:, tab_param:, tabs:, url:, relative_path:, app: "manager", use_resource_in_url: true,
  8. extra_params: {})
  9. @resource = resource
  10. @tab = tab_param.presence || tabs.keys.first
  11. @tabs = tabs
  12. @relative_path = relative_path
  13. @url = url
  14. @app = app
  15. @extra_params = extra_params
  16. @use_resource_in_url = use_resource_in_url
  17. end
  18. 1 def link_url(tab_link)
  19. then: 0 new_url = if @url.is_a?(String)
  20. else: 0 URI.parse(@url + "?"+ { turbo_frame: "tabs_content", tab: tab_link }.to_param)
  21. then: 0 elsif @use_resource_in_url
  22. URI.parse(@url.call(resource, turbo_frame: "tabs_content", tab: tab_link))
  23. else: 0 else
  24. URI.parse(@url.call(turbo_frame: "tabs_content", tab: tab_link))
  25. end
  26. new_params = Rack::Utils.parse_nested_query(new_url.query || "").merge(@extra_params)
  27. new_url.query = Rack::Utils.build_nested_query(new_params)
  28. new_url.to_s
  29. end
  30. 1 def frame_id
  31. then: 0 else: 0 @resource.respond_to?(:id) ? @resource.id : @resource
  32. end
  33. 1 private
  34. 1 def tab_name(resource, tab_link)
  35. name = tab_link.to_s.dup
  36. name = name.split("/").last.titleize
  37. name.delete!("_")
  38. name
  39. end
  40. end
  41. end
  42. end

app/components/loopos_ui/tabs_content.rb

64.29% lines covered

0.0% branches covered

14 relevant lines. 9 lines covered and 5 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 class TabsContent < LoopComponent
  3. 1 renders_many :rows, "RowLayout"
  4. 1 class RowLayout < LoopComponent
  5. 1 erb_template <<~HTML
  6. <% columns.each do |column| %>
  7. then: 0 else: 0 <div class="column <%= column.half ? 'column--half' : '' %>">
  8. <%= column %>
  9. </div>
  10. <% end %>
  11. HTML
  12. 1 renders_many :columns, "ColumnLayout"
  13. 1 class ColumnLayout < LoopComponent
  14. 1 option :half, optional: true, default: -> { false }
  15. 1 def call
  16. content
  17. end
  18. end
  19. end
  20. end
  21. end

app/components/loopos_ui/tabs_content/tabs_content.html.erb

0.0% lines covered

100.0% branches covered

5 relevant lines. 0 lines covered and 5 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <div class="lui-tabs_content">
  2. <% rows.each do |row| %>
  3. <div class="row">
  4. <%= row %>
  5. </div>
  6. <% end %>
  7. </div>

app/components/loopos_ui/tabs_layout.rb

49.21% lines covered

0.0% branches covered

63 relevant lines. 31 lines covered and 32 lines missed.
16 total branches, 0 branches covered and 16 branches missed.
    
  1. 1 module LooposUi
  2. 1 class TabsLayout < LoopComponent
  3. 1 option :keep_tab_in_url, default: -> { true }
  4. 1 option :active_tab_key, optional: true
  5. 1 include LooposUi::FaviconAware
  6. 1 renders_many :tabs, ->(**args, &block) do
  7. @tabs_count ||= 0
  8. @tabs_count += 1
  9. @active_tab_index ||= 0
  10. # Check if this tab should be active based on active_tab_key
  11. if active_tab_key.present?
  12. then: 0 # activa_tab_key has priority over the tab active flag
  13. args[:active] = false
  14. then: 0 else: 0 if Tab.keyfy(args[:key]) == Tab.keyfy(active_tab_key)
  15. args[:active] = true
  16. @active_tab_index = @tabs_count - 1
  17. else: 0 end
  18. then: 0 else: 0 elsif args[:active]
  19. @active_tab_index = @tabs_count - 1
  20. end
  21. LooposUi::TabsLayout::Tab.new(index: @tabs_count - 1, **args, &block)
  22. end
  23. 1 renders_one :impact, ->(**args, &block) do
  24. LooposUi::TabsLayout::Tab.new(index: @tabs_count, **args, impact: true, &block)
  25. end
  26. 1 def initialize(...)
  27. super(...)
  28. @tabs_count = 0
  29. @active_tab_found = false
  30. end
  31. 1 class Tab < LoopComponent
  32. 1 include Turbo::FramesHelper
  33. 1 option :index
  34. 1 option :title
  35. 1 option :key, default: -> { title }
  36. 1 option :icon, optional: true
  37. 1 option :active, optional: true, default: -> { false }
  38. 1 option :url, optional: true
  39. 1 option :disabled, optional: true, default: -> { false }
  40. 1 option :lazy_load, optional: true, default: -> { false }
  41. 1 option :load_only_on_click, optional: true, default: -> { false }
  42. 1 option :impact, optional: true
  43. 1 attr_reader :index
  44. 1 def initialize(**data)
  45. then: 0 else: 0 data[:key] = data[:key].to_s.parameterize.underscore if data.key?(:key)
  46. super(**data)
  47. end
  48. 1 def turbo_frame_args
  49. args = {
  50. data: {
  51. tabs_target: "panel",
  52. tab_id: index,
  53. tab_key: key,
  54. },
  55. hidden: !active,
  56. }
  57. then: 0 else: 0 if url.present?
  58. then: 0 if lazy_load
  59. args.deep_merge!({
  60. data: {
  61. lazy_load: lazy_load,
  62. url: url,
  63. load_only_on_click: load_only_on_click,
  64. },
  65. })
  66. else: 0 else
  67. args.deep_merge!({src: url})
  68. end
  69. end
  70. args
  71. end
  72. 1 class << self
  73. 1 def keyfy(key)
  74. key.to_s.parameterize.underscore
  75. end
  76. 1 def turbo_id(key)
  77. "lui-tab-#{keyfy(key)}"
  78. end
  79. end
  80. 1 def turbo_id
  81. self.class.turbo_id(key)
  82. end
  83. 1 def classes
  84. then: 0 if active
  85. else: 0 "lui-tabs_layout__tab_entry--active"
  86. then: 0 elsif disabled
  87. "cursor-default! opacity-40 hover:text-content"
  88. else: 0 else
  89. "text-content"
  90. end
  91. end
  92. 1 def key
  93. self.class.keyfy(@key)
  94. end
  95. # Helper component. Meant to be used in turbo_stream responses. On being rendered,
  96. # it will emit an event to request focus on the tab with the given key.
  97. 1 class RequestFocus < LoopComponent
  98. 1 option :key
  99. end
  100. end
  101. end
  102. end

app/components/loopos_ui/tabs_layout/request_focus.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= tag.span(
  2. data: {
  3. controller: "tab-focus",
  4. tab_focus_tab_key_value: key
  5. },
  6. style: "display: none;"
  7. ) %>

app/components/loopos_ui/tabs_layout/tab.html.erb

0.0% lines covered

0.0% branches covered

8 relevant lines. 0 lines covered and 8 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%= turbo_frame_tag turbo_id, class: "lui-tabs_layout__tab", data: {}, **turbo_frame_args do %>
  2. then: 0 <% if url.present? %>
  3. <%= render LooposUi::TabsContent.new do |tabs| %>
  4. <% tabs.with_row do |row| %>
  5. <% row.with_column do %><%= render LooposUi::Loadings::Skeleton.new %><% end %>
  6. <% end %>
  7. <% tabs.with_row do |row| %>
  8. <% row.with_column do %><%= render LooposUi::Loadings::Skeleton.new %><% end %>
  9. <% end %>
  10. <% end %>
  11. else: 0 <% else %>
  12. <%= content %>
  13. <% end %>
  14. <% end %>

app/components/loopos_ui/tabs_layout/tabs_layout.html.erb

0.0% lines covered

0.0% branches covered

19 relevant lines. 0 lines covered and 19 lines missed.
14 total branches, 0 branches covered and 14 branches missed.
    
  1. <%= tag.div(
  2. class: "lui-tabs_layout",
  3. data: {
  4. controller: "tabs",
  5. tabs_keep_tab_in_url_value: keep_tab_in_url,
  6. }
  7. ) do %>
  8. <div class="lui-tabs_layout__tab_list" data-tabs-target="tabList" data-controller="lui--blurred-scroll">
  9. <% tabs.each_with_index do |tab, i| %>
  10. <%= tag.span(
  11. data: {
  12. then: 0 else: 0 action: tab.disabled ? nil : "click->tabs#changeTab",
  13. tabs_target: "tab",
  14. tab_id: i,
  15. tab_title: tab.key,
  16. tab_key: tab.key,
  17. load_only_on_click: tab.load_only_on_click
  18. },
  19. class: "lui-tabs_layout__tab_entry #{tab.classes} #{(@active_tab_index == i) && "lui-tabs_layout__tab_entry--active"}"
  20. ) do %>
  21. then: 0 else: 0 <%= tag.i(class: faviconize(tab.icon)) if tab.icon %>
  22. <%= tab.title %>
  23. <% end %>
  24. <% end %>
  25. then: 0 else: 0 <% if impact.present? %>
  26. <%= tag.span(
  27. data: {
  28. then: 0 else: 0 action: impact.disabled ? nil : "click->tabs#changeTab",
  29. tabs_target: "tab",
  30. tab_id: tabs.length,
  31. tab_title: impact.key
  32. },
  33. class: "lui-tabs_layout__tab_entry #{impact.classes} lui-tabs_layout__tab_entry--impact"
  34. ) do %>
  35. <svg width="75" height="22" viewBox="0 0 75 22" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  36. <rect y="0.5" width="75" height="21" fill="url(#pattern0_249_146)"/>
  37. <defs>
  38. <pattern id="pattern0_249_146" patternContentUnits="objectBoundingBox" width="1" height="1">
  39. <use xlink:href="#image0_249_146" transform="matrix(0.000917431 0 0 0.00327654 0 -0.0258847)"/>
  40. </pattern>
  41. <image id="image0_249_146" width="1090" height="321" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABEIAAAFBCAMAAABEsPpCAAADAFBMVEVMaXEQICYRl4wQICYQICYQICYQICYQICYQICYQICYQICYQICYVn4sQICYQICYQICYkw4QoyoMQICYguIYQICYs04Ijv4UQICYQICYQICYQICYQICYQICYQICYQICYQICYQICYnyIMQICYQICYv2oAQICYpy4Mv2oAnyYMiv4Uv24As0oIbr4gr0YIYp4kcsIg05n4VoIooy4MhvIYs1IEy4n8ZqokSmowkw4QivoUhu4YWooo0534mx4QkwYUx3oAWpIocsYghvIYpzoM05X8lxIQetYcs0oIjwIUVoIolxIQTnYsXpoofuIYt1YE05X4oy4MnyYMWo4olw4QVoYoWo4o05n4lxYQqz4Iu14Eu2IEivoU05n42634lxIQWo4omx4Qu2IEft4YpzYM47X0kwYUnyIQz5H8s04Ift4Yftocx34AWooo37X0Unos05X8s0oInyIMSmowVoYoSmYwguoYUnos05X4u2IEYqIky4H8fuIc05n405X8SmowVoooy4n8fuIY37H03630u14E37H037H0153405n405X4z438qz4IXpYoy4H805H847X0s1IIarYghvIY47H026X4aqokXpokguIcqz4It1oEv2oEnyIMjwIU26X4x3oAQICYnx4QivYUlxIQmxYQmxoQUnoslw4QUnYsTnIss0oITm4sjvoUSmYwr0IIjv4URmIwt1IEt1YEhuoYr0YIu14EguYYu2IEs04IkwYUSmoskwIUqz4InyIMVn4soyoMqzoIivIYoyYMhu4YguIYpzYMu1oEpzIMVoIo15n41534y4H805H8ftocetIc26H405X4v2YA26X4ZqokbrYgarIknyIQSmowVoYos04EivIUnyYMWoooz438iu4YWo4oXpIoz4n83630Rl4wt1oE37X0YpokYp4kYqIkft4Yoy4MZqYkes4cx3oAv2oAx3YAlwoQcsIgw24Adsoccr4gbrogw3IAdsYckwoU26n4v2YEXpYoy4X836n4z4X8aq4gw2oBipTVSAAAAn3RSTlMAIYBEdxDwQMCAYNCAoDDgQMBmgJmAgLCIcJDMUO4z3VUQEaqAu4DAQDBAMEBgQMAQQCBgwCDAQFDwoGCgwNBQEIAgoGCgQNDgoMDAwPCwMJDQ4PDAUHCA8KDwwMDQcCCwkOBwgJDw0OCwwIAwQNBAQODw8CDQsODgsPBwgPCQcLCQ8IBwkODwsJDw4JDggLDw8JDw8JC3SLda2sq85Om+qbhCAAAACXBIWXMAACxKAAAsSgF3enRNAAAgAElEQVR4nO2dZ5xc13mf7wgzOzO72l0QBAXuYlEWFDpBAwQhVAIkQBAgFYIoLGIDCVIkRUrsJqlCkerFlC1aTj6PI9npRbZlp/dCKU7k0E6iJHbiFFOMFTu9OoqT385Oved9z3lPuf3/fAP2zr3n3DnvM6efqBjc/tRTH+xy11NP7S5ImgEAOWD3sQ/e/4XfGueBr34QIgEAGDn2+Qd+i+OBzx+DRgAALE8d+al/beCrd+3D+wMAqOz+4BdM/ljmqzfh7QEAxtl35DMygSzxmc+iKgIAGLL7iNwfyxyBRAAAPT5rUQMZSgRdqwCApU5UYR9InM98Fm8PgMqz+/M/dOaB26v+9gCoOrd/wd0gP/zhD1ERAaDS3PS/PHkA3aoAVJcjb3nzB59C+QGgmuy+398gb731FmaaAVBJdj8QxCBvvfV5lB8AqsfuB74biiMoPgBUjYAGgUMAqBxBDfLd796FEgRApfjSd8KCPlUAqsSXAxvkO1/ERFUAqsNNoQ3yne+8jFV3AFSF27/47fB8CcUHgIrweAIG+fa30R0CQDV4JRGDfPuLaMoAUAVu/xUZj9//5VdeeeXI/S8Lr/8VzA4BoAo8LrDB43c9M3wTu5955UsihzyD8gNA6bnJaIKXqYMejh35pPGD96P0AFB2dn/yl/W8zHaL3vSy4aO/jGoIAGXnJ/QS+KR2qvozBomgGgJAyTFUQu43bUJ2l/7zqIYAUG70lZCfMOd93+O6G2BQBoBy82O/wfNjsslhRzS3+A3MDQGgzNykM4h0pZzOIVj1D0CZuV8T/fK1thqHPI7iA0B52feveGyWuHyKvw0OhQCgvNzFh/6XbTK9+8eDmAgAUCweZyPfsgHyDHujT6FIAFBW9v1fFtv2x6e4G30cpQeAsnLX/+H4sG2O97G3wg6IAJSVT3Fh/3H76RzsvTCsC0BZ+fjfY7CuhETRMe5emKAKQEm5nYt6h0oI7yMstQOgpNwUtOJwhLsbig8A5eTDXNA79YCyLRlMLgOgnGxlYv7HnXK7j1PIDhQfAErJ479Dc49bZj/O3O4YSg8ApYQJ+d9xnJO+lbmdw/AOAKAAcApxnAx2CQoBoErs+A8Mju/gw8ztoBAASgmnELfeVF4hW1F8ACgjnEJcQz70/QAAuWbHf6dxVkjg+wEAoBAoBICyAoUAADzY8Z9pXEP+WOD7AQByTWiFvBcKAaBKpKWQ96JUAVBGdvw/GleFXGLuVwWFdBSaOUgVAImy43dpXBWylbkfFAJAKQmtkHuZ+1VhpS4UAorDzh3H3mvFrh30jh2BFbKPud3vVmG/ECgEFIGduy5tvfcfurH10q6dsSzuYG7kqJBjXLqqsGsZFALyzs7PnXG1x4B7z+wa3Vc5sELu4R5bhbIFhYBcs/PSR/93IE4NLbKDuaOjQu4Ne7tiAYWA/LLv0kd/KSj39Fo0O5ibusX8MS6JjtsoFgsoBOSVHVvD+mNZEt0ezrAKOcM97HNVKFtQCAhCO16OWp533RW4AjImkaAK2cc+Kt6LW0qgEBCEwAo59tFvJcfWfTuYezspZCuX0HsrUbSgEBCEoArZyUZlIM4EVAino29960wlihYUAoIQUCGLlxIWCI+DQhb5+tKuShQtKAQEIZxCdn7072TGdfbJPcOn9dpKFC0oBAQhmEI+l51AXBRyD3+zU9UoWVAICEIghSye+bdZYq2QezSJrUY7BgoBYQijkMVTP5cplgpZvEeX2MVqFC0oBAQhiEJ2vpGtQSwVslMrvEpMTYVCQChCKCRzg9gp5Ar9vSoxrwwKAaEIoJCdb3wzaywUsutD+rQ6DO4UEygEBMFfITkwiDjuFz9nEMg3v/lsVcoVFAKC4K2QxVPpWEKLSCGLx+4JdKdSAIWAIHgr5Lp/mgOMgb/47BWyhFamEgKFgDD4KuSKP5cHPnSFjjPXvSFNY1WGY6AQEApPhTybC4ME442KzAmJoBAQCj+FLH7oj5eKKhz+0AcKAUHwU8ilchmkGqv8e0AhIAheCnlWHJzXXXdJ210xzpnr3khUFQwfqlAzBgoBgfBSyKlfEPD62V0Oq+ev3XXiA5K7B+QDVZmXugwUAoLgo5Bd5vA99YjH5hvHrktVIRVZodsHCgFB8FHI66agPOH7u37tiXTs8QvVMwgUAsLgoZBdf1rPiRC7fz17yvCUUFxRtQIFhYAgeChEH9zXhdo+8IpUDHKicsUJCgFBcFfIs9qQfCTc17Pz9eQNUrVWDBQCQuGukBOaiAw7uLGYeGOmggaBQkAYnBWy+Jd4TgWeYLF4SvMwfyo2mtsDCgFBcFbILj5yXw8+RStRh1xWqRllA6AQEARnhZxhQzKJH/XF1xMzyJaKFiQoBATBWSF/jSWRjoWdH+Af6MNllWzERFAICIWrQg6wYZvQWrVHkhDIBwIOHBUNKAQEwVUhZ9mwTOo4ycvCC2RLNXtBloFCQBBcFXLubzMkNkfrWu6JjlRbIFAICISrQtiwTe5M67MhBfL6I9UWCBTSp9FcaLfnWz3WtNsLyb6HZrPdbvcf15pfel4j0QdGUa052W5Pj2RxslkLd3dHhWzjQvOhcEmLsxhOICcOJJfMopCAQmaG0THvFIojZb1b0BOOrcZCuzWhvodOZ3bN3EwCj2u2W7PU4zoTrfZCMnmdmZxvkY/sTE3PhXGlo0Ie+bsMScbmCe6hdjy0q+oVkC5hFdKYnJ6iiumkNDIaC1RZn0gklrvMtIkEjz56eiHk42pzTCgPX1Y7cFYbk9OkIMder391xFEhZ7mg9U6Qhp3++jh3FvWPHmqB4hQy04qhFPXJNXwxXTMpSIzuBrPzVqFlTm03uvT+6EXYdKA2jexxnc7sXLC6SEP3SkeZMlo+/kJjKBWrCcMHWq0lyVzGhOhlod4AyTkfe1x2Ysu2RFNXMNTCxMVLU39ho236rWvrC6nxBp0piYZkqZU90OnJHLV58eM6nekgvRQzpvrHKBOGZ8rvJGYpmH+bJtmpnluYp/722S1adm3bhrZLHPXbdlPIgqCwTsxpEjInKe2z4lA2K2TSIrxsnkzTmLZ5WhCJNE1tJrtn2t5MQBRFXCwn+zu/LZvHlhP1e3ZRSENaXebK6Iysit/pkA0Sy9R2U2wdX7M+zRl5hWeIodZmegHWGVximn+my+0MaBSS3JBuF+6xVVys74v6HTsoZIYeYSCYoH/MbWoEbVGODQppOoR0p+VaMWiK388Ys+4dqzWh0xX4mqLjDXVE0c6/yuCccxmXMY+1aT8tbjuwZcuJywJyYsuWA8VrKalfsL1CrJoE88Stld58LS3Jr7NeIXYPHKBtifHMuz2t0+m4Pc85f12mGHF53JIjirYxofyYY8alnPBTyLYtJzgJBWCpv7ZAIlG/XGuFTNqVm2nlzrbdBJJfZ22zy/aBQ9bYNy5q0jYahaZhwSNuFdIwovS6Jw2vkGQHZKJoC/Pcs8ZPLh44ey45ewx57qFHCrL2V/1qbRVi/Rsbd4h9QE+YHaJRSMMnwqwbF05NpiFT9g6Z83pghxOl911Vomjb36BJWiG73J6785FzzAcT4bGHfE7PSQv1e7VUiGUdpKM4xKVKYHYIrxAvg3Q6HbuhGYfXM46tQzyqWEOoxkyA28bJTiEuz7327HNp+qPHc2fzXhlRv1c7hTiFyKhD3JrtRoewCvE1iF0HhbdBbB3in70uxPsNct9xomjbP6dJXCG2z13cdY75SPI8lm+LqN+rlUJm3Krpw1/yBcfSN2EILE4hIUJM7c3hCGAQu8NpHb8PlQmlGAS68ShRtO0v01xvkWcXLJ977cnHmA+kxHMX89uiUb9XK4W4BmT/R67hXOSn9PniFOI62DmG1CFBDEIPYtEEMwjRYAt35wEFUci2h7L1xzIn8zrtTf1ebRTi3HM326tEeES0fn4IoxC273eqNd1epkUv2x1DFtQzulu05tvN3mL7WrPZ5lbSdpGu9JMYpL8+xSj/eFvGfGtromjbP6ZJXCHy5267nrk2dc7lc+Kb+q2LFTJFBNvUUmA0l7bDaM5po2JZAEQzZnZNe6HZbEQzzeZke40uJrTdIbRCyGbT1Pxk7FaNpv7Jsj5VvoY1O0+95OY8NwPN1GrrYTDI1Pzk2GMbzcl5nUhiDtHe240o2vbTNEnPC+Geqyhk+/XMlZnwXB4lon71YoUoTCu7XSzw67y6P8DxmJlSVp3PzLEzO7V9BKRCiKBWnyhIu2xLBM6gmoW/C8xnRC2nhm4O7BpmJa5uKe/s2Ec0N3clirZzgSjJsAdChSyezJNAfjqfElG/fEeFMCvUG5NMwZ5W+wqYVV7sag9dVYBUSDxaTItTNYvzJ4yT3ZmhJsMjm/QjJetz+MTqdw9osJYek3RTjzKWPGX4QLe2GnGhknDvoUwhFx9LzgXOPJe3PhF5adUqRLOSv8HE0kJzfOnIGj62FugyPqvJF6WQ+P8J1rExEW3uzmU6QgRr9cjXJRiVYeeDCBYZc56XrUeiky0bSOICKeE4kShk+7l8uWPA9fkanVGLjYtC9AvQJKvwJrRdhsxKYM1nlNROt9vjydAoaxR2AZChS5WsOonmx5PzWY3q4UZ/mIWNMRpMP7N8Lq6jQs79DM3d4gc7sY157IhC7mYuyQGPJfx27JAXVo1CTD9W5skYxvlTZBHXlFJTz41eWWOJ5zoLtFFNRrRwdJbqFjUFZI0xnXhRDz0R31TXGuKokOvNsZwERoVs59yWD85tT/b92CCPDD4ozb9zJocIugvJ8WO+ImFQiGi1bx+mIjKruQc5GiOeGk85xFBnovuL5KLkviNxU8ZRIY9yESlPuAsmhVx8LNcGSb6aZoFaaKwVIhrg1DpENOBAtfX5ueZ6hcgna3VhRks14UV1aFh0LBB1GH2S6Qk63Lp9GvI7MncbczmWKeTiNxiS3d14JfPUW7t/XTzJpSpH3JqXHhG10NgqRPbbqptmJZzsSe0Nz16sVYj1JoaMAdnwoioh8mnx5JQ7Xd8xMwXFeoUelUtpsh0VwsXyN07apd0SrUK2nyuAQb7xjQs52UNeLTOWCpH+tvLL6aQlnbIQ+1GdQhy2QaUdwkYJUSmQ9ypEtBN0NQpyNMZ+rxEyl8KajOtpdmyEJPojq1PIyguJRX1g8tGYUYuMnULka8DYqZfi2jZhIbatr1GI00bKtEO4d6XmVdwg6KHmVePqGpU2O2f1buTQkcslWPi5o19nSDQ+VjIPvTWK9nIJyiEn87C5mTwsyKC0iAxu1FG+dp74aWbjyqfzV/pwNkyIh9tuYag+TqMEqi/VYa8i5rXJvmJXhTzKBeaFJKshvELuLpBBvv71ozlwiFpirBRi0UXIVEO0bfwY6sguW05ZhVh1SYxC9ubQL0ttV9hXCdS8WqXMttbTh6jpyd6Yq0IOsOGR4Km6vEJOJhbtyZADh0ijgg5K4fqvZejeEJtKgRorE9ylnEJcqvc9qFEPOrzUCov9ARJqXtl7UD0hzidWEA020ZfsqpDoj7Ak2F24kn9qwTid+QwReeGjgtKmEkL/jNtUQqiKDHcloxDXH+cuVHuBup+6Hthm16A+Sl65thDVE2I5aj2CayPMWSEPsSF7IbnoKI9CknxLMtQCY6MQu4CkWjJ2fQRq7d5y/Mj1VIUuVGcjJVG1VuBSJ1DyymmBqN3ZidmUetHdnBWyl4+O5GrpYRRy4daTd19cGYyLd5+89YJDKo4n9ZZkqMXPIijX2D2LqnDbdfqpv+92CvEJLXmwKgt8nR6rdD9zMUmY2efgPaJSIxkzc1bI4n/kScwhKzUPlXHr3SsTSdziyosnT9slJeP+EHnxI4LScnCDiEBLCanF204hnkf0UztykOFVmxuziFPdR2n3Md04/mqPoZpe0ixyVkh08n/yJNXSX6l5ppnTj65MJll9Fg+cPG2RnmwdIg8zoqhadiwEGOtUbsD1xpAKcemSGMNi4nltZC8ftw4Y5VH0ZUTdzu9Eb9XTkj5od4Ws/PMaLuz1yovTM/VceDSdzofjF4+K05Tk4JURtfzJFWJbPyfC2vZgJ2W8wEohFovOGNTeEM1L6O8I5jgKpDyLvkytGTmPW/dQu40FzU13hUSn9eGRRFPfWSG3JqM0muMX9a9myKMppiqOGmdyhVgXVfVhtndQSreNQjx7QiK6Lab9ye9axHF4RMkrGcjEOJdfJYSqawlarB4K2asPjwvvCV9Pd1TIrQk3YIiEnpSlLMP1MmoBlCvEuomv3MG6ZeGlEK/hmGWIvkZjeC04HtCv5JX8ZtTZKt7NtYZyS4EEPRQSnf6Leg4+GromstLwRJL7UhfIEsffc1CQtoPZDe2qISFXiHXnpHIH63qMl0J8f50jcuaVb7OBRaYQtdXhOIN/BGWdsEAHPgrZKwjfvUEt4qCQ05kIZIlFiUSOZpU6L4VYh6RyB6upaZGnQjwmpg5Rf/QDNI9oZApRekzYCbty1EyaP+qjkOioJIaPPnogmEbsFfKeUI92YfGkOYGZdYeokSZXiP/DrH8wfRQSoB1DtmSc1rMJEClE7QrxG9HtEn95U4KalpdCVv4ZKffd954QHBI/cJmjWU8B3X7UmMasaklqRKSpEOumkI9CHLskYqgDIJ6TTVhECnHq+jQyvNvsmrYsf14KiWxDOmUyrYL0uHjQkOfTGc0OUSNNfpqd/8PSVEiACn5ETsMIUrshEClEHSIK1uMz0Wo35TUsP4UsmuIjSw5m1gsyhrEikpHoPBRi3/OfqUICVPAj8mfffUmbHpFClIuCdM1Mt9oLvnsk2RWPA38yt+RhT45lDhleUTaLZaqjEOuuWxq188F7EJVBpBBlhCixASIDngqJvpZXgxzKjUGiaK8+qfdlkqjqKMR/auoyyo2DjPQQiBSipCaQKa3xVUh0NC0n2HEoo/dJs/2gNvGZtLiqo5AwvanUzBCbT9eazbYQpeOWeFvqLLCkOndNeCtk8fQfziH5MsiSQ3Tv6MUsklQdhVinVpoE4Z1rC+2W4GRQDcTbCjBZJxDeCjFERzbkzSCmt5TmCp4+aoEtqUICDchQ2x6Zhy0aC9N+9uDeVnKmtMVfITl0SP4MYnhLpzNIkKigdim4QoJ1eqrDqKZcLHCn8loiUUgwU9oSQCG5c0geDWJ4Sxkst1MLMRSix1IhDbVPwxXiOSECNwxBUrL94D/LEdkMcJjZq3lFGaQZCrHFarlho82cuO9C+RUSbX/xr+eGrGZ7mvma5hWlPxNfVFC7FFwhwSaA2ShkLqBAKqGQaPG+vBgkw+XzRjQvKf3Gl6igdim4QoLNl5ArZIY+zNuZKigkii7/F/kgi8ENKccPsq/oYOp1J7U4QyF6xArhTgB1phoKiW5++G/mgCfCvp7AfIJ/QamrTy3OUIgeqULI0/a9qIhCosWvZW+Q9H/M7biPTXnq08vUIg2F6JEppBFoINfwnFIqJIpufvHnMybDvUhFbOdfT9qL7UQFtQsUwtyZnHgeuBuEe05JFRJFex/OVCH5bsYscYhN+ydSTomooHaBQpg7U7kI34qpmEIylkjGZ0wKOM6mPe2WjKigdsG8EObORC7USfAhqJZComjvk7+YEZcHeSXJcoh9NSn7DwqxRd2bWMmFevTvCBOt6XArdUutkCja/unbsnDIbTnvS+1ynE1+ymMyahGHQvSYJ7g32Alls/ML8q0EJfuFKAPHie0nbyIpmR04lL5FilAJiaInuOSn3JGjFvOSKiRYbKkKie9EwnSETMzbbVkiUUipVupybL/8yb+VJoWohETRAe6V3JZuOtSSjv1C9KijtbHLiSMqlwTStj0swk0hSR1JYSLhJtX2vZ9+8raUNFKMSkgUse8j3an5alkvq0JCxZZxyyFyRsga+8dLFKIea1PYXcsk3HzzgctDwdds8j8cs8ynuQykO6yrFvayKiRUbCk3jjWRiMOqOhMuG7e67Z0a4hgZF/LTsSvkiX/HkM9dQgi25yMHanEvq0ICHfeiGiL2HogB3QmnfVuVTlnqbSkXJXUkhYnCKeQ2LgDzPjF1CJeFh1NNhVrey6qQQKcjqAO2sRsTOwy57fwselvKCwkTuW2LkaPeJ5JJSGKwP+Hpxp8Xh7g8pJoKtbyXVSGBzmpQB2TGk0B0prrVf2Sbs6ujP475GmOmexTmnM3XUzSFfOLvM3w6B4kTspfLw81ppkIt8GVVSKD+VLU3dTwT6swzx1iS9eYIJro5MJhu0pqfFG4JXzSFHOLCL8c7DcU5zuUh1f5UNdJKq5AgZ1GpdYPYsQvqeIxjTKvbjVA3UvMZojNkrD9nQnQyd9EU8iITfSlPqvDj4TzUpNSIKK1CgnSGqF0hsT3Tla4Q1zltarcs+baUq0LMoVPWGZs3hi+aQrgf8MKMxyzxBJOJJ9NMhBpppVVIkPMR1L6H2HHfyt9dlwibWkw91F0F/I/tU0edzGeaF0whN3MKyfN+hwqXM5lItUtYjbTSKiRES4ZY/mLqTXXtmxB+M2plxb+2pXawmHuEC6aQvf+AoSjzyrrczOUizUQIC2opFGL+KTVC7Ic6/qMfbGI9sdqXfFtEy8q731it2Zj7VAumkBuZ2CtUV0h0nFPIDSkmQlhQS6GQAOfNqsEVax4FW95HrNWj35Z6ne/uSmo7RpCJginkCSb2Uu1F8IdTSJqjutKCWgqFeA9WmDtpg0USsWEA/bbUISDfaohqL8GLK5hCnmRi78YcpM2CF6EQ2zv4KcS7iq92ccbXpISKJGrXIvptEW0rv2oI0d8j6KEtmEIeZmKvUL2pvAnTnBgiLailUIhvTyM1zBOzUqhIImTFvC1ipoqfKtX5t5LGWMEU8u8ZUp3X6c8TTDbSrExJC2o5FOI54Eksf4lLKVAkkcln3hbRa+LTcUxUQiQz9EuikDT7IQNwIxRiewdfhXgtlFF/ntUcKA0Qt9koVCWEe1uBx68JI0kqNcVSyHFOITlImw1QSOoK8ekmoHYjU6r4YbY6olPPvS3izBr3pozrtN5iKeTm/0JToGW6XW5k8pHmHFt5QS2HQtybMuTpUsoGP+pzXSoERItJ87aow3tdA7hBPFo0Fl4OhfxIDtJmw94c5ENeUEuiEOffZ2o/Q7WVonZuOnThMsfQsG+LMo5jzzHRjJF9z1BIFuQhH/KCWhKFdKbcHEJuy048XbnGvjOEO4aGfVtUNcRtB0TqTrIvqVgKufp/0BRNIXnIh7zElEUhbg4hDTJL3EntCLWN5RnuGBr+bZEn+Do4hOruEY7uQCFZAIVkohAXh9BHw1BBqjZCKNFoqLEHWfFvi86ttUNIeZVyyyEoJBzygloehXSmbPtUaYOQI8REK8RqXv0MWaMwvS06gZYT+kmDSO9RMIX8J5rCKSQH+ZAX1BIppDNh9WByLMZi2ZtNdYBtxejfFnMGZ8umAkQ+Wtz7rCgk0F61yQCFhENeUMukEKv5IQtMVDO/z8TIjfwQiEmNQbRpZrpgLVypbhLSsRmSVufdZXWunoSyKORGKMT2DsEU0pkSPrtBHk6n6eKggll4FFWDOY23n2TdZ7mPTssimcmmfKa8aWf7fHH1f6UpnEKYfGB2Kou7QiZarVh7ZFrSUdhmqwVszYKcFyaJpwV6RtkQXYOIa23JDvNlsmkxiYaY/Z/V2ZwCoJBwyL/4giukRXQYmCTSaPNBzS89o1sExlpPM5a32bb6dN16N74XZaKtz2djjsumxfdDVfum/bdxTQgoJBzyYlN8hRATp1qT/O/szLSmY0JXw2cCUhtQk9QJuuock9np9oB4nYSbkdZNLp/PBT6bNk0RY8tRyV2WXP3faIqmkB9h8gGFsPgphFwAu2aOCO3awrS2VaGdWkJOFu1+iglkKorXcOt1x3Mkem4vn+rbbs6t0WjSbo689uEKUEgQOIXcmWIa5F9uGRTCjH12WtPthWaz2Yhmms1mu90ydUoYZotpQn9qfmHMWM2FNnl19wmWCjE4ZOnprXa7m9O5dnvelEvLUVl+PgsFFBKEn2TycXWKaVC/3TIrxLa6zWAapOVM1Wei1YO/pPtmmOV2Yzkaw+gQC2zn8OoTS+YvO67+NZqiKYTJxq9BISy+CgkSZOZpHt6mmhMklnr/4RxivQqAWl/DA4WE4COcQtJMhPodl1shAYJMMlHM8ym9Xgj1NIZRyPcfyiEO64isWjJQSAjuhEIyUAgz5ipHFlz6SWIGBo/QdobQ7183Q16Oy24jVvbKqULel22ybLkxD9mQf7llUYjnD/Ua4c+zh0OGktI2iJj339B3woqQ7LesYvPgrBXyj2gKppD3Mdn4WJqJkH+5pVGIl0PkUyWozZpFjDaUdFHJvn/nJ/eYdYxvfpsCFSgkBD/JZCPVA7XkX255FOJe2Z+1mW6pXTInfIYuKvn3r9kwQIC0nqWim9wWI2uF/D5NsRTyESYXv384zVTIv9wSKcS1sj9vF1xOkRzratHYTvf++aU9JmY9zo+wqN9BIQF4jVPIR9JMhfzLLZNCnCr7LfsVH/ZPUfLDO0T7/g2Lfvnn+y3Rb0rVBYUE4GOcQlJNhfzLLZdCrKsIs07bGdfsqjvUWjy2xmR4/zUHiYgWL2uRqitjhVzzqzTFUgiTiZRzIf9yS6aQKJo0TWMfYcpJIBGxAFfDBDMQwtzC+P5rds2ZCX+BdJ+qW504AArx505OIX8o1WTIv9zSKSSKJoU1kWmvAi+Lqc6spg1Rm1uj+k7y/ie5DZMUpubC7TK2MN8yZRkK8ecWTiGp9qaWFqHwZuaNVRHdfgBCGuZIXuNczTFQk1hkaj63O3skQxkUcgNnkF99MAepKz7yOtOMZsH77LS/P5ZpLMyzVZ5wT2Fo0quBe/qYngzSgCkU1/yApkgKOczk4Qfnc5C4EmDX7Ko12/Otscr3VGt+rhk6sJtz7fGntFrz7WY6AVxbaE+PLw2eak23J3O8OWGSlEEh52xQ6gUAAApvSURBVDmF3JKDxJWAAD03oLyUQCFcFn7wgzT3GyoxUAjQcM07NAVSyPuYLLzzzg05SF0JgEKAhuIrhMvBO++kusauxEAhQEPxFcJXQjCkGwYoBGi45vs0f6AoL+1OJgPf//730Y4JAxQCNBRdITecZw2CdkwgoBCgoegK+QpfCcF4TCCgEKDhmrdpCqIQLvlvv/025pWFAgoBGoqtkBvO8wp5dw7SVw6gEKCh2Ar5GG+Qt7E+JhRQCNBwze/RFEIhX2ESvwQmtwcDCgEaiqyQwxqD/F6qWx6WGygEaLjmN2kKoJDDTNK7FGZeSwGAQoCG4ipEa5DfvCYHKSwLUAjQUFiF6A2CaWUBgUKAhj3/kuaOnL+015h098BwTECgEKChoAq5RW+Qr+QgieUBCgEaCqmQB5/WG+QFLLALCRQCNOz5dZo8K+TwC0yi+2CVf1CgEKCheAp58CWDQHKtvyIChQANhVPIa6YqyK+/gL7UsEAhQMOe79HkVCF3nmfSOwKaMYGBQoCGQilkzx1mgXzvpRwktFxAIUBDgRRy+GmBQL53HqMxoYFCgIY9b9LkTSEPvvsFJqXjvIDldcGBQoCGQijkhsN3iPzx5ptvoiMkPFAI0JB/hTz42ktSf7z5JrYqSwAoBGjIt0IePHzLebk/3nwT+wwlARQCNOz5KzSZK2TP4Xff8QKTOA4MxiQCFBKc9fX62t7LXFuvrzDevlavb+5dv7ler+UqL5xCzr87O1664w5LdyzzNAZjEgEKCcvG1fEX2lm9SvOE9Vcq119ptk5qcAopIDBIQuRXIfXl9NRzkBQxqzYoQlhiHZeJFZvJ69duzEuG9vxsWXg/DJIQUEg4VqwldLDMBkoKm65ir9+ck/ZMaRSCntTEgEKCoTZJRrlKecx6usrSIx8VkbIoBAZJDigkFHSbZMjaTeMPWr9Of30uMr7nj5YCzChLECgkECaDxB1SMxik09H1wqZFKRTy/J6cl51iA4WEId6tcWW9Xo+3bNaOPinWb1JfInb9+uyzVQaFPP2j2b/HMgOFBGEs+kdGccfHeK9Ucrf838NR3BWj2tmQfb72/7HC82r2b7HcQCEhqI28wKvGB1M2jUpk6IphM+aqWB/JyPXZ573wCjm/P/N3WHagkBCMNGPUkZSNRL1ilUYTKwZ/W5d5xoqukFcxG6TCFEgh66l6xsifh1WOfhNn4Bx1rHfUOZmP7O7/E0XmaVRBKk2BFDJse9CpHTqh36M6kAo5hWxwv9VJplpCkRXyPJb2V5ziKGTTQAhUnSIac8zyKEstrpRxBrWazDtUC6yQW9CGqTrFUciwksFNSx9KZjk7g+6OK+nrByO+m+i/p8b+v1BQbsFILiiOQgYDsVwlZOSS5WrHxnGjKAxqLVlPDSmoQiAQUCiFDOoMfO/nsMO1W69YZVDIYNZI1uv+9/+T4vH8uyAQEBVJIZv68a4bgx20ZLr1ClNDZqCQrCe57/9TRePpw+gDAcs4KKRe7w+WGncLs7g0ilbU6z0FrK6rTYtBDWOz5hZXjUlhoBC6O3VYS4FCrHj+VRzyAAbYKmRVfKHbalYNpkt7Ed7rHK2PL4hbV4/1cQ46NpgqxfJNxpoug4oL01KBQlz8ccudGb8tkC/sFLKKWvi6mQxQ86WjCtlIXD3uClPHxvhFy58d3HQDOeaSH4X82YJw/lX4A8SwUch6br8wdW6W5NIRhdD7CG0Ybc6IOi5WjD9mOFFkbb72W45RCIU8/9Jr6EAFKhYK2RgP8Q4boaJLhwphdwEZGXxxUciKkVvlucM49wp5/6uH0f0BaOQKWdXRsG69/aUDhfCbm474wkUh427K057t4+z/N3nl+fe/9K47YQ+gQayQoRY2DMZLNg4bIOtq1pf2FVKPX7tqpF0zqIc4KWRkZd5yNvNZF/nRd+WQO/fvR8MFmJEqZBCMsaMTBpG91vrS0WZGfPP1gUQGanJSSBTfoiy/GgGgmEgV0t8HXRlSra2N/0V66ahClA7Zwcbr/WkgbgqJ1DOrutnNw46pAJQBoUL6AUxd1+9wqFleOqIQIqI3rR3/m6NCqHpIl6tgEQACIFNIfw0sPa1r7Wh9QX7pUCHk0/sO6S3Fd1UIOeWk99hcj/QCUAhkCuldxUwU7+/LUbO7dBDvzNLb/rWrRpNgrxDd6VWrIREA/JApZMOIIwjqIxUP+aX9eF9nuHbz6D9cFBJt4loz+ThGBoACI1LIem1tYdB42WB3aT/e2UUv/TZRd3a6j0KUcyJG4HcfAQCYESmkdxG/UUcvPtdbXdqPd37Dn9UjN/NUyJJF6uTxuvEjNAEAFogUsjx9VLNRx8ZBdFtc2ot3ptdk5NpuNcVfIUusqquLd1APAcAdkUI2mEJt0yDWLS5dMSII7bXdu4VRSNStjcTGaDDXDABnRAoxxvrQHBaXrjA/esOwnhJOIUs9NuP9qxiXAcAViUJq5muW54xttrm0H++6g6CWr+3ODAmqkNgWRzrjAQC0kSRXiC52V/di3eLSfrzr1s/2ZrNGCShk9HzN7I/EBKCo5Fshq30UMhjG5dopwylnWZ8CAUBhKYxC7Dc+NCpksCAQHaoAuFKYhoz99stmhdQl9wQAmIMu/92p9odAmA+rs+o6AQAQFGZQN+qHu/goqkEdg5XU4NxuKAQARwoztcz+QMzxQ2UooBAAfCnMBHf7Y7lNmwmMKAd9IQA4UphldiMHS3Ddo5sG7ZjYYXbcQjpzPQUAoCfkYv91dpfaLfYfEQR38+F6/mUrDZo+XO4GZ0Tk9ngIAPJOHrYcMlQS+kMwQ0PQqR1WU9aOfZ5NzaClw6YAAGAg5MaH6+0utdv4cKTzk641rB8uelkV/wC5J0htgyEBAAAjAbZf3iTffnmT+/bL4wc6qH0tI/vBb1A/QJypWxvuHIJ2DACu+B8Csanfo7De8lK7QyCWgn5kce1V407YNLqv4QrqA/HkrBr+TTdZDQCgJSdHUSnXru9fO9oiGtvlY/WwJrJxbI/2K5kP1IeVjdrYHohYZAeAM9kfiLliBXEts4g2vofylfV6Pb47+1gvTPzE76Xr45uWYVIIAO7wxyOMxF2U5LHcK3pxTh/LHWvijB3VTzLec7pJ3So1DvpSAfBArBD9levGKguySwcK2UTurD7y6BFMDomPvRgdgo4QAHyQK2Rk3oXChtiAh+jS4WL/GucQon3FH03XoasU8bbMOGjFAOCFhUKGXZxx1FVqkktH9gupkZevIwdbV/B1lnXkvPqN/Ac2YDgXAD9sFDI2EjpkMxmH5kvHthwiDptjKwj0kVKddWyfcLwD1fgBAIAQO4VE0ap4Z8Rq9ofcdOn4rmUrYg2Oum7SOXG85VXa03FXqR9YjeN0AciEer0f7fW6oSGgvVTZ+LDeP2putem+3aNgBgfTbTCmo/u44QfWij4AAMg1gr1TAQCArRZAIQAAd6AQAIAHUAgAwAMoBADgARQCAPAACgEAeACFAAA8gEIAAB5AIQAAD6AQAIAHUAgAwAMoBADgARQCAPAACgEAAAAAAAAAAAAAAAAAAAAAACAkiqL/DyN/4T80/Om0AAAAAElFTkSuQmCC"/>
  42. </defs>
  43. </svg>
  44. <% end %>
  45. <% end %>
  46. </div>
  47. <div class="lui-tabs_layout__main">
  48. <% tabs.each do |tab| %>
  49. then: 0 else: 0 <%= tab if !tab.disabled %>
  50. <% end %>
  51. then: 0 else: 0 <% if impact.present? %>
  52. then: 0 else: 0 <%= impact if !impact.disabled %>
  53. <% end %>
  54. </div>
  55. <% end %>

app/components/loopos_ui/tabs_section.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class TabsSection < LoopComponent
  3. 1 option :icon, optional: true
  4. 1 option :title, optional: true
  5. 1 option :description, optional: true
  6. 1 option :size, default: -> { "normal" }
  7. 1 option :tooltip, optional: true
  8. 1 option :no_margin_top, default: -> { false }
  9. 1 option :underline, default: -> { false }
  10. 1 renders_many :button_groups, "LooposUi::ActionButtons::ButtonGroup"
  11. 1 renders_many :corner_actions
  12. end
  13. end

app/components/loopos_ui/tabs_section/tabs_section.html.erb

0.0% lines covered

0.0% branches covered

11 relevant lines. 0 lines covered and 11 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. then: 0 else: 0 <div class="lui-tabs_section<%= no_margin_top ? "--no-margin" : "" %>">
  2. then: 0 else: 0 <% if title || description || icon || tooltip || button_groups.any? || corner_actions.any? %>
  3. then: 0 else: 0 <div class="lui-tabs_section__header <%= underline ? "lui-tabs_section__header--underline": ""%>">
  4. <%= render LooposUi::TitleDescription.new(icon: icon, title: title, description: description, size: size, tooltip: tooltip ) %>
  5. <div class="lui-tabs_section__buttons">
  6. <% corner_actions.each do |corner_action| %>
  7. <%= corner_action %>
  8. <% end %>
  9. <% button_groups.each do |bg| %>
  10. <%= bg %>
  11. <% end %>
  12. </div>
  13. </div>
  14. <% end %>
  15. <div class="lui-tabs_section__content">
  16. <%= content %>
  17. </div>
  18. </div>

app/components/loopos_ui/tag_token.rb

38.1% lines covered

0.0% branches covered

21 relevant lines. 8 lines covered and 13 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class TagToken < LooposUi::Token
  4. COLORS = {
  5. # text, bg
  6. 1 handling: [find_color("apps-handling-800-primary"), find_color("apps-handling-400")],
  7. danger: [find_color("general-danger-800"), find_color("general-danger-400")], # General/Danger/800, General/Danger/400
  8. core: [find_color("apps-core-800-primary"), find_color("apps-core-400")], # Apps/Core/800-Primary, Apps/Core/400
  9. submission: [find_color("apps-core-800-primary"), find_color("apps-core-400")], # Apps/Submission/800-Primary, Apps/Submission/400
  10. impact: [find_color("apps-impact-800-primary"), find_color("apps-impact-400")], # Apps/Impact/800-Primary, Apps/Impact/400
  11. manager: [find_color("apps-manager-800-primary"), find_color("apps-manager-400")], # Apps/Manager/800-Primary, Apps/Manager/400
  12. hubs: [find_color("apps-hubs-800-primary"), find_color("apps-hubs-400")], # Apps/Hubs/800-Primary, Apps/Hubs/400
  13. general: [find_color("general-gray-900"), find_color("general-gray-500")], # , General/Gray/900, General/Gray/500
  14. }
  15. 1 def initialize(...)
  16. super(...)
  17. @leading_icon ||= "fa-regular fa-tag"
  18. end
  19. 1 def classes
  20. "#{super} lui-tag-token"
  21. end
  22. 1 def styles
  23. <<~CSS.squish
  24. color: #{@text_color};
  25. border-color: #{@bg_color};
  26. CSS
  27. end
  28. 1 private
  29. 1 def set_color(color)
  30. then: 0 @text_color, @bg_color = if color.present?
  31. LooposUi.logger.warn("Manual color should not be set, use for testing only")
  32. then: 0 if COLORS.key?(color.to_sym)
  33. COLORS[color.to_sym]
  34. else: 0 else
  35. raise ArgumentError, "Invalid color: #{color}, available colors: #{COLORS.keys.join(", ")}"
  36. end
  37. else: 0 else
  38. valid_colors = COLORS.excluding(LooposUi.config.app_type.to_sym, :general).values
  39. then: 0 if @text.present?
  40. valid_colors[@text.hash % valid_colors.size]
  41. else: 0 else
  42. valid_colors.sample
  43. end
  44. end
  45. end
  46. end
  47. end

app/components/loopos_ui/theme_context.rb

62.86% lines covered

0.0% branches covered

35 relevant lines. 22 lines covered and 13 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 class ThemeContext < LoopComponent
  3. 1 class Theme < Dry::Struct
  4. # We could set defaults here or in the initializer
  5. 1 attribute? :primary_font, Types::String
  6. 1 attribute? :primary_color, Types::String
  7. 1 attribute? :text_color, Types::String
  8. 1 class Button < Dry::Struct
  9. # TODO: change the string type to a color type (valid CSS)
  10. 1 attribute :primary_color, Types::String
  11. 1 attribute :border_color, Types::String
  12. 1 attribute :text_color, Types::String
  13. 1 attribute :hover, Types::Hash.schema(
  14. primary_color: Types::String,
  15. border_color: Types::String,
  16. text_color: Types::String,
  17. )
  18. end
  19. 1 class TabsLayout < Dry::Struct
  20. 1 attribute :primary_color, Types::String
  21. end
  22. 1 attribute? :button, Button
  23. 1 attribute? :tabs_layout, TabsLayout
  24. 1 def self.keyfy(key)
  25. key.to_s.gsub("_", "-")
  26. end
  27. end
  28. 1 option :theme, Theme
  29. 1 attr_reader :theme
  30. 1 def call
  31. previous_theme_context = LooposUi::Current.theme_context
  32. LooposUi::Current.theme_context = self
  33. content_tag(:div, class: "contents lui-theme-context", style: theme_variables) do
  34. content
  35. end
  36. ensure
  37. LooposUi::Current.theme_context = previous_theme_context
  38. end
  39. 1 private
  40. 1 def theme_variables
  41. theme.to_h.flat_map do |key, value|
  42. _theme_variable(key, value)
  43. end
  44. end
  45. 1 def _theme_variable(key, value)
  46. key = Theme.keyfy(key)
  47. case value
  48. when: 0 when Hash
  49. value.map do |k, v|
  50. _theme_variable("#{key}-#{k}", v)
  51. end
  52. else: 0 else
  53. "--lui-theme-#{key}: #{value};"
  54. end
  55. end
  56. end
  57. end

app/components/loopos_ui/timeline.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class Timeline < LoopComponent
  3. 1 option :item
  4. 1 option :is_last
  5. end
  6. end

app/components/loopos_ui/timeline/timeline.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= react_component(
  2. "Timeline",
  3. {
  4. item: item,
  5. isLast: is_last,
  6. }
  7. )%>

app/components/loopos_ui/title_description.rb

92.31% lines covered

100.0% branches covered

13 relevant lines. 12 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. # TODO: Deprecate this component. Header is the new way to do this.
  3. 1 class TitleDescription < LoopComponent
  4. 1 option :icon, optional: true
  5. 1 option :title, optional: true
  6. 1 option :description, optional: true
  7. 1 option :size, Types::Coercible::Symbol.enum(:normal, :small), default: -> { :normal }
  8. 1 option :tooltip, optional: true
  9. 1 option :count, optional: true
  10. 1 option :show_ai_logo, Types::Bool, optional: true, default: -> { false }
  11. 1 renders_one :custom_description
  12. 1 renders_one :info
  13. 1 def show_ai_logo?
  14. @show_ai_logo
  15. end
  16. end
  17. end

app/components/loopos_ui/title_description/title_description.html.erb

0.0% lines covered

0.0% branches covered

20 relevant lines. 0 lines covered and 20 lines missed.
14 total branches, 0 branches covered and 14 branches missed.
    
  1. <% body = capture do %>
  2. <span class="lui-title_description__title">
  3. then: 0 else: 0 <%= render LooposUi::Icon.new(icon: icon, size:"16") if icon.present? %>
  4. <%= title %>
  5. then: 0 else: 0 <%= render LooposUi::IconTooltip.new(icon: "fa-regular fa-circle-info", text: tooltip, size: "12") if tooltip.present? %>
  6. then: 0 else: 0 <%= render LooposUi::Counter.new(count: count, kind: :neutral, size: :small) if count.present? %>
  7. then: 0 else: 0 <%= info if info? %>
  8. </span>
  9. then: 0 else: 0 <%= tag.span(class: "lui-title_description__description") do %>
  10. then: 0 <% if custom_description? %>
  11. <%= custom_description %>
  12. else: 0 <% else %>
  13. <%= description %>
  14. <% end %>
  15. <% end if custom_description? || description.present? %>
  16. <% end %>
  17. <div class="lui-title_description lui-title_description--<%= size %>">
  18. then: 0 <% if show_ai_logo? %>
  19. <div class="lui-title_description__row">
  20. <%= image_tag("loopos_ai_icon.svg", alt: "LoopOS AI", class: "lui-title_description__ai_logo") %>
  21. <div class="lui-title_description__content">
  22. <%= body %>
  23. </div>
  24. </div>
  25. else: 0 <% else %>
  26. <%= body %>
  27. <% end %>
  28. </div>

app/components/loopos_ui/toast.rb

63.64% lines covered

0.0% branches covered

11 relevant lines. 7 lines covered and 4 lines missed.
3 total branches, 0 branches covered and 3 branches missed.
    
  1. 1 module LooposUi
  2. 1 class Toast < LoopComponent
  3. 1 option :flash
  4. 1 option :user
  5. 1 option :type
  6. 1 private
  7. 1 def type_to_icon(type)
  8. case type
  9. when: 0 when "alert"
  10. "fa-light fa-circle-exclamation"
  11. when: 0 when "success"
  12. "fa-light fa-circle-check"
  13. else: 0 else
  14. "fa-light fa-circle-xmark"
  15. end
  16. end
  17. end
  18. end

app/components/loopos_ui/toast/toast.html.erb

0.0% lines covered

0.0% branches covered

12 relevant lines. 0 lines covered and 12 lines missed.
8 total branches, 0 branches covered and 8 branches missed.
    
  1. <div class="flash"
  2. data-controller="flash"
  3. then: 0 else: 0 then: 0 else: 0 id="flash_container_<%= user&.try(:fetch, "id", nil) || user&.id %>">
  4. then: 0 else: 0 <% if flash.present? %>
  5. <div data-controller="flash"
  6. data-flash-target="content"
  7. data-action="click->flash#hide">
  8. then: 0 <% if flash.is_a?(Array) %>
  9. <% flash.each do |name, msg| %>
  10. <%= react_component("GeneralAlert", {
  11. title: type.titleize,
  12. variant: type,
  13. coloredText: false,
  14. text: msg,
  15. icon: type_to_icon(type),
  16. size: "small"
  17. }) %>
  18. <% end %>
  19. else: 0 <% else %>
  20. <%= react_component("GeneralAlert", {
  21. title: type.titleize,
  22. variant: type,
  23. coloredText: false,
  24. text: flash,
  25. icon: type_to_icon(type),
  26. size: "small"
  27. }) %>
  28. <% end %>
  29. </div>
  30. <% end %>
  31. </div>

app/components/loopos_ui/toaster.rb

62.5% lines covered

0.0% branches covered

56 relevant lines. 35 lines covered and 21 lines missed.
14 total branches, 0 branches covered and 14 branches missed.
    
  1. 1 module LooposUi
  2. 1 class Toaster < LoopComponent
  3. 1 include LooposUi::FaviconAware
  4. 1 option :title, Types::Coercible::String
  5. 1 option :description, Types::Coercible::String, optional: true
  6. 1 option :variant,
  7. Types::Coercible::Symbol.enum(
  8. :informative,
  9. :success,
  10. :warning,
  11. :danger,
  12. ),
  13. default: -> { :informative }
  14. 1 mod :variant
  15. 1 option :is_toast, Types::Bool, default: -> { true }
  16. 1 mod :is_toast
  17. 1 option :fullwidth, Types::Bool, default: -> { false }
  18. 1 mod :fullwidth
  19. 1 option :dismissible, Types::Bool, default: -> { true }
  20. 1 mod :dismissible
  21. 1 option :animated, Types::Bool, default: -> { false }
  22. 1 mod :animated
  23. 1 TDuration = (Types::Integer | Types::Instance(ActiveSupport::Duration)).constructor do |value|
  24. then: 0 if value.is_a?(ActiveSupport::Duration)
  25. value.in_seconds * 1000
  26. else: 0 else
  27. value
  28. end
  29. end
  30. 1 option :timeout, TDuration, default: -> { 2.seconds }
  31. 1 option :persistent, Types::Bool, default: -> { false }
  32. 1 class << self
  33. 1 def variants
  34. 2 dry_initializer.definitions[:variant].type.values
  35. end
  36. 1 def message(*args, **kwargs)
  37. new(*args, **kwargs, is_toast: false)
  38. end
  39. 1 def toast(*args, **kwargs)
  40. new(*args, **kwargs, is_toast: true)
  41. end
  42. end
  43. 1 variants.each do |variant|
  44. 4 define_singleton_method(variant) do |**options|
  45. new(**options, variant: variant)
  46. end
  47. end
  48. 1 def icon_tag
  49. content_tag(:i, "", class: type_to_icon(variant))
  50. end
  51. 1 def dismissible?
  52. then: 0 else: 0 is_toast ? dismissible : false
  53. end
  54. 1 def animated?
  55. then: 0 else: 0 is_toast ? animated : false
  56. end
  57. 1 private
  58. 1 def persistent?
  59. then: 0 else: 0 is_toast ? persistent : true
  60. end
  61. 1 def data
  62. {
  63. controller: "toaster",
  64. toaster_persistent_value: persistent?,
  65. toaster_timeout_value: timeout,
  66. }
  67. end
  68. 1 def type_to_icon(type)
  69. case type
  70. when: 0 when :informative
  71. :info
  72. when: 0 when :success
  73. :check_circle
  74. when: 0 when :warning
  75. :error
  76. when: 0 when :danger
  77. :cancel
  78. else
  79. skipped # :nocov:
  80. skipped raise "Invalid variant: #{type}"
  81. skipped # :nocov:
  82. end
  83. end
  84. 1 def icon_class
  85. "lui-toaster__content-title__icon lui-toaster__content-title__icon__#{variant_selector}"
  86. end
  87. 1 def variant_selector
  88. then: 0 if variant == :danger
  89. :error
  90. else: 0 else
  91. variant
  92. end
  93. end
  94. end
  95. 1 Message = Toaster
  96. end

app/components/loopos_ui/toaster/toaster.html.erb

0.0% lines covered

0.0% branches covered

8 relevant lines. 0 lines covered and 8 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%= tag.div(class: classes, data: data) do %>
  2. <div class="lui-toaster__header">
  3. <%= render LooposUi::Header.new(title: title, description: description, size: :small) do |header| %>
  4. <% header.with_semantic_icon(type_to_icon(variant), semantic: variant_selector) %>
  5. <% end %>
  6. then: 0 else: 0 <%= tag.div(class: "lui-toaster__close", data: { action: "click->toaster#dismiss"}) do %>
  7. <%= tag.i(class: "fa-regular fa-xmark fa-xs") %>
  8. <% end if dismissible? %>
  9. </div>
  10. <div class="lui-toaster__content">
  11. <%= content %>
  12. </div>
  13. <% end %>

app/components/loopos_ui/toggle.rb

69.23% lines covered

0.0% branches covered

26 relevant lines. 18 lines covered and 8 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. 1 module LooposUi
  2. 1 class Toggle < LoopComponent
  3. 1 option :size, default: -> {:normal}
  4. 1 option :label, optional: true
  5. 1 option :position, default: -> {:right}
  6. 1 option :data_action, optional: true
  7. 1 option :turbo_frame, optional: true
  8. 1 option :checked, optional: true
  9. 1 option :name, optional: true # why wasnt this here before
  10. 1 option :color, optional: true # deprecated
  11. 1 option :autoload, default: -> {false}
  12. 1 option :readonly, default: -> {false}
  13. 1 option :attrs, Types::Hash, default: -> { {} }
  14. 1 def initialize(**kwargs)
  15. @value_passed = kwargs.key?(:value)
  16. @value = kwargs.delete(:value)
  17. super
  18. end
  19. 1 private
  20. 1 def attrs
  21. deep_merge_args(
  22. {
  23. class: "flex items-center gap-2 flex-row",
  24. data: {
  25. controller: "toggle",
  26. },
  27. },
  28. @attrs,
  29. )
  30. end
  31. 1 def checked?
  32. then: 0 if @value_passed
  33. @value
  34. else: 0 else # legacy usage
  35. checked.to_s == "true"
  36. end
  37. end
  38. 1 def input_value
  39. then: 0 else: 0 @value_passed ? @value : "true"
  40. end
  41. end
  42. end

app/components/loopos_ui/toggle/toggle.html.erb

0.0% lines covered

0.0% branches covered

19 relevant lines. 0 lines covered and 19 lines missed.
22 total branches, 0 branches covered and 22 branches missed.
    
  1. <%= tag.div **attrs do %>
  2. then: 0 else: 0 <% if label.present? && position == :left %>
  3. then: 0 else: 0 <p class="text-content <%= size == :small ? "copy-10" : "copy-12" %>">
  4. <%= label %>
  5. </p>
  6. <% end %>
  7. then: 0 else: 0 <label class="lui-toggle <%= "lui-toggle--small" if size == :small %> relative">
  8. <input name="<%=name%>"
  9. then: 0 else: 0 <%= readonly ? "disabled='disabled'" : "" %>
  10. type="checkbox"
  11. value="<%= input_value %>"
  12. data-toggle-target="input"
  13. then: 0 else: 0 data-action="<%= autoload == true ? "toggle#startLoading" : "" %> <%= data_action %>"
  14. data-method="patch"
  15. data-turbo_frame="<%= turbo_frame %>"
  16. then: 0 else: 0 <%= checked? ? "checked" : "" %>>
  17. then: 0 else: 0 <span class="lui-slider" style="<%= 'cursor: not-allowed;' if readonly %>">
  18. <span class="lui-toggle__spinner"
  19. data-toggle-target="spinner">
  20. then: 0 else: 0 <i class="absolute origin-center animate-spin <%= size == :small ? "text-[5px] top-px " : "text-[9px] top-px" %> left-px fa-solid fa-spinner"></i>
  21. </span>
  22. </span>
  23. then: 0 else: 0 <% if readonly %>
  24. <div class="lui-toggle__readonly" ></div>
  25. <% end %>
  26. </label>
  27. then: 0 else: 0 <% if label.present? && position == :right %>
  28. then: 0 else: 0 <p class="text-content <%= size == :small ? "copy-10" : "copy-12" %>">
  29. <%= label %>
  30. </p>
  31. <% end %>
  32. <% end %>

app/components/loopos_ui/token.rb

30.16% lines covered

0.0% branches covered

63 relevant lines. 19 lines covered and 44 lines missed.
26 total branches, 0 branches covered and 26 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class Token < LoopComponent
  4. 1 include LooposUi::FaviconAware
  5. COLORS = {
  6. # text, bg
  7. 1 general: [find_color("general-global-black"), nil],
  8. }
  9. 1 attr_reader :text
  10. 1 attr_accessor :draggable
  11. 1 renders_many :actions
  12. # TODO: migrate to modern option: syntax with dry initialzier
  13. 1 def initialize(text: nil, color: nil, icon: nil, leading_icon: nil, trailing_icon: nil, locked: false,
  14. tooltip: nil, id: nil, disabled: false, kind: :default, key_text: nil, key_value_actions: {}, draggable: false,
  15. ellipsis: nil, close: nil, **system_arguments)
  16. @text = text
  17. @color = color
  18. @tooltip = tooltip
  19. @locked = locked # TODO: Document this
  20. @id = id
  21. @disabled = disabled
  22. @kind = kind
  23. @key_text = key_text
  24. @key_value_actions = key_value_actions || {}
  25. @draggable = draggable
  26. @ellipsis = ellipsis
  27. @close = close
  28. @system_arguments = system_arguments || {}
  29. then: 0 if icon.present?
  30. @leading_icon = icon
  31. else: 0 else
  32. @leading_icon = leading_icon
  33. @trailing_icon = trailing_icon
  34. end
  35. set_color(color)
  36. end
  37. 1 def system_arguments
  38. @system_arguments.slice(:data)
  39. end
  40. 1 def unique_id
  41. then: 0 @unique_id ||= if @id.present?
  42. id
  43. else: 0 else
  44. "lui-token_#{rand(10**10)}"
  45. end
  46. end
  47. 1 def leading_icon
  48. then: 0 else: 0 if @leading_icon.present?
  49. if [
  50. "core",
  51. "submission",
  52. "hubs",
  53. "validation",
  54. "handling",
  55. "manager",
  56. "validation-rails",
  57. "handling-rails",
  58. then: 0 ].include?(@leading_icon)
  59. else: 0 helpers.app_svg(app: @leading_icon.to_sym, width: 12)
  60. then: 0 elsif @leading_icon.start_with?("fa-")
  61. tag.i(class: "lui-token__icon fa-regular #{@leading_icon}")
  62. else
  63. else: 0 # assume it's a img src
  64. tag.img(class: "lui-token__icon-img", src: @leading_icon)
  65. end
  66. end
  67. end
  68. 1 def trailing_icon
  69. then: 0 else: 0 tag.i(class: "lui-token__icon fa-regular #{@trailing_icon}") if @trailing_icon.present?
  70. end
  71. 1 def styles
  72. then: 0 else: 0 cursor_style = @draggable ? "cursor: grab;" : ""
  73. then: 0 else: 0 bg_style = @bg_color.present? ? "background-color: #{@bg_color};" : ""
  74. <<~CSS.squish
  75. color: #{@text_color};
  76. #{bg_style}
  77. #{cursor_style}
  78. CSS
  79. end
  80. 1 def classes
  81. [
  82. then: 0 else: 0 @locked ? "locked" : "",
  83. then: 0 else: 0 @disabled ? "lui-token--disabled" : "",
  84. "lui-entity-token lui-entity-token-general",
  85. ].compact.join(" ")
  86. end
  87. # This is just a heuristic, tested experimentally. Will change if we change the font size.
  88. # TODO: Fix and implement in subclass (should be done in frontend)
  89. 1 def text_too_long?
  90. text.present? && text.to_s.length >= 33
  91. end
  92. 1 def has_tooltip?
  93. @tooltip.present? || text_too_long?
  94. end
  95. 1 def tooltip_text
  96. @tooltip.presence || text
  97. end
  98. 1 private
  99. 1 def set_color(color)
  100. @text_color, @bg_color =
  101. then: 0 if color.present?
  102. then: 0 if COLORS.key?(color.to_sym)
  103. else: 0 COLORS[color.to_sym]
  104. then: 0 elsif color.to_sym == :app
  105. APP_COLORS[LooposUi.config.app_type.to_sym] || COLORS[:general]
  106. else: 0 else
  107. raise ArgumentError, "Invalid color: #{color}, available colors: #{COLORS.keys.join(", ")}, app."
  108. end
  109. else: 0 else
  110. COLORS.excluding(LooposUi.config.app_type.to_sym).values.sample
  111. end
  112. end
  113. end
  114. end

app/components/loopos_ui/token/token.html.erb

0.0% lines covered

0.0% branches covered

43 relevant lines. 0 lines covered and 43 lines missed.
34 total branches, 0 branches covered and 34 branches missed.
    
  1. then: 0 <% if @kind == :key_value %>
  2. then: 0 else: 0 <%= tag.span(id: unique_id, class: "lui-token #{classes}#{@draggable ? ' draggable' : ''}", style: styles, **system_arguments) do %>
  3. then: 0 else: 0 <%= render LooposUi::Tooltip.new(title: tooltip_text) if has_tooltip? %>
  4. <%= tag.span("#{@key_text.to_s}: ", class: "lui-token__key-text") %>
  5. <%= tag.span(text.presence || "-", class: "lui-token__text", **(@key_value_actions || {})) %>
  6. <div class="lui-token__actions">
  7. <% actions.each do |action| %>
  8. <%= action %>
  9. <% end %>
  10. then: 0 else: 0 <% if @ellipsis.present? %>
  11. <%= tag.button(
  12. type: :button,
  13. class: "lui-token__action-icon",
  14. then: 0 else: 0 **(@ellipsis.is_a?(Hash) ? @ellipsis : {})
  15. ) do %>
  16. <%= render LooposUi::MIcon.new(:more_vert, size: 12) %>
  17. <% end %>
  18. <% end %>
  19. then: 0 else: 0 <% if @close.present? %>
  20. <%= tag.button(
  21. type: :button,
  22. class: "lui-token__action-icon",
  23. then: 0 else: 0 **(@close.is_a?(Hash) ? @close : {})
  24. ) do %>
  25. <%= render LooposUi::MIcon.new(:close, size: 12) %>
  26. <% end %>
  27. <% end %>
  28. </div>
  29. <% end %>
  30. else: 0 <% else %>
  31. then: 0 else: 0 <%= tag.span(id: unique_id, class: "lui-token #{classes}#{@draggable ? ' draggable' : ''}", style: styles, **system_arguments) do %>
  32. then: 0 else: 0 <%= render LooposUi::Tooltip.new(title: tooltip_text) if has_tooltip? %>
  33. <%= leading_icon %>
  34. <%= trailing_icon %>
  35. then: 0 <% if @url.present? %>
  36. else: 0 <%= tag.a(text, href: @url, class: "lui-token__text lui-token__text--url", data: { turbo_frame: 'lui-main-layout', turbo_action: :advance }, **@href_options) %>
  37. then: 0 else: 0 <% elsif text.present? %>
  38. <%= tag.span(text, class: "lui-token__text") %>
  39. <% end %>
  40. <div class="lui-token__actions">
  41. <% actions.each do |action| %>
  42. <%= action %>
  43. <% end %>
  44. then: 0 else: 0 <% if @ellipsis.present? %>
  45. <%= tag.button(
  46. type: :button,
  47. class: "lui-token__action-icon",
  48. then: 0 else: 0 **(@ellipsis.is_a?(Hash) ? @ellipsis : {})
  49. ) do %>
  50. <%= render LooposUi::MIcon.new(:more_vert, size: 12) %>
  51. <% end %>
  52. <% end %>
  53. then: 0 else: 0 <% if @close.present? %>
  54. <%= tag.button(
  55. type: :button,
  56. class: "lui-token__action-icon",
  57. then: 0 else: 0 **(@close.is_a?(Hash) ? @close : {})
  58. ) do %>
  59. <%= render LooposUi::MIcon.new(:close, size: 12) %>
  60. <% end %>
  61. <% end %>
  62. </div>
  63. then: 0 else: 0 <% if @draggable %>
  64. <div class="lui-token__drag-handle">
  65. then: 0 else: 0 <%= render LooposUi::Icon.new(icon: "fa-solid fa-grip-dots-vertical", size: "12", color: self.is_a?(LooposUi::EntityToken) ? "#6D6D6D" : "") %>
  66. </div>
  67. <% end %>
  68. <% end %>
  69. <% end %>

app/components/loopos_ui/token_list.rb

65.91% lines covered

0.0% branches covered

44 relevant lines. 29 lines covered and 15 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class TokenList < LoopComponent
  4. 1 EXCESS_TOKENS_LIMIT = 10
  5. 1 renders_many :tokens, types: {
  6. tag: LooposUi::TagToken,
  7. token: LooposUi::Token,
  8. entity: LooposUi::EntityToken,
  9. manual: ->(&block) { capture(&block) },
  10. }
  11. 1 renders_one :association_button
  12. 1 option :leading_icon, optional: true
  13. 1 option :max_tokens, optional: true
  14. 1 option :id, optional: true
  15. 1 option :direction, optional: true # Optional direction
  16. 1 option :path, optional: true
  17. 1 def list
  18. @list ||= List.new(unique_id: unique_id, max_tokens: max_tokens, path: path)
  19. end
  20. 1 class List < LoopComponent
  21. 1 include Turbo::FramesHelper
  22. 1 option :unique_id, Types::String
  23. 1 option :max_tokens, optional: true
  24. 1 option :path, optional: true
  25. 1 renders_many :tokens
  26. 1 def turbo_frame_id
  27. "#{unique_id}_frame"
  28. end
  29. 1 def counter_id
  30. "#{unique_id}_counter"
  31. end
  32. 1 def out_of_bounds?
  33. out_of_bound_token_count.positive?
  34. end
  35. 1 def out_of_bound_token_count
  36. @out_of_bound_token_count ||= tokens.count - (max_tokens || tokens.count)
  37. end
  38. 1 def visible_tokens
  39. tokens.first(max_tokens || tokens.count)
  40. end
  41. 1 def hidden_tokens
  42. tokens.last(out_of_bound_token_count)
  43. end
  44. 1 def tooltip_tokens
  45. hidden_tokens.first(EXCESS_TOKENS_LIMIT)
  46. end
  47. 1 def excess_tooltip_tokens?
  48. hidden_tokens.count > EXCESS_TOKENS_LIMIT
  49. end
  50. end
  51. 1 private
  52. 1 def turbo_frame_id
  53. "#{unique_id}_frame"
  54. end
  55. 1 def unique_id
  56. then: 0 @unique_id ||= if id.present?
  57. id
  58. else: 0 else
  59. "lui-token_list_#{rand(10**10)}"
  60. end
  61. end
  62. 1 def direction_class
  63. then: 0 else: 0 direction == "vertical" ? "lui-token-list--vertical" : "lui-token-list--horizontal"
  64. end
  65. end
  66. end

app/components/loopos_ui/token_list/list.html.erb

0.0% lines covered

0.0% branches covered

19 relevant lines. 0 lines covered and 19 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. <%= turbo_frame_tag turbo_frame_id, class: "lui-token-list__items" do %>
  2. <div data-controller="drag" class="lui-token-list__items">
  3. <% visible_tokens.each do |token| %>
  4. <%= token %>
  5. <% end %>
  6. then: 0 else: 0 <% if out_of_bounds? %>
  7. <span>
  8. <%= render LooposUi::Counter.new(count: out_of_bound_token_count, increment: true) %>
  9. <%= render LooposUi::Tooltip.new(interactive: true) do |tooltip| %>
  10. <div class="flex flex-col gap-1">
  11. <% tooltip_tokens.each do |token| %>
  12. <%= token %>
  13. <% end %>
  14. then: 0 else: 0 <% if excess_tooltip_tokens? %>
  15. <span>...</span>
  16. then: 0 else: 0 <% if path.present? %>
  17. <div class="ml-auto mr-0">
  18. <%= render LooposUi::Button.new(text: "View all", kind: :neutral, size: :tiny, type: :tertiary, href: path) do |button| %>
  19. <% button.with_counter(count: tokens.count) %>
  20. <% end %>
  21. </div>
  22. <% end %>
  23. <% end %>
  24. </div>
  25. <% end %>
  26. </span>
  27. <% end %>
  28. </div>
  29. <% end %>

app/components/loopos_ui/token_list/token_list.html.erb

0.0% lines covered

100.0% branches covered

8 relevant lines. 0 lines covered and 8 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <div id="<%= unique_id %>" class="lui-token-list <%= direction_class %>"
  2. data-controller="token-list"
  3. data-token-list-frame-id-value="<%= turbo_frame_id %>"
  4. data-token-list-model-association-overlay-outlet="#<%=unique_id%> .lui-association-overlay">
  5. <%= render list do |l| %>
  6. <% tokens.each do |token| %>
  7. <% l.with_token do %>
  8. <%= token %>
  9. <% end %>
  10. <% end %>
  11. <% end %>
  12. <%= association_button %>
  13. </div>

app/components/loopos_ui/tooltip.rb

86.67% lines covered

100.0% branches covered

15 relevant lines. 13 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class Tooltip < LoopComponent
  4. 1 VALID_POSITIONS = [
  5. "top",
  6. "top-start",
  7. "top-end",
  8. "right",
  9. "right-start",
  10. "right-end",
  11. "bottom",
  12. "bottom-start",
  13. "bottom-end",
  14. "left",
  15. "left-start",
  16. "left-end",
  17. ]
  18. 1 renders_one :description # Deprecated, use the content slot instead
  19. 1 renders_many :links, ->(name:, href:) {
  20. content_tag(:a, name, href: href)
  21. }
  22. 1 renders_many :buttons, LooposUi::Button
  23. 1 renders_many :tokens, LooposUi::TagToken
  24. 1 option :title, optional: true
  25. # Deprecated, avoid using this. You can just nest the tooltip inside the target component
  26. 1 option :tippy_target_id, optional: true
  27. 1 option :position, default: proc { "top" }, type: Types::Coercible::String.enum(*VALID_POSITIONS)
  28. 1 option :interactive, default: proc { false }, type: Types::Params::Bool
  29. 1 private
  30. 1 def description_or_content
  31. content || description
  32. end
  33. end
  34. end

app/components/loopos_ui/tooltip/tooltip.html.erb

0.0% lines covered

0.0% branches covered

18 relevant lines. 0 lines covered and 18 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <div class="lui-tooltip hidden"
  2. data-controller="tooltips"
  3. data-tooltips-tippy-target-id-value="<%= tippy_target_id %>"
  4. data-tooltips-position-value="<%= position %>"
  5. data-tooltips-interactive-value="<%= interactive %>"
  6. >
  7. then: 0 else: 0 <% if title.present? %>
  8. <div class="lui-tooltip__title">
  9. <%= title %>
  10. </div>
  11. <% end %>
  12. then: 0 else: 0 <% if description_or_content.present? %>
  13. <div class="lui-tooltip__description">
  14. <%= description_or_content %>
  15. </div>
  16. <% end %>
  17. <% links.each do |link| %>
  18. <div class="lui-tooltip__link">
  19. <%= link %>
  20. </div>
  21. <% end %>
  22. <% tokens.each do |token| %>
  23. <%= token %>
  24. <% end %>
  25. <% buttons.each do |button| %>
  26. <%= button %>
  27. <% end %>
  28. </div>

app/components/loopos_ui/top_page_component/breadcrumbs.html.erb

0.0% lines covered

100.0% branches covered

1 relevant lines. 0 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= react_component("Breadcrumbs", { crumbs: breadcrumbs_hash}) %>

app/components/loopos_ui/top_page_component/top_page_actions_component.html.erb

0.0% lines covered

0.0% branches covered

7 relevant lines. 0 lines covered and 7 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. then: 0 else: 0 <%= turbo_stream_from("#{resource.respond_to?(:id) ? resource.id : resource}_broadcast") %>
  2. <div class="loopui-form-layout__header">
  3. then: 0 else: 0 <div id="breadcrumbs_<%= resource.respond_to?(:id) ? resource.id : resource %>">
  4. then: 0 else: 0 <%= render @breadcrumbs_component if @breadcrumbs_hash.present? %>
  5. </div>
  6. <div class="listing__header-right">
  7. <% buttons.each do |button| %>
  8. <div data-controller="modal">
  9. <%= button %>
  10. </div>
  11. <% end %>
  12. </div>
  13. </div>

app/components/loopos_ui/top_page_component/top_page_info_component.html.erb

0.0% lines covered

0.0% branches covered

45 relevant lines. 0 lines covered and 45 lines missed.
24 total branches, 0 branches covered and 24 branches missed.
    
  1. <div class="loopui-form-layout__main-info">
  2. <div class="loopui-form-layout__details">
  3. <div class="loopui-form-layout__details-section">
  4. then: 0 else: 0 <% if @image %>
  5. <%= render LooposUi::Image.new(resource: @resource, editable: @image_edit, image_url: @image_url) %>
  6. <% end %>
  7. <div class="loopui-form-layout__info-section">
  8. then: 0 else: 0 <% if top_tags.present? %>
  9. <div class="loopui-form-layout__line-wrapper">
  10. <% top_tags.each do |element| %>
  11. <%= element %>
  12. <% end %>
  13. </div>
  14. <% end %>
  15. <div class="loopui-form-layout__middle-wrapper--small">
  16. then: 0 else: 0 <% if title.present? %>
  17. <%= title %>
  18. <% end %>
  19. then: 0 else: 0 <% if descriptions.present? %>
  20. <div class="loopui-form-layout__line-wrapper">
  21. <% descriptions.each_with_index do |element, index| %>
  22. then: 0 else: 0 <% if index > 0 %>
  23. <hr class="loopui-form-layout__dot">
  24. <% end %>
  25. <%= element %>
  26. <% end %>
  27. </div>
  28. <% end %>
  29. </div>
  30. then: 0 else: 0 <% if bottom_tags.present? || under_bottom_tags.present? %>
  31. <div class="loopui-form-layout__middle-wrapper">
  32. then: 0 else: 0 <% if bottom_tags.present? %>
  33. <div class="loopui-form-layout__line-wrapper">
  34. <% bottom_tags.each_with_index do |element, index| %>
  35. then: 0 else: 0 <% if index > 0 %>
  36. <hr class="loopui-form-layout__dot">
  37. <% end %>
  38. <%= element %>
  39. <% end %>
  40. </div>
  41. <% end %>
  42. then: 0 else: 0 <% if under_bottom_tags.present? %>
  43. <div class="loopui-form-layout__line-wrapper">
  44. <% under_bottom_tags.each_with_index do |element, index| %>
  45. then: 0 else: 0 <% if index > 0 %>
  46. <hr class="loopui-form-layout__dot">
  47. <% end %>
  48. <%= element %>
  49. <% end %>
  50. </div>
  51. <% end %>
  52. </div>
  53. <% end %>
  54. </div>
  55. then: 0 else: 0 <% if right_side.present? %>
  56. <div class="loopui-form-layout__right">
  57. <%= right_side %>
  58. </div>
  59. <% end %>
  60. </div>
  61. then: 0 else: 0 <% if card.present? %>
  62. <%= card %>
  63. <% end %>
  64. </div>
  65. </div>

app/components/loopos_ui/universal_search_component/universal_search_filters.html.erb

0.0% lines covered

0.0% branches covered

12 relevant lines. 0 lines covered and 12 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <div class="loopui-usearch__advanced-search">
  2. then: 0 else: 0 <% if header.present? %>
  3. <div class="loopui-usearch__advanced-search__header">
  4. <%= header %>
  5. </div>
  6. <% end %>
  7. <div class="loopui-usearch__advanced-search__filters">
  8. <% filters.each_with_index do |element, index| %>
  9. <%= element %>
  10. <% end %>
  11. then: 0 else: 0 <% if date_filter.present? %>
  12. <div>
  13. <%= date_filter %>
  14. <%= react_component("DatetimePicker", { kind: "DateTimePicker", lang: I18n.locale, variant: @variant, isRange: true, granularity: "time", placeholder: I18n.t('global_search.select_date') }) %>
  15. </div>
  16. <% end %>
  17. </div>
  18. </div>

app/components/loopos_ui/universal_search_component/universal_search_filters.rb

87.5% lines covered

100.0% branches covered

8 relevant lines. 7 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module UniversalSearchComponent
  3. 1 class UniversalSearchFilters < ViewComponent::Base
  4. 1 renders_one :header
  5. 1 renders_many :filters
  6. 1 renders_one :date_filter
  7. 1 def initialize(variant:)
  8. @variant = variant
  9. end
  10. end
  11. end
  12. end

app/components/loopos_ui/universal_search_component/universal_search_footer.html.erb

0.0% lines covered

0.0% branches covered

8 relevant lines. 0 lines covered and 8 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <div class="loopui-usearch__footer">
  2. <div class="loopui-usearch__footer--selected-filters">
  3. <span class="loopui-usearch__footer--label"><%= I18n.t('global_search.filters_selected') %> <span id="filters-counter"></span></span>
  4. then: 0 else: 0 <% if selected_filters.present? %>
  5. <%= selected_filters %>
  6. <% end %>
  7. </div>
  8. then: 0 else: 0 <% if clear_button.present? %>
  9. <%= clear_button %>
  10. <% end %>
  11. </div>

app/components/loopos_ui/universal_search_component/universal_search_footer.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module UniversalSearchComponent
  3. 1 class UniversalSearchFooter < ViewComponent::Base
  4. 1 renders_one :selected_filters
  5. 1 renders_one :clear_button
  6. 1 def initialize()
  7. end
  8. end
  9. end
  10. end

app/components/loopos_ui/universal_search_component/universal_search_global_search.html.erb

0.0% lines covered

0.0% branches covered

4 relevant lines. 0 lines covered and 4 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <div class="loopui-usearch__global-search">
  2. then: 0 else: 0 <% if search_box.present? %>
  3. <%= search_box %>
  4. <% end %>
  5. </div>

app/components/loopos_ui/universal_search_component/universal_search_global_search.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module UniversalSearchComponent
  3. 1 class UniversalSearchGlobalSearch < ViewComponent::Base
  4. 1 renders_one :search_box
  5. 1 def initialize()
  6. end
  7. end
  8. end
  9. end

app/components/loopos_ui/universal_search_component/universal_search_results.html.erb

0.0% lines covered

0.0% branches covered

10 relevant lines. 0 lines covered and 10 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. then: 0 else: 0 <div id="results_container" class="loopui-usearch__tabs-results--container<%= @version.present? ? "_#{@version}" : "" %> hidden">
  2. then: 0 else: 0 <div class="loopui-usearch__tabs<%= @version.present? ? "_#{@version}" : "" %>">
  3. <% tabs.each_with_index do |tab, index| %>
  4. <%= tab %>
  5. <% end %>
  6. </div>
  7. <div class="loopui-usearch__results overflow-results">
  8. <% tables.each_with_index do |table, index| %>
  9. <%= table %>
  10. <% end %>
  11. </div>
  12. </div>
  13. <div id="global-search-no-search" class="flex items-center justify-center min-h-[200px]">
  14. <p><%= I18n.t('global_search.fill_search_fields') %></p>
  15. </div>
  16. <div id="empty-results-all-tables" class="hidden">
  17. <div class="flex flex-1 items-center justify-center absolute w-full" style="top: 200px">
  18. <p><%= I18n.t('global_search.no_results') %></p>
  19. </div>
  20. </div>
  21. <div id="loading_spinner" role="status" class="hidden absolute -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2">
  22. <svg aria-hidden="true" class="w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/><path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/></svg>
  23. <span class="sr-only">Loading...</span>
  24. </div>

app/components/loopos_ui/universal_search_component/universal_search_results.rb

88.89% lines covered

100.0% branches covered

9 relevant lines. 8 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module UniversalSearchComponent
  3. 1 class UniversalSearchResults < ViewComponent::Base
  4. 1 renders_many :tabs
  5. 1 renders_many :tables
  6. 1 def initialize(version: "")
  7. @version = version
  8. end
  9. # Backward compatibility with ViewComponent v2
  10. # Remove after updating universal_search usages of this component
  11. # app/views/loopos_universal_search/_algolia_search.html.erb
  12. 1 alias_method :tab, :with_tab
  13. 1 alias_method :table, :with_table
  14. end
  15. end
  16. end

app/components/loopos_ui/user_menu.rb

50.0% lines covered

0.0% branches covered

34 relevant lines. 17 lines covered and 17 lines missed.
5 total branches, 0 branches covered and 5 branches missed.
    
  1. 1 module LooposUi
  2. 1 class UserMenu < LoopComponent
  3. 1 option :context, Types::Symbol.enum(:sidebar, :user_menu)
  4. 1 mod :context, condition: -> { "in_#{context}" }
  5. 1 option :collapsed, Types::Bool, default: -> { false }
  6. 1 mod :collapsed
  7. 1 TUrl = (Types::String | Types::Symbol.enum(:default)).constructor do |v|
  8. valid = begin
  9. uri = URI.parse(v)
  10. uri.is_a?(URI::HTTP) && uri.host.present?
  11. rescue URI::InvalidURIError
  12. false
  13. end
  14. then: 0 else: 0 valid ? v : :default
  15. end
  16. 1 option :name, Types::String
  17. 1 option :user_image_url, TUrl
  18. 1 option :partner_name, Types::String
  19. 1 option :partner_image_url, TUrl
  20. 1 class << self
  21. 1 def user_menu(*args, **kwargs, &block)
  22. new(*args, **kwargs, context: :user_menu, &block)
  23. end
  24. 1 def sidebar(*args, **kwargs, &block)
  25. new(*args, **kwargs, context: :sidebar, &block)
  26. end
  27. end
  28. 1 def initialize(*args, **kwargs)
  29. # TODO: Maybe check if the user tried to pass a name or something
  30. # for now, it will be loaded from the sidebar singleton
  31. config = ::LooposUi.config.sidebar
  32. config_kwargs = {
  33. name: config.foo["user"]["full_name"],
  34. user_image_url: config.foo["user"]["avatar_url"],
  35. partner_name: config.foo["user"]["partner"]["name"],
  36. partner_image_url: config.foo["user"]["partner"]["icon_url"],
  37. }
  38. kwargs.merge!(config_kwargs)
  39. super(*args, **kwargs)
  40. end
  41. 1 def groups
  42. ::LooposUi.config.sidebar.foo["navigation"]["groups"].map do |group|
  43. group.with_indifferent_access
  44. end
  45. end
  46. 1 def logout_path
  47. path = ::LooposUi.config.sidebar.logout_path
  48. case path
  49. when: 0 when String
  50. path
  51. when: 0 when Proc
  52. instance_exec(&path)
  53. else: 0 else
  54. raise "Invalid logout_path: #{path.inspect}"
  55. end
  56. end
  57. end
  58. end

app/components/loopos_ui/user_menu/user_menu.html.erb

0.0% lines covered

0.0% branches covered

44 relevant lines. 0 lines covered and 44 lines missed.
8 total branches, 0 branches covered and 8 branches missed.
    
  1. then: 0 <% if context == :sidebar %>
  2. <%= render LooposUi::Popover.new(anchor: :bottom_left, position: :bottom_right) do |pop| %>
  3. <% pop.with_custom_toggle do %>
  4. <div class="<%= classes %>">
  5. <%= tag.div(class: "lui-user_menu__avatar") do %>
  6. <%= render LooposUi::GroupAvatar.new(main_avatar: user_image_url, secondary_avatar: partner_image_url) %>
  7. <% end %>
  8. <%= tag.div(class: "lui-user_menu__names") do %>
  9. <%= tag.div(class: "lui-user_menu__name") do %>
  10. <%= name %>
  11. <% end %>
  12. <%= tag.div(class: "lui-user_menu__partner") do %>
  13. <%= partner_name %>
  14. <% end %>
  15. <% end %>
  16. </div>
  17. <% end%>
  18. <% pop.with_target do %>
  19. <div class="lui-user_menu__dropdown">
  20. <%= render LooposUi::UserMenu.user_menu %>
  21. </div>
  22. <% end%>
  23. <% end %>
  24. <% else %>
  25. else: 0 <%# move these to css file %>
  26. <div class="flex flex-col items-stretch min-w-[250px] rounded-lg border border-(--color-border-primary) bg-(--color-background)">
  27. <div class="flex flex-col p-4 gap-4 items-stretch">
  28. <div class="<%= classes %>">
  29. <%= tag.div(class: "lui-user_menu__avatar") do %>
  30. <%= render LooposUi::Avatar.user(image_url: user_image_url, size: :xl) %>
  31. <% end %>
  32. <%= tag.div(class: "lui-user_menu__names") do %>
  33. <%= tag.div(class: "lui-user_menu__name") do %>
  34. <%= name %>
  35. <% end %>
  36. <%= tag.div(class: "lui-user_menu__partner") do %>
  37. <%= render LooposUi::Avatar.partner(image_url: partner_image_url, size: :xs) %>
  38. <%= partner_name %>
  39. <% end %>
  40. <% end %>
  41. </div>
  42. <div class="flex flex-col gap-1">
  43. <%# raise groups.inspect%>
  44. <% groups.each do |group| %>
  45. <div class="flex flex-col gap-2">
  46. <div class="group flex flex-col">
  47. <%= tag.span(group[:title], class: "copy-12-medium text-(--color-content-secondary)") %>
  48. <% group[:items].each do |item| %>
  49. then: 0 <% if item[:disabled] %>
  50. <span class="lui-user_menu__nav_link">
  51. <%= tag.span(class: "text-(--color-content-secondary) cursor-not-allowed") do %>
  52. <%= item[:title] %>
  53. then: 0 else: 0 <%= render LooposUi::Tooltip.new(title: item[:tooltip]) if item[:tooltip] %>
  54. <% end %>
  55. </span>
  56. else: 0 <% else %>
  57. <%= link_to item[:path], class: "lui-user_menu__nav_link", data: { turbo_frame: "lui-main-layout", turbo_action: "advance" } do %>
  58. <%= tag.span do %>
  59. <%= item[:title] %>
  60. then: 0 else: 0 <%= render LooposUi::Tooltip.new(title: item[:tooltip]) if item[:tooltip] %>
  61. <% end %>
  62. <% end %>
  63. <% end %>
  64. <% end %>
  65. </div>
  66. </div>
  67. <% end %>
  68. </div>
  69. </div>
  70. <footer class="flex px-4 py-2 bg-(--color-secondary) justify-between items-center">
  71. <div class="flex flex-col gap-[2px]">
  72. <span class="copy-12-medium text-(--color-content-secondary)">
  73. Powered by ...
  74. </span>
  75. <span class="copy-12-medium text-(--color-content-secondary)">
  76. Version 1.0.0
  77. </span>
  78. </div>
  79. <%= render LooposUi::Button.new(
  80. text: "Log out",
  81. type: :tertiary,
  82. size: :small,
  83. kind: :danger,
  84. href: logout_path,
  85. tag_options: { data: { turbo_method: :delete } }
  86. ) %>
  87. </footer>
  88. </div>
  89. <% end %>

app/components/loopos_ui/v2/app_layout.rb

74.07% lines covered

0.0% branches covered

27 relevant lines. 20 lines covered and 7 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. 1 module LooposUi
  2. 1 module V2
  3. 1 class AppLayout < LoopComponent
  4. 1 include Turbo::FramesHelper
  5. 1 include Turbo::StreamsHelper
  6. 1 include LooposUi::StreamToasters
  7. 1 option :stream_namespaces,
  8. default: -> { [] },
  9. 1 type: (Types::Coercible::Symbol | Types::Array.of(Types::Coercible::Symbol)).constructor { |value|
  10. then: 0 else: 0 value.is_a?(Symbol) ? [value] : value
  11. }
  12. 1 renders_one :navbar
  13. 1 renders_one :sidebar, ->(**kwargs) { LooposUi::Sidebar.new(**kwargs) }
  14. 1 renders_one :bottom_bar
  15. 1 renders_one :action_panel
  16. 1 renders_one :action_bar
  17. 1 renders_one :select_bar
  18. 1 def before_render
  19. else: 0 then: 0 with_sidebar unless sidebar? || @without_sidebar
  20. end
  21. 1 def without_sidebar
  22. @without_sidebar = true
  23. end
  24. # With default stream_namespaces = [], this is basically delegating to the controller's toasters_stream_target_key
  25. # Avoid passing in custom stream_namespaces, use the stream_toaster_namespaces DSL to configure
  26. # the stream keys if needed
  27. 1 def toasters_stream_target_key
  28. toasters_stream_target(keys: [*stream_namespaces, *controller_toasters_stream_target_key])
  29. end
  30. 1 private
  31. 1 def controller_toasters_stream_target_key
  32. controller.try(:lui_toaster_stream_namespaces) || []
  33. rescue ControllerCalledBeforeRenderError
  34. # Maybe this is called in some tests? Anyway, we can't do much here, so just return an empty array.
  35. []
  36. end
  37. 1 class RootStyles < LoopComponent
  38. end
  39. end
  40. end
  41. end

app/components/loopos_ui/v2/app_layout/app_layout.html.erb

0.0% lines covered

0.0% branches covered

29 relevant lines. 0 lines covered and 29 lines missed.
10 total branches, 0 branches covered and 10 branches missed.
    
  1. <turbo-frame id="lui-app-layout" data-turbo-action="advance" class="lui-v2-app-layout" data-controller="miniapps lui--layout">
  2. <%= render LooposUi::V2::AppLayout::RootStyles.new %>
  3. <div class="w-full lui-header-slot">
  4. <%= navbar %>
  5. </div>
  6. <div class="lui-v2-app-layout__content">
  7. <%= sidebar %>
  8. <div class="lui-v2-app-layout__content-container lui-app-layout_id">
  9. <turbo-frame
  10. data-turbo-action="advance"
  11. data-turbo-frame="lui-main-layout"
  12. id="lui-main-layout"
  13. class="lui-v2-app-layout__content-inner"
  14. style="width: 100%; transition: width 25ms ease-in;"
  15. >
  16. <%= select_bar %>
  17. <div id="lui-action-bar">
  18. then: 0 else: 0 <%= action_bar if action_bar.present? && action_bar.to_s.strip.present? %>
  19. </div>
  20. <div class="lui-action_bar lui-action_bar--loading">
  21. <div class="lui-action_bar__left w-1/4">
  22. <div class="lui-skeleton__bar"></div>
  23. </div>
  24. <div class="lui-action_bar__right w-1/4">
  25. <div class="lui-skeleton__bar"></div>
  26. </div>
  27. </div>
  28. <div class="flex-1 flex min-h-0 overflow-hidden">
  29. <%
  30. # I want to cal active_item but internaly it calls helpers (path etc) but I dont want to render the component just for that
  31. # So we pass in the current view context manually
  32. then: 0 else: 0 if LooposUi::Sidebar::USE_UI_2
  33. sd = LooposUi::Sidebar::V2::Sidebar.new
  34. sd.instance_variable_set(:@view_context, view_context)
  35. end
  36. %>
  37. then: 0 else: 0 <%= helpers.turbo_stream.update("lui-sidebar-active-item-id", sd.active_item) if LooposUi::Sidebar::USE_UI_2 %>
  38. <%= render LooposUi::LayoutLoading.new %>
  39. <%= tag.div(
  40. class: "lui-main-layout__container",
  41. data: {
  42. skeleton_loading: LooposUi.config.enable_loading_skeletons
  43. }
  44. ) do %>
  45. <%= tag.div class: "lui-main-layout__content", id: "lui-main-layout-content" do %>
  46. <%= content %>
  47. <% end %>
  48. then: 0 else: 0 <%= tag.div class: "lui-main-layout__action-panel", data: { "lui--layout-target": "actionPanel" }, id: "lui-action-panel" do %>
  49. <%= tag.div class: "lui-action-panel-resizer",
  50. data: {
  51. action: "pointerdown->lui--layout#handleDragStart"
  52. } %>
  53. <%= action_panel %>
  54. <% end if action_panel.present? && action_panel.to_s.strip.present? %>
  55. <% end %>
  56. </div>
  57. then: 0 else: 0 <%= tag.div class: "lui-v2-app-layout__bottom-bar" do %>
  58. <%= bottom_bar %>
  59. <% end if bottom_bar.present? %>
  60. </turbo-frame>
  61. </div>
  62. </div>
  63. <%= turbo_stream_from "lui-app-layout" %>
  64. <%= turbo_stream_from toasters_stream_target_key %>
  65. <%= tag.div(
  66. id: "lui-toasters",
  67. popover: "manual",
  68. data: {
  69. controller: "toasters",
  70. toasters_new_toaster_url_value: LooposUi::Engine.routes.url_helpers.toasters_path
  71. }
  72. ) %>
  73. </turbo-frame>

app/components/loopos_ui/v2/app_layout/root_styles.html.erb

0.0% lines covered

100.0% branches covered

10 relevant lines. 0 lines covered and 10 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <style>
  2. :root {
  3. --app-900-hover: <%= LooposUi::Colors.find("apps-900-hover") %>;
  4. --app-800-primary: <%= LooposUi::Colors.find("apps-800-primary") %>;
  5. --app-400: <%= LooposUi::Colors.find("apps-400") %>;
  6. --app-300: <%= LooposUi::Colors.find("apps-300") %>;
  7. --app-200: <%= LooposUi::Colors.find("apps-200") %>;
  8. --app-100: <%= LooposUi::Colors.find("apps-100") %>;
  9. --app-opacity5: <%= LooposUi::Colors.find("apps-opacity5") %>;
  10. /* Current app variables, for semantic colors */
  11. --current-surface-app: var(--color-surface-apps-<%= LooposUi.config.app_type %>);
  12. --current-text-app: var(--color-text-apps-<%= LooposUi.config.app_type %>);
  13. }
  14. </style>

app/components/loopos_ui/v2/card.rb

75.0% lines covered

0.0% branches covered

40 relevant lines. 30 lines covered and 10 lines missed.
12 total branches, 0 branches covered and 12 branches missed.
    
  1. 1 module LooposUi
  2. 1 module V2
  3. 1 class Card < LoopComponent
  4. 1 option :title, Types::String, optional: true
  5. 1 option :description, Types::String, optional: true
  6. 1 option :logo, Types::String, optional: true
  7. # Modifiers
  8. 1 option :draft, Types::Bool, optional: true, default: -> { false }
  9. 1 mod :draft
  10. 1 option :full, Types::Bool, optional: true, default: -> { false }
  11. 1 mod :full
  12. 1 option :borderless, Types::Bool, optional: true, default: -> { false }
  13. 1 mod :borderless
  14. 1 option :footer_button_args, Types::Hash, optional: true, default: -> { {} }
  15. 1 renders_one :title_description, ->(*args, **kwargs) { LooposUi::TitleDescription.new(*args, **kwargs, size: :small) }
  16. 1 renders_one :custom_description
  17. 1 renders_one :footer
  18. 1 renders_one :corner
  19. 1 renders_one :logo_img
  20. # Include this module in your card to make it lazy loadable
  21. # it will add src and id options. Specifying src makes the card lazy loadable.
  22. 1 module LazyLoading
  23. 1 extend ActiveSupport::Concern
  24. 1 included do
  25. 8 option :id, Types::Coercible::String, optional: true
  26. 8 option :src, Types::String, optional: true
  27. end
  28. 1 def lazy_loading_options
  29. { id: @id, src: @src }
  30. end
  31. 1 def turbo_options
  32. lazy_loading_options.slice(:src)
  33. end
  34. 1 def initialize(...)
  35. super(...)
  36. then: 0 else: 0 if turbo_options[:lazy] && (turbo_options[:src].blank? || @id.blank?)
  37. raise ArgumentError, "You must provide a `src` and an `id` if you want to use lazy loading"
  38. end
  39. end
  40. end
  41. 1 include LazyLoading
  42. 1 def before_render
  43. else: 0 then: 0 with_title_description(title: title, description: description) do |td|
  44. then: 0 else: 0 td.with_custom_description_content(custom_description) if custom_description
  45. end unless title_description || empty_title_description_slot?
  46. end
  47. 1 def empty_title_description_slot?
  48. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 title.blank? && title_description&.title.blank? && title_description&.description.blank? && title_description&.custom_description.blank?
  49. end
  50. 1 def footer_button_args
  51. {
  52. size: :small, type: :secondary,
  53. }
  54. end
  55. end
  56. end
  57. end

app/components/loopos_ui/v2/card/app_instance.html.erb

0.0% lines covered

100.0% branches covered

20 relevant lines. 0 lines covered and 20 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <%= render LooposUi::V2::Card.new(title: app_instance.title, **lazy_loading_options) do |card| %>
  2. <% card.with_custom_description do %>
  3. <%= render LooposUi::Entities::Partnable.new(partnable: app_instance.partner) %>
  4. <% end %>
  5. <div class="lui-app_card-content">
  6. <div class="lui-app_card-content-info">
  7. <span class="lui-app_card-content-info__title">
  8. <%= LooposUi::V2::Card.t(".app_instance.release") %>
  9. </span>
  10. <span class="lui-app_card-content-info__text">
  11. <%= app_instance.release %>
  12. </span>
  13. </div>
  14. <div class="lui-app_card-content-info">
  15. <span class="lui-app_card-content-info__title">
  16. <%= LooposUi::V2::Card.t(".app_instance.id") %>
  17. </span>
  18. <span class="lui-app_card-content-info__text">
  19. <%= app_instance.id %>
  20. </span>
  21. </div>
  22. </div>
  23. <% card.with_logo_img do %>
  24. <div style="width: 108px; height: 108px; overflow: hidden; position: relative;">
  25. <div style="position: absolute; bottom: -20px; right: -20px;">
  26. <%= helpers.app_svg(app: app_instance.app, size: :huge) %>
  27. </div>
  28. </div>
  29. <% end %>
  30. <% card.with_corner do %>
  31. <%= render LooposUi::StateLabel.new(text: app_instance.status, icon: tag_status.dig(:icon), color: tag_status.dig(:kind)) %>
  32. <% end %>
  33. <% card.with_footer do %>
  34. <%= render LooposUi::ActionButtons.new do |ab| %>
  35. <% ab.with_button_group do |bg| %>
  36. <% bg.with_button(
  37. text: LooposUi::V2::Card.t(".app_instance.details"),
  38. tooltip_text: LooposUi::V2::Card.t(".app_instance.details_tooltip"),
  39. icon: "fa-sharp fa-regular fa-arrow-up-right-from-square",
  40. href: details_href,
  41. **(card.footer_button_args.merge(footer_button_args.presence || {}))
  42. ) %>
  43. <% bg.with_button(
  44. text: LooposUi::V2::Card.t(".app_instance.open"),
  45. tooltip_text: LooposUi::V2::Card.t(".app_instance.open_tooltip", app: app_instance.app.capitalize),
  46. icon: "fa-sharp fa-regular fa-arrow-up-right-from-square",
  47. href: open_href,
  48. **(card.footer_button_args.merge(footer_button_args.presence || {}).merge({ tag_options: { target: :_blank } }))) %>
  49. <% end %>
  50. <% end %>
  51. <% end %>
  52. <% end %>

app/components/loopos_ui/v2/card/app_instance.rb

76.0% lines covered

0.0% branches covered

25 relevant lines. 19 lines covered and 6 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. 1 module LooposUi
  2. 1 module V2
  3. 1 class Card
  4. 1 class AppInstance < LoopComponent
  5. 1 include LooposUi::V2::Card::LazyLoading
  6. 1 class AppInstance < Dry::Struct
  7. 1 attribute :object, Types.Instance(Object)
  8. 1 attribute :title, Types::String
  9. 1 attribute :partner, Types::Any
  10. 1 attribute :app, Types::String
  11. 1 attribute :status, Types::String
  12. 1 attribute :release, Types::String
  13. 1 attribute :id, Types::String
  14. 1 def self.build(app_instance)
  15. new(
  16. object: app_instance,
  17. title: app_instance.name,
  18. partner: app_instance.partner,
  19. app: app_instance.app,
  20. status: app_instance.status,
  21. release: app_instance.app_release,
  22. id: app_instance.id,
  23. )
  24. end
  25. end
  26. 1 option :app_instance, Types.Instance(AppInstance).constructor(AppInstance.method(:build))
  27. 1 option :open_href, Types::String
  28. 1 option :details_href, Types::String
  29. 1 option :footer_button_args, Types::Hash, optional: true, default: -> { {} }
  30. 1 def tag_status
  31. case app_instance.status
  32. when: 0 when "creating", "updating", "deleting", "stopped"
  33. {
  34. kind: "notice",
  35. icon: "fa-regular fa-circle-exclamation",
  36. }
  37. when: 0 when "created", "to_create", "running"
  38. {
  39. kind: "success",
  40. icon: "fa-regular fa-check-circle",
  41. }
  42. when: 0 when "error", "error_creating", "error_running", "error_updating", "error_deleting", "to_delete", "deleted", "partially_deleted"
  43. {
  44. kind: "danger",
  45. icon: "fa-regular fa-circle-xmark",
  46. }
  47. else: 0 else
  48. {
  49. kind: "informative",
  50. icon: "fa-regular fa-circle",
  51. }
  52. end
  53. end
  54. end
  55. end
  56. end
  57. end

app/components/loopos_ui/v2/card/card.html.erb

0.0% lines covered

0.0% branches covered

17 relevant lines. 0 lines covered and 17 lines missed.
14 total branches, 0 branches covered and 14 branches missed.
    
  1. <%= tag.turbo_frame id: (id || "card-#{rand(10**10).to_s(16)}"), class: classes, **turbo_options do %>
  2. then: 0 else: 0 <%= tag.div(class: "lui-card_v2-top") do %>
  3. then: 0 else: 0 <%= title_description if title_description.present? %>
  4. then: 0 else: 0 <%= tag.div(class: "lui-card_v2-corner") do %>
  5. <%= corner %>
  6. <% end if corner.present? %>
  7. <% end if title_description.present? || corner.present? %>
  8. <%# For some reason. content? returns true even tho it appears to be empty %>
  9. then: 0 else: 0 <%= tag.div(content, class: "w-full") if content? && content.present? %>
  10. then: 0 else: 0 <% if footer.present? %>
  11. <div class="w-full">
  12. <%= footer %>
  13. </div>
  14. <%end%>
  15. then: 0 <% if logo_img.present? %>
  16. <div class="lui-card_v2-img">
  17. <%= logo_img %>
  18. else: 0 </div>
  19. then: 0 else: 0 <%elsif logo.present? %>
  20. <div class="lui-card_v2-img">
  21. <>
  22. <img src=<%=logo%> alt="Logo" />
  23. </div>
  24. </div>
  25. <% end %>
  26. <% end %>

app/components/loopos_ui/v2/card/dashboard.html.erb

0.0% lines covered

0.0% branches covered

5 relevant lines. 0 lines covered and 5 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <%= render LooposUi::V2::Card.new(full: true, **lazy_loading_options) do |card| %>
  2. <% card.with_title_description(title: dashboard_info.title, description: dashboard_info.description, tooltip: dashboard_info.tooltip, size: "small") %>
  3. then: 0 else: 0 <% card.with_corner do %>
  4. <%= select_form %>
  5. <% end if select_form.present? %>
  6. then: 0 else: 0 <%= content if content? %>
  7. <% end %>

app/components/loopos_ui/v2/card/dashboard.rb

92.86% lines covered

100.0% branches covered

14 relevant lines. 13 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module V2
  3. 1 class Card
  4. 1 class Dashboard < LoopComponent
  5. 1 include LooposUi::V2::Card::LazyLoading
  6. 1 class Dashboard < Dry::Struct
  7. 1 attribute :object, Types.Instance(Object)
  8. 1 attribute :title, Types::String
  9. 1 attribute :description, Types::String
  10. 1 attribute :tooltip, Types::String
  11. 1 def self.build(dashboard_info)
  12. new(
  13. object: dashboard_info,
  14. title: dashboard_info.title,
  15. description: dashboard_info.description,
  16. tooltip: dashboard_info.tooltip,
  17. )
  18. end
  19. end
  20. 1 option :dashboard_info, Types.Instance(Dashboard).constructor(Dashboard.method(:build))
  21. 1 renders_one :select_form
  22. end
  23. end
  24. end
  25. end

app/components/loopos_ui/v2/card/draft.html.erb

0.0% lines covered

0.0% branches covered

4 relevant lines. 0 lines covered and 4 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%= render LooposUi::V2::Card.new(draft: true, **lazy_loading_options) do |card| %>
  2. <% card.with_title_description(title: draft_info.title, description: draft_info.description, size: "small") %>
  3. then: 0 else: 0 <%= content if content? %>
  4. <% card.with_corner_content(corner) %>
  5. <% end %>

app/components/loopos_ui/v2/card/draft.rb

92.31% lines covered

100.0% branches covered

13 relevant lines. 12 lines covered and 1 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module V2
  3. 1 class Card
  4. 1 class Draft < LoopComponent
  5. 1 include LooposUi::V2::Card::LazyLoading
  6. 1 class Draft < Dry::Struct
  7. 1 attribute :object, Types.Instance(Object)
  8. 1 attribute :title, Types::String
  9. 1 attribute :description, Types::String
  10. 1 def self.build(draft_info)
  11. new(
  12. object: draft_info,
  13. title: draft_info.title,
  14. description: draft_info.description,
  15. )
  16. end
  17. end
  18. 1 option :draft_info, Types.Instance(Draft).constructor(Draft.method(:build))
  19. 1 renders_one :corner
  20. end
  21. end
  22. end
  23. end

app/components/loopos_ui/v2/card/favorites.html.erb

0.0% lines covered

0.0% branches covered

11 relevant lines. 0 lines covered and 11 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <div data-controller="collapsiblev2">
  2. <%= render LooposUi::V2::Card.new(full: true, **lazy_loading_options) do |card| %>
  3. <% card.with_title_description(title: "Favorites", icon: "fa-solid fa-star", size: "small") %>
  4. <% card.with_footer do %>
  5. <div class="w-full flex justify-end">
  6. <%= render LooposUi::Button.new(
  7. text: "View more",
  8. size: :tiny,
  9. type: :secondary,
  10. trailing_icon: "fa-regular fa-chevron-down",
  11. tag_options: { data: {"collapsiblev2-target": "button", "action": "click->collapsiblev2#toggle"}}
  12. ) %>
  13. </div>
  14. <% end %>
  15. then: 0 else: 0 <% if content.present? %>
  16. <div data-collapsiblev2-target="container">
  17. <%= content%>
  18. </div>
  19. <% end %>
  20. <% end %>
  21. </div>

app/components/loopos_ui/v2/card/favorites.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module V2
  3. 1 class Card
  4. 1 class Favorites < LoopComponent
  5. 1 include LooposUi::V2::Card::LazyLoading
  6. end
  7. end
  8. end
  9. end

app/components/loopos_ui/v2/card/financial_log.html.erb

0.0% lines covered

0.0% branches covered

16 relevant lines. 0 lines covered and 16 lines missed.
12 total branches, 0 branches covered and 12 branches missed.
    
  1. <%= render LooposUi::V2::Card.new(draft: draft, **lazy_loading_options) do |card| %>
  2. <% card.with_title_description(title: financial_info.title, description: financial_info.description, count: financial_info.count) do |title_description| %>
  3. then: 0 else: 0 <% title_description.with_info do %>
  4. <%= title_info %>
  5. <% end if title_info.present? %>
  6. <% end %>
  7. <% card.with_corner do %>
  8. <div class="flex items-center gap-2">
  9. then: 0 else: 0 <%= safe_join(corner_actions) if corner_actions.present? %>
  10. <%= modal %>
  11. </div>
  12. <% end %>
  13. <div>
  14. <div class="lui-financial_card-content-info">
  15. <span class="lui-financial_card-content-info__amount">
  16. then: 0 <% if financial_info.amount.nil? || financial_info.amount.zero? %>
  17. -
  18. else: 0 <% else %>
  19. <%= financial_info.amount.format(symbol_position: :after) %>
  20. <% end %>
  21. </span>
  22. then: 0 else: 0 <%= render LooposUi::StateLabel.new(text: financial_info.status.capitalize, color: tag_status.dig(:kind)) if financial_info.status.present? %>
  23. then: 0 else: 0 <%= render LooposUi::EntityToken.new(text: "FTI") if fti? %>
  24. </div>
  25. <span class="lui-financial_card-content-info__timestamp">
  26. then: 0 else: 0 <%= render LooposUi::DateShow.new(date: financial_info.timestamp, format: :short) if financial_info.timestamp.present? %>
  27. </span>
  28. </div>
  29. <% end %>

app/components/loopos_ui/v2/card/financial_log.rb

65.79% lines covered

0.0% branches covered

38 relevant lines. 25 lines covered and 13 lines missed.
9 total branches, 0 branches covered and 9 branches missed.
    
  1. 1 module LooposUi
  2. 1 module V2
  3. 1 class Card
  4. 1 class FinancialLog < LoopComponent
  5. 1 include LooposUi::V2::Card::LazyLoading
  6. 1 class FinancialLog < Dry::Struct
  7. 1 attribute :header_info, Types.Instance(Object)
  8. 1 attribute :item_value, Types.Instance(Object)
  9. 1 attribute :title, Types::String
  10. 1 attribute :description, Types::String
  11. 1 attribute :count, Types::String.optional
  12. 1 attribute :amount, Types.Instance(Money).optional
  13. 1 attribute :status, Types::String.optional
  14. 1 attribute :timestamp, Types::Date.optional
  15. 1 def self.build(header_info, item_value)
  16. new(
  17. header_info: header_info,
  18. item_value: item_value,
  19. title: header_info.title,
  20. description: header_info.description,
  21. then: 0 else: 0 count: header_info.try(:count)&.to_s,
  22. amount: item_value.amount || item_value.amount_cents.to_d / 100,
  23. status: item_value.try(:status),
  24. then: 0 else: 0 timestamp: item_value.try(:updated_at)&.to_date,
  25. )
  26. end
  27. end
  28. 1 option :header_info, Types.Instance(Object)
  29. 1 option :item_value, Types.Instance(Object)
  30. 1 option :fti, Types::Bool, default: -> { false }
  31. 1 alias_method :fti?, :fti
  32. 1 option :draft, Types::Bool, default: -> { false }
  33. 1 renders_one :title_info
  34. 1 renders_many :corner_actions, ->(&block) { capture(&block) }
  35. 1 renders_one :modal, ->(trigger_args: {}, show_footer: false, **args) {
  36. modal = Modal.new(show_footer: show_footer, **args)
  37. modal.with_header(title: financial_info.title, description: financial_info.description) do |header|
  38. then: 0 else: 0 header.with_info do
  39. title_info.to_s
  40. end if title_info.present?
  41. end
  42. # Default trigger button
  43. modal.with_trigger_button(
  44. icon: "fa-regular fa-arrow-up-right-and-arrow-down-left-from-center",
  45. tooltip_text: I18n.t("admin.items.financial_data.manual_item_value.open_modal"),
  46. size: :small,
  47. kind: :neutral,
  48. type: :secondary,
  49. **trigger_args,
  50. )
  51. modal
  52. }
  53. 1 def financial_info
  54. @financial_info ||= FinancialLog.build(header_info, item_value)
  55. end
  56. 1 def tag_status
  57. status = financial_info.status.to_s.downcase
  58. case status
  59. when: 0 when "agreed", "transacted"
  60. {
  61. kind: "success",
  62. }
  63. when: 0 when "waiting_agreement", "draft"
  64. {
  65. kind: "warning",
  66. }
  67. else: 0 else
  68. {
  69. kind: "neutral",
  70. }
  71. end
  72. end
  73. end
  74. end
  75. end
  76. end

app/components/loopos_ui/v2/card/financial_transaction.html.erb

0.0% lines covered

0.0% branches covered

21 relevant lines. 0 lines covered and 21 lines missed.
12 total branches, 0 branches covered and 12 branches missed.
    
  1. <%
  2. transaction = financial_transaction.transaction
  3. %>
  4. <%= render LooposUi::V2::Card.new(
  5. then: 0 else: 0 title: financial_transaction.type == :incoming ? I18n.t("admin.payments.financial_transaction.incoming_title") : I18n.t("admin.payments.financial_transaction.outgoing_title"),
  6. **lazy_loading_options
  7. ) do |card| %>
  8. <% card.with_corner do %>
  9. then: 0 else: 0 <% if transaction.provider.present? %>
  10. <%= render LooposUi::Entities::Provider.new(provider: financial_transaction.transaction.provider, tooltip: I18n.t("admin.payments.financial_transaction.provider_tooltip")) %>
  11. <% end %>
  12. <% end %>
  13. <% card.with_custom_description do %>
  14. then: 0 <% if transaction.show_url.present? %>
  15. <% link_to transaction.show_url, target: :_blank do %>
  16. <%= transaction.id %>
  17. <% end %>
  18. else: 0 <% else %>
  19. <%= transaction.id %>
  20. <% end %>
  21. <% end %>
  22. <div>
  23. <div class="lui-financial_card-content-info">
  24. <span class="lui-financial_card-content-info__amount">
  25. <%= transaction.amount %>
  26. </span>
  27. then: 0 else: 0 <%= render LooposUi::StateLabel.new(text: transaction.status.capitalize, color: tag_status.dig(:kind)) if transaction.status.present? %>
  28. </div>
  29. <span class="lui-financial_card-content-info__timestamp">
  30. then: 0 else: 0 <%= render LooposUi::DateShow.new(date: transaction.paid_date, format: :long) if transaction.paid_date.present? %>
  31. </span>
  32. </div>
  33. then: 0 else: 0 <% card.with_footer do %>
  34. <% financial_transaction.barcodes_html.each do |barcode_value, barcode_html| %>
  35. <div class="lui-financial_transaction-content-barcode">
  36. <%= raw barcode_html %>
  37. </div>
  38. <div class="lui-financial_transaction-content-barcode-value">
  39. <%= barcode_value %>
  40. </div>
  41. <% end %>
  42. <% end if financial_transaction.barcodes_html.present? %>
  43. <% end %>

app/components/loopos_ui/v2/card/financial_transaction.rb

37.21% lines covered

0.0% branches covered

43 relevant lines. 16 lines covered and 27 lines missed.
23 total branches, 0 branches covered and 23 branches missed.
    
  1. 1 module LooposUi
  2. 1 module V2
  3. 1 class Card
  4. 1 class FinancialTransaction < LoopComponent
  5. 1 include LooposUi::V2::Card::LazyLoading
  6. 1 class FinancialTransaction < Dry::Struct
  7. 1 attribute :incoming_payment, Types.Instance(LooposUi::Resources::IncomingPaymentResource::IncomingPayment).optional.default(nil)
  8. 1 attribute :outgoing_payment, Types.Instance(LooposUi::Resources::PaymentResource::Payment).optional.default(nil)
  9. 1 class << self
  10. 1 def build(incoming_payment: nil, outgoing_payment: nil)
  11. then: 0 if incoming_payment.present?
  12. else: 0 new(incoming_payment: incoming_payment)
  13. then: 0 elsif outgoing_payment.present?
  14. new(outgoing_payment: outgoing_payment)
  15. else: 0 else
  16. raise ArgumentError, "Either incoming_payment or outgoing_payment must be present"
  17. end
  18. end
  19. end
  20. 1 def transaction
  21. incoming_payment.presence || outgoing_payment.presence
  22. end
  23. 1 def type
  24. then: 0 if incoming_payment.present?
  25. else: 0 :incoming
  26. then: 0 else: 0 elsif outgoing_payment.present?
  27. :outgoing
  28. end
  29. end
  30. 1 def barcodes_html
  31. then: 0 else: 0 then: 0 else: 0 barcode_values = transaction.extra_data&.with_indifferent_access&.dig("barcode_codes")
  32. then: 0 else: 0 then: 0 else: 0 barcode_format = transaction.extra_data&.with_indifferent_access&.dig("barcode_format") || "code_128"
  33. then: 0 else: 0 return if barcode_values.blank? || barcode_format.blank?
  34. barcode_values = [barcode_values].flatten.compact
  35. possible_class = "Barby::#{barcode_format.camelize}".safe_constantize
  36. then: 0 else: 0 if possible_class.blank?
  37. possible_class = "Barby::#{barcode_format.camelize}A".safe_constantize
  38. end
  39. # In case the barcode format is not supported, use the default code 128A
  40. possible_class ||= "Barby::Code128A".safe_constantize
  41. barcode_values.map do |barcode_value|
  42. barcode = possible_class.new(barcode_value)
  43. outputter = Barby::HtmlOutputter.new(barcode)
  44. [barcode_value, outputter.to_html(class_name: "lui-financial_transaction-content-barcode")]
  45. end
  46. end
  47. end
  48. 1 option :financial_transaction, Types.Instance(FinancialTransaction)
  49. 1 renders_one :title_info
  50. 1 def tag_status
  51. status = financial_transaction.transaction.status.to_s.downcase
  52. case status
  53. when: 0 when "completed", "transacted"
  54. {
  55. kind: "success",
  56. }
  57. when: 0 when "waiting_agreement", "draft"
  58. {
  59. kind: "warning",
  60. }
  61. else: 0 else
  62. {
  63. kind: "neutral",
  64. }
  65. end
  66. end
  67. end
  68. end
  69. end
  70. end

app/components/loopos_ui/v2/card/script.html.erb

0.0% lines covered

0.0% branches covered

6 relevant lines. 0 lines covered and 6 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <%= render LooposUi::V2::Card.new(title: script.name, description: description, **lazy_loading_options) do |card| %>
  2. <% card.with_corner do %>
  3. <%= modal %>
  4. <% end %>
  5. <% card.with_footer do %>
  6. then: 0 else: 0 <% if script.last_run_result_file.present? %>
  7. <% render LooposUi::Button.new(
  8. icon: "fa-regular fa-file-arrow-down",
  9. tooltip_text: LooposUi::V2::Card.t(".script.download_result"),
  10. text: script.last_run_result_file_name,
  11. kind: :neutral,
  12. type: :tertiary,
  13. href: script.last_run_result_file,
  14. ) %>
  15. <% end %>
  16. <% end %>
  17. <% end %>

app/components/loopos_ui/v2/card/script.rb

63.33% lines covered

0.0% branches covered

30 relevant lines. 19 lines covered and 11 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. 1 module LooposUi
  2. 1 module V2
  3. 1 class Card
  4. 1 class Script < LoopComponent
  5. 1 include LooposUi::V2::Card::LazyLoading
  6. 1 class Script < Dry::Struct
  7. 1 attribute :object, Types.Instance(Object)
  8. 1 attribute :name, Types::String
  9. 1 attribute :last_run_result_file, Types::String.optional
  10. 1 attribute :last_run_result_file_name, Types::String.optional
  11. 1 attribute :last_run_date, Types::Date.optional
  12. 1 attribute :last_run_result_level, Types::String.optional
  13. 1 def self.build(script)
  14. new(
  15. object: script,
  16. last_run_result_file: script.last_run_result_file,
  17. last_run_result_file_name: script.last_run_result_file_name,
  18. last_run_date: script.last_run_date,
  19. last_run_result_level: script.last_run_result_level,
  20. name: script.name,
  21. )
  22. end
  23. end
  24. 1 option :script, Types.Instance(Script).constructor(Script.method(:build))
  25. 1 option :href, Types::String
  26. 1 option :scheduled, Types::Bool, default: -> { false }
  27. 1 renders_one :modal, ->(**args) {
  28. modal = Modal.new(title: script.name, **args)
  29. # Default trigger button
  30. modal.with_trigger_button(
  31. icon: "fa-solid fa-arrow-up-right-from-square",
  32. tooltip_text: "Open",
  33. href: href,
  34. size: :small,
  35. kind: :neutral,
  36. type: :secondary,
  37. )
  38. modal
  39. }
  40. 1 private
  41. 1 def description
  42. then: 0 else: 0 if script.last_run_date.present?
  43. tag.span(class: "flex gap-2") do
  44. concat(tag.span do
  45. concat(LooposUi::V2::Card.t(".script.last_run"))
  46. concat(" ")
  47. concat(script.last_run_date.strftime("%d-%m-%Y %H:%M"))
  48. end)
  49. then: 0 else: 0 concat(tag.i(class: "fa-regular fa-calendar-clock")) if scheduled
  50. end
  51. end
  52. end
  53. end
  54. end
  55. end
  56. end

app/components/loopos_ui/v2/image.rb

45.27% lines covered

0.0% branches covered

148 relevant lines. 67 lines covered and 81 lines missed.
80 total branches, 0 branches covered and 80 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 module V2
  4. 1 class Image < LoopComponent
  5. 3027 IMAGE_MIMES = ::MIME::Types.select { |type| type.media_type == "image" }
  6. 1 option :editable, default: -> { false }, type: Types::Bool
  7. 1 option :image_url, optional: true
  8. 1 option :signed_id, optional: true
  9. 1 option :size, default: -> { :full }, type: Types::Coercible::Symbol.enum(:full, :small)
  10. 1 option :accept,
  11. Types::Array | Types::Symbol.enum(:all),
  12. optional: true,
  13. default: -> { LooposUi.config.image_default_accept_formats }
  14. 1 option :model, optional: true
  15. 1 option :association, optional: true
  16. 1 option :rounded, default: -> { false }, type: Types::Bool
  17. 1 option :list_view, default: -> { false }, type: Types::Bool
  18. 1 option :in_list_view, default: -> { false }, type: Types::Bool
  19. 1 option :in_list_view_index, Types::Integer, optional: true
  20. 1 option :attachment, optional: true
  21. 1 option :force_new, default: -> { false }, type: Types::Bool
  22. # Permissions
  23. 1 option :can_detach, Types::Bool, default: -> { true }
  24. 1 option :path, Types::String, default: -> { LooposUi::Engine.routes.url_helpers.images_path }
  25. 1 option :turbo_frame_id, Types::String, optional: true
  26. # HTML name for upload inputs. Defaults: has-one uses "file", has-many uses "files[]".
  27. 1 option :name, Types::Coercible::String, optional: true
  28. # When set, file inputs use the HTML `form` attribute to associate with that id (no nested <form>).
  29. 1 option :form_id, Types::Coercible::String, optional: true
  30. # Override Active Storage direct upload target (e.g. Core app URL when the page uses CoreApi).
  31. # Expected form: `https://core.example.com/rails/active_storage/direct_uploads`
  32. 1 option :direct_upload_url, Types::Coercible::String.optional, optional: true
  33. # When using a remote direct upload URL (e.g. Core), set `Authorization` on the create-blob XHR.
  34. # LoopOS UI direct uploads use `X-Lui-Context` instead (see `upload_context_input_attributes`).
  35. 1 option :direct_upload_authorization, Types::Coercible::String.optional, optional: true
  36. 1 option :help, optional: true
  37. 1 option :error, optional: true
  38. # When true, forces multi-image/multi-picker behavior in standalone mode (protocol form).
  39. # When nil (default), inferred from the model's ActiveStorage reflection (has_many_attached?).
  40. 1 option :multiple, Types::Bool.optional, optional: true
  41. # Pre-filled images for standalone protocol form use.
  42. # Pass Core API answer values as: [{ signed_id: "...", url: "..." }, ...]
  43. # Only consumed in standalone list_view mode; model-backed mode reads from ActiveStorage.
  44. 1 option :preloaded_images, Types::Array, default: -> { [] }
  45. # When false, direct upload completion does NOT trigger requestSubmit() on the linked form.
  46. # Set to false in protocol forms where form submission is driven externally after all answers
  47. # are filled. Defaults to true to preserve existing model-backed upload-and-save behaviour.
  48. 1 option :submit_on_upload, default: -> { true }, type: Types::Bool
  49. 1 option :max_size, optional: true
  50. 1 option :max_size_batch, optional: true
  51. 1 def initialize(...)
  52. super
  53. validate_input_source!
  54. validate_list_view!
  55. validate_standalone_identity!
  56. mimefy_accept_types
  57. end
  58. # When editable without a model ("standalone"), only `accept` is required for direct upload
  59. # validation. LoopOS `images#create` attach still requires a full context with model keys.
  60. 1 def standalone_upload?
  61. editable? && model.blank?
  62. end
  63. # True when multiple images are expected.
  64. # Explicit `multiple` prop takes precedence over the ActiveStorage reflection so protocol
  65. # forms can opt into multi-image behaviour without a local model.
  66. 1 def multiple?
  67. else: 0 then: 0 return multiple unless multiple.nil?
  68. has_many_attached?
  69. end
  70. # True when rendering as a standalone image list without a model (protocol form input:images).
  71. # Matches validate_list_view!: model-less list mode requires multiple + name, not editable.
  72. 1 def standalone_list_view?
  73. list_view && model.blank? && multiple?
  74. end
  75. 1 def context
  76. payload =
  77. then: 0 if model.present?
  78. {
  79. model_class: model.class.name,
  80. model_id: model.id,
  81. association: association,
  82. accept: file_field_accept,
  83. }
  84. else: 0 else
  85. { accept: file_field_accept }
  86. end
  87. LooposUi::EncryptionService.encrypt(payload)
  88. end
  89. 1 def file_field_accept
  90. case @accept
  91. when: 0 when :all
  92. ["image/*"]
  93. else: 0 else
  94. (IMAGE_MIMES & @accept).flat_map(&:content_type)
  95. end.join(",")
  96. end
  97. # FIXME: small edge case here, if you pass a string like "png"
  98. 1 def mimefy_accept_types
  99. then: 0 else: 0 return if accept == :all
  100. @accept = accept.flat_map do |mime|
  101. ::MIME::Types.type_for(".#{mime}") || ::MIME::Types[mime.to_s]
  102. end.compact_blank!
  103. end
  104. 1 def unique_id
  105. @unique_id ||= [
  106. "multiple_image",
  107. then: 0 else: 0 model.present? && association.present? ? dom_id(model, association) : nil,
  108. standalone_identity_seed,
  109. image_url,
  110. size,
  111. then: 0 else: 0 in_list_view ? "list_view" : nil,
  112. in_list_view_index,
  113. ].compact.join("_")
  114. end
  115. 1 def unique_list_id
  116. @unique_list_id ||= "#{unique_id}_list"
  117. end
  118. 1 def classes
  119. [
  120. "lui-image--#{size}",
  121. then: 0 else: 0 rounded ? "lui-image--rounded" : nil,
  122. then: 0 else: 0 empty_uploader? ? "always-edit" : nil,
  123. ].compact.join(" ")
  124. end
  125. 1 def placeholder_classes
  126. [
  127. "lui-image__placeholder",
  128. then: 0 else: 0 editable? ? "lui-image__placeholder--editable" : nil,
  129. then: 0 else: 0 with_image? ? "hidden" : "flex",
  130. ].compact.join(" ")
  131. end
  132. 1 def empty_uploader?
  133. in_list_view && force_new
  134. end
  135. 1 def attached_image
  136. then: 0 else: 0 return attachment if attachment.present?
  137. then: 0 if has_many_attached?
  138. then: 0 else: 0 then: 0 else: 0 model&.send(association)&.first
  139. else: 0 else
  140. then: 0 else: 0 model&.send(association)
  141. end
  142. end
  143. 1 def with_attached?
  144. then: 0 else: 0 return false if force_new
  145. then: 0 if has_many_attached?
  146. then: 0 else: 0 attached_image&.present?
  147. else: 0 else
  148. then: 0 else: 0 !!attached_image&.attached?
  149. end
  150. end
  151. 1 def turbo_frame_id
  152. then: 0 else: 0 return @turbo_frame_id if @turbo_frame_id.present?
  153. then: 0 else: 0 in_list_view ? unique_list_id : unique_id
  154. end
  155. 1 def editable?
  156. !!@editable
  157. end
  158. 1 def show_field_messages?
  159. !in_list_view
  160. end
  161. 1 def wrapper_classes
  162. [
  163. "lui-image-wrapper",
  164. then: 0 else: 0 ("lui-image-wrapper--with-error" if error.present?),
  165. ].compact.join(" ")
  166. end
  167. 1 def single_file_field_name
  168. name.presence || "file"
  169. end
  170. # When `name` is set, append "[]" for the multi-file input unless already present (Rails array param).
  171. 1 def multiple_file_field_name
  172. then: 0 else: 0 return "files[]" if name.blank?
  173. n = name.to_s
  174. then: 0 else: 0 n.end_with?("[]") ? n : "#{n}[]"
  175. end
  176. # Root-relative path to the host application Active Storage direct upload endpoint.
  177. # `file_field(..., direct_upload: true)` resolves `rails_direct_uploads_url` in the current
  178. # routing context first; when the component is rendered under a mounted engine, that can
  179. # produce a relative URL under the mount (e.g. `/mount/loopos_ui/rails/active_storage/direct_uploads`),
  180. # which does not exist. The template sets `data-direct-upload-url` explicitly instead.
  181. #
  182. # Use `Rails.application.routes` only (path helper). Avoid `main_app.*_url` / mixed proxies so we
  183. # never require `default_url_options[:host]` (e.g. mailers, previews without request host).
  184. 1 def active_storage_direct_upload_path
  185. Rails.application.routes.url_helpers.rails_direct_uploads_path
  186. rescue ActionController::UrlGenerationError, NoMethodError
  187. "#{ActiveStorage.routes_prefix.to_s.chomp("/")}/direct_uploads"
  188. end
  189. 1 def effective_direct_upload_url
  190. direct_upload_url.presence || active_storage_direct_upload_path
  191. end
  192. # Encrypted direct-upload payload for X-Lui-Context. Omit `name` when linked to an outer form
  193. # (`form_id` + standalone) so it is not submitted as `params[:context]` on the host item page.
  194. 1 def upload_context_input_attributes
  195. attrs = {
  196. type: "hidden",
  197. autocomplete: "off",
  198. value: context,
  199. data: { images_target: "uploadContext" },
  200. }
  201. else: 0 then: 0 attrs[:name] = "context" unless standalone_upload? && form_id.present?
  202. attrs
  203. end
  204. 1 def with_image?
  205. with_attached? || image_url.present?
  206. end
  207. 1 def has_many_attached?
  208. attachment_reflection == :has_many_attached
  209. end
  210. 1 def has_one_attached?
  211. attachment_reflection == :has_one_attached
  212. end
  213. 1 def attachments
  214. else: 0 then: 0 raise unless has_many_attached?
  215. model.send(association).attachments
  216. end
  217. # Could probably generate this from the options, but this is fine for now
  218. 1 def configuration
  219. {
  220. editable: editable,
  221. image_url: image_url,
  222. size: size,
  223. model: model,
  224. association: association,
  225. rounded: rounded,
  226. list_view: list_view,
  227. in_list_view: in_list_view,
  228. attachment: attachment,
  229. force_new: force_new,
  230. can_detach: can_detach,
  231. name: name,
  232. form_id: form_id,
  233. direct_upload_url: direct_upload_url,
  234. direct_upload_authorization: direct_upload_authorization,
  235. multiple: multiple?,
  236. preloaded_images: preloaded_images,
  237. submit_on_upload: submit_on_upload,
  238. max_size: max_size,
  239. max_size_batch: max_size_batch,
  240. }
  241. end
  242. # Returns HTML attribute options for a pre-fill signed_id hidden input.
  243. # Associates the input with the outer form when form_id is present.
  244. 1 def preloaded_image_input_options
  245. opts = { autocomplete: "off" }
  246. then: 0 else: 0 opts[:form] = form_id if form_id.present?
  247. opts
  248. end
  249. 1 private
  250. 1 def validate_input_source!
  251. then: 0 else: 0 return if model.present? || name.present?
  252. then: 0 else: 0 return if display_only_preview?
  253. then: 0 else: 0 return if standalone_upload?
  254. raise ArgumentError, "#{self.class}: either model or name is required"
  255. end
  256. 1 def display_only_preview?
  257. !editable? && image_url.present?
  258. end
  259. # list_view normally requires a model (uses ActiveStorage attachments).
  260. # Exception: standalone protocol form mode is allowed when multiple: true and name are both set.
  261. 1 def validate_list_view!
  262. else: 0 then: 0 return unless list_view
  263. then: 0 else: 0 return if model.present?
  264. then: 0 else: 0 return if multiple? && name.present?
  265. raise ArgumentError,
  266. "#{self.class}: model is required when list_view is true (or pass multiple: true with name: for standalone protocol form mode)"
  267. end
  268. 1 def validate_standalone_identity!
  269. else: 0 then: 0 return unless standalone_upload?
  270. # Use @turbo_frame_id (explicit option), not turbo_frame_id — the reader falls back to unique_id.
  271. then: 0 else: 0 return if @turbo_frame_id.present? || name.present? || form_id.present?
  272. raise ArgumentError, "#{self.class}: standalone editable mode requires name, form_id, or turbo_frame_id to avoid duplicate ids"
  273. end
  274. 1 def standalone_identity_seed
  275. else: 0 then: 0 return unless standalone_upload?
  276. parts = [name.presence, form_id.presence].compact
  277. then: 0 else: 0 return if parts.empty?
  278. parts.join("_").parameterize(separator: "_")
  279. end
  280. 1 def attachment_reflection
  281. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 model&.class&.reflect_on_attachment(association.to_s)&.macro
  282. end
  283. end
  284. end
  285. end

app/components/loopos_ui/v2/image/image.html.erb

0.0% lines covered

0.0% branches covered

84 relevant lines. 0 lines covered and 84 lines missed.
48 total branches, 0 branches covered and 48 branches missed.
    
  1. <% image_content = capture do %>
  2. then: 0 <% if list_view %>
  3. <%= tag.turbo_frame id: turbo_frame_id, class: "lui-image-list" do %>
  4. <div class="flex flex-row gap-2 flex-wrap">
  5. <% if standalone_list_view? %>
  6. then: 0 <%# Standalone list (protocol form input:images): pre-filled display tiles + hidden signed_id inputs %>
  7. <% preloaded_images.each_with_index do |img, i| %>
  8. <%= render LooposUi::V2::Image.new(
  9. **configuration,
  10. image_url: img[:url] || img["url"],
  11. signed_id: img[:signed_id] || img["signed_id"],
  12. editable: true,
  13. can_detach: true,
  14. list_view: false,
  15. in_list_view: true,
  16. in_list_view_index: i,
  17. preloaded_images: [],
  18. multiple: false,
  19. ) %>
  20. <%= hidden_field_tag multiple_file_field_name, (img[:signed_id] || img["signed_id"]), **preloaded_image_input_options %>
  21. <% end %>
  22. <% else %>
  23. else: 0 <%# Model-backed list: iterate ActiveStorage attachments %>
  24. <% attachments.each_with_index do |a, i| %>
  25. <%= render LooposUi::V2::Image.new(
  26. **configuration,
  27. attachment: a,
  28. list_view: false,
  29. in_list_view: true,
  30. in_list_view_index: i,
  31. ) %>
  32. <% end %>
  33. <% end %>
  34. <%= render LooposUi::V2::Image.new(
  35. **configuration,
  36. force_new: true,
  37. list_view: false,
  38. in_list_view: true,
  39. in_list_view_index: -1,
  40. preloaded_images: [],
  41. ) %>
  42. </div>
  43. <% end %>
  44. else: 0 <% else %>
  45. then: 0 else: 0 <%= tag.turbo_frame id: turbo_frame_id, class: (in_list_view ? "lui-image-list__item" : nil) do %>
  46. <%= tag.div(
  47. data: {
  48. controller: "images",
  49. images_editable_value: editable?,
  50. images_standalone_value: standalone_upload?,
  51. images_form_id_value: form_id.to_s,
  52. images_with_attached_value: with_attached? || image_url.present?,
  53. images_turbo_frame_id_value: turbo_frame_id,
  54. images_unique_id_value: unique_id,
  55. images_list_view_value: in_list_view,
  56. images_has_many_value: has_many_attached?,
  57. images_force_new_value: force_new,
  58. images_submit_on_upload_value: submit_on_upload,
  59. images_direct_upload_authorization_value: direct_upload_authorization.presence || "",
  60. images_direct_upload_url_value: effective_direct_upload_url,
  61. images_max_size_value: max_size,
  62. images_max_size_batch_value: max_size_batch,
  63. images_max_size_error_message_value: I18n.t("image.max_size_exceeded"),
  64. images_max_size_batch_error_message_value: I18n.t("image.max_size_batch_exceeded"),
  65. images_urls_value: {
  66. then: 0 else: 0 attach: standalone_upload? ? nil : path,
  67. then: 0 else: 0 detach: (can_detach && !standalone_upload?) ? path : nil,
  68. }
  69. }) do %>
  70. <%= tag.div(class: ["lui-image", classes].join(" "), data: { images_target: 'imageComponent', action: "pubsub:action@window->images#executeAction"}) do %>
  71. <div
  72. role="status"
  73. class="hidden lui-image__loader"
  74. data-images-target="loader"
  75. >
  76. <%= render LooposUi::Button.new(
  77. icon: "spinner",
  78. disabled: true,
  79. size: :tiny,
  80. kind: :neutral,
  81. type: :secondary,
  82. ) %>
  83. </div>
  84. then: 0 <% if with_attached? %>
  85. else: 0 <%= image_tag(attached_image, class: "lui-image__image", data: { images_target: "image", blob_id: attached_image.blob_id }) %>
  86. then: 0 <% elsif image_url.present? %>
  87. <%= image_tag(image_url, class: "lui-image__image", data: { images_target: "image", signed_id: signed_id }) %>
  88. else: 0 <% else %>
  89. <%= image_tag("", class: "hidden lui-image__image", data: { images_target: "image" }) %>
  90. <% end %>
  91. <div
  92. class="<%= placeholder_classes %>"
  93. data-images-target="placeholder"
  94. >
  95. <%= render LooposUi::Icon.new(icon: :image, color: :white) %>
  96. </div>
  97. then: 0 else: 0 <% if editable? %>
  98. <div class="overflow-visible lui-image__image-edit">
  99. then: 0 <% if form_id.present? %>
  100. <%= tag.input(**upload_context_input_attributes) %>
  101. <div class="hidden" data-images-target="noImageUploader">
  102. <%= render LooposUi::Button.new(
  103. icon: :upload,
  104. size: :tiny,
  105. then: 0 else: 0 type: size == :full ? :secondary : :tertiary,
  106. kind: :neutral,
  107. then: 0 else: 0 tooltip_text: multiple? ? "Upload images here" : "Upload image here",
  108. tag_options: {
  109. data: {
  110. then: 0 else: 0 action: multiple? ? "click->images#openFilePickerMultiple" : "click->images#openFilePicker"
  111. } }
  112. ) %>
  113. </div>
  114. <div class="hidden" data-images-target="withImageUploader">
  115. <%= render LooposUi::ActionMenu.new(
  116. options: [
  117. { text: I18n.t("image.upload"), icon: "arrows-rotate", attributes: {
  118. data: {
  119. action: "click->pubsub#publish",
  120. pubsub_unique_id_param: unique_id,
  121. pubsub_action_param: "openFilePicker"
  122. }
  123. }
  124. },
  125. then: 0 multiple? ?
  126. { text: I18n.t("image.add"), icon: "plus", attributes: {
  127. data: {
  128. action: "click->pubsub#publish",
  129. pubsub_unique_id_param: unique_id,
  130. pubsub_action_param: "openFilePickerMultiple"
  131. }
  132. else: 0 }
  133. } : nil,
  134. then: 0 can_detach ?
  135. { text: I18n.t("image.remove"), icon: "trash",
  136. attributes: {
  137. data: {
  138. action: "click->pubsub#publish",
  139. pubsub_unique_id_param: unique_id,
  140. pubsub_action_param: "detachImage"
  141. }
  142. else: 0 }
  143. } : nil,
  144. ].compact
  145. ) do |menu| %>
  146. <% menu.with_trigger do %>
  147. <%= render LooposUi::Button.new(
  148. icon: "ellipsis-vertical",
  149. size: :tiny,
  150. then: 0 else: 0 type: size == :full ? :secondary : :tertiary,
  151. kind: :neutral,
  152. tag_options: { type: :button },
  153. ) %>
  154. <% end %>
  155. <% end %>
  156. </div>
  157. <%= file_field_tag single_file_field_name,
  158. accept: file_field_accept,
  159. class: "hidden",
  160. form: form_id,
  161. data: {
  162. direct_upload_url: effective_direct_upload_url,
  163. images_target: "file",
  164. action: "change->images#previewAndSubmit",
  165. } %>
  166. <%= file_field_tag multiple_file_field_name,
  167. accept: file_field_accept,
  168. multiple: true,
  169. class: "hidden",
  170. form: form_id,
  171. data: {
  172. direct_upload_url: effective_direct_upload_url,
  173. images_target: "fileMultiple",
  174. action: "change->images#previewAndSubmitMultiple",
  175. } %>
  176. <%= submit_tag I18n.t("image.save"), form: form_id, data: { images_target: "submit" }, class: "hidden" %>
  177. else: 0 <% else %>
  178. <%= form_with url: "#", html: { data: { images_target: "form" } } do |f| %>
  179. <%= hidden_field_tag :authenticity_token, form_authenticity_token %>
  180. <%= tag.input(**upload_context_input_attributes) %>
  181. <div class="hidden" data-images-target="noImageUploader">
  182. <%= render LooposUi::Button.new(
  183. icon: :upload,
  184. size: :tiny,
  185. then: 0 else: 0 type: size == :full ? :secondary : :tertiary,
  186. kind: :neutral,
  187. then: 0 else: 0 tooltip_text: multiple? ? "Upload images here" : "Upload image here",
  188. tag_options: {
  189. data: {
  190. then: 0 else: 0 action: multiple? ? "click->images#openFilePickerMultiple" : "click->images#openFilePicker"
  191. } }
  192. ) %>
  193. </div>
  194. <div class="hidden" data-images-target="withImageUploader">
  195. <%= render LooposUi::ActionMenu.new(
  196. options: [
  197. { text: I18n.t("image.upload"), icon: "arrows-rotate", attributes: {
  198. data: {
  199. action: "click->pubsub#publish",
  200. pubsub_unique_id_param: unique_id,
  201. pubsub_action_param: "openFilePicker"
  202. }
  203. }
  204. },
  205. then: 0 multiple? ?
  206. { text: I18n.t("image.add"), icon: "plus", attributes: {
  207. data: {
  208. action: "click->pubsub#publish",
  209. pubsub_unique_id_param: unique_id,
  210. pubsub_action_param: "openFilePickerMultiple"
  211. }
  212. else: 0 }
  213. } : nil,
  214. then: 0 can_detach ?
  215. { text: I18n.t("image.remove"), icon: "trash",
  216. attributes: {
  217. data: {
  218. action: "click->pubsub#publish",
  219. pubsub_unique_id_param: unique_id,
  220. pubsub_action_param: "detachImage"
  221. }
  222. else: 0 }
  223. } : nil,
  224. ].compact
  225. ) do |menu| %>
  226. <% menu.with_trigger do %>
  227. <%= render LooposUi::Button.new(
  228. icon: "ellipsis-vertical",
  229. size: :tiny,
  230. then: 0 else: 0 type: size == :full ? :secondary : :tertiary,
  231. kind: :neutral,
  232. tag_options: { type: :button },
  233. ) %>
  234. <% end %>
  235. <% end %>
  236. </div>
  237. <%= f.file_field single_file_field_name,
  238. accept: file_field_accept,
  239. class: "hidden",
  240. data: {
  241. direct_upload_url: effective_direct_upload_url,
  242. images_target: "file",
  243. action: "change->images#previewAndSubmit",
  244. } %>
  245. <%= f.file_field multiple_file_field_name,
  246. accept: file_field_accept,
  247. multiple: true,
  248. class: "hidden",
  249. data: {
  250. direct_upload_url: effective_direct_upload_url,
  251. images_target: "fileMultiple",
  252. action: "change->images#previewAndSubmitMultiple",
  253. } %>
  254. <%= f.submit I18n.t("image.save"), data: { images_target: :submit }, class: "hidden" %>
  255. <% end %>
  256. <% end %>
  257. </div>
  258. <% end %>
  259. <% end %>
  260. <span class="lui-image__error hidden" data-images-target="error"></span>
  261. <%# <span class="lui-image__hint" data-upload-target="hint">Hint</span> %>
  262. <% end %>
  263. <% end %>
  264. <% end %>
  265. <% end %>
  266. then: 0 <% if show_field_messages? %>
  267. <%= tag.div(class: wrapper_classes) do %>
  268. <%= image_content %>
  269. then: 0 else: 0 <%= content_tag(:span, help, class: "lui-input__help") if help.present? && error.blank? %>
  270. then: 0 else: 0 <%= content_tag(:span, error, class: "lui-input__error") if error.present? %>
  271. <% end %>
  272. else: 0 <% else %>
  273. <%= image_content %>
  274. <% end %>

app/components/loopos_ui/v2/select_bar.rb

45.16% lines covered

100.0% branches covered

31 relevant lines. 14 lines covered and 17 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module V2
  3. 1 class SelectBar < LoopComponent
  4. 1 erb_template <<~ERB
  5. <nav class="flex w-full min-h-8 box-content py-1.5 pr-4 pl-6 items-center space-between bg-secondary border-b border-gray-200">
  6. <%= content %>
  7. </nav>
  8. ERB
  9. 1 def render?
  10. has_any_selectors?
  11. end
  12. 1 def has_any_selectors?
  13. content.present? && Nokogiri::HTML(content.to_s).search(".lui-select-bar__selector").any?
  14. end
  15. 1 class Selector < LoopComponent
  16. 1 erb_template <<-ERB
  17. <div class="lui-select-bar__selector">
  18. <%= render LooposUi::FormEntry.new(label: label, orientation: :horizontal) do |fe| %>
  19. <% fe.with_input do %>
  20. <%= render LooposUi::Inputs::Select.new(
  21. name: name,
  22. options: options,
  23. value: value,
  24. placeholder: "Select..",
  25. mode: :autosubmit,
  26. )
  27. %>
  28. <% end %>
  29. <% end %>
  30. </div>
  31. ERB
  32. 1 option :type, Types::Coercible::Symbol.enum(:core), default: -> { :core }
  33. 1 option :options, Types::Array.of(Types::Hash.schema(
  34. value: Types::Coercible::String,
  35. text: Types::String,
  36. ))
  37. 1 option :value, Types::Coercible::String
  38. 1 option :name, Types::Coercible::String
  39. # TODO: these components are kinda hacked together
  40. # and should be refactored internally. The FormEntry label does not support colors or icons, but
  41. # with html_safe trick it works
  42. 1 def label
  43. helpers.content_tag(:span, class: "text-app-800-primary") do
  44. concat(helpers.content_tag(:icon, "", class: "fa-regular fa-map-pin mr-1"))
  45. concat(type.to_s.capitalize)
  46. concat(":")
  47. end.html_safe
  48. end
  49. 1 def render?
  50. options.present? && options.count > 1
  51. end
  52. end
  53. end
  54. end
  55. end

app/components/loopos_ui/v2/table.html.erb

0.0% lines covered

0.0% branches covered

39 relevant lines. 0 lines covered and 39 lines missed.
18 total branches, 0 branches covered and 18 branches missed.
    
  1. <%# HACK: Without this, the rows (and cells) do not render (in slotted mode) and cannot be captured in data_source method %>
  2. <%# TODO: this is not necessary if we call the slots with <%= instead of <%. Investigate why %>
  3. <% rows.each do |row| %>
  4. <% capture do %>
  5. <%= row %>
  6. <% end %>
  7. <% end %>
  8. <%# ENDHACK %>
  9. then: 0 else: 0 <% global_filters_html = capture do %>
  10. <% global_filters.each do |filter| %>
  11. <%# TODO type is igrored for now %>
  12. <%# type, key, default %>
  13. <span class="flex gap-1">
  14. <span class="copy-12">
  15. <%= filter[:text] %>
  16. </span>
  17. <%= render LooposUi::Toggle.new(
  18. color: LooposUi::Colors.find("general-informative-800"),
  19. name: filter[:key],
  20. then: 0 else: 0 checked: filter[:default] == true ? "true" : "false")
  21. %>
  22. </span>
  23. <% end %>
  24. <% end if global_filters.present? %>
  25. then: 0 else: 0 <% hiddable_columns_html = capture do %>
  26. <% hiddable_columns.each do |column| %>
  27. <span class="flex gap-1">
  28. <span class="copy-12">
  29. <%= column[:text] %>
  30. </span>
  31. <%= render LooposUi::Toggle.new(
  32. color: LooposUi::Colors.find("general-informative-800"),
  33. name: column[:key],
  34. then: 0 else: 0 checked: column[:default] == true ? "true" : "false")
  35. %>
  36. </span>
  37. <% end %>
  38. <% end if hiddable_columns.present? %>
  39. then: 0 else: 0 <%= render LooposUi::FilterBar.new(
  40. search_options: filter_bar_search_options,
  41. toggle_options: filter_bar_toggle_options,
  42. show_toggle_switch: filter_bar_toggle_options.present?,
  43. table_id: id
  44. ) do |bar| %>
  45. <% filter_bar_filters.each do |key, value| %>
  46. <% value.each do |text| %>
  47. <%= render LooposUi::FilterPill.new(
  48. text: "#{key}: #{text.humanize}",
  49. state: :active,
  50. hasCloseButton: true,
  51. data: {
  52. action: "click->table-filters#delete",
  53. key: "#{key}_#{text}"
  54. }
  55. ) %>
  56. <% end %>
  57. <% end %>
  58. <% end if filter_bar_filters.present? %>
  59. <%= content_tag(:div, id: unique_dom_id) do %>
  60. <%= tag.div(class: classes, id: unique_dom_id, data: { controller: "table", "table-data-source-id-value": data_source_id, table_float_bar_outlet: "##{unique_dom_id} .lui-table__floating_bar .lui-float_bar", "table-columns-value": transformed_columns_with_actions }) do %>
  61. <%= react_component("Table",
  62. {
  63. uniqueId: unique_dom_id,
  64. id: id,
  65. lang: I18n.locale,
  66. columns: transformed_columns_with_actions,
  67. then: 0 else: 0 searchQuery: params&.dig(:q, :search),
  68. globalFiltersHtml: global_filters_html,
  69. hiddableColumnsHtml: hiddable_columns_html,
  70. tableHeader: LooposUi::V2::Table::Header.new(searchable: searchable, show_filters: false).render_in(view_context) do |table|
  71. table.with_searchbar(
  72. name: "table_#{id}_search_input",
  73. then: 0 else: 0 value: params&.dig(:q, :search),
  74. placeholder: search_placeholder || t(".search.placeholder")
  75. )
  76. end,
  77. footerHtml: footer_html,
  78. clearFiltersHtml: render(LooposUi::Button.new(
  79. icon: :eraser,
  80. text: t(".clear_all_filters"),
  81. type: :tertiary,
  82. kind: :neutral,
  83. size: :small,
  84. )),
  85. filterCounterTemplateHtml: render(LooposUi::Counter.new(count: ":count:", kind: :neutral)),
  86. sortingEnabled: sortable,
  87. view_more: view_more,
  88. viewMoreHtml: capture {
  89. render(LooposUi::Button.new(
  90. text: t(".view_more"),
  91. type: :tertiary,
  92. kind: :neutral,
  93. size: :default,
  94. ))
  95. },
  96. **options
  97. })
  98. %>
  99. then: 0 else: 0 <% if action_zone_enabled? %>
  100. <div class="lui-table__floating_bar hidden" data-table-target="actionZone">
  101. <%= action_bar %>
  102. </div>
  103. <% end %>
  104. <% end %>
  105. <% end %>
  106. then: 0 else: 0 <%= render(LooposUi::Tooltip.new(
  107. title: t(".expand_collapse_all"),
  108. tippy_target_id: "#{unique_dom_id}-collapse-tooltip",
  109. position: :top)) if data_source.any? { |row| row.key?(:children) }
  110. %>
  111. <%= content_tag(:div, id: data_source_id, class: "hidden") do %>
  112. <%= data_source.to_json %>
  113. <% end %>
  114. <%= content_tag(:div, id: pagination_id, class: "hidden") do %>
  115. <%= pagination.to_json %>
  116. <% end %>

app/components/loopos_ui/v2/table.rb

39.07% lines covered

0.0% branches covered

215 relevant lines. 84 lines covered and 131 lines missed.
81 total branches, 0 branches covered and 81 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 module V2
  4. 1 class Table < LoopComponent
  5. 1 include Turbo::FramesHelper
  6. 1 DEFAULT_PAGE_SIZE = 15
  7. 1 renders_many :rows, ->(*args, **kwargs, &block) {
  8. LooposUi::V2::Table::Row.new(*args, **kwargs, manageable_rows: manageable_rows, table: self, &block)
  9. }
  10. 1 renders_one :action_bar, LooposUi::FloatBar
  11. 1 renders_one :footer, ->(*_args, **kwargs, &block) {
  12. @footer_args = kwargs
  13. capture(&block)
  14. }
  15. 1 option :id, optional: true, type: Types::Coercible::String, default: -> { "" }
  16. 1 option :columns, type: Types::Array.of(Types::Hash), default: -> { [] }
  17. 1 option :data, default: -> { [] }
  18. 1 option :pagination, default: -> {
  19. {
  20. then: 0 else: 0 current: 1, pageSize: DEFAULT_PAGE_SIZE, total: data&.count || 0,
  21. }
  22. }
  23. # TODO: move to pagination_config
  24. 1 option :show_pagination, default: -> { true }, type: Types::Bool
  25. 1 option :show_result_count, default: -> { true }, type: Types::Bool
  26. 1 option :show_pagination_size_changer, default: -> { true }, type: Types::Bool
  27. 1 option :show_header, default: -> { true }, type: Types::Bool
  28. 1 option :pagy, optional: true
  29. 1 option :selectable, optional: true
  30. 1 option :selectable_type, optional: true
  31. 1 option :searchable, default: -> { true }
  32. 1 option :search_placeholder, optional: true
  33. 1 option :sortable, default: -> { false }, type: Types::Bool
  34. 1 option :global_filters, optional: true
  35. 1 option :tree_indent_size, default: -> { 15 }
  36. 1 option :fetch_url, optional: true
  37. 1 option :filters_url, optional: true
  38. 1 option :view_more, default: -> { 10 }, type: Types::Integer
  39. 1 option :hidden_columns, optional: true, type: Types::Array.of(Types::Hash), default: -> { [] }
  40. 1 option :hiddable_columns, optional: true
  41. # See https://ant.design/components/table#expandable
  42. # Keys will be camelized when passing to the Antd Table component
  43. 1 option :expandable_config, optional: true, default: -> { {} } do
  44. 1 option :default_expanded_row_keys, optional: true, type: Types::Array.of(Types::Coercible::String)
  45. 1 option :default_expand_all_rows, optional: true, default: -> { false }, type: Types::Bool
  46. # Should be used instead of tree_indent_size, but would be breaking change
  47. 1 option :indent_size, optional: true, type: Types::Coercible::Integer
  48. end
  49. 1 option :manageable_rows, default: -> { false }, type: Types::Bool
  50. 1 option :add_new_url, optional: true
  51. 1 option :filter_bar_filters, optional: true, type: Types::Hash, default: -> { {} }
  52. 1 option :filter_bar_search_options, optional: true, type: Types::Hash, default: -> { {} }
  53. 1 option :filter_bar_toggle_options, optional: true, type: Types::Hash, default: -> { {} }
  54. 1 option :active_filters, optional: true, type: Types::Hash, default: -> { {} }
  55. 1 attr_reader :columns_hash
  56. 1 def initialize(**data)
  57. then: 0 else: 0 data[:pagination] = {
  58. current: data[:pagy].page,
  59. pageSize: data[:pagy].items,
  60. total: data[:pagy].count,
  61. } if data[:pagy].present?
  62. then: 0 else: 0 if data[:manageable_rows] && !data[:add_new_url]
  63. raise ArgumentError, "add_new_url is required when manageable_rows is true"
  64. end
  65. super
  66. end
  67. 1 def data_source
  68. then: 0 @entries ||= if slotting_mode?
  69. data_from_slotting
  70. else: 0 else
  71. @data.each do |row|
  72. row.each do |key, value|
  73. then: 0 else: 0 if value.is_a?(ViewComponent::Base)
  74. row[key] = ApplicationController.new.view_context.render(value)
  75. end
  76. end
  77. end
  78. @data
  79. end
  80. @entries
  81. end
  82. # Additional options that will be passed in the Table.tsx
  83. 1 def options
  84. {
  85. fetchUrl: fetch_url,
  86. filtersUrl: filters_url,
  87. selectable: selectable,
  88. selectableType: selectable_type,
  89. showPagination: should_show_pagination?,
  90. showPaginationSizeChanger: show_pagination_size_changer,
  91. showResultCount: show_result_count,
  92. searchable: searchable,
  93. theme: theme,
  94. treeIndentSize: tree_indent_size,
  95. expandableConfig: expandable_config.to_h.deep_transform_keys { |key| key.to_s.camelize(:lower).to_sym },
  96. initialFilters: active_filters.presence || {},
  97. }
  98. end
  99. 1 def transformed_columns
  100. @transformed_columns ||= begin
  101. # Create a set of dataIndex values from hidden_columns for efficient lookup
  102. hidden_data_indices = hidden_columns.map { |hc| (hc[:dataIndex] || hc[:key]).to_s }.to_set
  103. visible_columns = []
  104. columns.each do |column|
  105. column_key = (column[:dataIndex] || column[:key]).to_s
  106. # Only add column if it's not in hidden_columns
  107. else: 0 then: 0 unless hidden_data_indices.include?(column_key)
  108. visible_columns << column
  109. end
  110. end
  111. visible_columns.each_with_index do |column, i|
  112. then: 0 else: 0 then: 0 else: 0 if column[:type]&.to_sym == :html
  113. LooposUi.logger.warn("Setting the column type to :html is not necessary. It is the default and only value. You can safely remove it from the column definition. Column: {#{column.dig("title") || column.dig(:title)}}")
  114. end
  115. column[:type] ||= :html
  116. column[:className] = [
  117. column.dig(:className),
  118. "lui-table__cell",
  119. "lui-table__cell--n#{i}",
  120. else: 0 case i
  121. when: 0 when 0
  122. "lui-table__cell--first"
  123. when: 0 when visible_columns.size - 1
  124. "lui-table__cell--last"
  125. end,
  126. ].compact.join(" ")
  127. then: 0 else: 0 column[:hidable] = true if column[:hidable].nil?
  128. then: 0 else: 0 then: 0 else: 0 column.delete(:filters) if column.key?(:filters) && column.fetch(:filters)&.empty?
  129. then: 0 else: 0 if column[:filters].present?
  130. column[:filters] = column[:filters].deep_dup
  131. column[:filterMode] = :tree
  132. column[:filterSearch] = true
  133. # Create composite key for the filter. Necessary for heterogeneous data
  134. compose_filters(column[:filters], column)
  135. column[:filters] = column[:filters].sort_by do |filter|
  136. # sort_last: true — e.g. manual "empty" filter with "- (...)" text, which would otherwise sort first (ASCII)
  137. then: 0 else: 0 sort_group = filter[:sort_last] ? 1 : 0
  138. [sort_group, filter[:text].to_s]
  139. end
  140. column[:filters].each { |f| f.delete(:sort_last) }
  141. else: 0 if column[:empty_filter]
  142. then: 0 # Add empty filter
  143. filter_key = column[:filter_key] || column[:dataIndex]
  144. empty_filter = {
  145. base_key: column[:dataIndex] || column[:filter_key],
  146. type: :tag,
  147. text: "- (#{t(".filter_empty").downcase})",
  148. value: "empty",
  149. }
  150. empty_filter[:value] = "#{filter_key}$#{empty_filter[:value] || :_blank}"
  151. column[:filters] << empty_filter
  152. end
  153. end
  154. else: 0 then: 0 next unless column[:sortable]
  155. column[:sorter] = true
  156. column.delete(:sortable)
  157. column[:sortKey] = column[:sort_key] || column[:dataIndex]
  158. column.delete(:sort_key)
  159. then: 0 else: 0 then: 0 else: 0 column[:defaultSortOrder] = column[:default_sort] == :asc ? "ascend" : "descend" if column[:default_sort]
  160. column.delete(:default_sort)
  161. end
  162. visible_columns
  163. end
  164. end
  165. 1 def flat_filter_keys
  166. extract_leaf_filters(transformed_columns.flat_map { |c| c[:filters] }.compact)
  167. end
  168. 1 def extract_leaf_filters(tags)
  169. tags.flat_map do |tag|
  170. then: 0 else: 0 then: 0 if tag[:children]&.any?
  171. extract_leaf_filters(tag[:children])
  172. else: 0 else
  173. { key: tag[:value], text: tag[:text], base_key: tag[:base_key] }
  174. end
  175. end
  176. end
  177. 1 def columns_hash
  178. # TODO: remove dataIndex in favor of key. Use only one.
  179. @columns_hash ||= transformed_columns.index_by { |c| (c[:dataIndex] || c[:key]).to_sym }
  180. end
  181. # FIXME: running this method before render will cause a bug where the slots are not rendered
  182. 1 def transformed_columns_with_actions
  183. actions_count = rows.map { |row| row.actions.count }.max
  184. then: 0 else: 0 then: 0 if actions_count&.positive?
  185. transformed_columns + [action_column(actions_count)]
  186. else: 0 else
  187. transformed_columns
  188. end
  189. end
  190. 1 def unique_dom_id
  191. @unique_dom_id ||= "table_#{rand(10 ** 10)}"
  192. end
  193. 1 def indent_size
  194. expandable_config.indent_size || tree_indent_size
  195. end
  196. 1 private
  197. # FIXME: see other fixmes in this file
  198. # Warning!, due to a rendering bug (accessing slots before render),
  199. # this can only be called after data_source, and after the rendering has begun
  200. # There is a way to fix it, but it involves changes in the apps
  201. # (can be automated). You probably do _not_ want to use this function
  202. 1 def rows_with_children?
  203. rows.any? { |row| row.children.any? }
  204. end
  205. 1 def should_show_pagination?
  206. show_pagination
  207. end
  208. 1 def action_zone_enabled?
  209. selectable && action_bar?
  210. end
  211. 1 def data_source_id
  212. "#{id}_data_source"
  213. end
  214. 1 def pagination_id
  215. "#{id}_pagination"
  216. end
  217. 1 def slotting_mode?
  218. rows.any?
  219. end
  220. 1 def data_from_slotting
  221. @data_from_slotting ||= rows.map(&:data)
  222. end
  223. 1 def compose_filters(filters, column, key_prefix: nil)
  224. filters.map! do |filter|
  225. filter = filter.deep_symbolize_keys
  226. filter_key = filter[:key] || column[:filter_key] || column[:dataIndex]
  227. filter[:value] = "#{filter_key}$#{filter[:value]}"
  228. then: 0 else: 0 filter[:value] = "#{key_prefix}$#{filter[:value]}" if key_prefix.present?
  229. then: 0 else: 0 if filter[:enabled]
  230. column[:defaultFilteredValue] ||= []
  231. # Only leaf keys belong in filteredValue. Parent rows are structural;
  232. # a synthetic key like "Block name$" makes the Ant Design tree show all
  233. # descendants as checked even when only some micro-states are persisted.
  234. then: 0 else: 0 column[:defaultFilteredValue] << filter[:value].to_s if filter[:children].blank?
  235. end
  236. then: 0 else: 0 compose_filters(filter[:children], column, key_prefix: filter_key) if filter[:children].present?
  237. filter
  238. end
  239. end
  240. 1 def classes
  241. [
  242. "lui-table",
  243. then: 0 else: 0 !show_header ? "lui-table--no-header" : nil,
  244. then: 0 else: 0 rows_with_children? ? "lui-table--tree-mode" : nil,
  245. ].compact.join(" ")
  246. end
  247. # Check https://ant.design/components/table#design-token
  248. 1 def theme
  249. {
  250. token: {
  251. fontFamily: "IBM Plex Sans",
  252. borderRadius: 4,
  253. colorLink: find_color("general-global-black"),
  254. colorActive: find_color("general-global-black"),
  255. colorHover: find_color("general-global-black"),
  256. colorText: find_color("general-global-black"),
  257. },
  258. components: {
  259. Table: {
  260. footerBg: find_color("general-global-white"),
  261. headerBg: find_color("general-gray-100"),
  262. headerColor: find_color("general-gray-900"),
  263. headerSortActiveBg: find_color("general-gray-100"),
  264. headerSortHoverBg: find_color("general-gray-100"),
  265. headerSortSortBg: find_color("general-gray-100"),
  266. bodySortBg: find_color("general-global-white"),
  267. rowHoverBg: find_color("general-gray-100"),
  268. borderColor: find_color("general-gray-200"),
  269. headerBorderRadius: 4,
  270. rowSelectedBg: find_color("general-gray-100"),
  271. rowSelectedHoverBg: find_color("general-gray-100"),
  272. selectionColumnWidth: 32,
  273. headerSplitColor: find_color("general-gray-200"),
  274. cellPaddingInlineSM: 16,
  275. },
  276. Pagination: {
  277. itemActiveBg: "transparent",
  278. },
  279. },
  280. }
  281. end
  282. 1 def action_column(actions_count)
  283. {
  284. dataIndex: :_lui_actions,
  285. key: :_lui_actions,
  286. className: "lui-table__actions",
  287. width: 0,
  288. type: :html,
  289. }
  290. end
  291. 1 def default_sort_state
  292. transformed_columns.find { |column| column[:default_sort] }
  293. end
  294. 1 def footer_html
  295. then: 0 else: 0 if manageable_rows || footer.present?
  296. then: 0 else: 0 tag.div(class: @footer_args&.dig(:class)) do
  297. then: 0 else: 0 concat(render(add_new_button)) if manageable_rows
  298. then: 0 else: 0 concat(footer.to_s) if footer.present?
  299. end
  300. end
  301. end
  302. 1 def add_new_button
  303. LooposUi::Button.new(
  304. icon: :plus,
  305. text: t(".add_new_row"),
  306. type: :tertiary,
  307. kind: :neutral,
  308. size: :small,
  309. href: add_new_url,
  310. tag_options: { data: { target: "addNew" } },
  311. )
  312. end
  313. 1 class Row < LoopComponent
  314. 1 option :key
  315. 1 option :manageable_rows, default: -> { false }, type: Types::Bool
  316. 1 option :row_data, optional: true, type: Types::Hash, default: -> { {} }
  317. 1 option :table, optional: true
  318. 1 option :level, default: -> { 0 }
  319. 1 option :error_messages, optional: true, type: Types::Array.of(Types::String)
  320. 1 attr_writer :row_data
  321. 1 option :delete_action, optional: true do
  322. 1 option :method, type: Types::Coercible::String.enum("delete", "post", "patch"), default: -> { "delete" }
  323. 1 option :url, type: Types::String
  324. end
  325. 1 erb_template <<-ERB
  326. <% cells.each do |cell| %>
  327. <%= cell %>
  328. <% end %>
  329. <% actions.each do |action| %>
  330. <%= action %>
  331. <% end %>
  332. <% children.each do |child| %>
  333. <%= child %>
  334. <% end %>
  335. ERB
  336. 1 renders_many :cells, ->(*args, **kwargs, &block) {
  337. LooposUi::V2::Table::Cell.new(*args, row: self, table: table, **kwargs, &block)
  338. }
  339. 1 renders_many :children, ->(*args, **kwargs, &block) {
  340. LooposUi::V2::Table::Row.new(*args,**kwargs, table: table, level: level + 1, &block)
  341. }
  342. 1 renders_many :actions, ->(&block) {
  343. LooposUi::V2::Table::Cell.new(property: "_lui_action_cell", row: self, table: table, &block)
  344. }
  345. 1 def initialize(**kwargs)
  346. then: 0 else: 0 if kwargs[:manageable_rows] && !kwargs[:delete_action]
  347. raise ArgumentError, "row: delete_action is required when manageable_rows is true"
  348. end
  349. super
  350. end
  351. # Inside LooposUi::V2::Table::Row
  352. 1 def data
  353. @data ||= begin
  354. then: 0 else: 0 if error_messages.present? && cells.any?
  355. cells.first.error_messages = error_messages
  356. end
  357. datum = cells.to_h do |cell|
  358. [
  359. cell.property,
  360. ApplicationController.new.view_context.render(cell),
  361. ]
  362. end
  363. # Check if we need to render the actions column for THIS row
  364. then: 0 else: 0 if actions.any? || manageable_rows
  365. actions_html = ""
  366. # 1. Append custom actions if they exist
  367. then: 0 else: 0 if actions.any?
  368. actions_html += actions.map do |action|
  369. <<-HTML
  370. <div>
  371. #{ApplicationController.new.view_context.render(action)}
  372. </div>
  373. HTML
  374. end.join
  375. end
  376. # 2. Append the default delete button if rows are manageable
  377. then: 0 else: 0 if manageable_rows
  378. actions_html += <<-HTML
  379. <div>
  380. #{ApplicationController.new.view_context.render(LooposUi::Button.new(
  381. tooltip_text: I18n.t(".delete_row"),
  382. icon: :trash,
  383. type: :tertiary,
  384. kind: :neutral,
  385. size: :tiny,
  386. tag_options: {
  387. data: {
  388. row_key: key,
  389. injected_delete_button: true,
  390. },
  391. },
  392. ))}
  393. </div>
  394. HTML
  395. end
  396. # 3. Assign the combined HTML to the actions cell
  397. datum[:_lui_actions] = <<-HTML
  398. <div class="lui-table__actions">
  399. #{actions_html}
  400. </div>
  401. HTML
  402. end
  403. then: 0 else: 0 if children.any?
  404. datum[:children] = children.map(&:data)
  405. end
  406. datum.merge!(non_cell_data)
  407. end
  408. end
  409. 1 def non_cell_data
  410. {
  411. _lui_key: key,
  412. _lui_data: {
  413. delete_action: delete_action,
  414. **row_data,
  415. },
  416. }
  417. end
  418. # Have fun with this one
  419. 1 def tree_render(data,
  420. level = 0,
  421. get_children_fn:,
  422. key_fn:,
  423. render_row_fn:,
  424. parents: [])
  425. then: 0 else: 0 render_row_fn.call(self, data, level) if level.zero?
  426. (get_children_fn.call(data) || []).each do |child|
  427. with_child(key: key_fn.call(child, [*parents, data])) do |child_row|
  428. render_row_fn.call(child_row, child, level + 1)
  429. then: 0 else: 0 child_row.tree_render(
  430. child,
  431. level + 1,
  432. get_children_fn: get_children_fn,
  433. key_fn: key_fn,
  434. render_row_fn: render_row_fn,
  435. parents: [*parents, data],
  436. ) if (get_children_fn.call(child) || []).any?
  437. end
  438. end
  439. end
  440. end
  441. end
  442. end
  443. end

app/components/loopos_ui/v2/table/cell.html.erb

0.0% lines covered

0.0% branches covered

15 relevant lines. 0 lines covered and 15 lines missed.
8 total branches, 0 branches covered and 8 branches missed.
    
  1. <span class="<%= classes %>" style="<%= style %>" id="<%= unique_id %>" data-table-target="cell">
  2. then: 0 else: 0 then: 0 else: 0 <% if error_messages.present? || error_messages&.length.to_i > 0 %>
  3. <div class="absolute left-0 flex items-center justify-center">
  4. then: 0 <% if error_messages.any?(&:present?) %>
  5. <%= render LooposUi::IconTooltip.new(icon: :pipe, size: "16", icon_color: :danger) do |icon_tooltip| %>
  6. <% icon_tooltip.with_custom_tooltip do %>
  7. <%= render LooposUi::Tooltip.new do |tooltip| %>
  8. <div class="flex flex-col gap-1">
  9. <% error_messages.each do |message| %>
  10. <span><%= message %></span>
  11. <% end %>
  12. </div>
  13. <% end %>
  14. <% end %>
  15. <% end %>
  16. else: 0 <% else %>
  17. <%= render LooposUi::IconTooltip.new(icon: :pipe, size: "16", icon_color: :danger) %>
  18. <% end %>
  19. </div>
  20. <% end %>
  21. <div class="lui-table__cell-content_slot">
  22. <%= content_or_dash %>
  23. </div>
  24. <div class="flex flex-row items-center justify-center">
  25. then: 0 else: 0 <%= actions if actions.present? %>
  26. </div>
  27. </span>

app/components/loopos_ui/v2/table/cell.rb

50.0% lines covered

0.0% branches covered

36 relevant lines. 18 lines covered and 18 lines missed.
22 total branches, 0 branches covered and 22 branches missed.
    
  1. 1 module LooposUi
  2. 1 module V2
  3. 1 class Table
  4. 1 class Cell < LoopComponent
  5. 1 option :row
  6. 1 option :table, optional: true
  7. 1 option :property
  8. 1 option :error_messages, optional: true, type: Types::Array.of(Types::String)
  9. 1 renders_one :actions
  10. 1 private
  11. 1 def classes
  12. [
  13. "lui-table__cell-content",
  14. then: 0 else: 0 error_messages.present? ? "lui-table__cell-content-error" : nil,
  15. ].compact.join(" ")
  16. end
  17. 1 def content_or_dash
  18. then: 0 else: 0 return content if action_cell?
  19. then: 0 else: 0 return "-" if content.blank? || effectively_empty?
  20. try_typed_columns || content
  21. end
  22. 1 def try_typed_columns
  23. then: 0 else: 0 return if column.blank?
  24. else: 0 case column[:type]
  25. when: 0 when :date
  26. render(LooposUi::DateShow.new(date: content, format: :short))
  27. end
  28. end
  29. 1 def effectively_empty?
  30. fragment = Nokogiri::HTML::DocumentFragment.parse(content.to_s)
  31. fragment.children.all? do |c|
  32. text_blank = c.inner_text.strip.empty?
  33. has_visible_elements = c.at_css("i, img, iframe, video, object, embed, [data-react-class], [src], [href]")
  34. text_blank && has_visible_elements.nil?
  35. end
  36. end
  37. 1 def unique_id
  38. then: 0 else: 0 @unique_id ||= "#{table&.unique_dom_id}_cell_#{rand(10**10)}"
  39. end
  40. 1 def column
  41. then: 0 else: 0 then: 0 else: 0 table&.columns_hash&.dig(property)
  42. end
  43. 1 def action_cell?
  44. property == "_lui_actions"
  45. end
  46. # TODO: sometimes, we do not have the table reference :/
  47. # Maybe we could pass the column as an option, but this feels convoluted
  48. # Cells should not be rendered without tables, but we need this to manageable_rows functionality
  49. 1 def style
  50. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 column&.slice(:width, :max_width, :min_width)
  51. &.map { |k, v| "#{k.to_s.dasherize}: #{v.to_i - table.indent_size * row.level}px;" }
  52. &.join(" ")
  53. end
  54. end
  55. end
  56. end
  57. end

app/components/loopos_ui/v2/table/header.html.erb

0.0% lines covered

0.0% branches covered

9 relevant lines. 0 lines covered and 9 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. <div class="lui-table_header" data-controller="table-filters">
  2. then: 0 else: 0 <% if searchbar.present? && searchable %>
  3. <div class="w-80">
  4. <%= searchbar %>
  5. </div>
  6. <% end %>
  7. then: 0 else: 0 <% filters.each do |filter| %>
  8. <%= filter %>
  9. <% end if show_filters %>
  10. <div class="hidden" data-table-filters-target="button">
  11. <%= render LooposUi::Button.new(
  12. icon: :eraser,
  13. text: t(".clear_all_filters"),
  14. type: :tertiary,
  15. kind: :neutral,
  16. size: :small,
  17. tag_options: {
  18. data: { action: "click->table-filters#clearAllFilters" }
  19. }
  20. ) %>
  21. </div>
  22. </div>

app/components/loopos_ui/v2/table/header.rb

100.0% lines covered

0.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. 1 module LooposUi
  2. 1 module V2
  3. 1 class Table
  4. 1 class Header < LoopComponent
  5. 1 option :show_filters, Types::Bool, default: -> { true }
  6. 1 option :searchable, Types::Bool, default: -> { true }
  7. 1 renders_one :searchbar, LooposUi::Inputs::Search
  8. 1 renders_many :filters, LooposUi::TableFilter
  9. 1 then: 0 else: 0 delegate :t, to: LooposUi::V2::Table
  10. end
  11. end
  12. end
  13. end

app/components/loopos_ui/wysiwyg.rb

85.71% lines covered

100.0% branches covered

14 relevant lines. 12 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 class Wysiwyg < LoopComponent
  3. 1 option :initial_value, optional: true
  4. 1 option :input_name, default: -> { "" }
  5. 1 option :small, default: -> { false }
  6. 1 option :language, default: -> { "en" }
  7. 1 option :app, optional: true
  8. 1 mod :small
  9. 1 mod :neutral_small, condition: -> { app_kind.to_s == "neutral" && small? }
  10. 1 private
  11. 1 def small?
  12. small || false
  13. end
  14. 1 def app_kind
  15. app || LooposUi.config.try(:app_type) || "core"
  16. end
  17. end
  18. end

app/components/loopos_ui/wysiwyg/wysiwyg.html.erb

0.0% lines covered

100.0% branches covered

5 relevant lines. 0 lines covered and 5 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. <% dropkiq_preview_id = "wysiwyg-#{input_name}" %>
  2. <%= tag.div(class: classes) do %>
  3. <%= react_component(
  4. "Wysiwyg",
  5. {
  6. currentValue: initial_value,
  7. name: input_name,
  8. isSmallTheme: small?,
  9. app: app_kind,
  10. lang: language,
  11. dropkiqPreviewId: dropkiq_preview_id
  12. }
  13. ) %>
  14. <% end %>
  15. <!-- div id="<%= dropkiq_preview_id %>" class='dropkiq-preview'></!-->