loading
Generated 2026-05-12T13:06:20+00:00

All Files ( 38.38% covered at 4.71 hits/line )

652 files in total.
13311 relevant lines, 5109 lines covered and 8202 lines missed. ( 38.38% )
4063 total branches, 359 branches covered and 3704 branches missed. ( 8.84% )
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 100.00 % 5 4 4 0 2.50 100.00 % 0 0 0
app/components/loopos_ui/action_buttons/button_group.html.erb 100.00 % 5 3 3 0 7.33 100.00 % 0 0 0
app/components/loopos_ui/action_menu.rb 60.00 % 30 15 9 6 0.60 0.00 % 2 0 2
app/components/loopos_ui/action_menu/action_menu.html.erb 0.00 % 43 24 0 24 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 100.00 % 15 10 10 0 18.00 100.00 % 0 0 0
app/components/loopos_ui/app_logo.rb 100.00 % 26 15 15 0 1.33 50.00 % 4 2 2
app/components/loopos_ui/app_logo/app_logo.html.erb 100.00 % 3 2 2 0 2.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 100.00 % 10 6 6 0 4.67 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 88.03 % 278 117 103 14 94.28 68.49 % 73 50 23
app/components/loopos_ui/button/button.html.erb 76.00 % 35 25 19 6 142.48 59.09 % 22 13 9
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 % 56 20 0 20 0.00 0.00 % 8 0 8
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 % 12 10 10 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/chat/chat.html.erb 0.00 % 12 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 93.33 % 43 15 14 1 1.53 50.00 % 6 3 3
app/components/loopos_ui/counter/counter.html.erb 71.43 % 11 7 5 2 1.71 50.00 % 2 1 1
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 41.07 % 148 56 23 33 0.41 0.00 % 27 0 27
app/components/loopos_ui/date_picker/date_picker.html.erb 0.00 % 45 13 0 13 0.00 0.00 % 5 0 5
app/components/loopos_ui/date_picker/field.rb 84.62 % 21 13 11 2 0.85 100.00 % 0 0 0
app/components/loopos_ui/date_picker/field/field.html.erb 0.00 % 14 6 0 6 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/month.rb 100.00 % 12 8 8 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/month/month.html.erb 0.00 % 8 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 % 11 7 7 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/range/range.html.erb 0.00 % 8 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/single.rb 100.00 % 12 8 8 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/single/single.html.erb 0.00 % 8 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/year.rb 100.00 % 12 8 8 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/year/year.html.erb 0.00 % 8 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 100.00 % 18 11 11 0 2.82 100.00 % 0 0 0
app/components/loopos_ui/dots/dots.html.erb 100.00 % 5 3 3 0 7.33 100.00 % 2 2 0
app/components/loopos_ui/double_state_label.html.erb 100.00 % 14 5 5 0 120.00 100.00 % 0 0 0
app/components/loopos_ui/double_state_label.rb 100.00 % 22 15 15 0 17.40 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 100.00 % 30 12 12 0 11.75 100.00 % 12 12 0
app/components/loopos_ui/dummy_slot/dummy_slot.html.erb 100.00 % 3 2 2 0 51.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 83.78 % 80 37 31 6 12.89 35.00 % 20 7 13
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 44.44 % 99 54 24 30 0.44 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 % 16 0 16
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 100.00 % 25 12 12 0 5.00 50.00 % 2 1 1
app/components/loopos_ui/flex_layout/flex_layout.html.erb 100.00 % 5 4 4 0 7.50 100.00 % 0 0 0
app/components/loopos_ui/flex_layout/media_query_sizes_concern.rb 100.00 % 41 20 20 0 30.70 70.00 % 10 7 3
app/components/loopos_ui/flex_layout/section.html.erb 100.00 % 3 2 2 0 36.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 % 101 57 0 57 0.00 0.00 % 16 0 16
app/components/loopos_ui/form_entry.rb 90.00 % 16 10 9 1 0.90 100.00 % 0 0 0
app/components/loopos_ui/form_entry/form_entry.html.erb 0.00 % 16 9 0 9 0.00 0.00 % 8 0 8
app/components/loopos_ui/form_entry_ai.rb 100.00 % 9 7 7 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/form_entry_ai/form_entry_ai.html.erb 0.00 % 40 7 0 7 0.00 100.00 % 0 0 0
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 2.83 0.00 % 4 0 4
app/components/loopos_ui/header/header.html.erb 76.67 % 36 30 23 7 13.47 63.64 % 22 14 8
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 74.07 % 51 27 20 7 1.07 27.27 % 11 3 8
app/components/loopos_ui/human_view/human_view.html.erb 66.67 % 27 12 8 4 1.33 33.33 % 6 2 4
app/components/loopos_ui/human_view/tree_node.rb 95.65 % 43 23 22 1 6.83 77.78 % 9 7 2
app/components/loopos_ui/human_view/tree_node/tree_node.html.erb 100.00 % 37 19 19 0 10.21 100.00 % 2 2 0
app/components/loopos_ui/icon.rb 100.00 % 25 12 12 0 3.92 100.00 % 0 0 0
app/components/loopos_ui/icon/icon.html.erb 100.00 % 4 3 3 0 16.00 50.00 % 4 2 2
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 78.05 % 109 41 32 9 1.17 0.00 % 6 0 6
app/components/loopos_ui/inline_edit_component/base.html.erb 46.15 % 23 13 6 7 0.92 40.00 % 10 4 6
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 95.45 % 51 22 21 1 5.18 62.50 % 8 5 3
app/components/loopos_ui/input/input.html.erb 92.31 % 75 26 24 2 13.85 71.43 % 14 10 4
app/components/loopos_ui/inputs/ai.rb 75.00 % 13 8 6 2 0.75 0.00 % 2 0 2
app/components/loopos_ui/inputs/ai/ai.html.erb 0.00 % 36 10 0 10 0.00 0.00 % 2 0 2
app/components/loopos_ui/inputs/base_input_concern.rb 90.91 % 81 33 30 3 18.03 50.00 % 6 3 3
app/components/loopos_ui/inputs/checkbox.rb 70.00 % 31 20 14 6 0.70 0.00 % 8 0 8
app/components/loopos_ui/inputs/checkbox/checkbox.html.erb 0.00 % 28 8 0 8 0.00 0.00 % 2 0 2
app/components/loopos_ui/inputs/combobox.rb 44.83 % 299 174 78 96 0.45 0.00 % 82 0 82
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 46.43 % 95 56 26 30 0.95 0.00 % 24 0 24
app/components/loopos_ui/inputs/file/file_actions.html.erb 0.00 % 31 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 % 50 14 0 14 0.00 0.00 % 2 0 2
app/components/loopos_ui/inputs/file/form_file.rb 48.00 % 53 25 12 13 0.48 0.00 % 14 0 14
app/components/loopos_ui/inputs/file/multiple.html.erb 0.00 % 84 32 0 32 0.00 0.00 % 8 0 8
app/components/loopos_ui/inputs/file/multiple.rb 48.28 % 64 29 14 15 0.48 0.00 % 18 0 18
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 % 67 23 0 23 0.00 0.00 % 14 0 14
app/components/loopos_ui/inputs/file/single.rb 48.28 % 63 29 14 15 0.48 0.00 % 16 0 16
app/components/loopos_ui/inputs/money.rb 93.33 % 37 15 14 1 1.93 33.33 % 3 1 2
app/components/loopos_ui/inputs/money/money.html.erb 100.00 % 13 7 7 0 2.00 100.00 % 0 0 0
app/components/loopos_ui/inputs/number.rb 90.00 % 28 10 9 1 0.90 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/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 100.00 % 31 13 13 0 1.92 100.00 % 0 0 0
app/components/loopos_ui/inputs/search/search.html.erb 100.00 % 24 12 12 0 2.17 50.00 % 2 1 1
app/components/loopos_ui/inputs/select.rb 65.52 % 61 29 19 10 0.66 0.00 % 11 0 11
app/components/loopos_ui/inputs/select/select.html.erb 0.00 % 62 23 0 23 0.00 0.00 % 18 0 18
app/components/loopos_ui/inputs/select2.rb 57.58 % 132 66 38 28 0.58 0.00 % 27 0 27
app/components/loopos_ui/inputs/select2/select2.html.erb 0.00 % 140 42 0 42 0.00 0.00 % 32 0 32
app/components/loopos_ui/inputs/text.rb 100.00 % 21 8 8 0 4.63 100.00 % 0 0 0
app/components/loopos_ui/inputs/text/text.html.erb 100.00 % 3 2 2 0 10.00 100.00 % 0 0 0
app/components/loopos_ui/inputs/text_area.rb 100.00 % 33 15 15 0 4.80 100.00 % 2 2 0
app/components/loopos_ui/inputs/text_area/text_area.html.erb 100.00 % 3 2 2 0 6.00 100.00 % 0 0 0
app/components/loopos_ui/item/state_label.rb 91.43 % 74 35 32 3 93.14 62.50 % 8 5 3
app/components/loopos_ui/item/state_label/state_label.html.erb 100.00 % 24 7 7 0 68.00 100.00 % 2 2 0
app/components/loopos_ui/item_value.rb 86.67 % 23 15 13 2 1.67 100.00 % 0 0 0
app/components/loopos_ui/item_value/item_value.html.erb 100.00 % 10 1 1 0 2.00 100.00 % 0 0 0
app/components/loopos_ui/label.rb 100.00 % 36 19 19 0 142.84 50.00 % 6 3 3
app/components/loopos_ui/label/label.html.erb 87.50 % 14 8 7 1 215.25 72.73 % 11 8 3
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 100.00 % 15 10 10 0 1.70 100.00 % 0 0 0
app/components/loopos_ui/link/link.html.erb 75.00 % 12 8 6 2 1.75 50.00 % 4 2 2
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 62.83 % 272 113 71 42 1.42 27.50 % 40 11 29
app/components/loopos_ui/log/factories.rb 100.00 % 39 19 19 0 1.68 50.00 % 6 3 3
app/components/loopos_ui/log/factories/base.rb 61.11 % 34 18 11 7 0.78 0.00 % 2 0 2
app/components/loopos_ui/log/factories/core_item_log_presenter_hash.rb 29.73 % 85 37 11 26 0.30 0.00 % 14 0 14
app/components/loopos_ui/log/factories/script_log.rb 77.55 % 126 49 38 11 1.41 50.00 % 6 3 3
app/components/loopos_ui/log/log.html.erb 72.41 % 50 29 21 8 2.07 33.33 % 6 2 4
app/components/loopos_ui/log/script/info_modal_body.html.erb 46.15 % 63 39 18 21 0.92 18.18 % 44 8 36
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 100.00 % 10 7 7 0 1.57 100.00 % 0 0 0
app/components/loopos_ui/logo/logo.html.erb 100.00 % 8 4 4 0 2.50 50.00 % 2 1 1
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 91.43 % 79 35 32 3 205.23 33.33 % 6 2 4
app/components/loopos_ui/m_icon/m_icon.html.erb 75.00 % 7 4 3 1 188.00 50.00 % 2 1 1
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 66.67 % 15 9 6 3 1.78 50.00 % 4 2 2
app/components/loopos_ui/marketplace/header.rb 100.00 % 16 9 9 0 1.11 50.00 % 2 1 1
app/components/loopos_ui/marketplace/header/header.html.erb 100.00 % 10 7 7 0 2.57 50.00 % 2 1 1
app/components/loopos_ui/marketplace/item_card.rb 68.06 % 129 72 49 23 2.42 32.14 % 28 9 19
app/components/loopos_ui/marketplace/item_card/item_card.html.erb 87.50 % 35 24 21 3 7.50 44.44 % 18 8 10
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 100.00 % 19 11 11 0 4.55 100.00 % 0 0 0
app/components/loopos_ui/mini_app/mini_app.html.erb 100.00 % 11 4 4 0 12.50 100.00 % 0 0 0
app/components/loopos_ui/modal.rb 89.66 % 64 29 26 3 1.14 50.00 % 2 1 1
app/components/loopos_ui/modal/modal.html.erb 60.00 % 48 25 15 10 1.28 35.71 % 14 5 9
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 67.11 % 200 76 51 25 0.76 25.93 % 27 7 20
app/components/loopos_ui/model_association_list/model_association_list.html.erb 10.53 % 35 19 2 17 0.11 25.00 % 4 1 3
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 % 121 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 % 157 83 0 83 0.00 0.00 % 26 0 26
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 % 160 95 0 95 0.00 0.00 % 40 0 40
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 88.89 % 17 9 8 1 0.89 0.00 % 2 0 2
app/components/loopos_ui/page_section/item_tab/notes/notes.html.erb 0.00 % 25 12 0 12 0.00 100.00 % 0 0 0
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.00 % 90 50 15 35 0.30 0.00 % 34 0 34
app/components/loopos_ui/page_section/item_tab/services/services.html.erb 0.00 % 127 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 % 92 18 0 18 0.00 0.00 % 20 0 20
app/components/loopos_ui/pages/item_show.rb 54.17 % 101 48 26 22 0.54 0.00 % 8 0 8
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 % 56 28 0 28 0.00 0.00 % 2 0 2
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 % 55 26 0 26 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 % 95 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/preview.html.erb 0.00 % 15 10 0 10 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/preview.rb 100.00 % 7 4 4 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/protocol_builder_component.erb 0.00 % 7 5 0 5 0.00 0.00 % 2 0 2
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 % 63 29 0 29 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 % 216 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 % 171 87 0 87 0.00 0.00 % 38 0 38
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.57 % 52 28 15 13 0.54 0.00 % 6 0 6
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/radio_card.rb 90.91 % 38 22 20 2 1.91 33.33 % 3 1 2
app/components/loopos_ui/radio_card/content.html.erb 66.67 % 26 12 8 4 4.00 50.00 % 2 1 1
app/components/loopos_ui/radio_card/radio_card.html.erb 100.00 % 6 4 4 0 9.00 100.00 % 0 0 0
app/components/loopos_ui/script_editor.rb 100.00 % 22 17 17 0 1.24 100.00 % 0 0 0
app/components/loopos_ui/script_editor/script_editor.html.erb 100.00 % 17 1 1 0 2.00 100.00 % 0 0 0
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 41.86 % 104 43 18 25 0.47 0.00 % 18 0 18
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 46.15 % 77 39 18 21 0.49 0.00 % 12 0 12
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 41.30 % 89 46 19 27 0.43 0.00 % 14 0 14
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 45.00 % 94 40 18 22 0.50 0.00 % 12 0 12
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 42.22 % 108 45 19 26 0.44 0.00 % 18 0 18
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 41.86 % 95 43 18 25 0.44 0.00 % 16 0 16
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 85.71 % 27 7 6 1 0.86 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 63.64 % 47 22 14 8 0.64 0.00 % 6 0 6
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.78 % 184 92 32 60 0.35 0.00 % 48 0 48
app/components/loopos_ui/sidebar/v1/sidebar.html.erb 0.00 % 9 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar/v1/sidebar.rb 63.16 % 39 19 12 7 0.63 0.00 % 4 0 4
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 80.00 % 32 15 12 3 33.67 60.00 % 5 3 2
app/components/loopos_ui/spinner/spinner.html.erb 100.00 % 12 1 1 0 166.00 100.00 % 0 0 0
app/components/loopos_ui/state_label.rb 96.30 % 79 27 26 1 62.19 58.33 % 12 7 5
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 100.00 % 25 14 14 0 3.21 50.00 % 2 1 1
app/components/loopos_ui/tabs_content/tabs_content.html.erb 100.00 % 7 5 5 0 6.00 100.00 % 0 0 0
app/components/loopos_ui/tabs_layout.rb 82.54 % 121 63 52 11 3.29 37.50 % 16 6 10
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 37.50 % 14 8 3 5 3.00 50.00 % 2 1 1
app/components/loopos_ui/tabs_layout/tabs_layout.html.erb 78.95 % 55 19 15 4 3.05 35.71 % 14 5 9
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 95.24 % 55 21 20 1 24.76 83.33 % 6 5 1
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 100.00 % 7 1 1 0 8.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 87.30 % 133 63 55 8 71.22 50.00 % 26 13 13
app/components/loopos_ui/token/token.html.erb 32.56 % 69 43 14 29 52.28 29.41 % 34 10 24
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 93.33 % 41 15 14 1 9.00 100.00 % 0 0 0
app/components/loopos_ui/tooltip/tooltip.html.erb 77.78 % 28 18 14 4 26.67 100.00 % 4 4 0
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 78.26 % 37 23 18 5 0.78 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 44.07 % 283 118 52 66 26.08 0.00 % 66 0 66
app/components/loopos_ui/v2/image/image.html.erb 0.00 % 255 71 0 71 0.00 0.00 % 38 0 38
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 51.28 % 116 39 20 19 1.54 38.89 % 18 7 11
app/components/loopos_ui/v2/table.rb 78.14 % 516 215 168 47 10.90 39.51 % 81 32 49
app/components/loopos_ui/v2/table/cell.html.erb 33.33 % 27 15 5 10 85.33 37.50 % 8 3 5
app/components/loopos_ui/v2/table/cell.rb 94.44 % 72 36 34 2 124.94 68.18 % 22 15 7
app/components/loopos_ui/v2/table/header.html.erb 88.89 % 22 9 8 1 1.78 50.00 % 4 2 2
app/components/loopos_ui/v2/table/header.rb 100.00 % 14 9 9 0 1.22 0.00 % 2 0 2
app/components/loopos_ui/wysiwyg.rb 100.00 % 22 14 14 0 6.71 100.00 % 0 0 0
app/components/loopos_ui/wysiwyg/wysiwyg.html.erb 100.00 % 15 5 5 0 14.40 100.00 % 0 0 0

Components ( 38.38% covered at 4.71 hits/line )

652 files in total.
13311 relevant lines, 5109 lines covered and 8202 lines missed. ( 38.38% )
4063 total branches, 359 branches covered and 3704 branches missed. ( 8.84% )
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 100.00 % 5 4 4 0 2.50 100.00 % 0 0 0
app/components/loopos_ui/action_buttons/button_group.html.erb 100.00 % 5 3 3 0 7.33 100.00 % 0 0 0
app/components/loopos_ui/action_menu.rb 60.00 % 30 15 9 6 0.60 0.00 % 2 0 2
app/components/loopos_ui/action_menu/action_menu.html.erb 0.00 % 43 24 0 24 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 100.00 % 15 10 10 0 18.00 100.00 % 0 0 0
app/components/loopos_ui/app_logo.rb 100.00 % 26 15 15 0 1.33 50.00 % 4 2 2
app/components/loopos_ui/app_logo/app_logo.html.erb 100.00 % 3 2 2 0 2.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 100.00 % 10 6 6 0 4.67 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 88.03 % 278 117 103 14 94.28 68.49 % 73 50 23
app/components/loopos_ui/button/button.html.erb 76.00 % 35 25 19 6 142.48 59.09 % 22 13 9
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 % 56 20 0 20 0.00 0.00 % 8 0 8
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 % 12 10 10 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/chat/chat.html.erb 0.00 % 12 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 93.33 % 43 15 14 1 1.53 50.00 % 6 3 3
app/components/loopos_ui/counter/counter.html.erb 71.43 % 11 7 5 2 1.71 50.00 % 2 1 1
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 41.07 % 148 56 23 33 0.41 0.00 % 27 0 27
app/components/loopos_ui/date_picker/date_picker.html.erb 0.00 % 45 13 0 13 0.00 0.00 % 5 0 5
app/components/loopos_ui/date_picker/field.rb 84.62 % 21 13 11 2 0.85 100.00 % 0 0 0
app/components/loopos_ui/date_picker/field/field.html.erb 0.00 % 14 6 0 6 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/month.rb 100.00 % 12 8 8 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/month/month.html.erb 0.00 % 8 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 % 11 7 7 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/range/range.html.erb 0.00 % 8 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/single.rb 100.00 % 12 8 8 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/single/single.html.erb 0.00 % 8 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/year.rb 100.00 % 12 8 8 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/year/year.html.erb 0.00 % 8 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 100.00 % 18 11 11 0 2.82 100.00 % 0 0 0
app/components/loopos_ui/dots/dots.html.erb 100.00 % 5 3 3 0 7.33 100.00 % 2 2 0
app/components/loopos_ui/double_state_label.html.erb 100.00 % 14 5 5 0 120.00 100.00 % 0 0 0
app/components/loopos_ui/double_state_label.rb 100.00 % 22 15 15 0 17.40 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 100.00 % 30 12 12 0 11.75 100.00 % 12 12 0
app/components/loopos_ui/dummy_slot/dummy_slot.html.erb 100.00 % 3 2 2 0 51.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 83.78 % 80 37 31 6 12.89 35.00 % 20 7 13
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 44.44 % 99 54 24 30 0.44 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 % 16 0 16
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 100.00 % 25 12 12 0 5.00 50.00 % 2 1 1
app/components/loopos_ui/flex_layout/flex_layout.html.erb 100.00 % 5 4 4 0 7.50 100.00 % 0 0 0
app/components/loopos_ui/flex_layout/media_query_sizes_concern.rb 100.00 % 41 20 20 0 30.70 70.00 % 10 7 3
app/components/loopos_ui/flex_layout/section.html.erb 100.00 % 3 2 2 0 36.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 % 101 57 0 57 0.00 0.00 % 16 0 16
app/components/loopos_ui/form_entry.rb 90.00 % 16 10 9 1 0.90 100.00 % 0 0 0
app/components/loopos_ui/form_entry/form_entry.html.erb 0.00 % 16 9 0 9 0.00 0.00 % 8 0 8
app/components/loopos_ui/form_entry_ai.rb 100.00 % 9 7 7 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/form_entry_ai/form_entry_ai.html.erb 0.00 % 40 7 0 7 0.00 100.00 % 0 0 0
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 2.83 0.00 % 4 0 4
app/components/loopos_ui/header/header.html.erb 76.67 % 36 30 23 7 13.47 63.64 % 22 14 8
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 74.07 % 51 27 20 7 1.07 27.27 % 11 3 8
app/components/loopos_ui/human_view/human_view.html.erb 66.67 % 27 12 8 4 1.33 33.33 % 6 2 4
app/components/loopos_ui/human_view/tree_node.rb 95.65 % 43 23 22 1 6.83 77.78 % 9 7 2
app/components/loopos_ui/human_view/tree_node/tree_node.html.erb 100.00 % 37 19 19 0 10.21 100.00 % 2 2 0
app/components/loopos_ui/icon.rb 100.00 % 25 12 12 0 3.92 100.00 % 0 0 0
app/components/loopos_ui/icon/icon.html.erb 100.00 % 4 3 3 0 16.00 50.00 % 4 2 2
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 78.05 % 109 41 32 9 1.17 0.00 % 6 0 6
app/components/loopos_ui/inline_edit_component/base.html.erb 46.15 % 23 13 6 7 0.92 40.00 % 10 4 6
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 95.45 % 51 22 21 1 5.18 62.50 % 8 5 3
app/components/loopos_ui/input/input.html.erb 92.31 % 75 26 24 2 13.85 71.43 % 14 10 4
app/components/loopos_ui/inputs/ai.rb 75.00 % 13 8 6 2 0.75 0.00 % 2 0 2
app/components/loopos_ui/inputs/ai/ai.html.erb 0.00 % 36 10 0 10 0.00 0.00 % 2 0 2
app/components/loopos_ui/inputs/base_input_concern.rb 90.91 % 81 33 30 3 18.03 50.00 % 6 3 3
app/components/loopos_ui/inputs/checkbox.rb 70.00 % 31 20 14 6 0.70 0.00 % 8 0 8
app/components/loopos_ui/inputs/checkbox/checkbox.html.erb 0.00 % 28 8 0 8 0.00 0.00 % 2 0 2
app/components/loopos_ui/inputs/combobox.rb 44.83 % 299 174 78 96 0.45 0.00 % 82 0 82
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 46.43 % 95 56 26 30 0.95 0.00 % 24 0 24
app/components/loopos_ui/inputs/file/file_actions.html.erb 0.00 % 31 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 % 50 14 0 14 0.00 0.00 % 2 0 2
app/components/loopos_ui/inputs/file/form_file.rb 48.00 % 53 25 12 13 0.48 0.00 % 14 0 14
app/components/loopos_ui/inputs/file/multiple.html.erb 0.00 % 84 32 0 32 0.00 0.00 % 8 0 8
app/components/loopos_ui/inputs/file/multiple.rb 48.28 % 64 29 14 15 0.48 0.00 % 18 0 18
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 % 67 23 0 23 0.00 0.00 % 14 0 14
app/components/loopos_ui/inputs/file/single.rb 48.28 % 63 29 14 15 0.48 0.00 % 16 0 16
app/components/loopos_ui/inputs/money.rb 93.33 % 37 15 14 1 1.93 33.33 % 3 1 2
app/components/loopos_ui/inputs/money/money.html.erb 100.00 % 13 7 7 0 2.00 100.00 % 0 0 0
app/components/loopos_ui/inputs/number.rb 90.00 % 28 10 9 1 0.90 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/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 100.00 % 31 13 13 0 1.92 100.00 % 0 0 0
app/components/loopos_ui/inputs/search/search.html.erb 100.00 % 24 12 12 0 2.17 50.00 % 2 1 1
app/components/loopos_ui/inputs/select.rb 65.52 % 61 29 19 10 0.66 0.00 % 11 0 11
app/components/loopos_ui/inputs/select/select.html.erb 0.00 % 62 23 0 23 0.00 0.00 % 18 0 18
app/components/loopos_ui/inputs/select2.rb 57.58 % 132 66 38 28 0.58 0.00 % 27 0 27
app/components/loopos_ui/inputs/select2/select2.html.erb 0.00 % 140 42 0 42 0.00 0.00 % 32 0 32
app/components/loopos_ui/inputs/text.rb 100.00 % 21 8 8 0 4.63 100.00 % 0 0 0
app/components/loopos_ui/inputs/text/text.html.erb 100.00 % 3 2 2 0 10.00 100.00 % 0 0 0
app/components/loopos_ui/inputs/text_area.rb 100.00 % 33 15 15 0 4.80 100.00 % 2 2 0
app/components/loopos_ui/inputs/text_area/text_area.html.erb 100.00 % 3 2 2 0 6.00 100.00 % 0 0 0
app/components/loopos_ui/item/state_label.rb 91.43 % 74 35 32 3 93.14 62.50 % 8 5 3
app/components/loopos_ui/item/state_label/state_label.html.erb 100.00 % 24 7 7 0 68.00 100.00 % 2 2 0
app/components/loopos_ui/item_value.rb 86.67 % 23 15 13 2 1.67 100.00 % 0 0 0
app/components/loopos_ui/item_value/item_value.html.erb 100.00 % 10 1 1 0 2.00 100.00 % 0 0 0
app/components/loopos_ui/label.rb 100.00 % 36 19 19 0 142.84 50.00 % 6 3 3
app/components/loopos_ui/label/label.html.erb 87.50 % 14 8 7 1 215.25 72.73 % 11 8 3
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 100.00 % 15 10 10 0 1.70 100.00 % 0 0 0
app/components/loopos_ui/link/link.html.erb 75.00 % 12 8 6 2 1.75 50.00 % 4 2 2
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 62.83 % 272 113 71 42 1.42 27.50 % 40 11 29
app/components/loopos_ui/log/factories.rb 100.00 % 39 19 19 0 1.68 50.00 % 6 3 3
app/components/loopos_ui/log/factories/base.rb 61.11 % 34 18 11 7 0.78 0.00 % 2 0 2
app/components/loopos_ui/log/factories/core_item_log_presenter_hash.rb 29.73 % 85 37 11 26 0.30 0.00 % 14 0 14
app/components/loopos_ui/log/factories/script_log.rb 77.55 % 126 49 38 11 1.41 50.00 % 6 3 3
app/components/loopos_ui/log/log.html.erb 72.41 % 50 29 21 8 2.07 33.33 % 6 2 4
app/components/loopos_ui/log/script/info_modal_body.html.erb 46.15 % 63 39 18 21 0.92 18.18 % 44 8 36
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 100.00 % 10 7 7 0 1.57 100.00 % 0 0 0
app/components/loopos_ui/logo/logo.html.erb 100.00 % 8 4 4 0 2.50 50.00 % 2 1 1
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 91.43 % 79 35 32 3 205.23 33.33 % 6 2 4
app/components/loopos_ui/m_icon/m_icon.html.erb 75.00 % 7 4 3 1 188.00 50.00 % 2 1 1
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 66.67 % 15 9 6 3 1.78 50.00 % 4 2 2
app/components/loopos_ui/marketplace/header.rb 100.00 % 16 9 9 0 1.11 50.00 % 2 1 1
app/components/loopos_ui/marketplace/header/header.html.erb 100.00 % 10 7 7 0 2.57 50.00 % 2 1 1
app/components/loopos_ui/marketplace/item_card.rb 68.06 % 129 72 49 23 2.42 32.14 % 28 9 19
app/components/loopos_ui/marketplace/item_card/item_card.html.erb 87.50 % 35 24 21 3 7.50 44.44 % 18 8 10
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 100.00 % 19 11 11 0 4.55 100.00 % 0 0 0
app/components/loopos_ui/mini_app/mini_app.html.erb 100.00 % 11 4 4 0 12.50 100.00 % 0 0 0
app/components/loopos_ui/modal.rb 89.66 % 64 29 26 3 1.14 50.00 % 2 1 1
app/components/loopos_ui/modal/modal.html.erb 60.00 % 48 25 15 10 1.28 35.71 % 14 5 9
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 67.11 % 200 76 51 25 0.76 25.93 % 27 7 20
app/components/loopos_ui/model_association_list/model_association_list.html.erb 10.53 % 35 19 2 17 0.11 25.00 % 4 1 3
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 % 121 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 % 157 83 0 83 0.00 0.00 % 26 0 26
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 % 160 95 0 95 0.00 0.00 % 40 0 40
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 88.89 % 17 9 8 1 0.89 0.00 % 2 0 2
app/components/loopos_ui/page_section/item_tab/notes/notes.html.erb 0.00 % 25 12 0 12 0.00 100.00 % 0 0 0
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.00 % 90 50 15 35 0.30 0.00 % 34 0 34
app/components/loopos_ui/page_section/item_tab/services/services.html.erb 0.00 % 127 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 % 92 18 0 18 0.00 0.00 % 20 0 20
app/components/loopos_ui/pages/item_show.rb 54.17 % 101 48 26 22 0.54 0.00 % 8 0 8
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 % 56 28 0 28 0.00 0.00 % 2 0 2
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 % 55 26 0 26 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 % 95 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/preview.html.erb 0.00 % 15 10 0 10 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/preview.rb 100.00 % 7 4 4 0 1.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/protocol_builder_component.erb 0.00 % 7 5 0 5 0.00 0.00 % 2 0 2
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 % 63 29 0 29 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 % 216 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 % 171 87 0 87 0.00 0.00 % 38 0 38
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.57 % 52 28 15 13 0.54 0.00 % 6 0 6
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/radio_card.rb 90.91 % 38 22 20 2 1.91 33.33 % 3 1 2
app/components/loopos_ui/radio_card/content.html.erb 66.67 % 26 12 8 4 4.00 50.00 % 2 1 1
app/components/loopos_ui/radio_card/radio_card.html.erb 100.00 % 6 4 4 0 9.00 100.00 % 0 0 0
app/components/loopos_ui/script_editor.rb 100.00 % 22 17 17 0 1.24 100.00 % 0 0 0
app/components/loopos_ui/script_editor/script_editor.html.erb 100.00 % 17 1 1 0 2.00 100.00 % 0 0 0
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 41.86 % 104 43 18 25 0.47 0.00 % 18 0 18
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 46.15 % 77 39 18 21 0.49 0.00 % 12 0 12
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 41.30 % 89 46 19 27 0.43 0.00 % 14 0 14
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 45.00 % 94 40 18 22 0.50 0.00 % 12 0 12
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 42.22 % 108 45 19 26 0.44 0.00 % 18 0 18
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 41.86 % 95 43 18 25 0.44 0.00 % 16 0 16
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 85.71 % 27 7 6 1 0.86 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 63.64 % 47 22 14 8 0.64 0.00 % 6 0 6
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.78 % 184 92 32 60 0.35 0.00 % 48 0 48
app/components/loopos_ui/sidebar/v1/sidebar.html.erb 0.00 % 9 4 0 4 0.00 100.00 % 0 0 0
app/components/loopos_ui/sidebar/v1/sidebar.rb 63.16 % 39 19 12 7 0.63 0.00 % 4 0 4
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 80.00 % 32 15 12 3 33.67 60.00 % 5 3 2
app/components/loopos_ui/spinner/spinner.html.erb 100.00 % 12 1 1 0 166.00 100.00 % 0 0 0
app/components/loopos_ui/state_label.rb 96.30 % 79 27 26 1 62.19 58.33 % 12 7 5
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 100.00 % 25 14 14 0 3.21 50.00 % 2 1 1
app/components/loopos_ui/tabs_content/tabs_content.html.erb 100.00 % 7 5 5 0 6.00 100.00 % 0 0 0
app/components/loopos_ui/tabs_layout.rb 82.54 % 121 63 52 11 3.29 37.50 % 16 6 10
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 37.50 % 14 8 3 5 3.00 50.00 % 2 1 1
app/components/loopos_ui/tabs_layout/tabs_layout.html.erb 78.95 % 55 19 15 4 3.05 35.71 % 14 5 9
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 95.24 % 55 21 20 1 24.76 83.33 % 6 5 1
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 100.00 % 7 1 1 0 8.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 87.30 % 133 63 55 8 71.22 50.00 % 26 13 13
app/components/loopos_ui/token/token.html.erb 32.56 % 69 43 14 29 52.28 29.41 % 34 10 24
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 93.33 % 41 15 14 1 9.00 100.00 % 0 0 0
app/components/loopos_ui/tooltip/tooltip.html.erb 77.78 % 28 18 14 4 26.67 100.00 % 4 4 0
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 78.26 % 37 23 18 5 0.78 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 44.07 % 283 118 52 66 26.08 0.00 % 66 0 66
app/components/loopos_ui/v2/image/image.html.erb 0.00 % 255 71 0 71 0.00 0.00 % 38 0 38
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 51.28 % 116 39 20 19 1.54 38.89 % 18 7 11
app/components/loopos_ui/v2/table.rb 78.14 % 516 215 168 47 10.90 39.51 % 81 32 49
app/components/loopos_ui/v2/table/cell.html.erb 33.33 % 27 15 5 10 85.33 37.50 % 8 3 5
app/components/loopos_ui/v2/table/cell.rb 94.44 % 72 36 34 2 124.94 68.18 % 22 15 7
app/components/loopos_ui/v2/table/header.html.erb 88.89 % 22 9 8 1 1.78 50.00 % 4 2 2
app/components/loopos_ui/v2/table/header.rb 100.00 % 14 9 9 0 1.22 0.00 % 2 0 2
app/components/loopos_ui/wysiwyg.rb 100.00 % 22 14 14 0 6.71 100.00 % 0 0 0
app/components/loopos_ui/wysiwyg/wysiwyg.html.erb 100.00 % 15 5 5 0 14.40 100.00 % 0 0 0

Views ( 7.0% covered at 2.41 hits/line )

310 files in total.
5670 relevant lines, 397 lines covered and 5273 lines missed. ( 7.0% )
2204 total branches, 125 branches covered and 2079 branches missed. ( 5.67% )
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 100.00 % 5 4 4 0 2.50 100.00 % 0 0 0
app/components/loopos_ui/action_buttons/button_group.html.erb 100.00 % 5 3 3 0 7.33 100.00 % 0 0 0
app/components/loopos_ui/action_menu/action_menu.html.erb 0.00 % 43 24 0 24 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 100.00 % 15 10 10 0 18.00 100.00 % 0 0 0
app/components/loopos_ui/app_logo/app_logo.html.erb 100.00 % 3 2 2 0 2.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 100.00 % 10 6 6 0 4.67 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 76.00 % 35 25 19 6 142.48 59.09 % 22 13 9
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 % 56 20 0 20 0.00 0.00 % 8 0 8
app/components/loopos_ui/chat/chat.html.erb 0.00 % 12 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 71.43 % 11 7 5 2 1.71 50.00 % 2 1 1
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 % 45 13 0 13 0.00 0.00 % 5 0 5
app/components/loopos_ui/date_picker/field/field.html.erb 0.00 % 14 6 0 6 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/month/month.html.erb 0.00 % 8 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 % 8 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/single/single.html.erb 0.00 % 8 2 0 2 0.00 100.00 % 0 0 0
app/components/loopos_ui/date_picker/year/year.html.erb 0.00 % 8 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 100.00 % 5 3 3 0 7.33 100.00 % 2 2 0
app/components/loopos_ui/double_state_label.html.erb 100.00 % 14 5 5 0 120.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 100.00 % 3 2 2 0 51.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 % 16 0 16
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 100.00 % 5 4 4 0 7.50 100.00 % 0 0 0
app/components/loopos_ui/flex_layout/section.html.erb 100.00 % 3 2 2 0 36.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 % 101 57 0 57 0.00 0.00 % 16 0 16
app/components/loopos_ui/form_entry/form_entry.html.erb 0.00 % 16 9 0 9 0.00 0.00 % 8 0 8
app/components/loopos_ui/form_entry_ai/form_entry_ai.html.erb 0.00 % 40 7 0 7 0.00 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/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 76.67 % 36 30 23 7 13.47 63.64 % 22 14 8
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 66.67 % 27 12 8 4 1.33 33.33 % 6 2 4
app/components/loopos_ui/human_view/tree_node/tree_node.html.erb 100.00 % 37 19 19 0 10.21 100.00 % 2 2 0
app/components/loopos_ui/icon/icon.html.erb 100.00 % 4 3 3 0 16.00 50.00 % 4 2 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/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 46.15 % 23 13 6 7 0.92 40.00 % 10 4 6
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 92.31 % 75 26 24 2 13.85 71.43 % 14 10 4
app/components/loopos_ui/inputs/ai/ai.html.erb 0.00 % 36 10 0 10 0.00 0.00 % 2 0 2
app/components/loopos_ui/inputs/checkbox/checkbox.html.erb 0.00 % 28 8 0 8 0.00 0.00 % 2 0 2
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 % 31 8 0 8 0.00 100.00 % 0 0 0
app/components/loopos_ui/inputs/file/form_file.html.erb 0.00 % 50 14 0 14 0.00 0.00 % 2 0 2
app/components/loopos_ui/inputs/file/multiple.html.erb 0.00 % 84 32 0 32 0.00 0.00 % 8 0 8
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 % 67 23 0 23 0.00 0.00 % 14 0 14
app/components/loopos_ui/inputs/money/money.html.erb 100.00 % 13 7 7 0 2.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/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 100.00 % 24 12 12 0 2.17 50.00 % 2 1 1
app/components/loopos_ui/inputs/select/select.html.erb 0.00 % 62 23 0 23 0.00 0.00 % 18 0 18
app/components/loopos_ui/inputs/select2/select2.html.erb 0.00 % 140 42 0 42 0.00 0.00 % 32 0 32
app/components/loopos_ui/inputs/text/text.html.erb 100.00 % 3 2 2 0 10.00 100.00 % 0 0 0
app/components/loopos_ui/inputs/text_area/text_area.html.erb 100.00 % 3 2 2 0 6.00 100.00 % 0 0 0
app/components/loopos_ui/item/state_label/state_label.html.erb 100.00 % 24 7 7 0 68.00 100.00 % 2 2 0
app/components/loopos_ui/item_value/item_value.html.erb 100.00 % 10 1 1 0 2.00 100.00 % 0 0 0
app/components/loopos_ui/label/label.html.erb 87.50 % 14 8 7 1 215.25 72.73 % 11 8 3
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 75.00 % 12 8 6 2 1.75 50.00 % 4 2 2
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 72.41 % 50 29 21 8 2.07 33.33 % 6 2 4
app/components/loopos_ui/log/script/info_modal_body.html.erb 46.15 % 63 39 18 21 0.92 18.18 % 44 8 36
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 100.00 % 8 4 4 0 2.50 50.00 % 2 1 1
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 75.00 % 7 4 3 1 188.00 50.00 % 2 1 1
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 66.67 % 15 9 6 3 1.78 50.00 % 4 2 2
app/components/loopos_ui/marketplace/header/header.html.erb 100.00 % 10 7 7 0 2.57 50.00 % 2 1 1
app/components/loopos_ui/marketplace/item_card/item_card.html.erb 87.50 % 35 24 21 3 7.50 44.44 % 18 8 10
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 100.00 % 11 4 4 0 12.50 100.00 % 0 0 0
app/components/loopos_ui/modal/modal.html.erb 60.00 % 48 25 15 10 1.28 35.71 % 14 5 9
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 10.53 % 35 19 2 17 0.11 25.00 % 4 1 3
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 % 121 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 % 157 83 0 83 0.00 0.00 % 26 0 26
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 % 160 95 0 95 0.00 0.00 % 40 0 40
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 % 25 12 0 12 0.00 100.00 % 0 0 0
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 % 127 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 % 92 18 0 18 0.00 0.00 % 20 0 20
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 % 56 28 0 28 0.00 0.00 % 2 0 2
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 % 55 26 0 26 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 % 95 40 0 40 0.00 0.00 % 22 0 22
app/components/loopos_ui/protocol/preview.html.erb 0.00 % 15 10 0 10 0.00 100.00 % 0 0 0
app/components/loopos_ui/protocol/protocol_component.html.erb 0.00 % 63 29 0 29 0.00 0.00 % 20 0 20
app/components/loopos_ui/protocol/protocol_element_component.html.erb 0.00 % 216 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 % 171 87 0 87 0.00 0.00 % 38 0 38
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/radio_card/content.html.erb 66.67 % 26 12 8 4 4.00 50.00 % 2 1 1
app/components/loopos_ui/radio_card/radio_card.html.erb 100.00 % 6 4 4 0 9.00 100.00 % 0 0 0
app/components/loopos_ui/script_editor/script_editor.html.erb 100.00 % 17 1 1 0 2.00 100.00 % 0 0 0
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 % 9 4 0 4 0.00 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/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 100.00 % 12 1 1 0 166.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 100.00 % 7 5 5 0 6.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 37.50 % 14 8 3 5 3.00 50.00 % 2 1 1
app/components/loopos_ui/tabs_layout/tabs_layout.html.erb 78.95 % 55 19 15 4 3.05 35.71 % 14 5 9
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 100.00 % 7 1 1 0 8.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 32.56 % 69 43 14 29 52.28 29.41 % 34 10 24
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 77.78 % 28 18 14 4 26.67 100.00 % 4 4 0
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 % 255 71 0 71 0.00 0.00 % 38 0 38
app/components/loopos_ui/v2/table.html.erb 51.28 % 116 39 20 19 1.54 38.89 % 18 7 11
app/components/loopos_ui/v2/table/cell.html.erb 33.33 % 27 15 5 10 85.33 37.50 % 8 3 5
app/components/loopos_ui/v2/table/header.html.erb 88.89 % 22 9 8 1 1.78 50.00 % 4 2 2
app/components/loopos_ui/wysiwyg/wysiwyg.html.erb 100.00 % 15 5 5 0 14.40 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

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. 2 <div class="lui-action_buttons">
  2. 2 <% button_groups.each do |bg| %>
  3. 4 <%= bg %>
  4. <% end %>
  5. 2 </div>

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

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. 8 <%= tag.div(class: "lui-action_buttons__button-group") do %>
  2. 4 <% buttons.each do |button| %>
  3. 10 <%= button %>
  4. <% end %>
  5. <% end %>

app/components/loopos_ui/action_menu.rb

60.0% lines covered

0.0% branches covered

15 relevant lines. 9 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 :options, Types::Array.of(Types::Hash)
  10. 1 option :portal_data_controllers, default: -> { [] }, type: Types::Array.of(Types::String)
  11. 1 def data_controllers
  12. ["modal", "form-submit", "pubsub"].push(*portal_data_controllers).join(" ")
  13. end
  14. 1 def option_attributes(option)
  15. then: 0 if option[:disabled].present?
  16. {}
  17. else: 0 else
  18. option[:attributes] || {}
  19. end
  20. end
  21. 1 def toggle_attributes(option)
  22. option[:toggle_attributes] || {}
  23. end
  24. end
  25. end

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

0.0% lines covered

0.0% branches covered

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

100.0% lines covered

100.0% branches covered

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

app/components/loopos_ui/app_logo.rb

100.0% lines covered

50.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
4 total branches, 2 branches covered and 2 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. 2 app_logo_file = StringIO.new
  9. 2 app_logo_file << "app_logos/logo_"
  10. 2 then: 2 else: 0 app_logo_file << (app == :manager ? "loopos" : app.to_s)
  11. 2 then: 0 else: 2 app_logo_file << (expanded ? "_expanded.svg" : ".svg")
  12. 2 app_logo_file.string
  13. end
  14. 1 class << self
  15. 1 def apps
  16. 1 APPS
  17. end
  18. end
  19. end
  20. end

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

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. 2 <div class="lui-app_logo">
  2. 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-[8px] 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

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. 2 <div>
  2. 2 <% mini_apps.each do |mini_app| %>
  3. 10 <div class="draggable w-fit h-fit absolute" data-controller="miniapp-drag">
  4. 10 <%= mini_app %>
  5. </div>
  6. <% end %>
  7. 2 </div>
  8. <div class="lui-bottom_bar">
  9. 2 <%= 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

88.03% lines covered

68.49% branches covered

117 relevant lines. 103 lines covered and 14 lines missed.
73 total branches, 50 branches covered and 23 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. 160 then: 0 if APP_ICONS.include?(icon.to_s)
  43. else: 160 icon
  44. 160 then: 0 elsif MIcon.icon?(icon)
  45. LooposUi::MIcon.new(icon, tag: :i, size: icon_size)
  46. else
  47. else: 160 # To temprarily support fa- icons but keep the new sizes
  48. 160 content_tag(:div, class: "flex items-center justify-center", style: "width: #{icon_size}px; height: #{icon_size}px;") do
  49. 160 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. 84 then: 0 if APP_ICONS.include?(icon.to_s)
  55. else: 84 icon
  56. 84 then: 0 elsif MIcon.icon?(icon)
  57. LooposUi::MIcon.new(icon, tag: :i, size: icon_size)
  58. else
  59. else: 84 # To temprarily support fa- icons but keep the new sizes
  60. 84 content_tag(:div, class: "flex items-center justify-center", style: "width: #{icon_size}px; height: #{icon_size}px;") do
  61. 84 content_tag(:i, "", class: "lui-button__icon lui-button__icon--#{@size} #{icon}")
  62. end
  63. end
  64. }
  65. 1 def icon_size
  66. 488 case @size
  67. when: 140 when :default
  68. 140 16
  69. when: 172 when :small
  70. 172 14
  71. else: 176 else
  72. 176 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. 168 else: 168 then: 0 raise ArgumentError, "Invalid size: #{size}" unless SIZES.include?(size)
  101. 168 else: 168 then: 0 raise ArgumentError, "Invalid type: #{type}" unless TYPES.include?(type)
  102. 168 else: 168 then: 0 raise ArgumentError, "Invalid kind: #{kind}" unless KINDS.include?(kind)
  103. # Deprecated, will be removed
  104. 168 then: 18 else: 150 @app = kind == :app ? (app || LooposUi.config.app_type) : nil
  105. 168 @kind = kind
  106. 168 @text = text
  107. 168 @size = size
  108. 168 @type = type
  109. 168 @tooltip_text = initial_tooltip_text(tag_options: tag_options, tooltip_text: tooltip_text)
  110. 168 @app_icon_size = app_icon_size
  111. 168 @disabled = disabled
  112. 168 @active = active
  113. 168 @full = full
  114. 168 @href = href
  115. 168 then: 76 else: 92 @tag = tag || (href.present? && !disabled? ? :a : :button)
  116. 168 @tag_options = tag_options
  117. 168 @load_on_click = load_on_click
  118. 168 then: 0 @leading_icon = if (leading_icon && APP_ICONS.include?(leading_icon.to_s)) || MIcon.icon?(leading_icon)
  119. leading_icon
  120. else: 168 else
  121. 168 faviconize(leading_icon || icon)
  122. end
  123. 168 then: 0 @trailing_icon = if (trailing_icon && APP_ICONS.include?(trailing_icon.to_s)) || MIcon.icon?(trailing_icon)
  124. trailing_icon
  125. else: 168 else
  126. 168 faviconize(trailing_icon)
  127. end
  128. # Validate that text is present unless there's at least one icon
  129. 168 else: 168 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. 168 @status_dot = status_dot
  133. end
  134. 1 def before_render
  135. 166 then: 84 else: 82 with_trailing_icon(icon: @trailing_icon) if @trailing_icon
  136. 166 then: 160 else: 6 with_leading_icon(icon: @leading_icon) if @leading_icon
  137. 166 then: 0 else: 166 with_status_dot(kind: @status_dot) if @status_dot
  138. end
  139. 1 def classes
  140. [
  141. 166 then: 40 else: 126 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. 166 then: 74 options = if @tag == :a && !disabled?
  153. 74 deep_merge_args(
  154. {
  155. href: @href,
  156. 74 then: 74 else: 0 data: {}.merge(@load_on_click ? { action: "lui--button#startLoading" } : {}),
  157. },
  158. @tag_options,
  159. )
  160. else: 92 else
  161. 92 @tag_options
  162. end
  163. 166 then: 18 else: 148 if load_on_click_for_button?(options)
  164. 18 options = deep_merge_args(options, { data: { action: "lui--button#startLoading" } })
  165. end
  166. 166 then: 0 else: 166 options.merge!(disabled: true) if disabled?
  167. 166 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. 482 @disabled
  179. end
  180. 1 def active?
  181. 166 @active
  182. end
  183. 1 def full?
  184. 332 @full
  185. end
  186. 1 private
  187. 1 def load_on_click_for_button?(options)
  188. 166 else: 166 then: 0 return false unless @load_on_click
  189. 166 else: 92 then: 74 return false unless @tag == :button
  190. 92 then: 36 else: 56 options[:form].present? || options[:type]&.to_sym == :submit
  191. end
  192. 1 def initial_tooltip_text(tag_options:, tooltip_text:)
  193. 168 then: 50 else: 118 then: 0 else: 168 then: 0 else: 168 else: 0 then: 168 return tooltip_text unless tag_options[:data]&.dig(:controller)&.to_s&.include?("lui--button-tooltip-toggle")
  194. toggled = tag_options[:data][:"lui--button-tooltip-toggle-toggled-value"]
  195. then: 0 if toggled
  196. tag_options[:data][:"lui--button-tooltip-toggle-toggled-text-value"]
  197. else: 0 else
  198. tag_options[:data][:"lui--button-tooltip-toggle-untoggled-text-value"]
  199. end
  200. end
  201. 1 def icon_only?
  202. 166 only_one_icon = [@leading_icon.present?, @trailing_icon.present?].count(true) == 1
  203. 166 no_text = @text.blank?
  204. 166 no_status_dot = @status_dot.blank?
  205. 166 only_one_icon && no_text && no_status_dot
  206. end
  207. 1 def specific_app_classes
  208. 166 then: 18 else: 148 "lui-button--neutral--#{@type}" if @app.present?
  209. end
  210. 1 def app_classes
  211. 166 else: 18 then: 148 "lui-button--neutral--#{@type}" unless @app.present?
  212. end
  213. 1 def theme_class
  214. 166 else: 28 then: 138 return unless @type == :primary
  215. 28 else: 0 then: 28 return unless LooposUi::Current.theme_context.present?
  216. "lui-button--themed"
  217. end
  218. 1 def size_classes
  219. 166 "lui-button--size-#{@size}"
  220. end
  221. 1 def disabled_classes
  222. 166 then: 0 else: 166 "lui-button--disabled" if disabled?
  223. end
  224. 1 def active_classes
  225. 166 else: 166 then: 0 "lui-button--inactive" unless active?
  226. end
  227. 1 def full_classes
  228. 332 then: 0 else: 332 full? ? "w-full" : "w-fit"
  229. end
  230. end
  231. end

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

76.0% lines covered

59.09% branches covered

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

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

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

10 relevant lines. 10 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 :style, default: -> {""}
  11. end
  12. 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. }, {style: style}
  11. ) %>

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

93.33% lines covered

50.0% branches covered

15 relevant lines. 14 lines covered and 1 lines missed.
6 total branches, 3 branches covered and 3 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)
  16. 2 @kind = kind
  17. 2 @count = count
  18. 2 @size = size
  19. 2 @text_color, @bg_color = COLORS[kind] || COLORS[:neutral]
  20. 2 @increment = increment
  21. 2 then: 0 else: 2 raise "No color defined for: #{kind}" if !@increment && (@text_color.nil? || @bg_color.nil?)
  22. end
  23. 1 def styles
  24. 2 then: 0 else: 2 return if !@increment && (@text_color.nil? || @bg_color.nil?)
  25. 2 then: 2 if @kind == :neutral
  26. 2 <<~CSS.squish
  27. color: #{@text_color};
  28. background-color: #{@bg_color};
  29. border: var(--Spacings-0, 1px) solid var(--General-Gray-500, #C4CAD0);
  30. CSS
  31. else: 0 else
  32. <<~CSS.squish
  33. color: #{@text_color};
  34. background-color: #{@bg_color};
  35. CSS
  36. end
  37. end
  38. end
  39. end

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

71.43% lines covered

50.0% branches covered

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

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

41.07% lines covered

0.0% branches covered

56 relevant lines. 23 lines covered and 33 lines missed.
27 total branches, 0 branches covered and 27 branches missed.
    
  1. 1 module LooposUi
  2. 1 class DatePicker < LoopComponent
  3. 1 option :inline, Types::Bool, default: -> { false }
  4. 1 option :id, Types::String, optional: true
  5. 1 option :name, Types::String, optional: true
  6. 1 option :submit_on_select, Types::Bool, default: -> { false }
  7. 1 option :range, Types::Bool, default: -> { false }
  8. 1 option :presets, Types::Bool, default: -> { false }
  9. 1 option :format, Types::String, default: -> { "date" }
  10. 1 option :locale, Types::String, default: -> { I18n.locale.to_s }
  11. 1 option :initial_date, Types::Date, optional: true
  12. 1 option :end_date, Types::Date, optional: true
  13. 1 def dom_id
  14. id || "datepicker-#{SecureRandom.hex(4)}"
  15. end
  16. 1 def variant
  17. then: 0 else: 0 return :presets if presets
  18. then: 0 else: 0 return :range if range
  19. when: 0 case format.to_s
  20. when: 0 when "month" then :month
  21. else: 0 when "year" then :year
  22. else :single_date
  23. end
  24. end
  25. 1 def field_wrapper_class
  26. "lui-date_picker__field"
  27. end
  28. 1 def input_name
  29. then: 0 else: 0 return name || "date_range" if variant.in?([:range, :presets])
  30. then: 0 else: 0 return name || "month" if variant == :month
  31. then: 0 else: 0 return name || "year" if variant == :year
  32. name || "date"
  33. end
  34. 1 def input_class
  35. then: 0 else: 0 variant == :range ? "lui-date_picker-ranged-picker" : "air-datepicker-input"
  36. end
  37. 1 def placeholder
  38. key =
  39. when: 0 case variant
  40. when: 0 when :month then "loopos_ui.date_picker.placeholder.select_month"
  41. when: 0 when :year then "loopos_ui.date_picker.placeholder.select_year"
  42. else: 0 when :range, :presets then "loopos_ui.date_picker.placeholder.select_date_range"
  43. else "loopos_ui.date_picker.placeholder.select_date"
  44. end
  45. default =
  46. when: 0 case variant
  47. when: 0 when :month then "Select month..."
  48. when: 0 when :year then "Select year..."
  49. else: 0 when :range, :presets then "Select date range..."
  50. else "Select date..."
  51. end
  52. I18n.t(key, default: default, locale: locale)
  53. end
  54. 1 def label(key, default:)
  55. I18n.t("loopos_ui.date_picker.#{key}", default: default, locale: locale)
  56. end
  57. 1 def base_date_picker_data
  58. {
  59. controller: "date-picker",
  60. "date-picker-locale-value": locale,
  61. "date-picker-submit-on-select-value": submit_on_select,
  62. }
  63. end
  64. 1 def field_date_picker_data
  65. case variant
  66. when: 0 when :month
  67. base_date_picker_data.merge(
  68. "date-picker-start-view-value": "months",
  69. "date-picker-min-view-value": "months",
  70. "date-picker-date-format-value": "MMMM yyyy",
  71. "date-picker-show-this-month-button-value": true,
  72. "date-picker-this-month-label-value": label("this_month", default: "This month"),
  73. "date-picker-show-clear-button-value": true,
  74. "date-picker-clear-label-value": label("clear", default: "Clear"),
  75. )
  76. when: 0 when :year
  77. base_date_picker_data.merge(
  78. "date-picker-start-view-value": "years",
  79. "date-picker-min-view-value": "years",
  80. "date-picker-date-format-value": "yyyy",
  81. "date-picker-show-this-year-button-value": true,
  82. "date-picker-this-year-label-value": label("this_year", default: "This year"),
  83. "date-picker-show-clear-button-value": true,
  84. "date-picker-clear-label-value": label("clear", default: "Clear"),
  85. )
  86. when: 0 when :range
  87. base_date_picker_data.merge(
  88. "date-picker-range-value": true,
  89. "date-picker-max-date-value": end_date,
  90. "date-picker-show-today-button-value": true,
  91. "date-picker-today-label-value": label("today", default: "Today"),
  92. "date-picker-show-clear-button-value": true,
  93. "date-picker-clear-label-value": label("clear", default: "Clear"),
  94. )
  95. else: 0 else
  96. base_date_picker_data.merge(
  97. "date-picker-initial-date-value": initial_date,
  98. "date-picker-show-today-button-value": true,
  99. "date-picker-today-label-value": label("today", default: "Today"),
  100. "date-picker-show-clear-button-value": true,
  101. "date-picker-clear-label-value": label("clear", default: "Clear"),
  102. )
  103. end
  104. end
  105. 1 def presets_wrapper_data
  106. {
  107. controller: "dropdown-popover date-picker",
  108. "dropdown-popover-placement-value": "bottom-start",
  109. "dropdown-popover-auto-position-value": true,
  110. "date-picker-range-value": true,
  111. "date-picker-locale-value": locale,
  112. "date-picker-today-label-value": label("today", default: "Today"),
  113. "date-picker-now-label-value": label("now", default: "Now"),
  114. "date-picker-this-week-label-value": label("this_week", default: "This week"),
  115. "date-picker-this-month-label-value": label("this_month", default: "This month"),
  116. "date-picker-this-year-label-value": label("this_year", default: "This year"),
  117. "date-picker-clear-label-value": label("clear", default: "Clear"),
  118. "date-picker-submit-on-select-value": submit_on_select,
  119. }
  120. end
  121. 1 def presets_inline_calendar_data
  122. base_date_picker_data.merge(
  123. "date-picker-inline-value": true,
  124. "date-picker-range-value": true,
  125. "date-picker-show-clear-button-value": true,
  126. "date-picker-clear-label-value": label("clear", default: "Clear"),
  127. )
  128. end
  129. end
  130. end

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

0.0% lines covered

0.0% branches covered

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

app/components/loopos_ui/date_picker/field.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 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 :placeholder
  10. 1 def normalized_wrapper_data
  11. (wrapper_data || {}).compact
  12. end
  13. 1 def normalized_input_data
  14. (input_data || {}).compact.merge("date-picker-target": "input")
  15. end
  16. end
  17. end
  18. 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. data: normalized_input_data,
  7. placeholder: placeholder
  8. ) %>
  9. <div class="lui-date_picker__icon">
  10. <%= render LooposUi::Icon.new(icon: :calendar, size: 16, color: "currentColor") %>
  11. </div>
  12. <%= content %>
  13. <% end %>

app/components/loopos_ui/date_picker/month.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 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 :placeholder
  9. end
  10. end
  11. 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. placeholder: placeholder
  7. ) %>

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

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 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 :placeholder
  8. end
  9. end
  10. 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: nil,
  3. wrapper_data: field_date_picker_data,
  4. input_name: input_name,
  5. input_class: input_class,
  6. placeholder: placeholder
  7. ) %>

app/components/loopos_ui/date_picker/single.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 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 :placeholder
  9. end
  10. end
  11. 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. placeholder: placeholder
  7. ) %>

app/components/loopos_ui/date_picker/year.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 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 :placeholder
  9. end
  10. end
  11. 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. placeholder: placeholder
  7. ) %>

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

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 Dots < LoopComponent
  3. 1 option :amount, default: -> { 1 }, type: Types::Integer
  4. 1 option :active, default: -> { 1 }, type: Types::Integer
  5. 3 option :data, default: -> { {} }, type: Types::Hash
  6. 3 option :dot_data, default: -> { {} }, type: Types::Hash
  7. 1 private
  8. 1 def data_attributes
  9. 2 deep_merge_args({ controller: "dots", dots_index_value: active - 1 }, data)
  10. end
  11. 1 def dot_data_attributes
  12. 16 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

100.0% lines covered

100.0% branches covered

3 relevant lines. 3 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. 4 <%= tag.div class: "lui-dots", data: data_attributes do %>
  2. 2 <% amount.times do |i| %>
  3. 16 then: 2 else: 14 <%= 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

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. 120 <div class="lui-double-state-label">
  2. 120 <%= render LooposUi::StateLabel.new(
  3. text: leading_text,
  4. color: leading_color || :default,
  5. icon: leading_icon,
  6. condensed: leading_condensed,
  7. 120 light: light) %>
  8. 120 <%= render LooposUi::StateLabel.new(
  9. text: trailing_text,
  10. color: trailing_color || :white,
  11. icon: trailing_icon,
  12. condensed: trailing_condensed,
  13. 120 light: light) %>
  14. </div>

app/components/loopos_ui/double_state_label.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 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 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. 5 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. 5 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. 120 super
  16. 120 @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-general-gray-300 bg-general-gray-100 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

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
12 total branches, 12 branches covered and 0 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. 34 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. 44 then: 10 else: 24 then: 10 else: 24 else: 10 then: 24 "w-full" unless classes&.split&.find { |c| c.starts_with?("w-") }
  19. end
  20. 1 def default_height_class
  21. 54 then: 10 else: 24 then: 10 else: 24 else: 10 then: 24 "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

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. 68 <%= tag.div(class: all_classes) do %>
  2. 34 <%= 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

83.78% lines covered

35.0% branches covered

37 relevant lines. 31 lines covered and 6 lines missed.
20 total branches, 7 branches covered and 13 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. 24 @draft = kwargs.delete(:draft)
  16. 24 @url = kwargs.delete(:url)
  17. 24 @href_options = kwargs.delete(:href_options) || {}
  18. 24 @key_text = kwargs.delete(:key_text)
  19. 24 @key_value_actions = kwargs.delete(:key_value_actions)
  20. 24 @kind = kwargs.delete(:kind)
  21. 24 super(kind: @kind, key_text: @key_text, key_value_actions: @key_value_actions, **kwargs)
  22. 24 @color ||= :general
  23. 24 set_type
  24. end
  25. 1 def classes
  26. [
  27. 22 "lui-entity-token",
  28. "lui-entity-token-#{@type}",
  29. 22 then: 0 else: 22 draft? ? "lui-entity-token--draft" : "",
  30. 22 then: 0 else: 22 @disabled ? "lui-token--disabled" : "",
  31. ].compact.join(" ")
  32. end
  33. 1 def styles
  34. 22 then: 0 else: 22 cursor_style = @draggable ? "cursor: grab;" : ""
  35. 22 then: 0 else: 22 bg_style = @bg_color.present? ? "background-color: #{@bg_color};" : ""
  36. 22 <<~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. 24 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: 24 else
  54. 24 COLORS.excluding(LooposUi.config.app_type.to_sym).values.sample
  55. end
  56. end
  57. 1 def set_type
  58. 24 then: 24 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: 24 else
  62. 24 @color
  63. end
  64. end
  65. 1 def draft?
  66. 22 @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

44.44% lines covered

0.0% branches covered

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

100.0% lines covered

50.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
2 total branches, 1 branches covered and 1 branches missed.
    
  1. 1 module LooposUi
  2. 1 class FlexLayout < LoopComponent
  3. 1 renders_many :sections, ->(**kwargs) do
  4. 24 LooposUi::FlexLayout::Section.new(size: size, grow: grow, **kwargs)
  5. end
  6. 3 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. 24 then: 24 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

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. 2 <div class="lui-flex_layout">
  2. 2 <% sections.each do |section| %>
  3. 24 <%= section %>
  4. <% end %>
  5. 2 </div>

app/components/loopos_ui/flex_layout/media_query_sizes_concern.rb

100.0% lines covered

70.0% branches covered

20 relevant lines. 20 lines covered and 0 lines missed.
10 total branches, 7 branches covered and 3 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. 24 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. 144 then: 0 else: 144 return if size.nil?
  25. 144 then: 0 else: 144 size = 6 if size == :half
  26. 144 then: 24 if query.nil?
  27. 24 then: 24 else: 0 size == :full ? "basis-full" : "basis-#{size}/12"
  28. else: 120 else
  29. 120 then: 24 else: 96 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

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. 48 <%= tag.div(class: classes) do %>
  2. 24 <%= 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

57 relevant lines. 0 lines covered and 57 lines missed.
16 total branches, 0 branches covered and 16 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) do |layout| %>
  43. then: 0 else: 0 <% layout.with_tab(title: t(".items")) do %>
  44. <div class="px-4">
  45. <%= tag.turbo_frame id: turbo_id(block, :items) do %>
  46. then: 0 <% if block.items.any? %>
  47. <% states.each do |state| %>
  48. <% items, pagy = items_and_pagy_for_state(state) %>
  49. then: 0 else: 0 <% if items.any? || search_request? %>
  50. <% token_labels = block.token_labels %>
  51. <%= render LooposUi::Accordion.new(title: "#{token_labels[state]} (#{pagy.count})") do |acc| %>
  52. <% acc.with_body do %>
  53. <%= render LooposUi::V2::Table.new(pagy: pagy, **table_args(state)) do |table| %>
  54. <% items.each do |item| %>
  55. <% table.with_row(key: item.id) do |row| %>
  56. <% row.with_cell(property: :item_slug) do %>
  57. <%= render LooposUi::Entities::Item.new(
  58. item: item, url: helpers.main_app.edit_admin_v2_item_path(item)
  59. ) %>
  60. <% end %>
  61. <% row.with_cell(property: :updated_at) do %>
  62. then: 0 else: 0 <%= render LooposUi::DateShow.new(date: item.logs.first.created_at) if item.logs.any? %>
  63. <% end %>
  64. <% end %>
  65. <% end %>
  66. <% end %>
  67. <% end %>
  68. <% end %>
  69. <% end %>
  70. <% end %>
  71. else: 0 <% else %>
  72. <div class="text-center copy-14 text-gray-700">
  73. <%= t(".no_items") %>
  74. </div>
  75. <% end %>
  76. <% end %>
  77. </div>
  78. <% end if view_items %>
  79. then: 0 else: 0 <% layout.with_tab(title: t(".block_settings")) do %>
  80. <div class="px-4">
  81. <%# TODO: move the partials inside LoopOS UI %>
  82. <%= render partial: "admin/v2/flows/flows/tabs/block_settings", locals: { block: block, dropkiq_scope: dropkiq_scope }%>
  83. <div class="text-right text-xs text-gray-500 mt-4">
  84. <%= "#{block.id} - #{block.slug}" %>
  85. </div>
  86. </div>
  87. <% end if view_block_settings %>
  88. then: 0 else: 0 <% layout.with_tab(title: "Test") do %>
  89. <div class="px-4">
  90. <%= render partial: "admin/v2/flows/flows/tabs/test_block", locals: { block: block, dropkiq_scope: dropkiq_scope }%>
  91. </div>
  92. <% end if ai_decision_block? %>
  93. then: 0 else: 0 <% layout.with_tab(title: t(".app_settings")) do %>
  94. <div class="px-4">
  95. <%= render partial: "admin/v2/flows/flows/tabs/app_settings",
  96. locals: { block: block, dropkiq_scope: dropkiq_scope }
  97. %>
  98. </div>
  99. <% end if app_block? && view_app_settings %>
  100. <% end %>
  101. <% end %>

app/components/loopos_ui/form_entry.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 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
  10. ),
  11. default: -> { :vertical }
  12. 1 option :tooltip, optional: true
  13. 1 option :label_width, optional: true
  14. end
  15. end

app/components/loopos_ui/form_entry/form_entry.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. <div class="lui-form_entry lui-form_entry--<%= orientation %>">
  2. <div class="lui-form_entry__input">
  3. <%= input %>
  4. </div>
  5. then: 0 else: 0 then: 0 else: 0 <div class="lui-form_entry__label-wrapper <%= orientation == :horizontal ? "pt-2" : "pl-2" %>" style="<%= label_width ? "width: #{label_width}px; min-width: #{label_width}px; word-break: break-word;" : "" %>">
  6. <span class="lui-form_entry__label"><%= label %></span>
  7. then: 0 else: 0 <span class="lui-form_entry__required"><%= required ? "*" : "" %></span>
  8. <div class="lui-form_entry__icon">
  9. then: 0 <% if tooltip.present? %>
  10. <%= render LooposUi::IconTooltip.new(icon: "fa-regular fa-circle-info", text: tooltip, size: "12") %>
  11. else: 0 <% else %>
  12. <%= icon_tooltip %>
  13. <% end %>
  14. </div>
  15. </div>
  16. </div>

app/components/loopos_ui/form_entry_ai.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 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. end
  9. end

app/components/loopos_ui/form_entry_ai/form_entry_ai.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-ai-prompt">
  2. <div class="lui-ai-prompt__header">
  3. <div class="lui-ai-prompt__icon-container">
  4. <div class="lui-ai-prompt__icon">
  5. <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
  6. <g clip-path="url(#paint0_angular_3875_4242_clip_path)" data-figma-skip-parse="true"><g transform="matrix(0 0.0150864 -0.0150864 0 15.9974 16.9131)"><foreignObject x="-1187.4" y="-1187.4" width="2374.8" height="2374.8"><div xmlns="http://www.w3.org/1999/xhtml" style="background:conic-gradient(from 90deg,rgba(0, 197, 255, 1) 0deg,rgba(0, 216, 181, 1) 43.4361deg,rgba(255, 203, 51, 1) 107.75deg,rgba(242, 115, 53, 1) 179.299deg,rgba(228, 45, 39, 1) 229.644deg,rgba(128, 68, 255, 1) 283.066deg,rgba(0, 114, 255, 1) 333.657deg,rgba(0, 197, 255, 1) 360deg);height:100%;width:100%;opacity:1"></div></foreignObject></g></g><path d="M23.9974 15.9995C19.5774 15.9995 15.9974 12.4195 15.9974 7.99951C15.9974 3.57951 19.5774 -0.000488281 23.9974 -0.000488281H31.9974V7.99951C31.9974 12.4195 28.4174 15.9995 23.9974 15.9995Z" data-figma-gradient-fill="{&quot;type&quot;:&quot;GRADIENT_ANGULAR&quot;,&quot;stops&quot;:[{&quot;color&quot;:{&quot;r&quot;:0.0,&quot;g&quot;:0.84705883264541626,&quot;b&quot;:0.70980393886566162,&quot;a&quot;:1.0},&quot;position&quot;:0.12065579742193222},{&quot;color&quot;:{&quot;r&quot;:1.0,&quot;g&quot;:0.79607844352722168,&quot;b&quot;:0.20000000298023224,&quot;a&quot;:1.0},&quot;position&quot;:0.29930534958839417},{&quot;color&quot;:{&quot;r&quot;:0.94901961088180542,&quot;g&quot;:0.45098039507865906,&quot;b&quot;:0.20784313976764679,&quot;a&quot;:1.0},&quot;position&quot;:0.49805253744125366},{&quot;color&quot;:{&quot;r&quot;:0.89411765336990356,&quot;g&quot;:0.17647059261798859,&quot;b&quot;:0.15294118225574493,&quot;a&quot;:1.0},&quot;position&quot;:0.63789874315261841},{&quot;color&quot;:{&quot;r&quot;:0.50196081399917603,&quot;g&quot;:0.26666668057441711,&quot;b&quot;:1.0,&quot;a&quot;:1.0},&quot;position&quot;:0.78629583120346069},{&quot;color&quot;:{&quot;r&quot;:0.0,&quot;g&quot;:0.44705882668495178,&quot;b&quot;:1.0,&quot;a&quot;:1.0},&quot;position&quot;:0.92682510614395142},{&quot;color&quot;:{&quot;r&quot;:0.0,&quot;g&quot;:0.77254903316497803,&quot;b&quot;:1.0,&quot;a&quot;:1.0},&quot;position&quot;:1.0}],&quot;stopsVar&quot;:[{&quot;color&quot;:{&quot;r&quot;:0.0,&quot;g&quot;:0.84705883264541626,&quot;b&quot;:0.70980393886566162,&quot;a&quot;:1.0},&quot;position&quot;:0.12065579742193222},{&quot;color&quot;:{&quot;r&quot;:1.0,&quot;g&quot;:0.79607844352722168,&quot;b&quot;:0.20000000298023224,&quot;a&quot;:1.0},&quot;position&quot;:0.29930534958839417},{&quot;color&quot;:{&quot;r&quot;:0.94901961088180542,&quot;g&quot;:0.45098039507865906,&quot;b&quot;:0.20784313976764679,&quot;a&quot;:1.0},&quot;position&quot;:0.49805253744125366},{&quot;color&quot;:{&quot;r&quot;:0.89411765336990356,&quot;g&quot;:0.17647059261798859,&quot;b&quot;:0.15294118225574493,&quot;a&quot;:1.0},&quot;position&quot;:0.63789874315261841},{&quot;color&quot;:{&quot;r&quot;:0.50196081399917603,&quot;g&quot;:0.26666668057441711,&quot;b&quot;:1.0,&quot;a&quot;:1.0},&quot;position&quot;:0.78629583120346069},{&quot;color&quot;:{&quot;r&quot;:0.0,&quot;g&quot;:0.44705882668495178,&quot;b&quot;:1.0,&quot;a&quot;:1.0},&quot;position&quot;:0.92682510614395142},{&quot;color&quot;:{&quot;r&quot;:0.0,&quot;g&quot;:0.77254903316497803,&quot;b&quot;:1.0,&quot;a&quot;:1.0},&quot;position&quot;:1.0}],&quot;transform&quot;:{&quot;m00&quot;:1.8475537702652371e-15,&quot;m01&quot;:-30.172840118408203,&quot;m02&quot;:31.083795547485352,&quot;m10&quot;:30.172840118408203,&quot;m11&quot;:1.8475537702652371e-15,&quot;m12&quot;:1.8266723155975342},&quot;opacity&quot;:1.0,&quot;blendMode&quot;:&quot;NORMAL&quot;,&quot;visible&quot;:true}"/>
  7. <g clip-path="url(#paint1_angular_3875_4242_clip_path)" data-figma-skip-parse="true"><g transform="matrix(0 0.0150864 -0.0150864 0 15.9974 16.9131)"><foreignObject x="-1187.4" y="-1187.4" width="2374.8" height="2374.8"><div xmlns="http://www.w3.org/1999/xhtml" style="background:conic-gradient(from 90deg,rgba(0, 197, 255, 1) 0deg,rgba(0, 216, 181, 1) 43.4361deg,rgba(255, 203, 51, 1) 107.75deg,rgba(242, 115, 53, 1) 179.299deg,rgba(228, 45, 39, 1) 229.644deg,rgba(128, 68, 255, 1) 283.066deg,rgba(0, 114, 255, 1) 333.657deg,rgba(0, 197, 255, 1) 360deg);height:100%;width:100%;opacity:1"></div></foreignObject></g></g><path d="M15.9974 7.99951C15.9974 12.4195 12.4174 15.9995 7.99738 15.9995C3.57738 15.9995 -0.00262451 12.4195 -0.00262451 7.99951V-0.000488281H7.99738C12.4174 -0.000488281 15.9974 3.57951 15.9974 7.99951Z" data-figma-gradient-fill="{&quot;type&quot;:&quot;GRADIENT_ANGULAR&quot;,&quot;stops&quot;:[{&quot;color&quot;:{&quot;r&quot;:0.0,&quot;g&quot;:0.84705883264541626,&quot;b&quot;:0.70980393886566162,&quot;a&quot;:1.0},&quot;position&quot;:0.12065579742193222},{&quot;color&quot;:{&quot;r&quot;:1.0,&quot;g&quot;:0.79607844352722168,&quot;b&quot;:0.20000000298023224,&quot;a&quot;:1.0},&quot;position&quot;:0.29930534958839417},{&quot;color&quot;:{&quot;r&quot;:0.94901961088180542,&quot;g&quot;:0.45098039507865906,&quot;b&quot;:0.20784313976764679,&quot;a&quot;:1.0},&quot;position&quot;:0.49805253744125366},{&quot;color&quot;:{&quot;r&quot;:0.89411765336990356,&quot;g&quot;:0.17647059261798859,&quot;b&quot;:0.15294118225574493,&quot;a&quot;:1.0},&quot;position&quot;:0.63789874315261841},{&quot;color&quot;:{&quot;r&quot;:0.50196081399917603,&quot;g&quot;:0.26666668057441711,&quot;b&quot;:1.0,&quot;a&quot;:1.0},&quot;position&quot;:0.78629583120346069},{&quot;color&quot;:{&quot;r&quot;:0.0,&quot;g&quot;:0.44705882668495178,&quot;b&quot;:1.0,&quot;a&quot;:1.0},&quot;position&quot;:0.92682510614395142},{&quot;color&quot;:{&quot;r&quot;:0.0,&quot;g&quot;:0.77254903316497803,&quot;b&quot;:1.0,&quot;a&quot;:1.0},&quot;position&quot;:1.0}],&quot;stopsVar&quot;:[{&quot;color&quot;:{&quot;r&quot;:0.0,&quot;g&quot;:0.84705883264541626,&quot;b&quot;:0.70980393886566162,&quot;a&quot;:1.0},&quot;position&quot;:0.12065579742193222},{&quot;color&quot;:{&quot;r&quot;:1.0,&quot;g&quot;:0.79607844352722168,&quot;b&quot;:0.20000000298023224,&quot;a&quot;:1.0},&quot;position&quot;:0.29930534958839417},{&quot;color&quot;:{&quot;r&quot;:0.94901961088180542,&quot;g&quot;:0.45098039507865906,&quot;b&quot;:0.20784313976764679,&quot;a&quot;:1.0},&quot;position&quot;:0.49805253744125366},{&quot;color&quot;:{&quot;r&quot;:0.89411765336990356,&quot;g&quot;:0.17647059261798859,&quot;b&quot;:0.15294118225574493,&quot;a&quot;:1.0},&quot;position&quot;:0.63789874315261841},{&quot;color&quot;:{&quot;r&quot;:0.50196081399917603,&quot;g&quot;:0.26666668057441711,&quot;b&quot;:1.0,&quot;a&quot;:1.0},&quot;position&quot;:0.78629583120346069},{&quot;color&quot;:{&quot;r&quot;:0.0,&quot;g&quot;:0.44705882668495178,&quot;b&quot;:1.0,&quot;a&quot;:1.0},&quot;position&quot;:0.92682510614395142},{&quot;color&quot;:{&quot;r&quot;:0.0,&quot;g&quot;:0.77254903316497803,&quot;b&quot;:1.0,&quot;a&quot;:1.0},&quot;position&quot;:1.0}],&quot;transform&quot;:{&quot;m00&quot;:1.8475537702652371e-15,&quot;m01&quot;:-30.172840118408203,&quot;m02&quot;:31.083795547485352,&quot;m10&quot;:30.172840118408203,&quot;m11&quot;:1.8475537702652371e-15,&quot;m12&quot;:1.8266723155975342},&quot;opacity&quot;:1.0,&quot;blendMode&quot;:&quot;NORMAL&quot;,&quot;visible&quot;:true}"/>
  8. <g clip-path="url(#paint2_angular_3875_4242_clip_path)" data-figma-skip-parse="true"><g transform="matrix(0 0.0150864 -0.0150864 0 15.9974 16.9131)"><foreignObject x="-1187.4" y="-1187.4" width="2374.8" height="2374.8"><div xmlns="http://www.w3.org/1999/xhtml" style="background:conic-gradient(from 90deg,rgba(0, 197, 255, 1) 0deg,rgba(0, 216, 181, 1) 43.4361deg,rgba(255, 203, 51, 1) 107.75deg,rgba(242, 115, 53, 1) 179.299deg,rgba(228, 45, 39, 1) 229.644deg,rgba(128, 68, 255, 1) 283.066deg,rgba(0, 114, 255, 1) 333.657deg,rgba(0, 197, 255, 1) 360deg);height:100%;width:100%;opacity:1"></div></foreignObject></g></g><path d="M7.99738 15.9995C12.4174 15.9995 15.9974 19.5795 15.9974 23.9995C15.9974 28.4195 12.4174 31.9995 7.99738 31.9995H-0.00262451V23.9995C-0.00262451 19.5795 3.57738 15.9995 7.99738 15.9995Z" data-figma-gradient-fill="{&quot;type&quot;:&quot;GRADIENT_ANGULAR&quot;,&quot;stops&quot;:[{&quot;color&quot;:{&quot;r&quot;:0.0,&quot;g&quot;:0.84705883264541626,&quot;b&quot;:0.70980393886566162,&quot;a&quot;:1.0},&quot;position&quot;:0.12065579742193222},{&quot;color&quot;:{&quot;r&quot;:1.0,&quot;g&quot;:0.79607844352722168,&quot;b&quot;:0.20000000298023224,&quot;a&quot;:1.0},&quot;position&quot;:0.29930534958839417},{&quot;color&quot;:{&quot;r&quot;:0.94901961088180542,&quot;g&quot;:0.45098039507865906,&quot;b&quot;:0.20784313976764679,&quot;a&quot;:1.0},&quot;position&quot;:0.49805253744125366},{&quot;color&quot;:{&quot;r&quot;:0.89411765336990356,&quot;g&quot;:0.17647059261798859,&quot;b&quot;:0.15294118225574493,&quot;a&quot;:1.0},&quot;position&quot;:0.63789874315261841},{&quot;color&quot;:{&quot;r&quot;:0.50196081399917603,&quot;g&quot;:0.26666668057441711,&quot;b&quot;:1.0,&quot;a&quot;:1.0},&quot;position&quot;:0.78629583120346069},{&quot;color&quot;:{&quot;r&quot;:0.0,&quot;g&quot;:0.44705882668495178,&quot;b&quot;:1.0,&quot;a&quot;:1.0},&quot;position&quot;:0.92682510614395142},{&quot;color&quot;:{&quot;r&quot;:0.0,&quot;g&quot;:0.77254903316497803,&quot;b&quot;:1.0,&quot;a&quot;:1.0},&quot;position&quot;:1.0}],&quot;stopsVar&quot;:[{&quot;color&quot;:{&quot;r&quot;:0.0,&quot;g&quot;:0.84705883264541626,&quot;b&quot;:0.70980393886566162,&quot;a&quot;:1.0},&quot;position&quot;:0.12065579742193222},{&quot;color&quot;:{&quot;r&quot;:1.0,&quot;g&quot;:0.79607844352722168,&quot;b&quot;:0.20000000298023224,&quot;a&quot;:1.0},&quot;position&quot;:0.29930534958839417},{&quot;color&quot;:{&quot;r&quot;:0.94901961088180542,&quot;g&quot;:0.45098039507865906,&quot;b&quot;:0.20784313976764679,&quot;a&quot;:1.0},&quot;position&quot;:0.49805253744125366},{&quot;color&quot;:{&quot;r&quot;:0.89411765336990356,&quot;g&quot;:0.17647059261798859,&quot;b&quot;:0.15294118225574493,&quot;a&quot;:1.0},&quot;position&quot;:0.63789874315261841},{&quot;color&quot;:{&quot;r&quot;:0.50196081399917603,&quot;g&quot;:0.26666668057441711,&quot;b&quot;:1.0,&quot;a&quot;:1.0},&quot;position&quot;:0.78629583120346069},{&quot;color&quot;:{&quot;r&quot;:0.0,&quot;g&quot;:0.44705882668495178,&quot;b&quot;:1.0,&quot;a&quot;:1.0},&quot;position&quot;:0.92682510614395142},{&quot;color&quot;:{&quot;r&quot;:0.0,&quot;g&quot;:0.77254903316497803,&quot;b&quot;:1.0,&quot;a&quot;:1.0},&quot;position&quot;:1.0}],&quot;transform&quot;:{&quot;m00&quot;:1.8475537702652371e-15,&quot;m01&quot;:-30.172840118408203,&quot;m02&quot;:31.083795547485352,&quot;m10&quot;:30.172840118408203,&quot;m11&quot;:1.8475537702652371e-15,&quot;m12&quot;:1.8266723155975342},&quot;opacity&quot;:1.0,&quot;blendMode&quot;:&quot;NORMAL&quot;,&quot;visible&quot;:true}"/>
  9. <g clip-path="url(#paint3_angular_3875_4242_clip_path)" data-figma-skip-parse="true"><g transform="matrix(0 0.0150864 -0.0150864 0 15.9974 16.9131)"><foreignObject x="-1187.4" y="-1187.4" width="2374.8" height="2374.8"><div xmlns="http://www.w3.org/1999/xhtml" style="background:conic-gradient(from 90deg,rgba(0, 197, 255, 1) 0deg,rgba(0, 216, 181, 1) 43.4361deg,rgba(255, 203, 51, 1) 107.75deg,rgba(242, 115, 53, 1) 179.299deg,rgba(228, 45, 39, 1) 229.644deg,rgba(128, 68, 255, 1) 283.066deg,rgba(0, 114, 255, 1) 333.657deg,rgba(0, 197, 255, 1) 360deg);height:100%;width:100%;opacity:1"></div></foreignObject></g></g><path d="M15.9974 23.9995C15.9974 19.5795 19.5774 15.9995 23.9974 15.9995C28.4174 15.9995 31.9974 19.5795 31.9974 23.9995V31.9995H23.9974C19.5774 31.9995 15.9974 28.4195 15.9974 23.9995Z" data-figma-gradient-fill="{&quot;type&quot;:&quot;GRADIENT_ANGULAR&quot;,&quot;stops&quot;:[{&quot;color&quot;:{&quot;r&quot;:0.0,&quot;g&quot;:0.84705883264541626,&quot;b&quot;:0.70980393886566162,&quot;a&quot;:1.0},&quot;position&quot;:0.12065579742193222},{&quot;color&quot;:{&quot;r&quot;:1.0,&quot;g&quot;:0.79607844352722168,&quot;b&quot;:0.20000000298023224,&quot;a&quot;:1.0},&quot;position&quot;:0.29930534958839417},{&quot;color&quot;:{&quot;r&quot;:0.94901961088180542,&quot;g&quot;:0.45098039507865906,&quot;b&quot;:0.20784313976764679,&quot;a&quot;:1.0},&quot;position&quot;:0.49805253744125366},{&quot;color&quot;:{&quot;r&quot;:0.89411765336990356,&quot;g&quot;:0.17647059261798859,&quot;b&quot;:0.15294118225574493,&quot;a&quot;:1.0},&quot;position&quot;:0.63789874315261841},{&quot;color&quot;:{&quot;r&quot;:0.50196081399917603,&quot;g&quot;:0.26666668057441711,&quot;b&quot;:1.0,&quot;a&quot;:1.0},&quot;position&quot;:0.78629583120346069},{&quot;color&quot;:{&quot;r&quot;:0.0,&quot;g&quot;:0.44705882668495178,&quot;b&quot;:1.0,&quot;a&quot;:1.0},&quot;position&quot;:0.92682510614395142},{&quot;color&quot;:{&quot;r&quot;:0.0,&quot;g&quot;:0.77254903316497803,&quot;b&quot;:1.0,&quot;a&quot;:1.0},&quot;position&quot;:1.0}],&quot;stopsVar&quot;:[{&quot;color&quot;:{&quot;r&quot;:0.0,&quot;g&quot;:0.84705883264541626,&quot;b&quot;:0.70980393886566162,&quot;a&quot;:1.0},&quot;position&quot;:0.12065579742193222},{&quot;color&quot;:{&quot;r&quot;:1.0,&quot;g&quot;:0.79607844352722168,&quot;b&quot;:0.20000000298023224,&quot;a&quot;:1.0},&quot;position&quot;:0.29930534958839417},{&quot;color&quot;:{&quot;r&quot;:0.94901961088180542,&quot;g&quot;:0.45098039507865906,&quot;b&quot;:0.20784313976764679,&quot;a&quot;:1.0},&quot;position&quot;:0.49805253744125366},{&quot;color&quot;:{&quot;r&quot;:0.89411765336990356,&quot;g&quot;:0.17647059261798859,&quot;b&quot;:0.15294118225574493,&quot;a&quot;:1.0},&quot;position&quot;:0.63789874315261841},{&quot;color&quot;:{&quot;r&quot;:0.50196081399917603,&quot;g&quot;:0.26666668057441711,&quot;b&quot;:1.0,&quot;a&quot;:1.0},&quot;position&quot;:0.78629583120346069},{&quot;color&quot;:{&quot;r&quot;:0.0,&quot;g&quot;:0.44705882668495178,&quot;b&quot;:1.0,&quot;a&quot;:1.0},&quot;position&quot;:0.92682510614395142},{&quot;color&quot;:{&quot;r&quot;:0.0,&quot;g&quot;:0.77254903316497803,&quot;b&quot;:1.0,&quot;a&quot;:1.0},&quot;position&quot;:1.0}],&quot;transform&quot;:{&quot;m00&quot;:1.8475537702652371e-15,&quot;m01&quot;:-30.172840118408203,&quot;m02&quot;:31.083795547485352,&quot;m10&quot;:30.172840118408203,&quot;m11&quot;:1.8475537702652371e-15,&quot;m12&quot;:1.8266723155975342},&quot;opacity&quot;:1.0,&quot;blendMode&quot;:&quot;NORMAL&quot;,&quot;visible&quot;:true}"/>
  10. <defs><clipPath id="paint0_angular_3875_4242_clip_path"><path d="M23.9974 15.9995C19.5774 15.9995 15.9974 12.4195 15.9974 7.99951C15.9974 3.57951 19.5774 -0.000488281 23.9974 -0.000488281H31.9974V7.99951C31.9974 12.4195 28.4174 15.9995 23.9974 15.9995Z"/></clipPath><clipPath id="paint1_angular_3875_4242_clip_path"><path d="M15.9974 7.99951C15.9974 12.4195 12.4174 15.9995 7.99738 15.9995C3.57738 15.9995 -0.00262451 12.4195 -0.00262451 7.99951V-0.000488281H7.99738C12.4174 -0.000488281 15.9974 3.57951 15.9974 7.99951Z"/></clipPath><clipPath id="paint2_angular_3875_4242_clip_path"><path d="M7.99738 15.9995C12.4174 15.9995 15.9974 19.5795 15.9974 23.9995C15.9974 28.4195 12.4174 31.9995 7.99738 31.9995H-0.00262451V23.9995C-0.00262451 19.5795 3.57738 15.9995 7.99738 15.9995Z"/></clipPath><clipPath id="paint3_angular_3875_4242_clip_path"><path d="M15.9974 23.9995C15.9974 19.5795 19.5774 15.9995 23.9974 15.9995C28.4174 15.9995 31.9974 19.5795 31.9974 23.9995V31.9995H23.9974C19.5774 31.9995 15.9974 28.4195 15.9974 23.9995Z"/></clipPath></defs>
  11. </svg>
  12. </div>
  13. </div>
  14. <div class="lui-ai-prompt__title-desc">
  15. <div class="lui-ai-prompt__title-row">
  16. <p class="lui-ai-prompt__title">
  17. <%= title %>
  18. </p>
  19. <div class="lui-ai-prompt__state-label">
  20. <%= render LooposUi::StateLabel.new(
  21. text: "Beta",
  22. color: "neutral",
  23. ) %>
  24. </div>
  25. </div>
  26. <p class="lui-ai-prompt__description">
  27. <%= description || t(".description") %>
  28. </p>
  29. </div>
  30. </div>
  31. <div class="lui-ai-prompt__input-row">
  32. <%= render LooposUi::Inputs::Ai.new(
  33. **{
  34. placeholder: placeholder,
  35. spinner: spinner,
  36. max_rows: max_rows
  37. }.compact
  38. ) %>
  39. </div>
  40. </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. 19 option :icon_args, Types::Hash, default: -> { {} }, optional: true
  5. 19 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. 13 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. 13 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

76.67% lines covered

63.64% branches covered

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

74.07% lines covered

27.27% branches covered

27 relevant lines. 20 lines covered and 7 lines missed.
11 total branches, 3 branches covered and 8 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. 6 @parsed_data ||= parse_data(data)
  13. end
  14. 1 def tree_children(value = parsed_data)
  15. 2 case value
  16. when: 2 when Hash
  17. 2 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. 2 then: 2 else: 0 ICON_BEHAVIORS.include?(tree_icon_behavior) ? tree_icon_behavior : "rotate"
  30. end
  31. 1 private
  32. 1 def parse_data(value)
  33. 2 then: 2 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

66.67% lines covered

33.33% branches covered

12 relevant lines. 8 lines covered and 4 lines missed.
6 total branches, 2 branches covered and 4 branches missed.
    
  1. 2 <%= 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. 2 ) do %>
  8. <div class="lui-human_view__content">
  9. 2 then: 0 <% if parsed_data.blank? %>
  10. else: 2 <span class="lui-human_view__empty"><%= t("loopos_ui.human_view.empty") %></span>
  11. 2 then: 2 <% elsif parsed_data.is_a?(Hash) || parsed_data.is_a?(Array) %>
  12. 2 <% tree_children.each do |child_key, child_value| %>
  13. 2 <%= 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. 2 )) %>
  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. 2 </div>
  27. <% end %>

app/components/loopos_ui/human_view/tree_node.rb

95.65% lines covered

77.78% branches covered

23 relevant lines. 22 lines covered and 1 lines missed.
9 total branches, 7 branches covered and 2 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. 18 node_key.to_s
  11. end
  12. 1 def children
  13. 24 case node_value
  14. when: 8 when Hash
  15. 32 node_value.sort_by { |key, _| key.to_s }
  16. when: 4 when Array
  17. 12 node_value.each_with_index.map { |child, index| [(index + 1).to_s, child] }
  18. else: 12 else
  19. 12 []
  20. end
  21. end
  22. 1 def value_color_class
  23. 12 when: 4 case node_value
  24. 4 when: 6 when String then "lui-human_view-tree_node__value--string"
  25. 6 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: 2 else
  28. 2 "lui-human_view-tree_node__value--default"
  29. end
  30. end
  31. 1 def formatted_value
  32. 12 then: 0 else: 12 return "null" if node_value.nil?
  33. 12 node_value.to_s
  34. end
  35. end
  36. end
  37. end

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

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. 18 <div class="lui-human_view-tree_node">
  2. 18 then: 6 <% if children.any? %>
  3. 6 <% content_id = "extra-data-tree-#{SecureRandom.hex(6)}" %>
  4. 6 <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. 6 aria-controls="<%= content_id %>">
  11. <i
  12. data-tree-icon
  13. 6 data-tree-icon-closed="<%= tree_icon_closed %>"
  14. 6 data-tree-icon-open="<%= tree_icon_open %>"
  15. 6 class="fa-solid <%= tree_icon_closed %> lui-human_view-tree_node__icon"></i>
  16. 6 <span class="lui-human_view-tree_node__key"><%= node_key_label %></span>
  17. </button>
  18. 6 <div id="<%= content_id %>" data-tree-content data-state="closed" class="lui-human_view-tree_node__children" hidden>
  19. 6 <% children.each do |child_key, child_value| %>
  20. 16 <%= 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. 16 )) %>
  26. <% end %>
  27. 6 </div>
  28. </div>
  29. else: 12 <% else %>
  30. 12 <div class="lui-human_view-tree_node__leaf">
  31. 12 <i class="fa-solid <%= tree_icon_closed %> lui-human_view-tree_node__icon lui-human_view-tree_node__icon--placeholder"></i>
  32. 12 <span class="lui-human_view-tree_node__key"><%= node_key_label %>:</span>
  33. 12 <span class="lui-human_view-tree_node__value <%= value_color_class %>"><%= formatted_value %></span>
  34. </div>
  35. <% end %>
  36. 18 </div>

app/components/loopos_ui/icon.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. # 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. 3 option :color, default: -> { :black }, type: Types::Coercible::String
  9. 1 def initialize(...)
  10. 12 super
  11. 12 @icon = faviconize(icon)
  12. end
  13. 1 def styles
  14. 12 <<~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

100.0% lines covered

50.0% branches covered

3 relevant lines. 3 lines covered and 0 lines missed.
4 total branches, 2 branches covered and 2 branches missed.
    
  1. 24 <%= tag.div(class: "lui-icon h-[#{size}px] w-[#{size}px]") do %>
  2. 12 then: 12 else: 0 <%= tag.i(class: "#{icon} lui-icon__icon" , style: styles) if icon.present? %>
  3. 12 then: 0 else: 12 <%= 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

78.05% lines covered

0.0% branches covered

41 relevant lines. 32 lines covered and 9 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. 2 super
  34. 2 @model = model
  35. 2 @attribute = attribute
  36. 2 @type = type
  37. 2 @show_path = show_path
  38. 2 @edit_path = edit_path
  39. 2 @with_turbo_wrapper = with_turbo_wrapper
  40. 2 @custom_class = custom_class
  41. 2 @size = size
  42. 2 @has_margin = has_margin
  43. 2 @show_edit_button = show_edit_button
  44. 2 @custom_turbo_frame_id = custom_turbo_frame_id
  45. end
  46. 1 def show_action?
  47. 2 !(edit_action? || keep_inline_edit?)
  48. end
  49. 1 def edit_action?
  50. 2 view_context.action_name == "edit" || (Rails.env.development? && params[:edit_mode])
  51. end
  52. 1 def keep_inline_edit?
  53. 2 params[:keep_inline_edit] == "true"
  54. end
  55. 1 def turbo_frame_id
  56. 2 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-[8px]! 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-[8px]! 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

46.15% lines covered

40.0% branches covered

13 relevant lines. 6 lines covered and 7 lines missed.
10 total branches, 4 branches covered and 6 branches missed.
    
  1. <%# FIXME: looks ugly but it's fast rn %>
  2. 2 then: 2 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. 2 then: 2 <% if show_action? %>
  5. 2 then: 2 <% if show? %>
  6. 2 <%= 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. 2 </div>
  23. 2 then: 2 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

95.45% lines covered

62.5% branches covered

22 relevant lines. 21 lines covered and 1 lines missed.
8 total branches, 5 branches covered and 3 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. 20 then: 2 if kwargs[:model].present? && kwargs[:attribute].present?
  10. 2 kwargs[:name] = model_name(kwargs[:model], kwargs[:attribute])
  11. 2 kwargs[:value] = kwargs[:model].send(kwargs[:attribute])
  12. then: 0 else: 2 kwargs[:error] =
  13. 2 else: 18 kwargs[:model].errors[kwargs[:attribute]].first if kwargs[:model].errors[kwargs[:attribute]].present?
  14. 18 then: 0 else: 18 elsif kwargs[:name].blank?
  15. raise ArgumentError, "You must provide a name attribute, or a model"
  16. end
  17. 20 super(**kwargs)
  18. end
  19. 1 def classes
  20. 18 then: 0 else: 18 "lui-input #{error.present? ? "lui-input--with-error" : ""}"
  21. end
  22. 1 private
  23. 1 def model_name(model, attribute)
  24. 2 "#{model.model_name.param_key}[#{attribute}]"
  25. end
  26. 1 def merged_input_attributes
  27. {
  28. 18 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

92.31% lines covered

71.43% branches covered

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

app/components/loopos_ui/inputs/ai.rb

75.0% lines covered

0.0% branches covered

8 relevant lines. 6 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 :max_rows,
  7. Types::Coercible::Integer.constructor { |value|
  8. then: 0 else: 0 value.to_i <= 1 ? 1 : value.to_i + 1
  9. },
  10. default: -> { 7 }
  11. end
  12. end
  13. end

app/components/loopos_ui/inputs/ai/ai.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
  2. class="lui-ai-input__wrapper lui-ai-animated-border"
  3. data-controller="input-ai"
  4. data-input-ai-spinner-value="<%= spinner %>"
  5. data-input-ai-max-rows-value="<%= max_rows %>"
  6. >
  7. <textarea
  8. data-input-ai-target="input"
  9. rows="1"
  10. placeholder="<%= placeholder || t(".placeholder") %>"
  11. class="lui-ai-input__input lui-ai-input__textarea"
  12. style="--lui-ai-max-rows: <%= max_rows %>;"
  13. ></textarea>
  14. <div data-input-ai-target="buttonWrapper" class="lui-ai-input__button-wrapper lui-ai-input__button-wrapper--center">
  15. <%= render LooposUi::Button.new(
  16. leading_icon: "fa-solid fa-paper-plane-top",
  17. kind: :neutral,
  18. type: :secondary,
  19. size: :default,
  20. tag_options: {
  21. class: "lui-ai-input__button",
  22. data: {
  23. "input-ai-target": "button",
  24. action: "click->input-ai#handleButtonClick"
  25. },
  26. type: :button
  27. }
  28. ) %>
  29. then: 0 else: 0 <% if spinner %>
  30. <i
  31. data-input-ai-target="spinner"
  32. class="fa-solid fa-spinner fa-spin lui-ai-input__spinner"
  33. ></i>
  34. <% end %>
  35. </div>
  36. </div>

app/components/loopos_ui/inputs/base_input_concern.rb

90.91% lines covered

50.0% branches covered

33 relevant lines. 30 lines covered and 3 lines missed.
6 total branches, 3 branches covered and 3 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. 14 option :model, optional: true
  19. 14 option :attribute, optional: true
  20. # Manual usage
  21. 14 option :name, optional: true
  22. 14 option :value, Types::Coercible::String, optional: true
  23. 14 option :placeholder, optional: true
  24. 14 option :error, optional: true
  25. 14 option :help, optional: true
  26. 14 option :mode, default: -> { :inline }, type: Types::Coercible::Symbol.enum(
  27. :inline, :form, :autosubmit
  28. )
  29. 14 option :form, optional: true
  30. 28 option :readonly, default: -> { false }, type: Types::Bool
  31. 54 option :custom_readonly, default: -> { false }, type: Types::Bool # This will make the input render the content instead of the value
  32. 24 option :open_actions, default: -> { false }
  33. 54 option :extra_input_attributes, default: -> { {} }, type: Types::Hash
  34. end
  35. 1 def unique_id
  36. 4 @unique_id ||= [
  37. self.class.name.downcase.parameterize,
  38. 2 then: 0 else: 2 then: 2 else: 0 then: 2 else: 0 model.present? ? dom_id(model) : name&.to_s&.gsub(/\[|\]/, "_"),
  39. attribute.presence,
  40. rand(10**10),
  41. ].compact.join("_")
  42. end
  43. 1 def base_input_attributes
  44. 24 [
  45. :model,
  46. :attribute,
  47. :name,
  48. :value,
  49. :placeholder,
  50. :error,
  51. :help,
  52. :mode,
  53. :form,
  54. :readonly,
  55. :open_actions,
  56. ].to_h do |o|
  57. 264 [o, send(o)]
  58. end
  59. end
  60. 1 [:inline, :form, :autosubmit].each do |mode|
  61. 3 define_method("#{mode}?") do
  62. self.mode == mode
  63. end
  64. end
  65. end
  66. end
  67. end

app/components/loopos_ui/inputs/checkbox.rb

70.0% lines covered

0.0% branches covered

20 relevant lines. 14 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 def checked?
  13. then: 0 else: 0 return state == :checked if state.present?
  14. then: 0 else: 0 val = (value == "-" ? false : value)
  15. ActiveModel::Type::Boolean.new.cast(val)
  16. end
  17. 1 def indeterminate?
  18. state == :indeterminate
  19. end
  20. 1 def state_css_class
  21. then: 0 else: 0 return "lui-checkbox--#{state}" if state.present?
  22. then: 0 else: 0 checked? ? "lui-checkbox--checked" : nil
  23. end
  24. end
  25. end
  26. end

app/components/loopos_ui/inputs/checkbox/checkbox.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(
  2. class: ["lui-checkbox", state_css_class].compact.join(" "),
  3. data: { controller: "checkbox" }
  4. ) do %>
  5. <label class="lui-checkbox__wrapper">
  6. <span class="lui-checkbox__input">
  7. <%= tag.input(
  8. type: "checkbox",
  9. data: {
  10. checkbox_target: "input",
  11. action: "change->checkbox#toggle"
  12. }.merge(data || {}),
  13. checked: checked?,
  14. indeterminate: indeterminate?,
  15. class: "lui-checkbox__original",
  16. value: value,
  17. name: name,
  18. required: required,
  19. disabled: readonly,
  20. form: form
  21. ) %>
  22. <span class="lui-checkbox__inner"></span>
  23. </span>
  24. then: 0 else: 0 <%= tag.span(class: "lui-checkbox__label") do %>
  25. <%= label %><%= content %>
  26. <% end if label.present? || content.present? %>
  27. </label>
  28. <% end %>

app/components/loopos_ui/inputs/combobox.rb

44.83% lines covered

0.0% branches covered

174 relevant lines. 78 lines covered and 96 lines missed.
82 total branches, 0 branches covered and 82 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. then: 0 if autosubmit?
  195. else: 0 "lui-combobox--autosubmit"
  196. then: 0 elsif form?
  197. "lui-combobox--form"
  198. else: 0 else
  199. "lui-combobox--inline"
  200. end
  201. end
  202. 1 private
  203. 1 def prime_name_and_model_fields!(kwargs, multiple, helper)
  204. model = kwargs[:model]
  205. attribute = kwargs[:attribute]
  206. then: 0 if model.present? && attribute.present?
  207. else: 0 prime_from_model!(kwargs, model, attribute, multiple, helper)
  208. then: 0 else: 0 elsif kwargs[:name].blank?
  209. raise ArgumentError, "You must provide a name attribute, or a model and attribute"
  210. end
  211. end
  212. 1 def prime_from_model!(kwargs, model, attribute, multiple, helper)
  213. kwargs[:name] ||= helper.field_name(model.model_name.param_key, attribute, multiple: multiple)
  214. apply_raw_attribute_to_selection!(kwargs, model, attribute, multiple)
  215. copy_model_error!(kwargs, model, attribute)
  216. end
  217. 1 def apply_raw_attribute_to_selection!(kwargs, model, attribute, multiple)
  218. then: 0 else: 0 return if kwargs.key?(:selected) || kwargs.key?(:value)
  219. raw = model.public_send(attribute)
  220. assign_raw_to_selection!(kwargs, multiple, raw)
  221. end
  222. 1 def assign_raw_to_selection!(kwargs, multiple, raw)
  223. then: 0 if multiple
  224. kwargs[:selected] = Array.wrap(raw).map(&:to_s).reject(&:blank?)
  225. else: 0 else
  226. then: 0 else: 0 kwargs[:value] = raw&.to_s
  227. end
  228. end
  229. 1 def copy_model_error!(kwargs, model, attribute)
  230. errs = model.errors[attribute]
  231. then: 0 else: 0 then: 0 else: 0 kwargs[:error] ||= errs.first&.to_s if errs.present?
  232. end
  233. 1 def normalize_multiple!(kwargs, multiple)
  234. else: 0 then: 0 return unless multiple
  235. ensure_multiple_name!(kwargs)
  236. normalize_selected_vs_value!(kwargs)
  237. end
  238. 1 def ensure_multiple_name!(kwargs)
  239. n = kwargs[:name].to_s
  240. else: 0 then: 0 kwargs[:name] = "#{n}[]" unless n.end_with?("[]")
  241. end
  242. 1 def normalize_selected_vs_value!(kwargs)
  243. then: 0 if kwargs[:selected].nil?
  244. normalize_nil_selected!(kwargs)
  245. else: 0 else
  246. kwargs[:selected] = Array.wrap(kwargs[:selected]).map(&:to_s).reject(&:blank?)
  247. kwargs[:value] = nil
  248. end
  249. end
  250. 1 def normalize_nil_selected!(kwargs)
  251. then: 0 if kwargs[:value].is_a?(Array)
  252. kwargs[:selected] = kwargs[:value].map(&:to_s).reject(&:blank?)
  253. else: 0 kwargs[:value] = nil
  254. then: 0 elsif kwargs[:multiple] && !kwargs[:value].is_a?(Array)
  255. kwargs[:selected] = Array.wrap(kwargs[:value]).map(&:to_s).reject(&:blank?)
  256. kwargs[:value] = nil
  257. else: 0 else
  258. kwargs[:selected] = []
  259. end
  260. end
  261. end
  262. end
  263. 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--<%= mode %> 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

46.43% lines covered

0.0% branches covered

56 relevant lines. 26 lines covered and 30 lines missed.
24 total branches, 0 branches covered and 24 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 included do
  7. 4 option :name, optional: true
  8. 4 option :mode, Types::Symbol.enum(:inline, :form, :autosubmit), default: -> { :inline }
  9. 4 option :model, Types::Instance(ActiveRecord::Base), optional: true
  10. 4 option :attribute, Types::Coercible::Symbol, optional: true
  11. 4 option :multiple, default: -> { false }, type: Types::Bool
  12. 4 option :delete_path, Types::Coercible::String, optional: true
  13. 4 option :accept,
  14. Types::Array.of(Types::Coercible::String) | Types::Symbol.enum(:all),
  15. optional: true,
  16. default: -> { LooposUi.config.file_default_accept_formats }
  17. 4 option :form_id, Types::Coercible::String, optional: true
  18. # Update context can be used to render the component differently when streaming an update, for example
  19. 4 option :context, Types::Symbol.enum(:default, :action), default: -> { :default }
  20. end
  21. end
  22. 1 include FileInputConcern
  23. 1 option :value, Types::Coercible::String, optional: true
  24. 1 option :readonly, default: -> { false }, type: Types::Bool
  25. 1 def initialize(*params, **data)
  26. super
  27. validate_input_identity!
  28. validate_delete_path_requirement!
  29. end
  30. 1 def call
  31. mimefy_accept_types
  32. then: 0 if mode == :form
  33. else: 0 render(LooposUi::Inputs::File::FormFile.new(**args))
  34. then: 0 elsif multiple?
  35. render(LooposUi::Inputs::File::Multiple.new(**args))
  36. else: 0 else
  37. render(LooposUi::Inputs::File::Single.new(**args.except(:context)))
  38. end
  39. end
  40. 1 private
  41. 1 def validate_input_identity!
  42. then: 0 else: 0 return if mode == :form
  43. then: 0 else: 0 return if attribute.present? || name.present?
  44. raise ArgumentError, "attribute or name are required when mode is not :form"
  45. end
  46. 1 def validate_delete_path_requirement!
  47. then: 0 else: 0 return if mode == :form || readonly
  48. else: 0 then: 0 return unless model.present? && attribute.present?
  49. then: 0 else: 0 return if delete_path.present?
  50. raise ArgumentError, "delete_path is required for editable model-backed file inputs"
  51. end
  52. 1 def multiple?
  53. then: 0 else: 0 return multiple if name.present?
  54. model.class.reflect_on_attachment(attribute).macro == :has_many_attached
  55. end
  56. 1 def mimefy_accept_types
  57. then: 0 else: 0 if accept == :all
  58. @accept = nil
  59. return
  60. end
  61. @accept = accept.flat_map do |mime|
  62. ::MIME::Types.type_for(".#{mime}") || ::MIME::Types[mime.to_s]
  63. end.compact_blank!
  64. end
  65. 1 def args
  66. self.class.dry_initializer.definitions.keys.filter_map do |key|
  67. val = send(key)
  68. then: 0 else: 0 next [key, val] if val.present?
  69. then: 0 else: 0 next [key, val] if key == :value && !val.nil?
  70. then: 0 else: 0 next [key, val] if key == :readonly && val
  71. nil
  72. end.to_h
  73. end
  74. end
  75. end
  76. 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. download: true,
  11. data: { turbo: false }
  12. })%>
  13. <%= render LooposUi::Modal.new(title: t('.delete_file')) do |modal| %>
  14. <% modal.with_trigger_button(
  15. icon: :trash,
  16. size: :small,
  17. type: :tertiary,
  18. kind: :neutral,
  19. tooltip_text: t('.delete_file'))%>
  20. <% modal.with_custom_content do %>
  21. <p><%= t('.delete_file_warning') %></p>
  22. <% end %>
  23. <% modal.with_primary_action(
  24. text: t('.delete'),
  25. kind: :danger,
  26. tag_options: {
  27. type: :button,
  28. data: delete_data
  29. })%>
  30. <% end %>
  31. <% 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

14 relevant lines. 0 lines covered and 14 lines missed.
2 total branches, 0 branches covered and 2 branches missed.
    
  1. <% file_attributes = form_file_input_attributes %>
  2. then: 0 <% if readonly %>
  3. <div class="lui-input-file-form lui-input-file-form--readonly" id="div_<%= form_file_input_id %>">
  4. <span
  5. class="lui-input-file-form__file-name"
  6. id="span_<%= form_file_input_id %>"
  7. >
  8. <%= initial_form_file_display_name %>
  9. </span>
  10. </div>
  11. else: 0 <% else %>
  12. <div
  13. class="lui-input-file-form"
  14. data-controller="form-file"
  15. data-form-file-form-id-value="<%= form_id.to_s %>"
  16. id="div_<%= file_attributes[:id] %>"
  17. >
  18. <%= tag.input(
  19. **file_attributes.merge(
  20. class: "lui-input-file-form__input sr-only"
  21. )
  22. ) %>
  23. <%= render LooposUi::Button.new(
  24. kind: :neutral,
  25. type: :secondary,
  26. size: :tiny,
  27. text: t('.select_file'),
  28. icon: "fa-regular fa-file-arrow-up",
  29. tag: :label,
  30. tag_options: {
  31. for: file_attributes[:id],
  32. id: "#{file_attributes[:id]}_label",
  33. data: {
  34. form_file_target: "button",
  35. select_text: t('.select_file'),
  36. replace_text: t('.replace_file')
  37. }
  38. }
  39. ) %>
  40. <span
  41. data-form-file-target="fileName"
  42. class="lui-input-file-form__file-name"
  43. id="span_<%= file_attributes[:id] %>"
  44. >
  45. <%= initial_form_file_display_name %>
  46. </span>
  47. </div>
  48. <% end %>

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

48.0% lines covered

0.0% branches covered

25 relevant lines. 12 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 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. {
  25. type: "file",
  26. id: form_file_input_id,
  27. name: input_name,
  28. "data-form-file-target": "input",
  29. "data-action": "change->form-file#updateFileName",
  30. class: "lui-input-file-form__input",
  31. form: form_id,
  32. then: 0 else: 0 accept: if accept.present?
  33. then: 0 else: 0 accept.is_a?(Array) ? accept.join(",") : accept
  34. end,
  35. }.compact
  36. end
  37. # Initial label in the UI (file inputs cannot set a value).
  38. 1 def initial_form_file_display_name
  39. then: 0 else: 0 return t(".no_file_chosen") if value.blank?
  40. path = value.to_s.split(/[?#]/, 2).first.to_s
  41. ::File.basename(path)
  42. end
  43. end
  44. end
  45. end
  46. end

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

0.0% lines covered

0.0% branches covered

32 relevant lines. 0 lines covered and 32 lines missed.
8 total branches, 0 branches covered and 8 branches missed.
    
  1. then: 0 <% if readonly %>
  2. <%= tag.div(
  3. class: "lui-input-file-container lui-input-file-container--readonly",
  4. id: unique_id,
  5. ) do %>
  6. <div class="lui-input-file lui-input-file--readonly">
  7. <div class="flex justify-end items-center gap-[2px] min-w-0">
  8. <%= tag.span(class: "lui-input-file__text") do %>
  9. then: 0 <% if has_uploaded_files? %>
  10. <%= t('.files_uploaded', file_count: uploaded_files_count) %>
  11. else: 0 <% else %>
  12. <%= t('.no_file_chosen') %>
  13. <% end %>
  14. <% end %>
  15. </div>
  16. </div>
  17. <% end %>
  18. else: 0 <% else %>
  19. <%= tag.div(
  20. class: "lui-input-file-container",
  21. id: unique_id,
  22. data: {
  23. controller: "input-file",
  24. "input-file-form-value": form_id,
  25. "input-file-delete-path-value": delete_path,
  26. action: "click->input-file#toggleActions"
  27. }) do %>
  28. <div class="lui-input-file">
  29. <%= render LooposUi::Button.new(
  30. kind: :neutral,
  31. type: :secondary,
  32. size: :tiny,
  33. text: t('.select_files'),
  34. icon: "fa-regular fa-file-arrow-up",
  35. tag: :label,
  36. tag_options: { for: "#{unique_id}-file-input" }
  37. ) %>
  38. <%= tag.input(**file_input_attributes) %>
  39. <div class="flex justify-end items-center gap-[2px] min-w-0">
  40. <%= tag.span(class: "lui-input-file__text") do %>
  41. then: 0 <% if has_uploaded_files? %>
  42. <%= t('.files_uploaded', file_count: uploaded_files_count) %>
  43. else: 0 <% else %>
  44. <%= t('.no_file_chosen') %>
  45. <% end %>
  46. <% end %>
  47. then: 0 else: 0 <%= tag.div(class: "lui-input-file__actions") do %>
  48. <%= render LooposUi::Popover.new(
  49. open: context == :action,
  50. position: :bottom_right,
  51. anchor: :top_right,
  52. anchor_selector: "##{unique_id}",
  53. rotate_toggle: true,
  54. system_arguments: {
  55. data: {
  56. "input-file-target": "popover"
  57. }
  58. }
  59. ) do |popover| %>
  60. <% popover.with_custom_toggle do %>
  61. <i class="fa-regular fa-chevron-down text-[10px]"></i>
  62. <% end %>
  63. <% popover.with_target do %>
  64. <div class="lui-input-files lui-action-menu">
  65. <% files.each do |file| %>
  66. <%= render LooposUi::Inputs::File::Option.new(file, delete_data: {
  67. action: "click->input-file#destroy",
  68. "input-file-attachment-signed-id-param": file.signed_id,
  69. }) %>
  70. <% end %>
  71. </div>
  72. <% end %>
  73. <% end %>
  74. <% end if can_manage_uploaded_files? %>
  75. <span class="lui-input-file__spinner">
  76. <%= tag.i(class: "fa-regular fa-spinner") %>
  77. </span>
  78. </div>
  79. </div>
  80. <% end %>
  81. <% end %>

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

48.28% lines covered

0.0% branches covered

29 relevant lines. 14 lines covered and 15 lines missed.
18 total branches, 0 branches covered and 18 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. 1 then: 0 else: 0 delegate :t, to: LooposUi::Inputs::File
  8. 1 def files
  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: true)
  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 uploaded_files_count
  21. then: 0 else: 0 return files.count if files.present?
  22. then: 0 else: 0 return 0 if value.blank?
  23. then: 0 else: 0 return value.size if value.is_a?(Array)
  24. 1
  25. end
  26. 1 def has_uploaded_files?
  27. uploaded_files_count.positive?
  28. end
  29. 1 def can_manage_uploaded_files?
  30. files.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. multiple: true,
  39. name: input_name,
  40. data: {
  41. "input-file-target": "file",
  42. "action": "change->input-file#upload",
  43. },
  44. id: "#{unique_id}-file-input",
  45. class: "hidden",
  46. type: "file",
  47. form: form_id,
  48. accept: accept_attribute,
  49. }
  50. end
  51. end
  52. end
  53. end
  54. 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

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

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

48.28% lines covered

0.0% branches covered

29 relevant lines. 14 lines covered and 15 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: input_name,
  39. data: {
  40. "input-file-target": "file",
  41. "action": "change->input-file#upload",
  42. },
  43. id: "#{unique_id}-file-input",
  44. class: "hidden",
  45. type: "file",
  46. form: form_id,
  47. accept: accept_attribute,
  48. }
  49. end
  50. end
  51. end
  52. end
  53. end

app/components/loopos_ui/inputs/money.rb

93.33% lines covered

33.33% branches covered

15 relevant lines. 14 lines covered and 1 lines missed.
3 total branches, 1 branches covered and 2 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. 3 option :min, default: -> { 0 }
  7. 3 option :max, default: -> { nil }
  8. 3 option :step, default: -> { 0.01 }
  9. 3 option :data_attributes, default: -> { {} }
  10. 1 def input_attributes
  11. 6 base_input_attributes.merge({
  12. input_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. type: :number,
  21. mode: mode,
  22. })
  23. end
  24. 1 def currency_symbol
  25. 2 else: 0 case currency
  26. when: 2 when String
  27. 2 ::Money::Currency.new(currency).symbol
  28. when: 0 when ::Money::Currency
  29. currency.symbol
  30. end
  31. end
  32. end
  33. end
  34. end

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

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. 2 <div class="lui-money lui-money--<%= mode %> relative" data-controller="money-input">
  2. 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. 2 )) do |input| %>
  7. 2 <% input.with_left_addon do %>
  8. 2 <span class="lui-money__currency text-gray-700">
  9. 2 <%= currency_symbol %>
  10. </span>
  11. <% end %>
  12. <% end %>
  13. 2 </div>

app/components/loopos_ui/inputs/number.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 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 def input_attributes
  10. base_input_attributes.merge({
  11. input_attributes: {
  12. min: min,
  13. max: max,
  14. step: step,
  15. inputmode: "decimal",
  16. data: {
  17. "number-input-target": "input",
  18. },
  19. },
  20. type: :number,
  21. mode: mode,
  22. })
  23. end
  24. end
  25. end
  26. 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--<%= mode %> 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/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

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 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 Search < LoopComponent
  4. 1 include BaseInputConcern
  5. 3 option :expanded, Types::Bool, default: -> { false } # DEPRECATED, will not be used anymore
  6. 3 option :data_attributes, default: -> { {} }
  7. 3 option :clearable, Types::Bool, default: -> { true }
  8. # Overrides from BaseInputConcern
  9. 3 option :mode, Types::Coercible::Symbol.enum(:autosubmit), default: -> { :autosubmit }
  10. 3 option :event_only, Types::Bool, default: -> { true }
  11. 1 def input_attributes
  12. 2 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. 2 "lui-search"
  24. end
  25. end
  26. end
  27. end

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

100.0% lines covered

50.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
2 total branches, 1 branches covered and 1 branches missed.
    
  1. 2 <div data-controller="search"
  2. 2 id="<%= unique_id %>"
  3. 2 class="<%= classes %>"
  4. 2 data-search-input-outlet="<%="##{unique_id} .lui-inner-input" %>"
  5. 2 data-search-event-only-value="<%= event_only %>"
  6. >
  7. 4 <%= render LooposUi::Input.new(**input_attributes) do |input| %>
  8. 2 <% input.with_left_addon do %>
  9. 2 <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. 2 then: 2 else: 0 <% if clearable %>
  14. 2 <% input.with_right_addon do %>
  15. 2 <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. 2 </div>

app/components/loopos_ui/inputs/select.rb

65.52% lines covered

0.0% branches covered

29 relevant lines. 19 lines covered and 10 lines missed.
11 total branches, 0 branches covered and 11 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 mod :blank_option_selected, base: "lui-input-select", condition: -> {
  17. then: 0 else: 0 include_blank && selected_option&.value == ""
  18. }
  19. 1 def input_attributes
  20. base_input_attributes.merge({
  21. then: 0 else: 0 value: selected_option&.text,
  22. input_attributes: {
  23. readonly: true,
  24. placeholder: placeholder,
  25. then: 0 else: 0 value: selected_option&.text,
  26. data: {
  27. "input-select-target": "textInput",
  28. },
  29. }.merge(extra_input_attributes.presence || {}),
  30. })
  31. end
  32. 1 def selected_option
  33. then: 0 if include_blank
  34. options.find { |o| o.value == value } || options.first
  35. else: 0 else
  36. options.find { |o| o.value == value }
  37. end
  38. end
  39. 1 def options
  40. case include_blank
  41. when: 0 when String
  42. [Struct.new(:value, :text, :icon, :tooltip).new("", include_blank, nil, nil), *@options]
  43. when: 0 when true
  44. [Struct.new(:value, :text, :icon, :tooltip).new("", "", nil, nil), *@options]
  45. else: 0 else
  46. @options
  47. end
  48. end
  49. 1 def blank_option?(option)
  50. option.value.blank? && (option.text.blank? || option.text == include_blank)
  51. end
  52. end
  53. end
  54. end

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

0.0% lines covered

0.0% branches covered

23 relevant lines. 0 lines covered and 23 lines missed.
18 total branches, 0 branches covered and 18 branches missed.
    
  1. then: 0 <% if !readonly %>
  2. <%= tag.div(
  3. id: unique_id,
  4. class: "grow",
  5. data: {
  6. controller: "input-select",
  7. input_select_input_outlet: "##{unique_id} .lui-inner-input",
  8. input_select_mode_value: mode,
  9. input_select_form_value: form,
  10. input_select_portal_target_value: portal_target,
  11. }) do
  12. %>
  13. <div class="lui-input-select lui-input-select--<%= mode %> relative <%= classes %>">
  14. <%= render LooposUi::Input.new(**input_attributes) do |input| %>
  15. <% input.with_right_addon do %>
  16. <span class="flex flex-row items-center gap-2">
  17. then: 0 else: 0 then: 0 else: 0 <i class="<%= selected_option.blank? ? 'opacity-0' : '' %> fa-solid fa-xmark cursor-pointer text-gray-400 lui-input-select__clear <%= clearable ? '' : 'hidden!' %>"
  18. data-input-select-target="clearButton"
  19. data-action="click->input-select#clear">
  20. </i>
  21. <div data-input-select-target="icon">
  22. <%# FIXME: Color should not be hardcoded %>
  23. <%= render LooposUi::Icon.new(icon: "fa-regular fa-chevron-down", size: 12, color: "#212529") %>
  24. </div>
  25. </span>
  26. <% end %>
  27. <% end %>
  28. <div data-input-select-target="menu" class="lui-input-select__wrapper">
  29. <div class="lui-input-select__options" role="listbox">
  30. <% options.each do |option| %>
  31. then: 0 else: 0 <div class="lui-input-select__option <%= blank_option?(option) ? 'lui-input-select__option--blank' : '' %>"
  32. role="option"
  33. data-input-select-target="option"
  34. data-text="<%= option.text %>"
  35. data-value="<%= option.value %>">
  36. <div class="lui-input-select__option-wrapper">
  37. <div class="lui-input-select__option-text">
  38. then: 0 else: 0 <%= tag.i(class: faviconize(option.icon)) if option.icon %>
  39. <span role="text"><%= option.text %></span>
  40. then: 0 else: 0 <%= render LooposUi::IconTooltip.new(icon: "fa-regular fa-circle-info", size: "12", text: option.tooltip) if option.tooltip %>
  41. </div>
  42. 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" }) %>
  43. </div>
  44. </div>
  45. <% end %>
  46. </div>
  47. </div>
  48. </div>
  49. <%= hidden_field_tag(
  50. input_attributes[:name],
  51. value,
  52. form: form,
  53. data: {
  54. input_select_target: "hiddenInput"
  55. })%>
  56. <% end %>
  57. <% else %>
  58. else: 0 <%
  59. selected_option = (options.presence || []).find { |o| o.value.to_s == value.to_s }
  60. %>
  61. then: 0 else: 0 <%= content_tag(:span, selected_option&.text.presence || "-", class: "lui-input__value") %>
  62. <% end %>

app/components/loopos_ui/inputs/select2.rb

57.58% lines covered

0.0% branches covered

66 relevant lines. 38 lines covered and 28 lines missed.
27 total branches, 0 branches covered and 27 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. 1 class Option < Dry::Struct
  7. 1 attribute :value, Types::Coercible::String
  8. 1 attribute :text, Types::Coercible::String
  9. 1 attribute :icon, Types::Coercible::String.optional.default("".freeze)
  10. 1 attribute :tooltip, Types::Coercible::String.optional.default("".freeze)
  11. 1 attribute :child_options, Types::Array.of(Option).optional.default([].freeze)
  12. 1 attribute :attributes, Types::Hash.optional.default({}.freeze)
  13. end
  14. 1 option :options, type: [Option], default: -> { [] }
  15. 1 option :multiple, type: Types::Bool, default: -> { false }
  16. 1 option :placeholder, default: -> { nil }
  17. 1 option :include_blank, Types::Bool | Types::String, default: -> { false }
  18. 1 option :option_indent_size, type: Types::Coercible::Integer, default: -> { 20 }
  19. 1 option :parent_selects_children, default: -> { true }
  20. 1 option :show_actions_in_menu, default: -> { false }
  21. 1 option :portal_target, default: -> { nil }
  22. 1 option :clearable, Types::Bool, default: -> { true }
  23. 1 option :searchable, Types::Bool, default: -> { true }
  24. 1 option :submit_selected_value, Types::Bool, default: -> { false }
  25. 1 option :bind_hidden_input_to_form, Types::Bool, default: -> { false }
  26. 1 option :searchable, Types::Bool, default: -> { true }
  27. 1 option :show_loading_on_submit, Types::Bool, default: -> { true }
  28. 1 def input_attributes
  29. base_input_attributes.merge({
  30. value: presented_selected_text,
  31. input_attributes: {
  32. readonly: true,
  33. placeholder: placeholder,
  34. value: presented_selected_text,
  35. data: {
  36. "input-select-target": "textInput",
  37. "action": "select2:updateController->input-select#updateValues",
  38. },
  39. },
  40. })
  41. end
  42. 1 def hidden_input_name
  43. attribute_name = input_attributes[:name].presence
  44. then: 0 else: 0 if attribute_name.blank? && model.present? && attribute.present?
  45. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 model_name = model.present? ? dom_id(model) : name&.to_s&.gsub(/\[|\]/, "_")
  46. "#{model_name}[#{attribute}]"
  47. end
  48. else: 0 then: 0 return attribute_name unless multiple
  49. attribute_name + "[]"
  50. end
  51. 1 def hidden_input_value
  52. then: 0 else: 0 then: 0 else: 0 return multiple ? Array(value).first : value if submit_selected_value
  53. input_attributes[:value]
  54. end
  55. 1 def hidden_input_form
  56. then: 0 else: 0 return form if bind_hidden_input_to_form
  57. nil
  58. end
  59. # TODO: The JS is missing receiving these options so that they start as selected
  60. 1 def selected_options
  61. then: 0 if value.is_a?(Array)
  62. formatted_value = value.map(&:to_s)
  63. else: 0 all_options.filter { |o| formatted_value.include?(o.value.to_s) }
  64. then: 0 elsif include_blank
  65. [all_options.find { |o| o.value.to_s == value.to_s } || all_options.find { |o| blank_option?(o) }].compact
  66. else: 0 else
  67. [all_options.find { |o| o.value.to_s == value.to_s }].compact
  68. end
  69. end
  70. 1 def presented_selected_text
  71. then: 0 else: 0 if selected_options.present?
  72. selected_options.map(&:text).join(", ").gsub(/([\w\s]{40}).+/,'\1...')
  73. else
  74. nil
  75. end
  76. end
  77. 1 def options
  78. root_options = @options
  79. else: 0 then: 0 return root_options unless include_blank
  80. [blank_option_struct, *root_options]
  81. end
  82. 1 def all_options(root = options)
  83. root.map { |o| [o, all_options(o.child_options)] }.flatten
  84. end
  85. 1 def blank_option?(option)
  86. option.value.blank? && (option.text.blank? || option.text == include_blank)
  87. end
  88. 1 private
  89. 1 def blank_option_struct
  90. Option.new(
  91. value: "",
  92. text: blank_option_text,
  93. icon: nil,
  94. tooltip: nil,
  95. child_options: [],
  96. attributes: {},
  97. )
  98. end
  99. 1 def blank_option_text
  100. case include_blank
  101. when: 0 when String
  102. include_blank
  103. when: 0 when true
  104. ""
  105. else: 0 else
  106. ""
  107. end
  108. end
  109. end
  110. end
  111. end

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

0.0% lines covered

0.0% branches covered

42 relevant lines. 0 lines covered and 42 lines missed.
32 total branches, 0 branches covered and 32 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. <%= tag.div(
  38. id: unique_id,
  39. data: {
  40. controller: "input-select2",
  41. input_select2_input_outlet: "##{unique_id} .lui-inner-input",
  42. input_select2_mode_value: mode,
  43. input_select2_form_value: form,
  44. input_select2_multiple_value: multiple,
  45. input_select2_portal_target_value: portal_target,
  46. input_select2_show_loading_on_submit_value: show_loading_on_submit,
  47. }) do
  48. %>
  49. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 <div class="lui-input-select lui-input-select--<%= mode %> 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' : '' %>">
  50. <%# FIXME: The name is changed so not to interfere with the hidden field below %>
  51. <%= render LooposUi::Input.new(**input_attributes.merge(name: "_")) do |input| %>
  52. <% input.with_right_addon do %>
  53. <span class="flex flex-row items-center gap-2">
  54. 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!' %>"
  55. data-input-select2-target="clearButton"
  56. data-action="click->input-select2#clear">
  57. </i>
  58. <div data-input-select2-target="icon">
  59. <%# FIXME: Color should not be hardcoded %>
  60. <%= render LooposUi::Icon.new(icon: "fa-regular fa-chevron-down", size: 12, color: "#212529") %>
  61. </div>
  62. </span>
  63. <% end %>
  64. <% end %>
  65. <%= tag.div(
  66. class: "lui-input-select__wrapper",
  67. data: {
  68. controller: "select2",
  69. "input-select2-target": "menu",
  70. select2_parent_selects_children_value: parent_selects_children,
  71. select_target_id: unique_id,
  72. select2_id: unique_id,
  73. }
  74. ) do %>
  75. <div class="lui-input-select__options" role="listbox">
  76. then: 0 else: 0 <% if searchable %>
  77. <%= render LooposUi::Inputs::Search.new(
  78. name: "query",
  79. value: nil,
  80. placeholder: I18n.t('loopos_ui.inputs.select2.search_placeholder', default: 'Search...'),
  81. expanded: true,
  82. clearable: false,
  83. data_attributes: {
  84. action: "input->select2#handleSearch"
  85. }
  86. ) %>
  87. <div class="lui-association-overlay__divider"></div>
  88. <% end %>
  89. <div class="lui-association-overlay__results" data-select2-target="results">
  90. <% options.each do |option| %>
  91. <%= render_function(option, 0) %>
  92. <% end %>
  93. <div class="hidden lui-association-overlay__empty-search" data-select2-target="emptySearch">
  94. <%= I18n.t('loopos_ui.inputs.select2.no_results', default: 'No results found') %>
  95. </div>
  96. </div>
  97. then: 0 else: 0 <% if multiple && show_actions_in_menu %>
  98. <div class="lui-select2__actions" data-select2-target="actions">
  99. <%= render LooposUi::Button.new(
  100. text: I18n.t('loopos_ui.inputs.select2.reset', default: 'Clear'),
  101. kind: :neutral,
  102. type: :secondary,
  103. size: :tiny,
  104. tag_options: {
  105. data: {
  106. "action": "click->select2#clear"
  107. },
  108. type: :button
  109. }
  110. )
  111. %>
  112. <%= render LooposUi::Button.new(
  113. text: I18n.t('loopos_ui.inputs.select2.apply', default: 'Apply'),
  114. kind: :success,
  115. type: :primary,
  116. size: :tiny,
  117. tag_options: {
  118. type: :button,
  119. data: {
  120. "action": "click->select2#save"
  121. },
  122. }
  123. )
  124. %>
  125. </div>
  126. <% end %>
  127. </div>
  128. <% end %>
  129. </div>
  130. <%= tag.div(data: { input_select2_target: "hiddenInputWrapper" }) do %>
  131. <%= hidden_field_tag(
  132. hidden_input_name,
  133. hidden_input_value,
  134. form: hidden_input_form,
  135. data: {
  136. input_select2_target: "hiddenInput"
  137. })
  138. %>
  139. <% end %>
  140. <% end %>

app/components/loopos_ui/inputs/text.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 Inputs
  3. 1 class Text < LoopComponent
  4. 1 include BaseInputConcern
  5. 11 option :maxlength, default: -> { nil }
  6. 11 option :minlength, default: -> { nil }
  7. 1 def input_attributes
  8. 10 base_input_attributes.merge({
  9. type: :text,
  10. mode: mode,
  11. input_attributes: {
  12. maxlength: maxlength,
  13. minlength: minlength,
  14. }.merge(extra_input_attributes.presence || {}),
  15. })
  16. end
  17. end
  18. end
  19. end

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

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. 10 <div class="lui-text lui-text--<%= mode %> relative">
  2. 10 <%= render LooposUi::Input.new(**input_attributes) %>
  3. </div>

app/components/loopos_ui/inputs/text_area.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. 1 module LooposUi
  2. 1 module Inputs
  3. 1 class TextArea < LoopComponent
  4. 1 include BaseInputConcern
  5. 7 option :rows, default: -> { nil }
  6. 7 option :cols, default: -> { nil }
  7. 7 option :maxlength, default: -> { nil }
  8. 7 option :minlength, default: -> { nil }
  9. 7 option :wrap, default: -> { "soft" }
  10. 7 option :resize, default: -> { "vertical" }
  11. 7 option :data_attributes, default: -> { {} }
  12. 1 def input_attributes
  13. 6 attrs = base_input_attributes
  14. 6 then: 2 else: 4 attrs.delete(:readonly) if readonly
  15. 6 attrs.merge({
  16. input_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. type: "textarea",
  27. })
  28. end
  29. end
  30. end
  31. end

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

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. 6 <div data-controller="textarea" class="lui-text_area">
  2. 6 <%= render LooposUi::Input.new(**input_attributes) %>
  3. </div>

app/components/loopos_ui/item/state_label.rb

91.43% lines covered

62.5% branches covered

35 relevant lines. 32 lines covered and 3 lines missed.
8 total branches, 5 branches covered and 3 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. 116 then: 0 else: 116 if item.is_a?(self)
  21. return item
  22. end
  23. 116 args = schema.keys.each_with_object({}) do |key, args|
  24. 928 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. 928 then: 696 else: 232 args[key.name] = value if value.present?
  28. end
  29. 116 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. 113 option :macro_condensed, Types::Bool, default: -> { false }
  47. 113 option :micro_condensed, Types::Bool, default: -> { false }
  48. 1 def initialize(...)
  49. 116 super
  50. 116 @leading_text = item.macro_name
  51. 116 then: 116 else: 0 @leading_color = item.macro_class || item&.macro_kind
  52. 116 @trailing_text = item.micro_name
  53. 116 then: 116 else: 0 @trailing_color = item.micro_class || item&.micro_kind
  54. 116 @leading_icon = item.macro_icon
  55. 116 @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

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. 116 <div class="inline-flex">
  2. 4 then: 4 else: 112 <%= render LooposUi::Tooltip.new do |tooltip| %>
  3. 4 <%= 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. 4 ) %>
  12. 116 <% end if macro_condensed || micro_condensed %>
  13. 116 <%= 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. 116 ) %>
  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. 3 option :accept_value_btn, default: -> { true }
  4. 3 option :cancel_value_btn, default: -> { true }
  5. 3 option :proposal, default: -> { {} }
  6. 3 option :reason_edit_value, default: -> { "" }
  7. 3 option :proposal_kind, default: -> { "in" }
  8. 1 option :price_details
  9. 1 option :status
  10. 3 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

100.0% lines covered

100.0% branches covered

1 relevant lines. 1 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 2 <%= 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

100.0% lines covered

50.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
6 total branches, 3 branches covered and 3 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. 246 @text = text
  8. 246 @icon = icon
  9. 246 then: 246 else: 0 @color = color&.to_sym
  10. 246 @count = count
  11. 246 @condensed = condensed
  12. 246 @system_args = system_args
  13. 246 then: 0 else: 246 @text_color, @bg_color = color if color.is_a?(Array) && color.size == 2
  14. end
  15. 1 def system_args
  16. 246 @system_args.slice(:data, :id)
  17. end
  18. 1 def text_too_long?
  19. 246 text.present? && text.length >= 33
  20. end
  21. 1 def styles
  22. 246 then: 0 else: 246 return if text_color.nil? || bg_color.nil?
  23. 246 <<~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

87.5% lines covered

72.73% branches covered

8 relevant lines. 7 lines covered and 1 lines missed.
11 total branches, 8 branches covered and 3 branches missed.
    
  1. 492 <%= tag.span(class: "lui-label", style: styles, **system_args) do %>
  2. 246 then: 0 else: 246 <%= render LooposUi::Tooltip.new(title: text) if text_too_long? %>
  3. 246 <% case icon %>
  4. when: 92 <% when "core", "submission", "hubs", "validation", "handling", "exits", "impact" %>
  5. 92 <%= helpers.app_svg(app: icon, size: :tiny) %>
  6. when: 0 <% when "decision_ai" %>
  7. <%= helpers.app_svg(app: "ai_decision", size: :tiny) %>
  8. else: 154 <% else %>
  9. 154 then: 148 else: 6 <%= 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. 246 then: 240 else: 6 <%= tag.span(text, class: "lui-label__text", data: { "lui--label": "content" }) if !condensed %>
  13. 246 then: 0 else: 246 <%= 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

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 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. 3 option :data, default: -> { {} }, type: Types::Hash
  6. 3 option :turbo_target, default: -> { "lui-main-layout" }, type: Types::String.optional
  7. 3 option :turbo_action, default: -> { "navigate" }, type: Types::String.optional
  8. 1 def merged_data_attributes
  9. 2 { 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

75.0% lines covered

50.0% branches covered

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

62.83% lines covered

27.5% branches covered

113 relevant lines. 71 lines covered and 42 lines missed.
40 total branches, 11 branches covered and 29 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. 2 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. 4 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. 2 log = LooposUi::Log::Factories.create_from_any_source(source)
  38. 2 else: 2 then: 0 raise "Failed to build log from source: #{source}" unless log
  39. 2 log
  40. end
  41. 1 def before_render
  42. 2 then: 2 else: 0 @defer_render_blocks&.each do |block|
  43. 2 block.call(self, @view_context)
  44. end
  45. 2 @defer_render_blocks = nil
  46. end
  47. 1 def defer_render(&block)
  48. 2 @defer_render_blocks ||= []
  49. 2 @defer_render_blocks << block
  50. end
  51. 1 def initialize(...)
  52. 2 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. 2 then: 2 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. 2 with_private_button(*args, **default_button_args.merge(kwargs), &block)
  83. end
  84. 1 def with_button_manual(*args, **kwargs, &block)
  85. 2 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. 8 @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. 2 then: 2 if user.present?
  112. 2 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. 2 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: 2 when "item"
  177. 2 :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. 2 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: 2 else
  193. 2 maybe_unescape_escaped_html(source)
  194. end
  195. 2 fragment = Nokogiri::HTML::DocumentFragment.parse(html)
  196. 2 text_nodes = fragment.xpath("./text()")
  197. 2 text_nodes.each do |node|
  198. 2 node_content = node.content.to_s
  199. 2 words = node_content.split(/\s/).reject(&:blank?)
  200. 2 then: 0 else: 2 next if words.blank?
  201. 2 words = words.each_with_index.map do |word, i|
  202. 8 is_last_word = i == words.length - 1
  203. 8 should_append_space = !is_last_word || node_content.match?(/\s\z/)
  204. 8 then: 6 else: 2 word = "#{word.strip} " if should_append_space
  205. 8 new_node = Nokogiri::XML::Node.new("div", fragment).tap do |span|
  206. 8 span["class"] = "lui-log__word"
  207. 8 span.content = word
  208. end
  209. 8 node.add_previous_sibling(new_node)
  210. end
  211. 2 node.remove # remove the original text node
  212. end
  213. 2 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. 2 else: 2 then: 0 return source unless source.is_a?(String)
  220. # Quick heuristic: detect escaped HTML tags like "&lt;div" or "&lt;/span".
  221. 2 else: 0 then: 2 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

100.0% lines covered

50.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
6 total branches, 3 branches covered and 3 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. 2 Base.subclasses
  9. end
  10. 1 def find_factory_for(source)
  11. 2 else: 2 then: 0 return unless source
  12. 6 then: 2 else: 0 factories&.find { |factory| factory.can_create?(source) }
  13. end
  14. 1 def create_from_any_source(source, factory_class: nil)
  15. 2 klass = factory_class || find_factory_for(source)
  16. 2 else: 2 then: 0 return unless klass
  17. 2 factory = klass.new(source)
  18. 2 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

61.11% lines covered

0.0% branches covered

18 relevant lines. 11 lines covered and 7 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. 2 @source = source
  8. 2 @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. 2 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

29.73% lines covered

0.0% branches covered

37 relevant lines. 11 lines covered and 26 lines missed.
14 total branches, 0 branches covered and 14 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 log.with_expand_content(@source[:extra_message]) if @source[:extra_message].present?
  49. add_source(log)
  50. log
  51. end
  52. 1 private
  53. 1 def valid_block?(block)
  54. self.class.valid_block?(block)
  55. end
  56. 1 def add_source(log)
  57. kind, name = @source[:app_instance].fetch_values(:kind, :name)
  58. then: 0 if kind == "neutral"
  59. log.with_entity_token_source(text: name, color: :general)
  60. else: 0 else
  61. log.with_source(entity: LooposUi::Entities::AppInstance.from_hash(@source[:app_instance]))
  62. end
  63. end
  64. end
  65. end
  66. end
  67. end

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

77.55% lines covered

50.0% branches covered

49 relevant lines. 38 lines covered and 11 lines missed.
6 total branches, 3 branches covered and 3 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. 2 defined?(LoopOs::ScriptLog) && source.is_a?(LoopOs::ScriptLog)
  8. end
  9. 1 def create
  10. 2 @log = LooposUi::Log.new(
  11. date: @source.created_at,
  12. user: @source.user_identification,
  13. error: @source.level.to_s == "error",
  14. )
  15. 2 @log.with_content(@source.name)
  16. 2 then: 2 else: 0 add_download_button if @source.files.present?
  17. 2 then: 0 else: 2 add_retry_button if should_add_retry_button?
  18. 2 add_info_modal
  19. 2 add_source
  20. 2 @log
  21. end
  22. 1 def initialize(...)
  23. 2 super(...)
  24. end
  25. 1 private
  26. 1 def should_add_retry_button?
  27. 2 @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. 2 @log.with_entity_token_source(text: t(".system"))
  69. end
  70. 1 def add_download_button
  71. 2 @log.with_button(
  72. **download_button_args,
  73. )
  74. end
  75. 1 def t(key, **kwargs)
  76. 10 LooposUi::Log.t(key, scope: [:loopos_ui, :log, :script_log], **kwargs)
  77. end
  78. 1 def add_info_modal
  79. 2 @log.defer_render do |log, view_context|
  80. 2 log.with_button_manual do
  81. 2 view_context.render(LooposUi::Modal.new(title: t(".modal_title"), modal_width: 1080, show_footer: false)) do |modal|
  82. 2 modal.with_trigger do
  83. 2 view_context.render(LooposUi::Button.new(
  84. text: t(".show_details"),
  85. **@log.default_button_args,
  86. ))
  87. end
  88. 2 then: 2 else: 0 modal.with_primary_action(**download_button_args) if @source.files.present?
  89. 2 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. 4 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

72.41% lines covered

33.33% branches covered

29 relevant lines. 21 lines covered and 8 lines missed.
6 total branches, 2 branches covered and 4 branches missed.
    
  1. 2 <%= tag.div(class: classes, data: {
  2. controller: "log",
  3. log_expanded_copy_text_value: t("loopos_ui.log.script_log.show_details"),
  4. 2 log_collapsed_copy_text_value: t("loopos_ui.log.script_log.hide_details") }) do %>
  5. 4 <%= tag.div(class: "lui-log__main") do %>
  6. 4 <%= tag.div(class: "lui-log__content") do %>
  7. 4 <%= tag.div(class: "lui-log__content__icon") do %>
  8. 2 <%= render LooposUi::Tooltip.new(title: I18n.t("admin.items.logs.log_classes.#{log_class}", default: log_class.to_s.humanize)) %>
  9. 2 <%= render LooposUi::MIcon.new(log_class_icon, size: 20) %>
  10. <% end %>
  11. 4 <%= tag.div(class: "lui-log__content__header") do %>
  12. 4 <%= tag.div(class: "lui-log__content__header__title", data: { log_target: "content" }) do %>
  13. 2 <%= parsed_content %>
  14. <% end %>
  15. 4 <%= tag.div(class: "lui-log__content__header__description") do %>
  16. 2 <%= t("loopos_ui.log.script_log.by") %>
  17. 2 <%= user_string %>
  18. <% end %>
  19. 2 then: 0 else: 2 <%= 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. 2 <% end if private_expand? %>
  24. 4 <%= tag.div(class: "lui-log__buttons") do %>
  25. 2 <% private_buttons.each do |button| %>
  26. 4 <%= button %>
  27. <% end %>
  28. <% end %>
  29. <% end %>
  30. <% end %>
  31. 4 <%= tag.div(class: "lui-log__source_content") do %>
  32. 2 <%= date.strftime("%d/%m/%Y %H:%M") %>
  33. 2 then: 0 else: 2 <% 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

46.15% lines covered

18.18% branches covered

39 relevant lines. 18 lines covered and 21 lines missed.
44 total branches, 8 branches covered and 36 branches missed.
    
  1. <%# TODO: this is copied from the script log modal, not yet fully migrated to UI %>
  2. 2 <div class="flex flex-col w-full gap-2">
  3. 2 <span class="text-[14px]"><%= I18n.t('loop_os_scripts.logs.parameters') %></span>
  4. <%
  5. 2 then: 0 else: 2 then: 0 else: 2 has_input_data = log.input_data&.keys&.present?
  6. 2 has_input_files = log.respond_to?(:input_files) && log.input_files.attached?
  7. %>
  8. 2 <% 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: 2 <% else %>
  37. 2 -
  38. <% end %>
  39. 2 </div>
  40. <div class="flex flex-col w-full gap-2">
  41. 2 <span class="text-[14px]"><%= I18n.t('loop_os_scripts.logs.time_running') %></span>
  42. 2 then: 0 else: 2 then: 0 else: 0 then: 0 <% if log.extra_data&.is_a?(Hash) && log.extra_data&.dig("time_running").present? %>
  43. else: 2 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. 2 then: 0 else: 2 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: 2 <% else %>
  47. 2 -
  48. <% end %>
  49. 2 </div>
  50. <div class="flex flex-col w-full gap-2">
  51. 2 <span class="text-[14px]"><%= I18n.t('loop_os_scripts.logs.results') %></span>
  52. 2 <%= 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. 2 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: 2 <% else %>
  57. 2 <div class="text-center text-gray-500 py-8">
  58. 2 <p><%= I18n.t('loop_os_scripts.logs.empty_results') %></p>
  59. </div>
  60. <% end %>
  61. 2 </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

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 class Logo < LoopComponent
  3. 1 def initialize(app: "manager", count: nil, size: "small", icon: true)
  4. 2 @app = app
  5. 2 @count = count
  6. 2 @size = size
  7. 2 @icon = icon
  8. end
  9. end
  10. end

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

100.0% lines covered

50.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
2 total branches, 1 branches covered and 1 branches missed.
    
  1. 4 <%= tag.div(class: "lui-logo") do %>
  2. 2 <%= react_component("LogoApp", {
  3. app: @app,
  4. size: @size,
  5. icon: @icon
  6. 2 }) %>
  7. 2 then: 0 else: 2 <%= 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

91.43% lines covered

33.33% branches covered

35 relevant lines. 32 lines covered and 3 lines missed.
6 total branches, 2 branches covered and 4 branches missed.
    
  1. 1 module LooposUi
  2. 1 class MIcon < LoopComponent
  3. 1 class << self
  4. 1 def icon?(icon)
  5. 580 icon.present? && icons.include?(icon.to_sym)
  6. end
  7. 1 def icons
  8. 447 [*svg_icons, *font_icons]
  9. end
  10. 1 def font_icons
  11. 447 @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. 635 @svg_icons ||= Pathname.new(__dir__)
  26. .join("m_icon", "custom_icons").glob("*.svg").map do |n|
  27. 2 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. 188 svg_icons.include?(icon.to_sym)
  37. end
  38. end
  39. 1 TMaterialIconSymbol = Types::Coercible::Symbol.enum(*icons)
  40. 1 param :icon, TMaterialIconSymbol
  41. 189 option :tag, Types::Coercible::Symbol.enum(:i, :span, :button, :a), default: -> { :i }
  42. 21 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. 188 *super,
  48. "material-symbols-rounded",
  49. 188 then: 0 else: 188 tag == :button ? "cursor-pointer" : nil,
  50. 188 then: 0 else: 188 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. 188 <<~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

75.0% lines covered

50.0% branches covered

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

66.67% lines covered

50.0% branches covered

9 relevant lines. 6 lines covered and 3 lines missed.
4 total branches, 2 branches covered and 2 branches missed.
    
  1. 4 <%= tag.div class: "lui-marketplace-footer" do %>
  2. 4 <%= tag.div class: "lui-marketplace-footer__actions" do %>
  3. 2 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: 2 <% else %>
  10. 2 <%= render LooposUi::Logo.new(app: "manager", size: "small", icon: false) %>
  11. <% end %>
  12. 2 <%= social_buttons %>
  13. <% end %>
  14. 2 then: 0 else: 2 <%= tag.span email, class: "lui-marketplace-footer__text" if email.present? %>
  15. <% end %>

app/components/loopos_ui/marketplace/header.rb

100.0% lines covered

50.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
2 total branches, 1 branches covered and 1 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. 2 then: 0 else: 2 logo_href ? :a : :div
  10. end
  11. end
  12. end
  13. end

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

100.0% lines covered

50.0% branches covered

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

app/components/loopos_ui/marketplace/item_card.rb

68.06% lines covered

32.14% branches covered

72 relevant lines. 49 lines covered and 23 lines missed.
28 total branches, 9 branches covered and 19 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. 7 option :favorite, Types::Bool, default: -> { false }
  11. 7 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. 7 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. 6 super
  34. 6 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: 6 @limit_date = @item.limit_date
  43. 6 then: 0 else: 6 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. 12 then: 12 else: 0 href.present? ? :a : :div
  51. end
  52. 1 def formatted_price
  53. 6 then: 0 else: 6 if transaction_mode == :donation
  54. return I18n.t("loopos_ui.marketplace.item_card.free")
  55. end
  56. 6 then: 6 else: 0 price&.format || "-"
  57. end
  58. 1 def formatted_limit_date
  59. 6 then: 6 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. 6 then: 0 else: 6 return if date.blank?
  79. 6 date.strftime("%d/%m/%Y")
  80. end
  81. 1 def horizontal_with_status?
  82. 12 orientation == :horizontal && status.present?
  83. end
  84. 1 def horizontal_with_date?
  85. 6 orientation == :horizontal && date.present?
  86. end
  87. 1 def size_styles
  88. 6 then: 0 else: 6 return if size.blank?
  89. 6 <<~CSS
  90. width: #{size};
  91. height: #{size};
  92. CSS
  93. end
  94. 1 def data_options
  95. 6 curr_data = data || {}
  96. 6 curr_data[:controller] ||= ""
  97. 6 curr_data[:controller] += " lui--marketplace-item"
  98. 6 then: 0 else: 6 curr_data[:"lui--marketplace-item-limit-date-value"] = limit_date.iso8601 if limit_date.present? && limit_date_under_day?
  99. 6 curr_data
  100. end
  101. 1 def transaction_mode_auction?
  102. 6 transaction_mode == :auction
  103. end
  104. end
  105. end
  106. end

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

87.5% lines covered

44.44% branches covered

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

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 MiniApp < LoopComponent
  3. 11 option :open, Types::Bool, default: -> { false }
  4. 1 mod :open
  5. 1 option :miniapp_id, Types::String, default: -> { SecureRandom.uuid }
  6. 11 option :restriction, Types::String, default: -> { "parent" }
  7. 11 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. 10 data_attributes.merge("restriction": restriction)
  12. end
  13. end
  14. end

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

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. 20 <%= 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. 10 <%= render LooposUi::Icon.new(icon: "fa-solid fa-grip-dots", size: "20", color: "#6D6D6D") %>
  5. </div>
  6. </div>
  7. 10 <%= header %>
  8. <div class="lui-mini_app__content">
  9. 10 <%= content %>
  10. </div>
  11. <% end %>

app/components/loopos_ui/modal.rb

89.66% lines covered

50.0% branches covered

29 relevant lines. 26 lines covered and 3 lines missed.
2 total branches, 1 branches covered and 1 branches missed.
    
  1. 1 module LooposUi
  2. 1 class Modal < LoopComponent
  3. 1 include Presets
  4. 1 renders_one :primary_action, ->(**args) {
  5. 2 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. 2 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. 3 option :open, Types::Bool, default: -> { false }
  49. 3 option :single_use, Types::Bool, default: -> { false }
  50. 1 private
  51. 1 def before_render
  52. 2 then: 0 else: 2 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

60.0% lines covered

35.71% branches covered

25 relevant lines. 15 lines covered and 10 lines missed.
14 total branches, 5 branches covered and 9 branches missed.
    
  1. 4 <%= 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. 2 <%= trigger %>
  4. </div>
  5. 2 <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. 2 then: 0 <% if header? %>
  9. else: 2 <%= header %>
  10. 2 then: 0 <% elsif header_text.present? %>
  11. <%= render LooposUi::Header.new(title: header_text) %>
  12. else: 2 <% else %>
  13. 2 <%= render LooposUi::Header.new(title: title, description: description) %>
  14. <% end %>
  15. 2 <div class="-mt-1" >
  16. <button data-action="click->modal#close" class="cursor-pointer -mr-1 p-1">
  17. 2 <%= render LooposUi::Icon.new(icon: "fa-regular fa-xmark", size: "10") %>
  18. </button>
  19. </div>
  20. </div>
  21. 2 then: 2 else: 0 <%= tag.div class: "lui-modal__content" do %>
  22. 2 <%= content || custom_content %> <%# TODO: deprecate custom_content %>
  23. 2 <% end if content.present? || custom_content.present? %>
  24. 2 <%= tag.div(hidden_content, class: "hidden") if hidden_content.present? %>
  25. 2 then: 0 else: 2 <% if show_footer %>
  26. 2 then: 0 else: 2 <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. 2 </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

67.11% lines covered

25.93% branches covered

76 relevant lines. 51 lines covered and 25 lines missed.
27 total branches, 7 branches covered and 20 branches missed.
    
  1. 1 module LooposUi
  2. 1 class ModelAssociationList < LoopComponent
  3. 1 option :model
  4. 2 option :max_tokens, default: -> { 4 }, type: Types::Integer
  5. 1 option :association
  6. 1 option :association_scope, optional: true
  7. 2 option :association_params, default: -> { {} }
  8. 1 option :component_class, optional: true
  9. 1 option :entity_argument_builder, optional: true
  10. 2 option :case_sensitive, default: -> { false }
  11. 1 option :entangled_group, optional: true
  12. 2 option :draggable, Types::Bool, default: -> { false }
  13. 1 option :custom_detach_action, optional: true
  14. 2 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. 1 super(...)
  32. 1 then: 1 else: 0 if component_class.nil?
  33. 1 @component_class = guess_component_class || LooposUi::TagToken
  34. end
  35. 1 then: 0 else: 1 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. 1 then: 1 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. 1 then: 0 else: 1 return if association_button?
  55. 1 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. 1 case policy
  86. when: 0 when TrueClass, FalseClass
  87. policy
  88. when: 1 when Hash
  89. 1 then: 1 else: 0 then: 1 if policy[method]&.respond_to?(:call)
  90. 1 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. 1 policy == false ||
  101. 1 [: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. 1 "::LooposUi::Entities::#{association_name.to_s.classify}".safe_constantize
  109. end
  110. 1 def unique_id
  111. 1 @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. 1 "#{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

10.53% lines covered

25.0% branches covered

19 relevant lines. 2 lines covered and 17 lines missed.
4 total branches, 1 branches covered and 3 branches missed.
    
  1. 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: 1 <% else %>
  11. 1 <%= 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. }, {
  56. style: "width: 100%; min-width: 453px"
  57. }
  58. ) %>
  59. <%#= render LooposUi::Chat.new(
  60. item_token: @item.token,
  61. user_id: current_user.id,
  62. app_id: ItemChatConfig.app_id,
  63. api_key: ItemChatConfig.api_key,
  64. type: "public",
  65. user_name: LoopOsManager::Client::User.get_personal_data(loopos_user_id: current_user.id)&.body&.dig("personal_data", "full_name") || "LoopOS || Core",
  66. locale: I18n.locale.to_s,
  67. style:"width: 100%; min-width: 453px"
  68. ) %>
  69. </div>
  70. </div>
  71. <% end %>
  72. <% row.with_column do %>
  73. <div class="private-chat flex flex-col items-start gap-6 flex-1">
  74. <div class="flex flex-col gap-[4px] self-start w-full">
  75. <%= react_component("ItemChat", {
  76. itemToken: item.token,
  77. userId: helpers.current_user.id,
  78. appId: ::ItemChatConfig.app_id,
  79. apiKey: ::ItemChatConfig.api_key,
  80. type: "private",
  81. locale: I18n.locale.to_s,
  82. 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",
  83. }, {
  84. style: "width: 100%; min-width: 453px"
  85. }
  86. ) %>
  87. <%#= render LooposUi::Chat.new(
  88. item_token: @item.token,
  89. user_id: current_user.id,
  90. app_id: ItemChatConfig.app_id,
  91. api_key: ItemChatConfig.api_key,
  92. type: "private",
  93. user_name: LoopOsManager::Client::User.get_personal_data(loopos_user_id: current_user.id)&.body&.dig("personal_data", "full_name") || "LoopOS || Core",
  94. locale: I18n.locale.to_s,
  95. style:"width: 100%; min-width: 453px"
  96. ) %>
  97. </div>
  98. </div>
  99. <script>
  100. function hideTags() {
  101. const publicTag = document.getElementById('public-tag');
  102. const privateTag = document.getElementById('private-tag');
  103. if (publicTag) publicTag.style.visibility = 'hidden';
  104. if (privateTag) privateTag.style.visibility = 'hidden';
  105. }
  106. function checkTags() {
  107. if (!document.hidden) {
  108. hideTags();
  109. }
  110. }
  111. document.addEventListener('visibilitychange', checkTags);
  112. document.addEventListener('DOMContentLoaded', checkTags);
  113. setTimeout(checkTags, 3000);
  114. </script>
  115. <% end %>
  116. <% end %>
  117. <% 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

83 relevant lines. 0 lines covered and 83 lines missed.
26 total branches, 0 branches covered and 26 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. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.proposals.title"), size: "small", underline: true) do %>
  29. <div class="flex flex-row gap-4">
  30. then: 0 <% if curr_in_item_values.any? %>
  31. else: 0 <%= render LooposUi::PageSection::ItemTab::FinancialData::ItemValueCard.new(item: item, item_value: first_in_item_value, type: "in", source: source, policy: policy) %>
  32. then: 0 else: 0 <% elsif price_estimation_log.present? %>
  33. <%= render LooposUi::PageSection::ItemTab::FinancialData::ItemValueCard.new(item: item, item_value: price_estimation_log, type: "price_estimation", source: source, policy: policy) %>
  34. <% end %>
  35. then: 0 else: 0 <% if curr_out_item_values.any? %>
  36. <%= render LooposUi::PageSection::ItemTab::FinancialData::ItemValueCard.new(item: item, item_value: first_out_item_value, type: "out", source: source, policy: policy) %>
  37. <% end %>
  38. then: 0 else: 0 <% if curr_extra_item_values.any? %>
  39. <%= render LooposUi::PageSection::ItemTab::FinancialData::ItemValueCard.new(item: item, item_value: first_extra_item_value, type: "top-up", source: source, policy: policy) %>
  40. <% end %>
  41. then: 0 else: 0 <% if has_marketplace_value_out %>
  42. <%= render LooposUi::V2::Card.new(
  43. id: "marketplace-value-out",
  44. title: I18n.t("admin.items.financial_data.marketplace_value_out"),
  45. description: I18n.t("admin.items.financial_data.marketplace_value_out_description"),
  46. src: LooposUi::Engine.routes.url_helpers.marketplace_value_out_item_path(token: item.token, context: source.source_context),
  47. ) do |card| %>
  48. <% card.with_corner do %>
  49. <%= render LooposUi::Loadings::Skeleton::Bar.new(width: "100px") %>
  50. <% end %>
  51. <%= render LooposUi::Loadings::Skeleton.new(n_rows: 2) %>
  52. <% end %>
  53. <% end %>
  54. </div>
  55. <% end %>
  56. <% end %>
  57. <% end %>
  58. <% end %>
  59. <% tabs.with_row do |row| %>
  60. <% row.with_column do %>
  61. <%= 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 %>
  62. <div class="flex flex-row gap-4">
  63. <%= render LooposUi::V2::Card.new(
  64. id: "other-values-#{item.token}",
  65. ) do |card| %>
  66. <% card.with_title_description do |td| %>
  67. <% end %>
  68. <% card.with_corner do %>
  69. <%= render LooposUi::Loadings::Skeleton::Bar.new(width: "100px") %>
  70. <% end %>
  71. <% card.with_custom_description do %>
  72. <%= render LooposUi::Loadings::Skeleton::Bar.new %>
  73. <% end %>
  74. <%= render LooposUi::Loadings::Skeleton.new(n_rows: 2) %>
  75. <% end %>
  76. </div>
  77. <% end %>
  78. <% end %>
  79. <% end %>
  80. <% tabs.with_row do |row| %>
  81. <% row.with_column do %>
  82. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.financial_data.transactions"), size: "small", underline: true) do %>
  83. <%= tag.turbo_frame id: "transactions-#{item.token}", src: LooposUi::Engine.routes.url_helpers.transactions_item_path(token: item.token, context: source.source_context) do %>
  84. <div class="flex flex-row gap-4">
  85. <%= render LooposUi::V2::Card.new(
  86. id: "transactions-#{item.token}",
  87. ) do |card| %>
  88. <% card.with_title_description do |td| %>
  89. <% end %>
  90. <% card.with_corner do %>
  91. <%= render LooposUi::Loadings::Skeleton::Bar.new(width: "100px") %>
  92. <% end %>
  93. <% card.with_custom_description do %>
  94. <%= render LooposUi::Loadings::Skeleton::Bar.new %>
  95. <% end %>
  96. <%= render LooposUi::Loadings::Skeleton.new(n_rows: 2) %>
  97. <% end %>
  98. </div>
  99. <% end %>
  100. <% end %>
  101. <% end %>
  102. <% end %>
  103. <% tabs.with_row do |row| %>
  104. <% row.with_column do %>
  105. <%= 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| %>
  106. <%= form.hidden_field :context, value: source.source_context %>
  107. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.financial_data.financial_history"), size: "small", underline: true) do |section| %>
  108. then: 0 else: 0 <% section.with_corner_action do %>
  109. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.financial_data.apps"), required: false, tooltip: nil, orientation: "horizontal") do |f| %>
  110. <% f.with_input do %>
  111. <%= render LooposUi::Inputs::Select.new(
  112. name: "app",
  113. options: item_value_apps_select,
  114. value: "all",
  115. placeholder: I18n.t("admin.items.financial_data.select_app"),
  116. open_actions: true,
  117. mode: :autosubmit,
  118. form: "log-list-form"
  119. ) %>
  120. <% end %>
  121. <% end %>
  122. <% end if LooposUi.config.app_type?(:core) %>
  123. <%# FIXME: UI doesnt have LoopOsManager::Applications, which means it breaks when its used by an app that natively doesnt have them %>
  124. <% section.with_corner_action do %>
  125. <%= render LooposUi::FormEntry.new(label: I18n.t("admin.items.financial_data.value_kind"), required: false, tooltip: nil, orientation: "horizontal") do |f| %>
  126. <% f.with_input do %>
  127. <%= render LooposUi::Inputs::Select.new(
  128. name: "kind",
  129. options: item_value_kinds_select,
  130. value: "all",
  131. placeholder: I18n.t("admin.items.financial_data.select_value_kind"),
  132. open_actions: true,
  133. mode: :autosubmit,
  134. form: "log-list-form"
  135. ) %>
  136. <% end %>
  137. <% end %>
  138. <% end %>
  139. <% end %>
  140. <%= 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 %>
  141. <%= render LooposUi::Loadings::Skeleton.new(n_rows: 2) %>
  142. <% end %>
  143. <% end %>
  144. <% end %>
  145. <% end %>
  146. <% tabs.with_row do |row| %>
  147. <% row.with_column do %>
  148. <%= 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")) %>
  149. <% end %>
  150. <% end %>
  151. <% 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-general-global-black">
  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-general-global-black">
  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-general-global-black") %>
  121. <% end %>
  122. <% row.with_cell(property: :value) do %>
  123. <%= tag.span(item_value.amount.format, class: "text-lg font-semibold text-general-global-black") %>
  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-general-global-black 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

95 relevant lines. 0 lines covered and 95 lines missed.
40 total branches, 0 branches covered and 40 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.present? %>
  43. then: 0 else: 0 <%= render LooposUi::Entities::Product.new(product: OpenStruct.new(name: item.respond_to?(:product_name) ? item.product_name : item.product)) %>
  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. then: 0 else: 0 <%= render LooposUi::GridLayout.new(cols: 1) do |tab| %>
  127. then: 0 else: 0 <% item.internal_notes&.each do |app, note| %>
  128. <% tab.with_section do %>
  129. <%= render LooposUi::Accordion.new(open: true) do |accordion| %>
  130. <% accordion.with_header(title: app, size: :tiny) %>
  131. <%= render LooposUi::Inputs::RichText.new(name: "notes_#{app.downcase}", value: note.presence || "-" , readonly: true) %>
  132. <% end %>
  133. <% end %>
  134. <% end %>
  135. <% end if item.internal_notes.present? %>
  136. <% end %>
  137. <% end %>
  138. <% end %>
  139. <% tab.with_row do |row| %>
  140. then: 0 else: 0 <% row.with_column(half: true) do %>
  141. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.item_info.identifiers.title"), size: "small", underline: "yes") do %>
  142. then: 0 <% if policy[:manage_identifiers] %>
  143. <%= render LooposUi::PageSection::ItemTab::Info::IdentifiersTable.new(
  144. identifiers: item.identifier_records,
  145. readonly: false,
  146. source_context: item.source.source_context,
  147. identifiable_id: item.id,
  148. identifiable_type: "Item",
  149. available_kinds: fetch_available_kinds,
  150. ) %>
  151. else: 0 <% else %>
  152. <%= render LooposUi::PageSection::ItemTab::Info::IdentifiersTable.new(identifiers: item.identifiers) %>
  153. <% end %>
  154. <% end %>
  155. <% end if show_identifiers? %>
  156. <% row.with_column(half: show_identifiers?) do %>
  157. <%= helpers.flex_cards_tab_section("Item", :show_item_info_tab, instance_id: item.id, title: I18n.t("admin.items.item_info.custom_information")) %>
  158. <% end %>
  159. <% end %>
  160. <% 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. format: "date",
  21. locale: I18n.locale.to_s,
  22. name: "filter_date_range",
  23. end_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

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 PageSection
  3. 1 module ItemTab
  4. 1 class Notes < LoopComponent
  5. 1 KEY = :item_notes
  6. 1 option :item
  7. 1 then: 0 else: 0 delegate :source, to: :@item
  8. 1 def notes
  9. item.notes(app: LooposUi.config.app_type)
  10. end
  11. end
  12. end
  13. end
  14. end

app/components/loopos_ui/page_section/item_tab/notes/notes.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. <%= 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. <% tab.with_row do |row| %>
  8. <% row.with_column do %>
  9. <%= form_with url: helpers.loopos_ui.update_notes_item_path(token: item.token, context: source.source_context), method: :patch do |form| %>
  10. <%= render LooposUi::Inputs::RichText.new(
  11. name: "content",
  12. placeholder: I18n.t("admin.items.item_notes.placeholder"),
  13. value: notes,
  14. mode: :autosubmit,
  15. upload_endpoint: helpers.loopos_ui.upload_attachment_item_path(token: item.token, context: source.source_context, authenticity_token: form_authenticity_token),
  16. ) %>
  17. <% end %>
  18. <% end %>
  19. <% end %>
  20. <% tab.with_row do |row| %>
  21. <% row.with_column do %>
  22. <%= helpers.flex_cards_tab_section("Item", :show_item_notes_tab, instance_id: item.id) %>
  23. <% end %>
  24. <% end %>
  25. <% 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.0% lines covered

0.0% branches covered

50 relevant lines. 15 lines covered and 35 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 private
  13. # TODO: This methods wouldnt be needed if the presenter were setup in the from_hash file
  14. 1 def transformed_emails
  15. then: 0 if LooposUi.config.app_type?(:core)
  16. emails
  17. else: 0 else
  18. Array(emails).map { |email| LooposUi::Services::EmailPresenter.new(email, item_data: item).serialize }
  19. end
  20. rescue => e
  21. Rails.logger.error("Error transforming emails: #{e.message}")
  22. then: 0 else: 0 Sentry.capture_exception(e, extra: { tab: :services, type: :emails }) if defined?(Sentry)
  23. []
  24. end
  25. 1 def transformed_incoming_payments
  26. then: 0 if LooposUi.config.app_type?(:core)
  27. incoming_payments
  28. else: 0 else
  29. Array(incoming_payments).map { |incoming_payment| LooposUi::Services::IncomingPaymentPresenter.new(incoming_payment, item_data: item).serialize }
  30. end
  31. rescue => e
  32. Rails.logger.error("Error transforming incoming payments: #{e.message}")
  33. then: 0 else: 0 Sentry.capture_exception(e, extra: { tab: :services, type: :incoming_payments }) if defined?(Sentry)
  34. []
  35. end
  36. 1 def transformed_invoices
  37. then: 0 if LooposUi.config.app_type?(:core)
  38. invoices
  39. else: 0 else
  40. Array(invoices).map { |invoice| LooposUi::Services::InvoicePresenter.new(invoice, item_data: item).serialize }
  41. end
  42. rescue => e
  43. Rails.logger.error("Error transforming invoices: #{e.message}")
  44. then: 0 else: 0 Sentry.capture_exception(e, extra: { tab: :services, type: :invoices }) if defined?(Sentry)
  45. []
  46. end
  47. 1 def transformed_payments
  48. payments
  49. rescue => e
  50. Rails.logger.error("Error transforming payments: #{e.message}")
  51. then: 0 else: 0 Sentry.capture_exception(e, extra: { tab: :services, type: :payments }) if defined?(Sentry)
  52. []
  53. end
  54. 1 def transformed_sms_messages
  55. then: 0 if LooposUi.config.app_type?(:core)
  56. sms_messages
  57. else: 0 else
  58. Array(sms_messages).map { |sms_message| LooposUi::Services::SmsMessagePresenter.new(sms_message, item_data: item).serialize }
  59. end
  60. rescue => e
  61. Rails.logger.error("Error transforming sms messages: #{e.message}")
  62. then: 0 else: 0 Sentry.capture_exception(e, extra: { tab: :services, type: :sms_messages }) if defined?(Sentry)
  63. []
  64. end
  65. 1 def transformed_shippings
  66. then: 0 if LooposUi.config.app_type?(:core)
  67. shippings
  68. else: 0 else
  69. Array(shippings).map { |shipping| LooposUi::Services::ShippingPresenter.new(shipping, item_data: item).serialize }
  70. end
  71. rescue => e
  72. Rails.logger.error("Error transforming shippings: #{e.message}")
  73. then: 0 else: 0 Sentry.capture_exception(e, extra: { tab: :services, type: :shippings }) if defined?(Sentry)
  74. []
  75. end
  76. end
  77. end
  78. end
  79. 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. pagination: { current: 1, pageSize: transformed_shippings.count, total: transformed_shippings.count },
  18. permissions: permissions,
  19. show_shipping_guide: true,
  20. searchable: false,
  21. ) %>
  22. <% end %>
  23. <% end %>
  24. <% end %>
  25. <% end %>
  26. <% end %>
  27. <% tab.with_row do |row| %>
  28. <% row.with_column do %>
  29. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.services.email"), size: "small", underline: true) do %>
  30. <%= tag.turbo_frame id: "item_#{item.id}_emails" do %>
  31. then: 0 <% if transformed_emails.empty? %>
  32. <%= render LooposUi::TitleDescription.new(description: I18n.t("admin.items.services.no_data")) %>
  33. else: 0 <% else %>
  34. <%= render LooposUi::Services::EmailMessages::Table.new(
  35. id: "item_#{item.id}_emails_table",
  36. data: transformed_emails,
  37. pagination: { current: 1, pageSize: transformed_emails.count, total: transformed_emails.count },
  38. permissions: permissions,
  39. searchable: false,
  40. ) %>
  41. <% end %>
  42. <% end %>
  43. <% end %>
  44. <% end %>
  45. <% end %>
  46. <% tab.with_row do |row| %>
  47. <% row.with_column do %>
  48. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.services.incoming_payments"), size: "small", underline: true) do %>
  49. <%= tag.turbo_frame id: "item_#{item.id}_incoming_payments" do %>
  50. then: 0 <% if transformed_incoming_payments.empty? %>
  51. <%= render LooposUi::TitleDescription.new(description: I18n.t("admin.items.services.no_data")) %>
  52. else: 0 <% else %>
  53. <%= render LooposUi::Services::IncomingPayments::Table.new(
  54. id: "item_#{item.id}_incoming_payments_table",
  55. data: transformed_incoming_payments,
  56. pagination: { current: 1, pageSize: transformed_incoming_payments.count, total: transformed_incoming_payments.count },
  57. permissions: permissions,
  58. searchable: false,
  59. ) %>
  60. <% end %>
  61. <% end %>
  62. <% end %>
  63. <% end %>
  64. <% end %>
  65. <% tab.with_row do |row| %>
  66. <% row.with_column do %>
  67. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.services.outgoing_payments"), size: "small", underline: true) do %>
  68. <%= tag.turbo_frame id: "item_#{item.id}_payments" do %>
  69. then: 0 <% if transformed_payments.empty? %>
  70. <%= render LooposUi::TitleDescription.new(description: I18n.t("admin.items.services.no_data")) %>
  71. else: 0 <% else %>
  72. <%= render LooposUi::Services::Payments::Table.new(
  73. id: "item_#{item.id}_payments_table",
  74. data: transformed_payments,
  75. pagination: { current: 1, pageSize: transformed_payments.count, total: transformed_payments.count },
  76. permissions: permissions,
  77. searchable: false,
  78. ) %>
  79. <% end %>
  80. <% end %>
  81. <% end %>
  82. <% end %>
  83. <% end %>
  84. <% tab.with_row do |row| %>
  85. <% row.with_column do %>
  86. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.services.invoices"), size: "small", underline: true) do %>
  87. <%= tag.turbo_frame id: "item_#{item.id}_invoices" do %>
  88. then: 0 <% if transformed_invoices.empty? %>
  89. <%= render LooposUi::TitleDescription.new(description: I18n.t("admin.items.services.no_data")) %>
  90. else: 0 <% else %>
  91. <%= render LooposUi::Services::Invoices::Table.new(
  92. id: "item_#{item.id}_invoices_table",
  93. data: transformed_invoices,
  94. pagination: { current: 1, pageSize: transformed_invoices.count, total: transformed_invoices.count },
  95. permissions: permissions,
  96. searchable: false,
  97. ) %>
  98. <% end %>
  99. <% end %>
  100. <% end %>
  101. <% end %>
  102. <% end %>
  103. <% tab.with_row do |row| %>
  104. <% row.with_column do %>
  105. <%= render LooposUi::TabsSection.new(title: I18n.t("admin.items.services.sms"), size: "small", underline: true) do %>
  106. <%= tag.turbo_frame id: "item_#{item.id}_sms_messages" do %>
  107. then: 0 <% if transformed_sms_messages.empty? %>
  108. <%= render LooposUi::TitleDescription.new(description: I18n.t("admin.items.services.no_data")) %>
  109. else: 0 <% else %>
  110. <%= render LooposUi::Services::SmsMessages::Table.new(
  111. id: "item_#{item.id}_sms_messages_table",
  112. data: transformed_sms_messages,
  113. pagination: { current: 1, pageSize: transformed_sms_messages.count, total: transformed_sms_messages.count },
  114. permissions: permissions,
  115. searchable: false,
  116. ) %>
  117. <% end %>
  118. <% end %>
  119. <% end %>
  120. <% end %>
  121. <% end %>
  122. <% tab.with_row do |row| %>
  123. <% row.with_column do %>
  124. <%= helpers.flex_cards_tab_section("Item", :show_services_tab, instance_id: item.id) %>
  125. <% end %>
  126. <% end %>
  127. <% 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

18 relevant lines. 0 lines covered and 18 lines missed.
20 total branches, 0 branches covered and 20 branches missed.
    
  1. <%= tag.turbo_frame id: "item_show_tabs" 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. <% 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. ) %>
  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. then: 0 else: 0 <% layout.with_tab(
  78. title: I18n.t("admin.items.tabs.validation_widget"),
  79. key: LooposUi::PageSection::ItemTab::ValidationWidget::KEY,
  80. url: LooposUi::Engine.routes.url_helpers.tab_item_path(**tab_url_base, tab_key: LooposUi::PageSection::ItemTab::ValidationWidget::KEY),
  81. lazy_load: true,
  82. ) if LooposUi.config.app_type?(:core) %>
  83. then: 0 else: 0 <% layout.with_impact(
  84. title: I18n.t("admin.items.tabs.impact_widget"),
  85. icon: impact_icon,
  86. key: LooposUi::PageSection::ItemTab::ImpactWidget::KEY,
  87. url: LooposUi::Engine.routes.url_helpers.tab_item_path(**tab_url_base, tab_key: LooposUi::PageSection::ItemTab::ImpactWidget::KEY),
  88. lazy_load: true,
  89. ) if LooposUi.config.app_type?(:core) %>
  90. <% end %>
  91. <% end %>

app/components/loopos_ui/pages/item_show.rb

54.17% lines covered

0.0% branches covered

48 relevant lines. 26 lines covered and 22 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::Core) | Types::Instance(LooposUi::PageSources::CoreApi)
  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 private
  30. 1 def impact_icon
  31. "fa-regular fa-seedling"
  32. end
  33. 1 def item
  34. @item ||= source.item(token)
  35. end
  36. 1 def show_logs?
  37. item_from_model? || policy.can_view_item_logs?
  38. end
  39. 1 def show_financial_data?
  40. item_from_model? || policy.can_view_payments?
  41. end
  42. 1 def show_client_info?
  43. item_from_model? || policy.can_view_client_info?
  44. end
  45. 1 def can_edit_client_info?
  46. policy.can_edit_client_info?
  47. end
  48. 1 def can_edit_client_iban?
  49. item_from_model? || policy.can_edit_client_iban?
  50. end
  51. 1 def item_from_model?
  52. source.is_a?(LooposUi::PageSources::Core)
  53. end
  54. 1 def can_manage_identifiers?
  55. item_from_model? || policy.can_manage_identifiers?
  56. end
  57. 1 def can_view_flows?
  58. policy.can_view_flows?
  59. end
  60. 1 def can_view_advanced_details?
  61. then: 0 else: 0 LooposUi.config.app_type?(:core) && helpers.current_user&.can_view_extra_data?
  62. end
  63. 1 def enable_rfi_feature?
  64. enable_rfi_feature = begin
  65. LoopOsManager::Applications.get_manager_configs.dig("enable_rfi_feature")
  66. rescue
  67. nil
  68. end
  69. then: 0 else: 0 enable_rfi_feature.present? ? enable_rfi_feature : false
  70. end
  71. 1 def can_view_rfis?
  72. policy.can_view_rfis?
  73. end
  74. 1 def state_label_args
  75. LooposUi::Item::StateLabel::ItemStruct.new(**item.state_badge_args)
  76. end
  77. end
  78. end
  79. 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

28 relevant lines. 0 lines covered and 28 lines missed.
2 total branches, 0 branches covered and 2 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. <div class="flex flex-col gap-2 self-stretch">
  16. then: 0 else: 0 <% if presenter.loopos_ai_protocol_generator_path.present? %>
  17. <div
  18. data-controller="ai-protocol"
  19. data-ai-protocol-protocol-id-value="<%= protocol.id %>"
  20. data-ai-protocol-catalog-node-id-value="<%= catalog_node.id %>"
  21. data-ai-protocol-generator-path-value="<%= presenter.loopos_ai_protocol_generator_path %>"
  22. data-ai-protocol-waiting-path-value="<%= waiting_for_ai_admin_v2_protocol_path(protocol) %>"
  23. >
  24. <%= render LooposUi::FormEntryAi.new(
  25. description: "Start the prompt with an action. Give all the needed details.",
  26. placeholder: "Generate protocol with LoopOS AI",
  27. ) %>
  28. </div>
  29. <% end %>
  30. <%= render LooposUi::FormEntry.new(
  31. label: "Description",
  32. required: false,
  33. orientation: "vertical",
  34. ) do |entry| %>
  35. <%= entry.with_input do %>
  36. <%= tag.turbo_frame id: "edit_protocol_description" do %>
  37. <%= 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 %>
  38. <%= render LooposUi::Inputs::TextArea.new(
  39. model: protocol,
  40. attribute: :description,
  41. placeholder: "Insert a protocol description",
  42. rows: 4,
  43. ) %>
  44. <% end %>
  45. <% end %>
  46. <% end %>
  47. <% end %>
  48. </div>
  49. </div>
  50. <% end %>
  51. <%= render LooposUi::Protocol::Settings.new(
  52. protocol: protocol,
  53. presenter: presenter,
  54. catalog_node: catalog_node,
  55. ) %>
  56. <% 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

26 relevant lines. 0 lines covered and 26 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. <div class="flex w-full" id="<%= dom_id option %>_protocol" data-option-id="<%= option.id %>">
  14. <%= @form.fields_for :select_options, option do |oform| %>
  15. <div id="<%= dom_id option %>_div" data-controller="update-index-inputs" data-id="<%= dom_id option %>_protocol" data-update-index-inputs-target="div"></div>
  16. <%= oform.hidden_field :active, value: true, id: "#{oform.object.id}_option_destroy" %>
  17. <%= oform.hidden_field :id, value: oform.object.id, id: "#{oform.object.id}_option_id" %>
  18. <div class="grid grid-cols-[calc(100%-64px)_24px_24px] gap-xs items-center w-full justify-between">
  19. <div>
  20. <div class="flex flex-row items center gap-2xs">
  21. <%= oform.text_field :label,
  22. value: option.label(locale: locale),
  23. class: "protocol-input",
  24. data: {
  25. action: "keydown.enter->autosubmit#submit blur->autosubmit#submit"
  26. } %>
  27. <div class="dealbreaker element__toggle-wrapper border-0! relative">
  28. <input name="protocol_element[select_options_attributes][<%= index - 1 %>][deal_breaker]" type="hidden" value="0" autocomplete="off" />
  29. 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" } }) %>
  30. <span class="text-gray-base">DB</span>
  31. </div>
  32. </div>
  33. </div>
  34. <%= render LooposUi::Modal.for_delete(model_name: "Protocol Element") do |modal| %>
  35. <% modal.with_trigger do %>
  36. <button type="button" class="flex items-center justify-center w-[24px] h-[24px] bg-[#FFE7E7] text-general-danger-800 rounded text-[10px] fa-regular fa-trash-can"></button>
  37. <% end %>
  38. <% modal.with_primary_action(
  39. text: t(".confirm"),
  40. kind: :app,
  41. tag_options: {
  42. id: "#{oform.object.id}_button_destroy",
  43. type: 'button',
  44. data: {
  45. input_id: "#{oform.object.id}_option_destroy",
  46. controller: "destroy-input",
  47. action: "click->destroy-input#changeDestroyInput click->lui--button#startLoading click->actions_loading#run"
  48. }
  49. }
  50. ) %>
  51. <% end %>
  52. </div>
  53. <% end %>
  54. </div>

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-general-gray-900 copy-14 font-bold">Associate to apps</p>
  41. <p class="text-general-gray-700 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-general-gray-900 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-general-gray-900 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. <%= link_to presenter.preview_path(catalog_node) , data: { turbo_frame: "protocol_sidebar", 'protocol-elements-target': "preview" }, class: 'hidden' do%>
  15. <% end %>
  16. <%= link_to presenter.show_path(catalog_node) , data: { turbo_frame: element, 'protocol-elements-target': "preview" }, class: 'hidden' do%>
  17. <% end %>
  18. else: 0 <% else %>
  19. <%= link_to presenter.settings_path(catalog_node) , data: { turbo_frame: "protocol_sidebar", 'protocol-elements-target': "button" }, class: 'hidden' do%>
  20. <% end %>
  21. <%= link_to presenter.edit_path(catalog_node) , data: { turbo_frame: element, 'protocol-elements-target': "button" }, class: 'hidden' do%>
  22. <% end %>
  23. <% end %>
  24. <div class="element__number hidden!"><%= presenter.position %></div>
  25. <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;
  26. border: 1px solid var(--Colors-General-Grays-300, #DEE2E6);
  27. background: var(--Colors-General-Grays-100, #F8F9FA);">
  28. <i class="fa-sharp fa-regular text-xs fa-grip-dots-vertical"></i>
  29. </div>
  30. <% end %>
  31. 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;">
  32. <div class="absolute right-0 top-1/2 transform -translate-y-1/2 flex flex-row gap-2">
  33. then: 0 else: 0 <% if @edit %>
  34. then: 0 else: 0 <% if presenter.input? %>
  35. <div class="element__toggle-wrapper">
  36. <span class="copy-12 text-black">Mark as required</span>
  37. <%= turbo_frame_tag element, :required do %>
  38. then: 0 else: 0 <% if current_user.can_edit_protocols? %>
  39. <%= form_with model: [:admin_v2, element.becomes(::Protocol::Element)], data: { controller: "autosubmit", autosubmit_target: "form" } do |form| %>
  40. <input name="protocol_element[required]" type="hidden" value="0" autocomplete="off">
  41. 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" } }) %>
  42. then: 0 else: 0 <%= form.hidden_field :catalog_node_id, value: @catalog_node&.id %>
  43. <% end %>
  44. <% end %>
  45. <% end %>
  46. </div>
  47. <% end %>
  48. <%= render LooposUi::Modal.for_duplicate(model_name: "Protocol Element") do |modal| %>
  49. <% modal.with_trigger do %>
  50. <button type="button"
  51. class="bg-[#F5F5F5] w-[24px] h-[24px] rounded items-center flex justify-center text-[10px] fa-kit fa-regular-copy-circle-plus text-[#5F5F5F]"
  52. data-toggle="tooltip"
  53. title="Duplicate protocol element"
  54. ></button>
  55. <% end %>
  56. <% modal.with_primary_action(
  57. text: t(".confirm"),
  58. href: duplicate_element_admin_v2_protocol_element_path(element),
  59. kind: :app,
  60. tag_options: { data: { "turbo-method": :post, type: "ignore", action: "click->lui--button#startLoading click->actions_loading#run" } }
  61. ) %>
  62. <% end %>
  63. <%= render LooposUi::Modal.for_delete(model_name: "Protocol Element") do |modal| %>
  64. <% modal.with_trigger do %>
  65. <button type="button" class="flex items-center justify-center w-[24px] h-[24px] bg-[#FFE7E7] text-general-danger-800 rounded text-[10px] fa-regular fa-trash-can"></button>
  66. <% end %>
  67. <% modal.with_custom_content do %>
  68. <%= form_with(
  69. model: [:admin_v2, element.becomes(::Protocol::Element)],
  70. id: "#{element.id}",
  71. class: "flex items-center justify-center w-fit h-fit",
  72. method: "delete",
  73. data: { turbo_frame: "#{dom_id(protocol)}_protocol_elements" }) do |form| %>
  74. then: 0 else: 0 <%= form.hidden_field :catalog_node_id, name: :catalog_node_id, value: catalog_node&.id %>
  75. <% end %>
  76. <% end %>
  77. <% modal.with_primary_action(
  78. text: t(".confirm"),
  79. kind: :app,
  80. tag_options: {
  81. form: "#{element.id}",
  82. type: "submit",
  83. data: {
  84. action: "click->lui--button#startLoading click->actions_loading#run"
  85. }
  86. }
  87. ) %>
  88. <% end %>
  89. <% end %>
  90. </div>
  91. </div>
  92. </div>
  93. <% 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/preview.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. <%= turbo_frame_tag 'protocol_sidebar' do %>
  2. <%= turbo_stream_from "protocol_sidebar_stream" %>
  3. <%= render LooposUi::Protocol::ProtocolSidebarComponent.new do |sidebar| %>
  4. <% sidebar.with_header do %>
  5. <h1>What are Protocols?</h1>
  6. <% end %>
  7. <% sidebar.with_content_section do %>
  8. <div class="protocol_sidebar__section--padded">
  9. <span class="copy-14-medium text-gray-900"><%= t(".a") %></span>
  10. <span class="copy-14 font-bold text-gray-900"><%= t(".protocol") %></span>
  11. <span class="copy-14-medium text-gray-900"><%= t(".description") %></span>
  12. </div>
  13. <% end %>
  14. <% end %>
  15. <% end %>

app/components/loopos_ui/protocol/preview.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 class LooposUi::Protocol::Preview < ViewComponent::Base
  2. 1 include Turbo::FramesHelper
  3. 1 include Turbo::StreamsHelper
  4. 1 def initialize
  5. end
  6. end

app/components/loopos_ui/protocol/protocol_builder_component.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 <% 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. <%= settings_section %>
  7. </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

29 relevant lines. 0 lines covered and 29 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. %>
  12. <%= turbo_frame_tag "#{turbo_id(protocol)}", data: {id: protocol.id } do %>
  13. <%= turbo_stream_from(turbo_stream_id(protocol)) %>
  14. <div class="flex">
  15. then: 0 else: 0 <% if !show_inheritance_path %>
  16. <div class="protocol-component__grip-wrapper">
  17. <div class="protocol-component__grip">
  18. <i class="protocol-component__grip-icon text-base fa-regular fa-grip-dots"></i>
  19. </div>
  20. </div>
  21. <% end %>
  22. then: 0 else: 0 <div class="protocol-component <%= show_inheritance_path.present? ? "": "protocol-component--open" %>"
  23. data-accordion="open"
  24. data-sortable-protocol-target="applicableCatalogNodeProtocol"
  25. then: 0 else: 0 data-id="<%= apn&.protocol_id %>"
  26. data-controller="protocol-accordion"
  27. data-protocol-accordion-target="wrapper"
  28. then: 0 else: 0 data-position="<%= apn&.position %>"
  29. >
  30. <%= render LooposUi::Protocol::ProtocolHeader.new( protocol: protocol, readonly: readonly, current_catalog_node: current_catalog_node, show_opened: show_opened, current_user: current_user ) %>
  31. then: 0 else: 0 <div class="protocol-component__elements-wrapper <%= current_catalog_node.present? && !show_opened ? "hidden" : "" %>" id="accordion-collapse-<%= dom_id(protocol) %>"
  32. aria-labelledby="accordion-collapse-<%= dom_id(protocol) %>-header">
  33. <div id="<%= dom_id(protocol) %>_protocol_elements" class="protocol-component__elements-container" data-controller="sortable" data-sortable-target="main"
  34. then: 0 else: 0 data-id="<%= protocol.id %>" data-catalog-node-id="<%= current_catalog_node&.id %>">
  35. <%= elements_list %>
  36. </div>
  37. then: 0 else: 0 <% if current_user.can_edit_protocols? %>
  38. <%= render LooposUi::Protocol::AddElement::AddElementComponent.new(
  39. filter_path: filter_element_options_admin_v2_protocol_path(protocol.id),
  40. assign_path: assign_element_admin_v2_protocol_path(protocol.id),
  41. import_path: import_admin_v2_protocol_elements_path,
  42. filter_wrapper_id: "protocol_elements_add_#{dom_id(protocol)}",
  43. param_key: "element_type",
  44. data_attributes: {
  45. assign_method: "POST",
  46. payload_data: {
  47. # element_position: elements.size + 1,
  48. protocol_id: protocol.id,
  49. then: 0 else: 0 catalog_node_id: current_catalog_node&.id,
  50. }
  51. },
  52. ) %>
  53. <% end %>
  54. then: 0 else: 0 <% if readonly %>
  55. <div class="absolute top-0 left-0 w-full h-full cursor-not-allowed z-50">
  56. </div>
  57. <% end %>
  58. </div>
  59. </div>
  60. </div>
  61. <% 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. <%= link_to presenter.preview_path(catalog_node) , data: { turbo_method: :get,turbo_frame: "protocol_sidebar", 'protocol-elements-target': "preview" }, class: 'hidden' do%>
  24. <% end %>
  25. <%= link_to presenter.show_path(catalog_node) , data: { turbo_method: :get,turbo_frame: element, 'protocol-elements-target': "preview show" }, class: 'hidden' do%>
  26. <% end %>
  27. else: 0 <% else %>
  28. <%= link_to presenter.settings_path(catalog_node) , data: { turbo_method: :get,turbo_frame: "protocol_sidebar", 'protocol-elements-target': "button" }, class: 'hidden' do%>
  29. <% end %>
  30. <%= link_to presenter.edit_path(catalog_node) , data: { turbo_method: :get,turbo_frame: element, 'protocol-elements-target': "button" }, class: 'hidden' do%>
  31. <% end %>
  32. <% end %>
  33. <div class="element__number hidden!"><%= presenter.position %></div>
  34. then: 0 else: 0 <% if !@edit %>
  35. <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">
  36. <i class="fa-sharp fa-regular text-xs fa-grip-dots-vertical"></i>
  37. </div>
  38. <% end %>
  39. <% end %>
  40. <div class="protocol-element__wrapper">
  41. <div class="protocol-element__header">
  42. <div class="protocol-element__header-left">
  43. then: 0 <% if @edit %>
  44. <div class="
  45. then: 0 else: 0 <% if presenter.input? %> protocol-element__input
  46. <% else %> protocol-element__info
  47. <% end %>">
  48. <i class="copy-12 <%= presenter.icon%>"></i>
  49. </div>
  50. <span class="copy-14-medium text-black!"><%= presenter.type %></span>
  51. else: 0 <% else %>
  52. then: 0 else: 0 <% if !presenter.structure? %>
  53. <div class="relative w-full">
  54. <div class="absolute top-0 left-0 h-full w-full z-2 cursor-pointer"></div>
  55. <div class="opacity-70">
  56. <%= react_component("ProtocolPreview", { jsonData: element.presenter.json_data}) %>
  57. </div>
  58. </div>
  59. <% end %>
  60. <% end %>
  61. </div>
  62. <div class="protocol-element__header-right">
  63. then: 0 <% if @edit %>
  64. then: 0 else: 0 <% if presenter.input? %>
  65. <div class="element__toggle-wrapper">
  66. <span class="copy-12-medium text-black! w-max"><%= t(".mark_as_required") %></span>
  67. <%= turbo_frame_tag element, :required do %>
  68. then: 0 else: 0 <% if current_user.can_edit_protocols? %>
  69. <%= form_with model: [:admin_v2, element.becomes(::Protocol::Element)], data: { controller: "autosubmit", autosubmit_target: "form" } do |form| %>
  70. <input name="protocol_element[required]" type="hidden" value="0" autocomplete="off">
  71. 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" } }) %>
  72. then: 0 else: 0 <%= form.hidden_field :catalog_node_id, value: @catalog_node&.id %>
  73. <% end %>
  74. <% end %>
  75. <% end %>
  76. </div>
  77. <% end %>
  78. <div data-controller="clipboard">
  79. <button data-action="click->clipboard#copy" data-clipboard-export-param="<%= @export_url %>" data-clipboard-broadcast-param="<%= @broadcast_url %>"
  80. class="bg-[#F5F5F5] w-[24px] h-[24px] rounded items-center flex justify-center text-[10px] fa-regular fa-copy text-[#5F5F5F]"
  81. data-toggle="tooltip"
  82. title="<%= t('.copy_to_clipboard') %>"
  83. ></button>
  84. </div>
  85. <div data-controller="modal">
  86. <%= render LooposUi::Modal.for_duplicate(model_name: "Element") do |modal| %>
  87. <% modal.with_trigger do %>
  88. <button data-action="click->modal#open"
  89. class="bg-[#F5F5F5] w-[24px] h-[24px] rounded items-center flex justify-center text-[10px] fa-kit fa-regular-copy-circle-plus text-[#5F5F5F]"
  90. data-toggle="tooltip"
  91. title="<%= t('.duplicate_protocol_element') %>"
  92. ></button>
  93. <% end %>
  94. 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" }) %>
  95. <% end %>
  96. </div>
  97. <%= render LooposUi::Modal.for_delete(model_name: "Protocol Element") do |modal| %>
  98. <% modal.with_trigger do %>
  99. <button type="button" class="flex items-center justify-center w-[24px] h-[24px] bg-[#FFE7E7] text-general-danger-800 rounded text-[10px] fa-regular fa-trash-can"></button>
  100. <% end %>
  101. <% modal.with_custom_content do %>
  102. <%= form_with(
  103. model: [:admin_v2, element.becomes(::Protocol::Element)],
  104. id: "#{element.id}",
  105. class: "flex items-center justify-center w-fit h-fit",
  106. method: "delete",
  107. data: { turbo_frame: "#{dom_id(protocol)}_protocol_elements", "protocol-elements-target": "deleteForm" }) do |form| %>
  108. then: 0 else: 0 <%= form.hidden_field :catalog_node_id, name: :catalog_node_id, value: catalog_node&.id %>
  109. <% end %>
  110. <% end %>
  111. <% modal.with_primary_action(
  112. text: t(".confirm"),
  113. kind: :app,
  114. tag_options: {
  115. form: "#{element.id}",
  116. type: "submit",
  117. data: {
  118. action: "click->lui--button#startLoading click->actions_loading#run"
  119. }
  120. }
  121. ) %>
  122. <% end %>
  123. else: 0 <% else %>
  124. <% presenter.enabled_apps.each do |app, enabled| %>
  125. then: 0 <% if app.to_s == "submission_extra" && enabled %>
  126. <div class="relative inline-block">
  127. <%= react_component("LogoApp", {
  128. app: "submission",
  129. size: "small",
  130. icon: true
  131. }) %>
  132. <div class="absolute bottom-[15px] left-[15px] w-3 h-3">
  133. <%= image_tag('submission-extra-arrow-core.svg', width: '12', height: '12') %>
  134. </div>
  135. else: 0 </div>
  136. then: 0 else: 0 <% elsif enabled %>
  137. <%= react_component("LogoApp", {
  138. app: app,
  139. size: "small",
  140. icon: true
  141. }) %>
  142. <% end %>
  143. <% end %>
  144. <% end %>
  145. </div>
  146. </div>
  147. then: 0 else: 0 <% if @edit %>
  148. then: 0 else: 0 <div class="protocol-element__content <%= presenter.input? ? "protocol-element__content--input" : "protocol-element__content--info" %>">
  149. <%= 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| %>
  150. <%
  151. element = @element
  152. presenter = element.presenter
  153. schema = presenter.left_settings_schema
  154. protocol = element.protocol
  155. %>
  156. <%# label %>
  157. <% edit_value = element.untranslated_label_html(locale: locale).presence || element.untranslated_label(locale: locale).to_s %>
  158. <div class='w-full'>
  159. then: 0 <% if @element.is_a?(Protocol::Element::Info::Html) %>
  160. <%= render LooposUi::Inputs::TextArea.new(
  161. value: edit_value,
  162. name: "protocol_element[untranslated_label_html]",
  163. rows: 5
  164. ) %>
  165. else: 0 <% else %>
  166. <%= render(LooposUi::Wysiwyg.new(initial_value: edit_value, input_name: "protocol_element[untranslated_label_html]", small: true, app: "neutral")) %>
  167. <% end %>
  168. <%#= form.text_field :label, class: "protocol-input", data: { controller: "#dropkiq", "preview-id": "left-setting-label-#{form.object.id}-preview" }, value: edit_value %>
  169. then: 0 else: 0 <%= form.hidden_field "catalog_node_id", value: @catalog_node&.id %>
  170. <div id="left-setting-label-<%= form.object.id %>-preview" class='dropkiq-preview'></div>
  171. </div>
  172. <%# placeholder%>
  173. <% if false %>
  174. <% edit_value = form.object[:placeholder] %>
  175. <div class='w-full'>
  176. <%= form.text_field :placeholder, class: "protocol-input", data: { controller: "#dropkiq", "preview-id": "left-setting-placeholder-#{form.object.id}-preview" }, value: edit_value %>
  177. <%= form.hidden_field "catalog_node_id", value: @catalog_node&.id %>
  178. <div id="left-setting-placeholder-<%= form.object.id %>-preview" class='dropkiq-preview'></div>
  179. </div>
  180. <% end %>
  181. <% schema[:properties].each do |attribute, properties| %>
  182. <%
  183. specific_component = "LooposUi::Protocol::Elements::Fields::#{properties[:type].camelize}".constantize
  184. then: 0 else: 0 default_value = element.respond_to?(attribute) ? element.send(attribute) : nil
  185. translatable_setting = element.class.respond_to?(:translatable_settings) && element.class.translatable_settings.include?(attribute.to_s)
  186. then: 0 value = if translatable_setting && element.respond_to?("#{attribute}_#{locale}")
  187. element.send("#{attribute}_#{locale}")
  188. else: 0 else
  189. default_value
  190. end
  191. then: 0 else: 0 settings_key = translatable_setting ? "#{attribute}_#{locale}" : attribute
  192. edit_value = element.settings_hash.dig(settings_key.to_s) || value
  193. properties[:value] = value
  194. %>
  195. <%= render specific_component.new(
  196. attribute: attribute,
  197. properties: properties,
  198. value: value,
  199. element: element,
  200. protocol: protocol,
  201. edit_value: edit_value,
  202. form: form) %>
  203. <% end %>
  204. <% end %>
  205. </div>
  206. <% end %>
  207. </div>
  208. </div>
  209. </div>
  210. <% end %>
  211. <% 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

87 relevant lines. 0 lines covered and 87 lines missed.
38 total branches, 0 branches covered and 38 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. %>
  14. then: 0 else: 0 <div class="protocol-component__header <%= show_inheritance_path.present? ? "": "protocol-component__header--open" %>" data-protocol-accordion-target="header">
  15. <div class="protocol-component__header-content">
  16. <div class="protocol-component__header-left">
  17. then: 0 else: 0 <div class="protocol-component__trigger--close <%= show_inheritance_path.present? ? "rotate-[0]!": "" %>" id="#accordion-collapse-<%= dom_id(protocol)%>-header"
  18. data-accordion-target="#accordion-collapse-<%= dom_id(protocol)%>"
  19. data-action="click->protocol-accordion#toggleClass"
  20. then: 0 else: 0 aria-expanded="<%= show_inheritance_path.present? ? "false": "true" %>"
  21. aria-controls="accordion-collapse-<%= dom_id(protocol)%>">
  22. <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">
  23. 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" %>"/>
  24. </svg>
  25. </div>
  26. <span class="protocol-component__name copy-14-medium" id="<%= dom_id(protocol)%>_name">
  27. <%= protocol.name %>
  28. </span>
  29. </div>
  30. then: 0 <% if applicable_catalog_node_protocol.present? %>
  31. <div class="protocol-component__header-right">
  32. then: 0 else: 0 <% if show_inheritance_path %>
  33. <% icon = capture do %>
  34. <%= link_to protocol_presenter.link_to_attached_node(current_catalog_node), remote: false, data: { turbo_frame: '_top' } do %>
  35. <div class="protocol-component__header-inheritance copy-12 tooltip-wrapper">
  36. <span class="flex">
  37. <i class="fa-regular fa-diagram-nested text-app-800-primary"></i>
  38. </span>
  39. <span class="protocol-component__header-inheritance-title copy-12 font-bold underline text-app-800-primary"><%= inherited_protocol_path %></span>
  40. </div>
  41. <% end %>
  42. <% end %>
  43. <%= 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" }) %>
  44. <% end %>
  45. then: 0 else: 0 <% if readonly && current_user.can_edit_protocols? %>
  46. <div class="form-layout__status">
  47. <%# FIXME: This should not reload the page %>
  48. <%= 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| %>
  49. <input name="referrer" type="hidden" value="<%= request.original_url %>" autocomplete="off">
  50. <input name="applicable_catalog_node_protocol[id]" type="hidden" value="<%= applicable_catalog_node_protocol.id %>" autocomplete="off">
  51. <input name="applicable_catalog_node_protocol[active]" type="hidden" value="0" autocomplete="off">
  52. <%= react_component(
  53. "Toggle",
  54. {
  55. id: "applicable_catalog_node_protocol_active",
  56. name: "applicable_catalog_node_protocol[active]",
  57. then: 0 else: 0 checked: applicable_catalog_node_protocol.active ? "checked" : "false",
  58. dataAttributes:{ controller: "loading", "loading-target": "toggle"},
  59. inputDataAttributes: {
  60. then: 0 else: 0 action: current_user.can_edit_protocol_inheritance? ? "change->autosubmit#submit change->loading#setToggleLoading" : "",
  61. turbo_frame: "_top"
  62. },
  63. blocked: !current_user.can_edit_protocol_inheritance?,
  64. }
  65. )
  66. %>
  67. <% end %>
  68. </div>
  69. <% end %>
  70. then: 0 else: 0 <% if !show_inheritance_path %>
  71. <%= 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| %>
  72. <input name="protocol[highlight]" type="hidden" value="<%= !protocol.highlight %>" autocomplete="off">
  73. then: 0 else: 0 <input name="protocol[catalog_node_id]" type="hidden" value="<%= current_catalog_node&.id %>" autocomplete="off">
  74. <% move = capture do %>
  75. then: 0 else: 0 <button 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>
  76. <% end %>
  77. then: 0 else: 0 <%= react_component("Tooltip", { text: protocol.highlight ? t(".move_to_main") : t(".move_to_catalog") , children: move, placement: "top" }) %>
  78. <% end %>
  79. then: 0 <% if readonly && current_user.can_edit_protocols? %>
  80. <i class="fa-regular fa-clone text-gray-400"></i>
  81. <i class="fa-regular fa-trash-alt text-gray-400"></i>
  82. else: 0 <% else %>
  83. <% settings = capture do %>
  84. 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 %>
  85. <i class="fa-regular fa-gear text-app-800-primary cursor-pointer"></i>
  86. <% end %>
  87. <% end %>
  88. <%= react_component("Tooltip", { text: t(".open_settings") , children: settings, placement: "top" }) %>
  89. <%= render LooposUi::Modal.for_export(model_name: "Protocol", id: export_modal_protocol_id) do |modal| %>
  90. <% export = capture do %>
  91. <i class="fa-regular fa-download text-app-800-primary cursor-pointer"></i>
  92. <% end %>
  93. <% modal.with_trigger do %>
  94. <%= react_component("Tooltip", { text: t(".export_protocol") , children: export, placement: "top" }) %>
  95. <% end %>
  96. <% modal.with_primary_action(
  97. text: t(".confirm"),
  98. href: export_admin_v2_protocol_path(protocol, format: :json),
  99. kind: :app,
  100. tag_options: { data: { action: "click->lui--button#startLoading click->actions_loading#run" } }
  101. ) %>
  102. <% end %>
  103. <div data-controller="clipboard">
  104. <% copy = capture do %>
  105. <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>
  106. <% end %>
  107. <%= react_component("Tooltip", { text: t(".copy_protocol") , children: copy, placement: "top" }) %>
  108. </div>
  109. <%= render LooposUi::Modal.for_duplicate(model_name: "Protocol", id: duplicate_modal_protocol_id) do |modal| %>
  110. <% clone = capture do %>
  111. <i class="fa-kit fa-regular-copy-circle-plus text-app-800-primary cursor-pointer"></i>
  112. <% end %>
  113. <% modal.with_trigger do %>
  114. <%= react_component("Tooltip", { text: t(".duplicate_protocol") , children: clone, placement: "top" }) %>
  115. <% end %>
  116. <% modal.with_primary_action(
  117. text: t(".confirm"),
  118. href: duplicate_admin_v2_protocol_path(protocol, catalog_node_id: current_catalog_node.id),
  119. kind: :app,
  120. tag_options: { data: { "turbo-method": :post, action: "click->lui--button#startLoading click->actions_loading#run" } }
  121. ) %>
  122. <% end %>
  123. <%= render LooposUi::Modal.new(title: t(".delete_protocol")) do |modal| %>
  124. <% remove = capture do %>
  125. <i class="fa-regular fa-trash-alt text-app-800-primary cursor-pointer"></i>
  126. <% end %>
  127. <% modal.with_trigger do %>
  128. <%= react_component("Tooltip", { text: t(".delete_protocol") , children: remove, placement: "top" }) %>
  129. <% end %>
  130. <% modal.with_primary_action(
  131. text: t(".confirm"),
  132. href: admin_v2_protocol_path(protocol),
  133. kind: :app,
  134. tag_options: { data: { "turbo-method": :delete, action: "click->lui--button#startLoading click->actions_loading#run" } }
  135. ) %>
  136. <%= tag.span(t(".delete_protocol_text")) %>
  137. <% end %>
  138. <% end %>
  139. <% end %>
  140. <div class="protocol-component__trigger" id="#accordion-collapse-<%= dom_id(protocol)%>-header"
  141. data-accordion-target="#accordion-collapse-<%= dom_id(protocol)%>"
  142. data-action="click->protocol-accordion#toggleClass"
  143. aria-controls="accordion-collapse-<%= dom_id(protocol)%>">
  144. <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">
  145. 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" %>"/>
  146. </svg>
  147. </div>
  148. </div>
  149. else: 0 <% else %>
  150. <div class="protocol-component__header-right">
  151. then: 0 else: 0 <% if !show_inheritance_path %>
  152. then: 0 else: 0 <% if readonly && current_user.can_edit_protocols? %>
  153. <i class="fa-regular fa-clone text-gray-400"></i>
  154. <i class="fa-regular fa-trash-alt text-gray-400"></i>
  155. <% end %>
  156. <% end %>
  157. <div class="protocol-component__trigger" id="#accordion-collapse-<%= dom_id(protocol)%>-header"
  158. data-accordion-target="#accordion-collapse-<%= dom_id(protocol)%>"
  159. data-action="click->protocol-accordion#toggleClass"
  160. aria-controls="accordion-collapse-<%= dom_id(protocol)%>">
  161. <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">
  162. 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" %>"/>
  163. </svg>
  164. </div>
  165. </div>
  166. <% end %>
  167. </div>
  168. </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-general-gray-900 copy-14 font-bold"><%= protocol_name %></p>
  26. <% end %>
  27. <p class="copy-12 text-general-gray-700">
  28. <%= t(".protocol_info.update_date") %>: <%= formatted_updated_at %>
  29. </p>
  30. <p class="copy-12 text-general-gray-700">
  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-general-gray-900 copy-14 font-bold"><%= t(".duplicate_protocol.action") %></p>
  45. <p class="copy-12 text-general-gray-700"><%= 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-general-gray-900 copy-14 font-bold"><%= t(".copy_protocol.action") %></p>
  66. <p class="copy-12 text-general-gray-700"><%= 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-general-gray-900 copy-14 font-bold"><%= t(".export_protocol.action") %></p>
  79. <p class="copy-14 text-general-gray-700"><%= 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-general-gray-900 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-general-gray-900 copy-14 font-bold"><%= t(".detach_protocol.action") %></p>
  115. <p class="copy-12 text-general-gray-700"><%= 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-general-gray-900 copy-14 font-bold"><%= t(".delete_protocol.action") %></p>
  137. <p class="copy-12 text-general-gray-700"><%= 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.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::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 preview_protocol_path
  29. then: 0 else: 0 preview_admin_v2_protocol_path(protocol, catalog_node_id: catalog_node&.id)
  30. end
  31. 1 def export_protocol_path
  32. export_admin_v2_protocol_path(protocol, format: :json)
  33. end
  34. 1 def copy_protocol_path
  35. export_admin_v2_protocol_path(protocol)
  36. end
  37. 1 def detach_protocol_path
  38. then: 0 else: 0 detach_protocol_admin_v2_catalog_node_path(catalog_node, protocol_id: protocol&.id)
  39. end
  40. 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/radio_card.rb

90.91% lines covered

33.33% branches covered

22 relevant lines. 20 lines covered and 2 lines missed.
3 total branches, 1 branches covered and 2 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. 3 option :checked, Types::Bool, default: -> { false }
  6. 5 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. 12 "#{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. 6 else: 6 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

66.67% lines covered

50.0% branches covered

12 relevant lines. 8 lines covered and 4 lines missed.
2 total branches, 1 branches covered and 1 branches missed.
    
  1. 6 then: 6 <% if kind == :full %>
  2. 6 <div class="lui-radio_card__content__full">
  3. 6 <%= header %>
  4. <div class="w-full">
  5. 6 <%= money_input %>
  6. </div>
  7. <div class="w-full">
  8. 6 <%= text_area %>
  9. </div>
  10. </div>
  11. <div class="lui-radio_card__content__compact">
  12. 6 <%= header %>
  13. <div class="lui-radio_card__content__compact__price">
  14. 6 <%= label_tag %>
  15. 6 <%= 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

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. 12 <%= tag.label for: option_id, class: classes, data: data do %>
  2. 6 <%= tag.input type: "radio", id: option_id, name: name, value: value, checked: checked, disabled: disabled, class: "lui-radio_card__input", form: form %>
  3. 12 <%= tag.div class: "flex-1 ml-8" do %>
  4. 6 <%= 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. 3 option :title, default: -> { "" }
  10. 1 option :turbo_id
  11. 1 option :default_value
  12. 3 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

100.0% lines covered

100.0% branches covered

1 relevant lines. 1 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 2 <%= 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/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

41.86% lines covered

0.0% branches covered

43 relevant lines. 18 lines covered and 25 lines missed.
18 total branches, 0 branches covered and 18 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. super(columns: [], **kwargs)
  19. @kwargs = kwargs.except(:columns_extra, :permissions)
  20. end
  21. 1 private
  22. 1 def table_columns
  23. table_columns = HashWithIndifferentAccess.new
  24. table_columns[:id] = { title: t(".email_id"), dataIndex: "id", key: "id", sortable: true }
  25. table_columns[:status] = { title: t(".state"), dataIndex: "status", key: "status", sortable: true }
  26. table_columns[:item] = { title: t(".item_id"), dataIndex: "item", key: "item", sortable: true }
  27. then: 0 else: 0 table_columns[:partnable] =
  28. {
  29. title: t(".partner"),
  30. dataIndex: "partnable",
  31. key: "partnable",
  32. sortable: true,
  33. } if show_partner?
  34. then: 0 else: 0 table_columns[:provider] =
  35. {
  36. title: t(".service_provider"),
  37. dataIndex: "provider",
  38. key: "provider",
  39. sortable: true,
  40. } if can_view_providers?
  41. then: 0 else: 0 table_columns[:template] =
  42. {
  43. title: t(".template"),
  44. dataIndex: "template",
  45. key: "template",
  46. sortable: true,
  47. } if can_view_templates?
  48. table_columns[:to] = { title: t(".to"), dataIndex: "to", key: "to", sortable: true }
  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: false } 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. table_columns.values
  64. end
  65. 1 PERMISSIONS.each do |permission_name|
  66. 3 define_method(:"#{permission_name}?") do
  67. then: 0 else: 0 @permissions&.dig(permission_name)
  68. end
  69. end
  70. 1 def show_partner?
  71. LooposUi.config.app_type?(:manager)
  72. end
  73. 1 def show_catalog?
  74. !LooposUi.config.app_type?(:manager)
  75. end
  76. 1 def services_status(status)
  77. case status
  78. when: 0 when "created", "pending"
  79. :informative
  80. when: 0 when "failed"
  81. :danger
  82. when: 0 when "sent"
  83. :success
  84. else: 0 else
  85. :neutral
  86. end
  87. end
  88. end
  89. end
  90. end
  91. 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-general-global-black" )%>
  9. <% end %>
  10. else: 0 <% else %>
  11. <%= tag.div(service.id, class: "copy-14 text-general-global-black" )%>
  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-general-global-black" )%>
  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-general-global-black" )%>
  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-general-global-black" )%>
  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-general-global-black" )%>
  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-general-global-black" )%>
  9. <% end %>
  10. else: 0 <% else %>
  11. <%= tag.div(service.id, class: "copy-14 text-general-global-black" )%>
  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-general-global-black" )%>
  18. <% end %>
  19. else: 0 <% else %>
  20. <%= tag.div(service.name, class: "copy-14 text-general-global-black" )%>
  21. <% end %>
  22. <% end %>
  23. <% row.with_cell(property: :subject) do %>
  24. <%= tag.div(service.subject, class: "copy-14 text-general-global-black" )%>
  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-general-global-black" )%>
  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-general-global-black" )%>
  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

46.15% lines covered

0.0% branches covered

39 relevant lines. 18 lines covered and 21 lines missed.
12 total branches, 0 branches covered and 12 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. super(columns: [], **kwargs)
  23. @kwargs = kwargs.except(:columns_extra, :permissions, :float_bar_actions_params)
  24. end
  25. 1 def table_columns
  26. table_columns = HashWithIndifferentAccess.new
  27. table_columns[:id] =
  28. { title: t(".incoming_payment_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. table_columns[:amount] = { title: t(".amount"), dataIndex: "amount", key: "amount", sortable: false }
  32. then: 0 else: 0 table_columns[:partnable] =
  33. { title: t(".partnable"), dataIndex: "partnable", key: "partnable", sortable: true } if show_partner?
  34. then: 0 else: 0 table_columns[:provider] =
  35. { title: t(".provider"), dataIndex: "provider", key: "provider", sortable: true } if can_view_providers?
  36. then: 0 else: 0 table_columns[:categories] =
  37. { title: t(".categories"), dataIndex: "categories", key: "categories", sortable: false } if show_catalog?
  38. then: 0 else: 0 table_columns[:product] =
  39. { title: t(".product"), dataIndex: "product", key: "product", sortable: true } if show_catalog?
  40. then: 0 else: 0 table_columns[:brands] =
  41. { title: t(".brands"), dataIndex: "brands", key: "brands", sortable: false } if show_catalog?
  42. table_columns[:created_at] =
  43. { title: t(".created_at"), dataIndex: "created_at", key: "created_at", sortable: true }
  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. 2 define_method(:"#{permission_name}?") do
  49. then: 0 else: 0 @permissions&.dig(permission_name)
  50. end
  51. end
  52. 1 def show_partner?
  53. LooposUi.config.app_type?(:manager)
  54. end
  55. 1 def show_catalog?
  56. !LooposUi.config.app_type?(:manager)
  57. end
  58. 1 def services_status(status)
  59. LooposUi::Resources::IncomingPaymentResource::STATUS_LABEL_MAPPING[status.to_sym]
  60. end
  61. end
  62. end
  63. end
  64. 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-general-global-black") %>
  9. <% end %>
  10. else: 0 <% else %>
  11. <%= tag.div(service.id, class: "copy-14 text-general-global-black") %>
  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-general-global-black") %>
  17. else: 0 <% else %>
  18. <%= tag.div('-', class: "copy-14 text-general-global-black" )%>
  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-general-global-black" )%>
  38. <% end %>
  39. <% end %>
  40. <% row.with_cell(property: :amount) do %>
  41. <%= tag.div(service.amount, class: "copy-14 text-general-global-black") %>
  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-general-global-black" )%>
  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

41.3% lines covered

0.0% branches covered

46 relevant lines. 19 lines covered and 27 lines missed.
14 total branches, 0 branches covered and 14 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. 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(".invoice_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[:invoice_kind] = { title: t(".invoice_kind"), dataIndex: "kind", key: "kind", sortable: true }
  33. table_columns[:amount] = { title: t(".amount"), dataIndex: "amount", key: "amount", sortable: true }
  34. table_columns[:vat] = { title: t(".vat"), dataIndex: "vat", key: "vat", sortable: true }
  35. then: 0 else: 0 table_columns[:partnable] =
  36. { title: t(".partnable"), dataIndex: "partnable", key: "partnable", sortable: true } if show_partner?
  37. then: 0 else: 0 table_columns[:provider] =
  38. { title: t(".provider"), dataIndex: "provider", key: "provider", sortable: true } if can_view_providers?
  39. then: 0 else: 0 table_columns[:categories] =
  40. { title: t(".categories"), dataIndex: "categories", key: "categories", sortable: false } if show_catalog?
  41. then: 0 else: 0 table_columns[:product] =
  42. { title: t(".product"), dataIndex: "product", key: "product", sortable: true } if show_catalog?
  43. then: 0 else: 0 table_columns[:brands] =
  44. { title: t(".brands"), dataIndex: "brands", key: "brands", sortable: false } if show_catalog?
  45. table_columns[:created_at] =
  46. { title: t(".created_at"), dataIndex: "created_at", key: "created_at", sortable: true }
  47. table_columns.deep_merge!(columns_extra.slice(*table_columns.keys.map(&:to_sym)))
  48. table_columns.values
  49. end
  50. 1 PERMISSIONS.each do |permission_name|
  51. 2 define_method(:"#{permission_name}?") do
  52. permissions.dig(permission_name)
  53. end
  54. end
  55. 1 def show_partner?
  56. LooposUi.config.app_type?(:manager)
  57. end
  58. 1 def show_catalog?
  59. !LooposUi.config.app_type?(:manager)
  60. end
  61. 1 def services_status(status)
  62. case status
  63. when: 0 when "created", "pending"
  64. :informative
  65. when: 0 when "failed"
  66. :danger
  67. when: 0 when "finalized", "sent"
  68. :success
  69. else: 0 else
  70. :neutral
  71. end
  72. end
  73. end
  74. end
  75. end
  76. 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-general-global-black") %>
  9. <% end %>
  10. else: 0 <% else %>
  11. <%= tag.div(service.id, class: "copy-14 text-general-global-black") %>
  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-general-global-black" )%>
  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-general-global-black" )%>
  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-general-global-black" )%>
  61. <% end %>
  62. <% row.with_cell(property: :vat) do %>
  63. <%= tag.div(service.vat || "-", class: "copy-14 text-general-global-black" )%>
  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-general-global-black" )%>
  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-general-global-black" )%>
  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-general-global-black 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

45.0% lines covered

0.0% branches covered

40 relevant lines. 18 lines covered and 22 lines missed.
12 total branches, 0 branches covered and 12 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. 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(".outgoing_payment_id"), dataIndex: "id", key: "id", sortable: true }
  29. table_columns[:reference] = { title: t(".reference"), dataIndex: "reference", key: "reference", 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] =
  33. { title: t(".amount"), dataIndex: "amount", key: "amount", sortable: false }
  34. then: 0 else: 0 table_columns[:partnable] =
  35. {
  36. title: t(".outgoing_payment_id"),
  37. dataIndex: "partnable",
  38. key: "partnable",
  39. sortable: true,
  40. } if show_partner?
  41. then: 0 else: 0 table_columns[:provider] =
  42. {
  43. title: t(".provider"),
  44. dataIndex: "provider",
  45. key: "provider",
  46. sortable: true,
  47. } if can_view_providers?
  48. then: 0 else: 0 table_columns[:categories] =
  49. {
  50. title: t(".categories"),
  51. dataIndex: "categories",
  52. key: "categories",
  53. sortable: false,
  54. } if show_catalog?
  55. then: 0 else: 0 table_columns[:product] =
  56. { title: t(".product"), dataIndex: "product", key: "product", sortable: true } if show_catalog?
  57. then: 0 else: 0 table_columns[:brands] =
  58. { title: t(".brands"), dataIndex: "brands", key: "brands", sortable: false } if show_catalog?
  59. table_columns[:created_at] =
  60. { title: t(".created_at"), dataIndex: "created_at", key: "created_at", sortable: true }
  61. table_columns.deep_merge!(columns_extra.slice(*table_columns.keys.map(&:to_sym)))
  62. table_columns.values
  63. end
  64. 1 PERMISSIONS.each do |permission_name|
  65. 3 define_method(:"#{permission_name}?") do
  66. then: 0 else: 0 @permissions&.dig(permission_name)
  67. end
  68. end
  69. 1 def show_partner?
  70. LooposUi.config.app_type?(:manager)
  71. end
  72. 1 def show_catalog?
  73. !LooposUi.config.app_type?(:manager)
  74. end
  75. 1 def services_status(status)
  76. LooposUi::Resources::PaymentResource::STATUS_LABEL_MAPPING[status.to_sym]
  77. end
  78. end
  79. end
  80. end
  81. 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-general-global-black") %>
  9. <% end %>
  10. else: 0 <% else %>
  11. <%= tag.div(service.id, class: "copy-14 text-general-global-black") %>
  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-general-global-black") %>
  17. else: 0 <% else %>
  18. <%= tag.div('-', class: "copy-14 text-general-global-black" )%>
  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-general-global-black" )%>
  31. <% end %>
  32. <% end %>
  33. <% row.with_cell(property: :amount) do %>
  34. <%= tag.div(service.amount, class: "copy-14 text-general-global-black") %>
  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-general-global-black" )%>
  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-general-global-black" )%>
  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-general-global-black") %>
  11. <% end %>
  12. else: 0 <% else %>
  13. <%= tag.div(service.external_id, class: "copy-14 text-general-global-black") %>
  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-general-global-black") %>
  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

42.22% lines covered

0.0% branches covered

45 relevant lines. 19 lines covered and 26 lines missed.
18 total branches, 0 branches covered and 18 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. super(columns: [], **kwargs)
  40. @kwargs = kwargs.except(:columns_extra, :permissions, :float_bar_actions_params, :show_shipping_guide)
  41. end
  42. 1 def table_columns
  43. table_columns = HashWithIndifferentAccess.new
  44. table_columns[:id] = { title: t(".id"), dataIndex: "id", key: "id", sortable: true, default_sort: :desc }
  45. table_columns[:reference] =
  46. { title: t(".reference"), dataIndex: "reference", key: "reference", sortable: true }
  47. table_columns[:status] = { title: t(".status"), dataIndex: "status", key: "status", sortable: true }
  48. table_columns[:items] = { title: t(".items"), dataIndex: "items", key: "items" }
  49. then: 0 else: 0 table_columns[:partnable] =
  50. { title: t(".partnable"), dataIndex: "partnable", key: "partnable", sortable: true } if show_partner?
  51. then: 0 else: 0 table_columns[:provider] =
  52. { title: t(".provider"), dataIndex: "provider", key: "provider", sortable: true } if can_view_providers?
  53. table_columns[:tracking_code] =
  54. { title: t(".tracking_code"), dataIndex: "tracking_code", key: "tracking_code", sortable: false }
  55. table_columns[:pickup_id] =
  56. { title: t(".pickup_id"), dataIndex: "pickup_id", key: "pickup_id", sortable: false }
  57. then: 0 else: 0 table_columns[:categories] =
  58. { title: t(".categories"), dataIndex: "categories", key: "categories", sortable: false } if show_catalog?
  59. then: 0 else: 0 table_columns[:product] =
  60. { title: t(".product"), dataIndex: "product", key: "product", sortable: true } if show_catalog?
  61. then: 0 else: 0 table_columns[:brands] =
  62. { title: t(".brands"), dataIndex: "brands", key: "brands", sortable: false } if show_catalog?
  63. then: 0 else: 0 table_columns[:shipping_guide] =
  64. { title: t(".shipping_guide"), dataIndex: "shipping_guide", key: "shipping_guide", sortable: false } if show_shipping_guide
  65. table_columns[:created_at] =
  66. { title: t(".created_at"), dataIndex: "created_at", key: "created_at", sortable: true }
  67. then: 0 else: 0 then: 0 else: 0 if columns_extra&.dig(:status, :filter_key).present?
  68. table_columns[:status][:filters] = STATUSES.map do |status|
  69. {
  70. text: t(".states.#{status}"),
  71. value: status,
  72. }
  73. end
  74. end
  75. table_columns.deep_merge!(columns_extra.slice(*table_columns.keys.map(&:to_sym)))
  76. table_columns.values
  77. end
  78. 1 PERMISSIONS.each do |permission_name|
  79. 2 define_method(:"#{permission_name}?") do
  80. then: 0 else: 0 @permissions&.dig(permission_name)
  81. end
  82. end
  83. 1 def show_partner?
  84. LooposUi.config.app_type?(:manager)
  85. end
  86. 1 def show_catalog?
  87. LooposUi.config.app_type?(:core)
  88. end
  89. 1 def services_status(status)
  90. LooposUi::Resources::ShippingResource::STATUS_LABEL_MAPPING.fetch(status.to_sym, :neutral)
  91. end
  92. end
  93. end
  94. end
  95. 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-general-global-black") %>
  9. <% end %>
  10. else: 0 <% else %>
  11. <%= tag.div(service.id, class: "copy-14 text-general-global-black") %>
  12. <% end %>
  13. <% end %>
  14. <% row.with_cell(property: :reference) do %>
  15. <%= tag.div(service.reference, class: "copy-14 text-general-global-black") %>
  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-general-global-black" )%>
  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-general-global-black") %>
  46. <% end %>
  47. else: 0 <% else %>
  48. <%= tag.div(service.tracking_code || '-', class: "copy-14 text-general-global-black") %>
  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-general-global-black") %>
  55. <% end %>
  56. else: 0 <% else %>
  57. <%= tag.div(service.pickup_id || '-', class: "copy-14 text-general-global-black") %>
  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-general-global-black" )%>
  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-general-global-black") %>
  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

41.86% lines covered

0.0% branches covered

43 relevant lines. 18 lines covered and 25 lines missed.
16 total branches, 0 branches covered and 16 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. super(columns: [], **kwargs)
  23. @kwargs = kwargs.except(:columns_extra, :permissions, :float_bar_actions_params)
  24. end
  25. 1 def table_columns
  26. table_columns = HashWithIndifferentAccess.new
  27. table_columns[:id] = { title: t(".sms_id"), dataIndex: "id", key: "id", sortable: true }
  28. table_columns[:status] = { title: t(".state"), dataIndex: "status", key: "status", sortable: true }
  29. table_columns[:item] = { title: t(".item_id"), dataIndex: "item", key: "item", sortable: true }
  30. then: 0 else: 0 table_columns[:partnable] =
  31. {
  32. title: t(".partnable"),
  33. dataIndex: "partnable",
  34. key: "partnable",
  35. sortable: true,
  36. } if show_partner?
  37. then: 0 else: 0 table_columns[:provider] =
  38. {
  39. title: t(".provider"),
  40. dataIndex: "provider",
  41. key: "provider",
  42. sortable: true,
  43. } if can_view_providers?
  44. table_columns[:to] = { title: t(".to"), dataIndex: "to", key: "to", sortable: true }
  45. then: 0 else: 0 table_columns[:categories] =
  46. { title: t(".categories"), dataIndex: "categories", key: "categories", sortable: false } if show_catalog?
  47. then: 0 else: 0 table_columns[:product] =
  48. { title: t(".product"), dataIndex: "product", key: "product", sortable: true } if show_catalog?
  49. then: 0 else: 0 table_columns[:brands] =
  50. { title: t(".brands"), dataIndex: "brands", key: "brands", sortable: false } if show_catalog?
  51. table_columns[:created_at] =
  52. { title: t(".created_at"), dataIndex: "created_at", key: "created_at", sortable: true }
  53. table_columns.deep_merge!(columns_extra.slice(*table_columns.keys.map(&:to_sym)))
  54. table_columns.values
  55. end
  56. 1 PERMISSIONS.each do |permission_name|
  57. 2 define_method(:"#{permission_name}?") do
  58. then: 0 else: 0 @permissions&.dig(permission_name)
  59. end
  60. end
  61. 1 def show_partner?
  62. LooposUi.config.app_type?(:manager)
  63. end
  64. 1 def show_catalog?
  65. !LooposUi.config.app_type?(:manager)
  66. end
  67. 1 def services_status(status)
  68. case status
  69. when: 0 when "created", "sent"
  70. :success
  71. when: 0 when "pending"
  72. :informative
  73. when: 0 when "failed"
  74. :danger
  75. else: 0 else
  76. :neutral
  77. end
  78. end
  79. end
  80. end
  81. end
  82. 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-general-global-black") %>
  9. <% end %>
  10. else: 0 <% else %>
  11. <%= tag.div(service.id, class: "copy-14 text-general-global-black") %>
  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-general-global-black" )%>
  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-general-global-black") %>
  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-general-global-black" )%>
  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

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. # 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. }
  22. 1 renders_one :highlight
  23. end
  24. 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

63.64% lines covered

0.0% branches covered

22 relevant lines. 14 lines covered and 8 lines missed.
6 total branches, 0 branches covered and 6 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module LooposUi
  3. 1 class Sidebar < LoopComponent
  4. 1 option :config, default: -> { LooposUi.config.sidebar }
  5. 1 USE_UI_2 = false
  6. 1 def call
  7. then: 0 if USE_UI_2
  8. render(LooposUi::Sidebar::V2::Sidebar.new(config: config))
  9. else: 0 else
  10. render(LooposUi::Sidebar::V1::Sidebar.new(config: config))
  11. end
  12. end
  13. 1 class << self
  14. 1 attr_writer :configuration
  15. 1 def configuration
  16. 1 @configuration ||= Configuration.new
  17. end
  18. 1 def configure
  19. 1 yield configuration
  20. end
  21. end
  22. # TODO: test check visible?
  23. # TODO: update documentation, active no longer necessary, explain :exact, :icluded, :exculed
  24. 1 def items
  25. item_config_list.map do |attrs|
  26. LooposUi::Sidebar::Item.from_hash(attrs)
  27. end
  28. end
  29. 1 private
  30. 1 def item_config_list
  31. then: 0 else: 0 then: 0 if config.items&.any?
  32. config.items
  33. else: 0 else # backward compatibility to old initializer
  34. (config.top_items || []) + (config.bottom_items || [])
  35. end || []
  36. end
  37. end
  38. 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.78% lines covered

0.0% branches covered

92 relevant lines. 32 lines covered and 60 lines missed.
48 total branches, 0 branches covered and 48 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. # TODO: review docs, this is not documented
  44. @active = attrs.fetch(:active, :inclusive)
  45. @visible = attrs.fetch(:visible, true)
  46. @disabled = attrs.fetch(:disabled, false)
  47. then: 0 else: 0 if @disabled && @tooltip_title.blank?
  48. @tooltip_title = "Disabled"
  49. end
  50. @color = case attrs[:color]
  51. when: 0 when Symbol
  52. COLOR_MAPPING[attrs[:color]]
  53. else: 0 else
  54. attrs[:color]
  55. end
  56. @bg_color = attrs[:bg_color]
  57. @highlight = attrs[:highlight]
  58. end
  59. 1 def id
  60. then: 0 else: 0 @_id ||= "#{title&.parameterize}_#{self.class.name.parameterize}"
  61. end
  62. 1 def visible?
  63. # If @visible is callable, execute it in the context of this instance
  64. then: 0 @_visible ||= if @visible.nil? # default visible
  65. true
  66. else: 0 else
  67. then: 0 else: 0 @visible.respond_to?(:call) ? instance_exec(&@visible) : @visible
  68. end
  69. end
  70. 1 def render?
  71. visible?
  72. end
  73. 1 def disabled?
  74. disabled == true
  75. end
  76. 1 def active?
  77. raise NotImplementedError.new("Implement in subclass")
  78. end
  79. 1 def sidebar_item_classes
  80. "#{active_classes} #{disabled_classes}"
  81. end
  82. 1 def color
  83. then: 0 else: 0 @_color ||= @color.respond_to?(:call) ? instance_exec(&@color) : @color
  84. end
  85. 1 def bg_color
  86. then: 0 else: 0 @_bg_color ||= @bg_color.respond_to?(:call) ? instance_exec(&@bg_color) : @bg_color
  87. end
  88. 1 def highlight
  89. then: 0 else: 0 @_highlight ||= @highlight.respond_to?(:call) ? instance_exec(&@highlight) : @highlight
  90. end
  91. 1 def title
  92. then: 0 else: 0 @_title ||= @title.respond_to?(:call) ? instance_exec(&@title) : @title
  93. end
  94. 1 def sidebar_item_styles
  95. s = StringIO.new
  96. then: 0 else: 0 s << "color: #{color}; border-color: #{color};" if color.present?
  97. then: 0 else: 0 s << "pointer-events: none;" if coming_soon
  98. then: 0 else: 0 s << "background-color: #{bg_color};" if bg_color.present?
  99. s.string
  100. end
  101. 1 def active_classes
  102. then: 0 else: 0 active? ? "lui-sidebar__item--active" : ""
  103. end
  104. 1 def disabled_classes
  105. then: 0 else: 0 disabled? ? "lui-sidebar__item--disabled" : ""
  106. end
  107. 1 def icon_classes
  108. "lui-sidebar-item__icon #{icon}"
  109. end
  110. 1 def short_title
  111. then: 0 @_short_title ||= if @short_title.respond_to?(:call)
  112. else: 0 instance_exec(&@short_title)
  113. then: 0 elsif @title.respond_to?(:call)
  114. instance_exec(&@title)
  115. else: 0 else
  116. @short_title || @title
  117. end
  118. end
  119. 1 def tooltip_title
  120. then: 0 else: 0 @_tooltip_title ||= @tooltip_title.respond_to?(:call) ? instance_exec(&@tooltip_title) : @tooltip_title
  121. end
  122. 1 def render_counter?
  123. then: 0 else: 0 counter&.positive?
  124. end
  125. 1 def counter
  126. then: 0 @_counter ||= if @counter.respond_to?(:call)
  127. instance_exec(&@counter)
  128. else: 0 else
  129. @counter
  130. end
  131. end
  132. 1 def counter_kind
  133. @_counter_kind ||= begin
  134. then: 0 kind = if @counter_kind.respond_to?(:call)
  135. instance_exec(&@counter_kind)
  136. else: 0 else
  137. @counter_kind
  138. end
  139. then: 0 else: 0 VALID_COUNTER_KINDS.include?(kind) ? kind : :warning
  140. end
  141. end
  142. end
  143. end
  144. end
  145. end

app/components/loopos_ui/sidebar/v1/sidebar.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. <turbo-frame id="lui-sidebar" target="lui-main-layout" data-controller="sidebar">
  2. <aside class="lui-sidebar">
  3. <div class="lui-sidebar__item-list">
  4. <% items.each do |item| %>
  5. <%= render(item) %>
  6. <% end %>
  7. </div>
  8. </aside>
  9. </turbo-frame>

app/components/loopos_ui/sidebar/v1/sidebar.rb

63.16% lines covered

0.0% branches covered

19 relevant lines. 12 lines covered and 7 lines missed.
4 total branches, 0 branches covered and 4 branches missed.
    
  1. 1 module LooposUi
  2. 1 class Sidebar
  3. 1 module V1
  4. 1 class Sidebar < LoopComponent
  5. 1 option :config, default: -> { LooposUi.config.sidebar }
  6. 1 class << self
  7. 1 attr_writer :configuration
  8. 1 def configuration
  9. @configuration ||= ::LooposUi::Sidebar::Configuration.new
  10. end
  11. 1 def configure
  12. yield configuration
  13. end
  14. end
  15. # TODO: test check visible?
  16. # TODO: update documentation, active no longer necessary, explain :exact, :icluded, :exculed
  17. 1 def items
  18. item_config_list.map do |attrs|
  19. Item.from_hash(attrs)
  20. end
  21. end
  22. 1 private
  23. 1 def item_config_list
  24. then: 0 else: 0 then: 0 if config.items&.any?
  25. config.items
  26. else: 0 else # backward compatibility to old initializer
  27. (config.top_items || []) + (config.bottom_items || [])
  28. end || []
  29. end
  30. end
  31. end
  32. end
  33. 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) 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

80.0% lines covered

60.0% branches covered

15 relevant lines. 12 lines covered and 3 lines missed.
5 total branches, 3 branches covered and 2 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. 166 "lui-spinner lui-spinner--#{@size} lui-spinner--#{@theme}"
  18. end
  19. 1 def size_px
  20. 166 when: 66 case @size
  21. 66 when: 62 when :tiny then 12
  22. 62 when :small then 14
  23. else: 38 else
  24. 38 16
  25. end
  26. end
  27. end
  28. end

app/components/loopos_ui/spinner/spinner.html.erb

100.0% lines covered

100.0% branches covered

1 relevant lines. 1 lines covered and 0 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. 166 <%= render LooposUi::MIcon.new(:progress_activity, class: spinner_classes, size: size_px, class: "animate-spin") %>

app/components/loopos_ui/state_label.rb

96.3% lines covered

58.33% branches covered

27 relevant lines. 26 lines covered and 1 lines missed.
12 total branches, 7 branches covered and 5 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. 246 super(text: text, icon: icon, color: color, count: count, condensed: condensed)
  47. 246 color = color.to_sym
  48. 246 then: 246 else: 0 @text_color, @bg_color = light ? LIGHT_COLORS[color] : COLORS[color]
  49. # If the color is not defined, use the default color
  50. 246 then: 0 else: 246 @text_color, @bg_color = LIGHT_COLORS[:default] if @text_color.nil? || @bg_color.nil?
  51. 246 then: 0 else: 246 raise "No color defined for: #{color}" if @text_color.nil? || @bg_color.nil?
  52. end
  53. 1 def styles
  54. 246 else: 62 then: 184 return super unless @color == :draft || @color == :white
  55. 62 then: 0 if @color == :draft
  56. <<~CSS.squish
  57. #{super}
  58. border: 1px dashed var(--General-Gray-900, #495057);
  59. else: 62 CSS
  60. 62 then: 62 else: 0 elsif @color == :white
  61. <<~CSS.squish
  62. 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

100.0% lines covered

50.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
2 total branches, 1 branches covered and 1 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. 6 <% columns.each do |column| %>
  7. 6 then: 0 else: 6 <div class="column <%= column.half ? 'column--half' : '' %>">
  8. 6 <%= column %>
  9. </div>
  10. <% end %>
  11. 6 HTML
  12. 1 renders_many :columns, "ColumnLayout"
  13. 1 class ColumnLayout < LoopComponent
  14. 7 option :half, optional: true, default: -> { false }
  15. 1 def call
  16. 6 content
  17. end
  18. end
  19. end
  20. end
  21. end

app/components/loopos_ui/tabs_content/tabs_content.html.erb

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. 6 <div class="lui-tabs_content">
  2. 6 <% rows.each do |row| %>
  3. 6 <div class="row">
  4. 6 <%= row %>
  5. </div>
  6. <% end %>
  7. 6 </div>

app/components/loopos_ui/tabs_layout.rb

82.54% lines covered

37.5% branches covered

63 relevant lines. 52 lines covered and 11 lines missed.
16 total branches, 6 branches covered and 10 branches missed.
    
  1. 1 module LooposUi
  2. 1 class TabsLayout < LoopComponent
  3. 3 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. 6 @tabs_count ||= 0
  8. 6 @tabs_count += 1
  9. 6 @active_tab_index ||= 0
  10. # Check if this tab should be active based on active_tab_key
  11. 6 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: 6 end
  18. 6 then: 0 else: 6 elsif args[:active]
  19. @active_tab_index = @tabs_count - 1
  20. end
  21. 6 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. 2 super(...)
  28. 2 @tabs_count = 0
  29. 2 @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. 7 option :key, default: -> { title }
  36. 1 option :icon, optional: true
  37. 7 option :active, optional: true, default: -> { false }
  38. 1 option :url, optional: true
  39. 7 option :disabled, optional: true, default: -> { false }
  40. 7 option :lazy_load, optional: true, default: -> { false }
  41. 7 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. 6 then: 0 else: 6 data[:key] = data[:key].to_s.parameterize.underscore if data.key?(:key)
  46. 6 super(**data)
  47. end
  48. 1 def turbo_frame_args
  49. args = {
  50. 6 data: {
  51. tabs_target: "panel",
  52. tab_id: index,
  53. },
  54. hidden: !active,
  55. }
  56. 6 then: 0 else: 6 if url.present?
  57. then: 0 if lazy_load
  58. args.deep_merge!({
  59. data: {
  60. lazy_load: lazy_load,
  61. url: url,
  62. load_only_on_click: load_only_on_click,
  63. },
  64. })
  65. else: 0 else
  66. args.deep_merge!({src: url})
  67. end
  68. end
  69. 6 args
  70. end
  71. 1 class << self
  72. 1 def keyfy(key)
  73. 24 key.to_s.parameterize.underscore
  74. end
  75. 1 def turbo_id(key)
  76. 6 "lui-tab-#{keyfy(key)}"
  77. end
  78. end
  79. 1 def turbo_id
  80. 6 self.class.turbo_id(key)
  81. end
  82. 1 def classes
  83. 6 then: 0 if active
  84. else: 6 "lui-tabs_layout__tab_entry--active"
  85. 6 then: 0 elsif disabled
  86. "cursor-default! opacity-40 hover:text-general-global-black"
  87. else: 6 else
  88. 6 "text-general-global-black"
  89. end
  90. end
  91. 1 def key
  92. 18 self.class.keyfy(@key)
  93. end
  94. # Helper component. Meant to be used in turbo_stream responses. On being rendered,
  95. # it will emit an event to request focus on the tab with the given key.
  96. 1 class RequestFocus < LoopComponent
  97. 1 option :key
  98. end
  99. end
  100. end
  101. 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

37.5% lines covered

50.0% branches covered

8 relevant lines. 3 lines covered and 5 lines missed.
2 total branches, 1 branches covered and 1 branches missed.
    
  1. 12 <%= turbo_frame_tag turbo_id, class: "lui-tabs_layout__tab", data: {}, **turbo_frame_args do %>
  2. 6 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: 6 <% else %>
  12. 6 <%= content %>
  13. <% end %>
  14. <% end %>

app/components/loopos_ui/tabs_layout/tabs_layout.html.erb

78.95% lines covered

35.71% branches covered

19 relevant lines. 15 lines covered and 4 lines missed.
14 total branches, 5 branches covered and 9 branches missed.
    
  1. 2 <%= 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. 2 ) do %>
  8. <div class="lui-tabs_layout__tab_list" data-tabs-target="tabList" data-controller="lui--blurred-scroll">
  9. 2 <% tabs.each_with_index do |tab, i| %>
  10. 6 <%= tag.span(
  11. data: {
  12. 6 then: 0 else: 6 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. 6 class: "lui-tabs_layout__tab_entry #{tab.classes} #{(@active_tab_index == i) && "lui-tabs_layout__tab_entry--active"}"
  20. 6 ) do %>
  21. 6 then: 0 else: 6 <%= tag.i(class: faviconize(tab.icon)) if tab.icon %>
  22. 6 <%= tab.title %>
  23. <% end %>
  24. <% end %>
  25. 2 then: 0 else: 2 <% 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. 2 </div>
  47. <div class="lui-tabs_layout__main">
  48. 2 <% tabs.each do |tab| %>
  49. 6 then: 6 else: 0 <%= tab if !tab.disabled %>
  50. <% end %>
  51. 2 then: 0 else: 2 <% if impact.present? %>
  52. then: 0 else: 0 <%= impact if !impact.disabled %>
  53. <% end %>
  54. 2 </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

95.24% lines covered

83.33% branches covered

21 relevant lines. 20 lines covered and 1 lines missed.
6 total branches, 5 branches covered and 1 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. 64 super(...)
  17. 64 @leading_icon ||= "fa-regular fa-tag"
  18. end
  19. 1 def classes
  20. 64 "#{super} lui-tag-token"
  21. end
  22. 1 def styles
  23. 64 <<~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. 64 then: 32 @text_color, @bg_color = if color.present?
  31. 32 LooposUi.logger.warn("Manual color should not be set, use for testing only")
  32. 32 then: 32 if COLORS.key?(color.to_sym)
  33. 32 COLORS[color.to_sym]
  34. else: 0 else
  35. raise ArgumentError, "Invalid color: #{color}, available colors: #{COLORS.keys.join(", ")}"
  36. end
  37. else: 32 else
  38. 32 valid_colors = COLORS.excluding(LooposUi.config.app_type.to_sym, :general).values
  39. 32 then: 30 if @text.present?
  40. 30 valid_colors[@text.hash % valid_colors.size]
  41. else: 2 else
  42. 2 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

100.0% lines covered

100.0% branches covered

1 relevant lines. 1 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 8 <%= 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-general-global-black <%= 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-general-global-black <%= size == :small ? "copy-10" : "copy-12" %>">
  29. <%= label %>
  30. </p>
  31. <% end %>
  32. <% end %>

app/components/loopos_ui/token.rb

87.3% lines covered

50.0% branches covered

63 relevant lines. 55 lines covered and 8 lines missed.
26 total branches, 13 branches covered and 13 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. 152 @text = text
  17. 152 @color = color
  18. 152 @tooltip = tooltip
  19. 152 @locked = locked # TODO: Document this
  20. 152 @id = id
  21. 152 @disabled = disabled
  22. 152 @kind = kind
  23. 152 @key_text = key_text
  24. 152 @key_value_actions = key_value_actions || {}
  25. 152 @draggable = draggable
  26. 152 @ellipsis = ellipsis
  27. 152 @close = close
  28. 152 @system_arguments = system_arguments || {}
  29. 152 then: 22 if icon.present?
  30. 22 @leading_icon = icon
  31. else: 130 else
  32. 130 @leading_icon = leading_icon
  33. 130 @trailing_icon = trailing_icon
  34. end
  35. 152 set_color(color)
  36. end
  37. 1 def system_arguments
  38. 150 @system_arguments.slice(:data)
  39. end
  40. 1 def unique_id
  41. 150 then: 0 @unique_id ||= if @id.present?
  42. id
  43. else: 150 else
  44. 150 "lui-token_#{rand(10**10)}"
  45. end
  46. end
  47. 1 def leading_icon
  48. 150 then: 86 else: 64 if @leading_icon.present?
  49. 86 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: 86 helpers.app_svg(app: @leading_icon.to_sym, width: 12)
  60. 86 then: 86 elsif @leading_icon.start_with?("fa-")
  61. 86 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. 150 then: 0 else: 150 tag.i(class: "lui-token__icon fa-regular #{@trailing_icon}") if @trailing_icon.present?
  70. end
  71. 1 def styles
  72. 64 then: 0 else: 64 cursor_style = @draggable ? "cursor: grab;" : ""
  73. 64 then: 0 else: 64 bg_style = @bg_color.present? ? "background-color: #{@bg_color};" : ""
  74. 64 <<~CSS.squish
  75. color: #{@text_color};
  76. #{bg_style}
  77. #{cursor_style}
  78. CSS
  79. end
  80. 1 def classes
  81. [
  82. 128 then: 0 else: 128 @locked ? "locked" : "",
  83. 128 then: 0 else: 128 @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. 150 text.present? && text.to_s.length >= 33
  91. end
  92. 1 def has_tooltip?
  93. 150 @tooltip.present? || text_too_long?
  94. end
  95. 1 def tooltip_text
  96. 22 @tooltip.presence || text
  97. end
  98. 1 private
  99. 1 def set_color(color)
  100. @text_color, @bg_color =
  101. 64 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: 64 else
  110. 64 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

32.56% lines covered

29.41% branches covered

43 relevant lines. 14 lines covered and 29 lines missed.
34 total branches, 10 branches covered and 24 branches missed.
    
  1. 150 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: 150 <% else %>
  31. 300 then: 0 else: 150 <%= tag.span(id: unique_id, class: "lui-token #{classes}#{@draggable ? ' draggable' : ''}", style: styles, **system_arguments) do %>
  32. 150 then: 22 else: 128 <%= render LooposUi::Tooltip.new(title: tooltip_text) if has_tooltip? %>
  33. 150 <%= leading_icon %>
  34. 150 <%= trailing_icon %>
  35. 150 then: 0 <% if @url.present? %>
  36. else: 150 <%= 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. 150 then: 148 else: 2 <% elsif text.present? %>
  38. 148 <%= tag.span(text, class: "lui-token__text") %>
  39. <% end %>
  40. 150 <div class="lui-token__actions">
  41. 150 <% actions.each do |action| %>
  42. <%= action %>
  43. <% end %>
  44. 150 then: 0 else: 150 <% 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. 150 then: 0 else: 150 <% 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. 150 </div>
  63. 150 then: 0 else: 150 <% 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

93.33% lines covered

100.0% branches covered

15 relevant lines. 14 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 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. 39 option :position, default: proc { "top" }, type: Types::Coercible::String.enum(*VALID_POSITIONS)
  28. 41 option :interactive, default: proc { false }, type: Types::Params::Bool
  29. 1 private
  30. 1 def description_or_content
  31. 44 content || description
  32. end
  33. end
  34. end

app/components/loopos_ui/tooltip/tooltip.html.erb

77.78% lines covered

100.0% branches covered

18 relevant lines. 14 lines covered and 4 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. 40 <div class="lui-tooltip hidden"
  2. data-controller="tooltips"
  3. 40 data-tooltips-tippy-target-id-value="<%= tippy_target_id %>"
  4. 40 data-tooltips-position-value="<%= position %>"
  5. 40 data-tooltips-interactive-value="<%= interactive %>"
  6. >
  7. 40 then: 36 else: 4 <% if title.present? %>
  8. 36 <div class="lui-tooltip__title">
  9. 36 <%= title %>
  10. </div>
  11. <% end %>
  12. 40 then: 4 else: 36 <% if description_or_content.present? %>
  13. 4 <div class="lui-tooltip__description">
  14. 4 <%= description_or_content %>
  15. </div>
  16. <% end %>
  17. 40 <% links.each do |link| %>
  18. <div class="lui-tooltip__link">
  19. <%= link %>
  20. </div>
  21. <% end %>
  22. 40 <% tokens.each do |token| %>
  23. <%= token %>
  24. <% end %>
  25. 40 <% buttons.each do |button| %>
  26. <%= button %>
  27. <% end %>
  28. 40 </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

78.26% lines covered

0.0% branches covered

23 relevant lines. 18 lines covered and 5 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. 1 def toasters_stream_target_key
  25. toasters_stream_target(keys: stream_namespaces)
  26. end
  27. 1 class RootStyles < LoopComponent
  28. end
  29. end
  30. end
  31. 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

44.07% lines covered

0.0% branches covered

118 relevant lines. 52 lines covered and 66 lines missed.
66 total branches, 0 branches covered and 66 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 :size, default: -> { :full }, type: Types::Coercible::Symbol.enum(:full, :small)
  9. 1 option :accept,
  10. Types::Array | Types::Symbol.enum(:all),
  11. optional: true,
  12. default: -> { LooposUi.config.image_default_accept_formats }
  13. 1 option :model, optional: true
  14. 1 option :association, optional: true
  15. 1 option :rounded, default: -> { false }, type: Types::Bool
  16. # TODO: only allowed when has_many_attached
  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 def initialize(...)
  37. super
  38. validate_input_source!
  39. then: 0 else: 0 if list_view && !model
  40. raise ArgumentError, "#{self.class}: Argument error, model is required when list_view is true"
  41. end
  42. validate_standalone_identity!
  43. mimefy_accept_types
  44. end
  45. # When editable without a model ("standalone"), only `accept` is required for direct upload
  46. # validation. LoopOS `images#create` attach still requires a full context with model keys.
  47. 1 def standalone_upload?
  48. editable? && model.blank?
  49. end
  50. 1 def context
  51. payload =
  52. then: 0 if model.present?
  53. {
  54. model_class: model.class.name,
  55. model_id: model.id,
  56. association: association,
  57. accept: file_field_accept,
  58. }
  59. else: 0 else
  60. { accept: file_field_accept }
  61. end
  62. LooposUi::EncryptionService.encrypt(payload)
  63. end
  64. 1 def file_field_accept
  65. case @accept
  66. when: 0 when :all
  67. ["image/*"]
  68. else: 0 else
  69. (IMAGE_MIMES & @accept).flat_map(&:content_type)
  70. end.join(",")
  71. end
  72. # FIXME: small edge case here, if you pass a string like "png"
  73. 1 def mimefy_accept_types
  74. then: 0 else: 0 return if accept == :all
  75. @accept = accept.flat_map do |mime|
  76. ::MIME::Types.type_for(".#{mime}") || ::MIME::Types[mime.to_s]
  77. end.compact_blank!
  78. end
  79. 1 def unique_id
  80. @unique_id ||= [
  81. "multiple_image",
  82. then: 0 else: 0 model.present? && association.present? ? dom_id(model, association) : nil,
  83. standalone_identity_seed,
  84. image_url,
  85. size,
  86. then: 0 else: 0 in_list_view ? "list_view" : nil,
  87. in_list_view_index,
  88. ].compact.join("_")
  89. end
  90. 1 def unique_list_id
  91. @unique_list_id ||= "#{unique_id}_list"
  92. end
  93. 1 def classes
  94. [
  95. "lui-image--#{size}",
  96. then: 0 else: 0 rounded ? "lui-image--rounded" : nil,
  97. then: 0 else: 0 empty_uploader? ? "always-edit" : nil,
  98. ].compact.join(" ")
  99. end
  100. 1 def placeholder_classes
  101. [
  102. "lui-image__placeholder",
  103. then: 0 else: 0 editable? ? "lui-image__placeholder--editable" : nil,
  104. then: 0 else: 0 with_image? ? "hidden" : "flex",
  105. ].compact.join(" ")
  106. end
  107. 1 def empty_uploader?
  108. in_list_view && force_new
  109. end
  110. 1 def attached_image
  111. then: 0 else: 0 return attachment if attachment.present?
  112. then: 0 if has_many_attached?
  113. then: 0 else: 0 then: 0 else: 0 model&.send(association)&.first
  114. else: 0 else
  115. then: 0 else: 0 model&.send(association)
  116. end
  117. end
  118. 1 def with_attached?
  119. then: 0 else: 0 return false if force_new
  120. then: 0 if has_many_attached?
  121. then: 0 else: 0 attached_image&.present?
  122. else: 0 else
  123. then: 0 else: 0 !!attached_image&.attached?
  124. end
  125. end
  126. 1 def turbo_frame_id
  127. then: 0 else: 0 return @turbo_frame_id if @turbo_frame_id.present?
  128. then: 0 else: 0 in_list_view ? unique_list_id : unique_id
  129. end
  130. 1 def editable?
  131. !!@editable
  132. end
  133. 1 def single_file_field_name
  134. name.presence || "file"
  135. end
  136. # When `name` is set, append "[]" for the multi-file input unless already present (Rails array param).
  137. 1 def multiple_file_field_name
  138. then: 0 else: 0 return "files[]" if name.blank?
  139. n = name.to_s
  140. then: 0 else: 0 n.end_with?("[]") ? n : "#{n}[]"
  141. end
  142. # Root-relative path to the host application Active Storage direct upload endpoint.
  143. # `file_field(..., direct_upload: true)` resolves `rails_direct_uploads_url` in the current
  144. # routing context first; when the component is rendered under a mounted engine, that can
  145. # produce a relative URL under the mount (e.g. `/mount/loopos_ui/rails/active_storage/direct_uploads`),
  146. # which does not exist. The template sets `data-direct-upload-url` explicitly instead.
  147. #
  148. # Use `Rails.application.routes` only (path helper). Avoid `main_app.*_url` / mixed proxies so we
  149. # never require `default_url_options[:host]` (e.g. mailers, previews without request host).
  150. 1 def active_storage_direct_upload_path
  151. Rails.application.routes.url_helpers.rails_direct_uploads_path
  152. rescue ActionController::UrlGenerationError, NoMethodError
  153. "#{ActiveStorage.routes_prefix.to_s.chomp("/")}/direct_uploads"
  154. end
  155. 1 def effective_direct_upload_url
  156. direct_upload_url.presence || active_storage_direct_upload_path
  157. end
  158. # Encrypted direct-upload payload for X-Lui-Context. Omit `name` when linked to an outer form
  159. # (`form_id` + standalone) so it is not submitted as `params[:context]` on the host item page.
  160. 1 def upload_context_input_attributes
  161. attrs = {
  162. type: "hidden",
  163. autocomplete: "off",
  164. value: context,
  165. data: { images_target: "uploadContext" },
  166. }
  167. else: 0 then: 0 attrs[:name] = "context" unless standalone_upload? && form_id.present?
  168. attrs
  169. end
  170. 1 def with_image?
  171. with_attached? || image_url.present?
  172. end
  173. 1 def has_many_attached?
  174. attachment_reflection == :has_many_attached
  175. end
  176. 1 def has_one_attached?
  177. attachment_reflection == :has_one_attached
  178. end
  179. 1 def attachments
  180. else: 0 then: 0 raise unless has_many_attached?
  181. model.send(association).attachments
  182. end
  183. # Could probably generate this from the options, but this is fine for now
  184. 1 def configuration
  185. {
  186. editable: editable,
  187. image_url: image_url,
  188. size: size,
  189. model: model,
  190. association: association,
  191. rounded: rounded,
  192. list_view: list_view,
  193. in_list_view: in_list_view,
  194. attachment: attachment,
  195. force_new: force_new,
  196. can_detach: can_detach,
  197. name: name,
  198. form_id: form_id,
  199. direct_upload_url: direct_upload_url,
  200. direct_upload_authorization: direct_upload_authorization,
  201. }
  202. end
  203. 1 private
  204. 1 def validate_input_source!
  205. then: 0 else: 0 return if model.present? || name.present?
  206. raise ArgumentError, "#{self.class}: either model or name is required"
  207. end
  208. 1 def validate_standalone_identity!
  209. else: 0 then: 0 return unless standalone_upload?
  210. then: 0 else: 0 return if turbo_frame_id.present? || name.present? || form_id.present?
  211. raise ArgumentError, "#{self.class}: standalone editable mode requires name, form_id, or turbo_frame_id to avoid duplicate ids"
  212. end
  213. 1 def standalone_identity_seed
  214. else: 0 then: 0 return unless standalone_upload?
  215. parts = [name.presence, form_id.presence].compact
  216. then: 0 else: 0 return if parts.empty?
  217. parts.join("_").parameterize(separator: "_")
  218. end
  219. 1 def attachment_reflection
  220. then: 0 else: 0 then: 0 else: 0 then: 0 else: 0 model&.class&.reflect_on_attachment(association.to_s)&.macro
  221. end
  222. end
  223. end
  224. end

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

0.0% lines covered

0.0% branches covered

71 relevant lines. 0 lines covered and 71 lines missed.
38 total branches, 0 branches covered and 38 branches missed.
    
  1. then: 0 <% if list_view %>
  2. <%= tag.turbo_frame id: turbo_frame_id, class: "lui-image-list" do %>
  3. <div class="flex flex-row gap-2 flex-wrap">
  4. <% attachments.each_with_index do |a,i| %>
  5. <%= render LooposUi::V2::Image.new(
  6. **configuration,
  7. attachment: a,
  8. list_view: false,
  9. in_list_view: true,
  10. in_list_view_index: i,
  11. ) %>
  12. <% end %>
  13. <%= render LooposUi::V2::Image.new(
  14. **configuration,
  15. force_new: true,
  16. list_view: false,
  17. in_list_view: true,
  18. in_list_view_index: -1,
  19. ) %>
  20. </div>
  21. <% end %>
  22. else: 0 <% else %>
  23. <%= tag.turbo_frame id: turbo_frame_id do %>
  24. <%= tag.div(
  25. data: {
  26. controller: "images",
  27. images_editable_value: editable?,
  28. images_standalone_value: standalone_upload?,
  29. images_form_id_value: form_id.to_s,
  30. images_with_attached_value: with_attached? || image_url.present?,
  31. images_turbo_frame_id_value: turbo_frame_id,
  32. images_unique_id_value: unique_id,
  33. images_list_view_value: in_list_view,
  34. images_has_many_value: has_many_attached?,
  35. images_force_new_value: force_new,
  36. images_direct_upload_authorization_value: direct_upload_authorization.presence || "",
  37. images_direct_upload_url_value: effective_direct_upload_url,
  38. images_urls_value: {
  39. then: 0 else: 0 attach: standalone_upload? ? nil : path,
  40. then: 0 else: 0 detach: (can_detach && !standalone_upload?) ? path : nil,
  41. }
  42. }) do %>
  43. <%= tag.div(class: ["lui-image", classes].join(" "), data: { images_target: 'imageComponent', action: "pubsub:action@window->images#executeAction"}) do %>
  44. <div
  45. role="status"
  46. class="hidden lui-image__loader"
  47. data-images-target="loader"
  48. >
  49. <%= render LooposUi::Button.new(
  50. icon: "spinner",
  51. disabled: true,
  52. size: :tiny,
  53. kind: :neutral,
  54. type: :secondary,
  55. ) %>
  56. </div>
  57. then: 0 <% if with_attached? %>
  58. else: 0 <%= image_tag(attached_image, class: "lui-image__image", data: { images_target: "image", blob_id: attached_image.blob_id }) %>
  59. then: 0 <% elsif image_url.present? %>
  60. <%= image_tag(image_url, class: "lui-image__image", data: { images_target: "image" }) %>
  61. else: 0 <% else %>
  62. <%= image_tag("", class: "hidden lui-image__image", data: { images_target: "image" }) %>
  63. <% end %>
  64. <div
  65. class="<%= placeholder_classes %>"
  66. data-images-target="placeholder"
  67. >
  68. <%= render LooposUi::Icon.new(icon: :image, color: :white) %>
  69. </div>
  70. then: 0 else: 0 <% if editable? %>
  71. <div class="overflow-visible lui-image__image-edit">
  72. then: 0 <% if form_id.present? %>
  73. <%= tag.input(**upload_context_input_attributes) %>
  74. <div class="hidden" data-images-target="noImageUploader">
  75. <%= render LooposUi::Button.new(
  76. icon: :upload,
  77. size: :tiny,
  78. then: 0 else: 0 type: size == :full ? :secondary : :tertiary,
  79. kind: :neutral,
  80. then: 0 else: 0 tooltip_text: has_many_attached? ? "Upload images here" : "Upload image here",
  81. tag_options: {
  82. data: {
  83. then: 0 else: 0 action: has_many_attached? ? "click->images#openFilePickerMultiple" : "click->images#openFilePicker"
  84. } }
  85. ) %>
  86. </div>
  87. <div class="hidden" data-images-target="withImageUploader">
  88. <%= render LooposUi::ActionMenu.new(
  89. options: [
  90. { text: I18n.t("image.upload"), icon: "arrows-rotate", attributes: {
  91. data: {
  92. action: "click->pubsub#publish",
  93. pubsub_unique_id_param: unique_id,
  94. pubsub_action_param: "openFilePicker"
  95. }
  96. }
  97. },
  98. then: 0 has_many_attached? ?
  99. { text: I18n.t("image.add"), icon: "plus", attributes: {
  100. data: {
  101. action: "click->pubsub#publish",
  102. pubsub_unique_id_param: unique_id,
  103. pubsub_action_param: "openFilePickerMultiple"
  104. }
  105. else: 0 }
  106. } : nil,
  107. then: 0 can_detach ?
  108. { text: I18n.t("image.remove"), icon: "trash",
  109. attributes: {
  110. data: {
  111. action: "click->pubsub#publish",
  112. pubsub_unique_id_param: unique_id,
  113. pubsub_action_param: "detachImage"
  114. }
  115. else: 0 }
  116. } : nil,
  117. ].compact
  118. ) do |menu| %>
  119. <% menu.with_trigger do %>
  120. <%= render LooposUi::Button.new(
  121. icon: "ellipsis-vertical",
  122. size: :tiny,
  123. then: 0 else: 0 type: size == :full ? :secondary : :tertiary,
  124. kind: :neutral,
  125. tag_options: { type: :button },
  126. ) %>
  127. <% end %>
  128. <% end %>
  129. </div>
  130. <%= file_field_tag single_file_field_name,
  131. accept: file_field_accept,
  132. class: "hidden",
  133. form: form_id,
  134. data: {
  135. direct_upload_url: effective_direct_upload_url,
  136. images_target: "file",
  137. action: "change->images#previewAndSubmit",
  138. } %>
  139. <%= file_field_tag multiple_file_field_name,
  140. accept: file_field_accept,
  141. multiple: true,
  142. class: "hidden",
  143. form: form_id,
  144. data: {
  145. direct_upload_url: effective_direct_upload_url,
  146. images_target: "fileMultiple",
  147. action: "change->images#previewAndSubmitMultiple",
  148. } %>
  149. <%= submit_tag I18n.t("image.save"), form: form_id, data: { images_target: "submit" }, class: "hidden" %>
  150. else: 0 <% else %>
  151. <%= form_with url: "#", html: { data: { images_target: "form" } } do |f| %>
  152. <%= hidden_field_tag :authenticity_token, form_authenticity_token %>
  153. <%= tag.input(**upload_context_input_attributes) %>
  154. <div class="hidden" data-images-target="noImageUploader">
  155. <%= render LooposUi::Button.new(
  156. icon: :upload,
  157. size: :tiny,
  158. then: 0 else: 0 type: size == :full ? :secondary : :tertiary,
  159. kind: :neutral,
  160. then: 0 else: 0 tooltip_text: has_many_attached? ? "Upload images here" : "Upload image here",
  161. tag_options: {
  162. data: {
  163. then: 0 else: 0 action: has_many_attached? ? "click->images#openFilePickerMultiple" : "click->images#openFilePicker"
  164. } }
  165. ) %>
  166. </div>
  167. <div class="hidden" data-images-target="withImageUploader">
  168. <%= render LooposUi::ActionMenu.new(
  169. options: [
  170. { text: I18n.t("image.upload"), icon: "arrows-rotate", attributes: {
  171. data: {
  172. action: "click->pubsub#publish",
  173. pubsub_unique_id_param: unique_id,
  174. pubsub_action_param: "openFilePicker"
  175. }
  176. }
  177. },
  178. then: 0 has_many_attached? ?
  179. { text: I18n.t("image.add"), icon: "plus", attributes: {
  180. data: {
  181. action: "click->pubsub#publish",
  182. pubsub_unique_id_param: unique_id,
  183. pubsub_action_param: "openFilePickerMultiple"
  184. }
  185. else: 0 }
  186. } : nil,
  187. then: 0 can_detach ?
  188. { text: I18n.t("image.remove"), icon: "trash",
  189. attributes: {
  190. data: {
  191. action: "click->pubsub#publish",
  192. pubsub_unique_id_param: unique_id,
  193. pubsub_action_param: "detachImage"
  194. }
  195. else: 0 }
  196. } : nil,
  197. ].compact
  198. ) do |menu| %>
  199. <% menu.with_trigger do %>
  200. <%= render LooposUi::Button.new(
  201. icon: "ellipsis-vertical",
  202. size: :tiny,
  203. then: 0 else: 0 type: size == :full ? :secondary : :tertiary,
  204. kind: :neutral,
  205. tag_options: { type: :button },
  206. ) %>
  207. <% end %>
  208. <% end %>
  209. </div>
  210. <%= f.file_field single_file_field_name,
  211. accept: file_field_accept,
  212. class: "hidden",
  213. data: {
  214. direct_upload_url: effective_direct_upload_url,
  215. images_target: "file",
  216. action: "change->images#previewAndSubmit",
  217. } %>
  218. <%= f.file_field multiple_file_field_name,
  219. accept: file_field_accept,
  220. multiple: true,
  221. class: "hidden",
  222. data: {
  223. direct_upload_url: effective_direct_upload_url,
  224. images_target: "fileMultiple",
  225. action: "change->images#previewAndSubmitMultiple",
  226. } %>
  227. <%= f.submit I18n.t("image.save"), data: { images_target: :submit }, class: "hidden" %>
  228. <% end %>
  229. <% end %>
  230. </div>
  231. <% end %>
  232. <% end %>
  233. <span class="lui-image__error" data-images-target="error">
  234. Error message
  235. </span>
  236. <%# <span class="lui-image__hint" data-upload-target="hint">Hint</span> %>
  237. <% end %>
  238. <% end %>
  239. <% 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-general-gray-100 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

51.28% lines covered

38.89% branches covered

39 relevant lines. 20 lines covered and 19 lines missed.
18 total branches, 7 branches covered and 11 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. 2 <% rows.each do |row| %>
  4. 6 <% capture do %>
  5. 6 <%= row %>
  6. <% end %>
  7. <% end %>
  8. <%# ENDHACK %>
  9. then: 0 else: 2 <% 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. 2 <% end if global_filters.present? %>
  25. then: 0 else: 2 <% 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. 2 <% end if hiddable_columns.present? %>
  39. then: 0 else: 2 <%= 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. 2 <% end if filter_bar_filters.present? %>
  59. 4 <%= content_tag(:div, id: unique_dom_id) do %>
  60. 4 <%= 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. 2 <%= react_component("Table",
  62. {
  63. uniqueId: unique_dom_id,
  64. id: id,
  65. lang: I18n.locale,
  66. columns: transformed_columns_with_actions,
  67. then: 2 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. 2 table.with_searchbar(
  72. name: "table_#{id}_search_input",
  73. then: 2 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. 2 render(LooposUi::Button.new(
  90. text: t(".view_more"),
  91. type: :tertiary,
  92. kind: :neutral,
  93. size: :default,
  94. ))
  95. },
  96. **options
  97. })
  98. 2 %>
  99. 2 then: 0 else: 2 <% 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. 2 then: 2 else: 0 <%= render(LooposUi::Tooltip.new(
  107. title: t(".expand_collapse_all"),
  108. tippy_target_id: "#{unique_dom_id}-collapse-tooltip",
  109. 6 position: :top)) if data_source.any? { |row| row.key?(:children) }
  110. 2 %>
  111. 4 <%= content_tag(:div, id: data_source_id, class: "hidden") do %>
  112. 2 <%= data_source.to_json %>
  113. <% end %>
  114. 4 <%= content_tag(:div, id: pagination_id, class: "hidden") do %>
  115. 2 <%= pagination.to_json %>
  116. <% end %>

app/components/loopos_ui/v2/table.rb

78.14% lines covered

39.51% branches covered

215 relevant lines. 168 lines covered and 47 lines missed.
81 total branches, 32 branches covered and 49 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. 6 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. 3 option :id, optional: true, type: Types::Coercible::String, default: -> { "" }
  16. 1 option :columns, type: Types::Array.of(Types::Hash), default: -> { [] }
  17. 3 option :data, default: -> { [] }
  18. 1 option :pagination, default: -> {
  19. {
  20. 2 then: 2 else: 0 current: 1, pageSize: DEFAULT_PAGE_SIZE, total: data&.count || 0,
  21. }
  22. }
  23. # TODO: move to pagination_config
  24. 3 option :show_pagination, default: -> { true }, type: Types::Bool
  25. 3 option :show_result_count, default: -> { true }, type: Types::Bool
  26. 3 option :show_pagination_size_changer, default: -> { true }, type: Types::Bool
  27. 3 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. 3 option :searchable, default: -> { true }
  32. 1 option :search_placeholder, optional: true
  33. 3 option :sortable, default: -> { false }, type: Types::Bool
  34. 1 option :global_filters, optional: true
  35. 3 option :tree_indent_size, default: -> { 15 }
  36. 1 option :fetch_url, optional: true
  37. 1 option :filters_url, optional: true
  38. 3 option :view_more, default: -> { 10 }, type: Types::Integer
  39. 3 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. 3 option :manageable_rows, default: -> { false }, type: Types::Bool
  50. 1 option :add_new_url, optional: true
  51. 3 option :filter_bar_filters, optional: true, type: Types::Hash, default: -> { {} }
  52. 3 option :filter_bar_search_options, optional: true, type: Types::Hash, default: -> { {} }
  53. 3 option :filter_bar_toggle_options, optional: true, type: Types::Hash, default: -> { {} }
  54. 3 option :active_filters, optional: true, type: Types::Hash, default: -> { {} }
  55. 1 attr_reader :columns_hash
  56. 1 def initialize(**data)
  57. then: 0 else: 2 data[:pagination] = {
  58. current: data[:pagy].page,
  59. pageSize: data[:pagy].items,
  60. total: data[:pagy].count,
  61. 2 } if data[:pagy].present?
  62. 2 then: 0 else: 2 if data[:manageable_rows] && !data[:add_new_url]
  63. raise ArgumentError, "add_new_url is required when manageable_rows is true"
  64. end
  65. 2 super
  66. end
  67. 1 def data_source
  68. 4 then: 2 @entries ||= if slotting_mode?
  69. 2 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. 4 @entries
  81. end
  82. # Additional options that will be passed in the Table.tsx
  83. 1 def options
  84. {
  85. 2 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. 4 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. 6 @transformed_columns ||= begin
  101. # Create a set of dataIndex values from hidden_columns for efficient lookup
  102. 2 hidden_data_indices = hidden_columns.map { |hc| (hc[:dataIndex] || hc[:key]).to_s }.to_set
  103. 2 visible_columns = []
  104. 2 columns.each do |column|
  105. 6 column_key = (column[:dataIndex] || column[:key]).to_s
  106. # Only add column if it's not in hidden_columns
  107. 6 else: 0 then: 6 unless hidden_data_indices.include?(column_key)
  108. 6 visible_columns << column
  109. end
  110. end
  111. 2 visible_columns.each_with_index do |column, i|
  112. 6 then: 0 else: 6 then: 0 else: 6 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. 6 column[:type] ||= :html
  116. 6 column[:className] = [
  117. column.dig(:className),
  118. "lui-table__cell",
  119. "lui-table__cell--n#{i}",
  120. else: 2 case i
  121. when: 2 when 0
  122. 2 "lui-table__cell--first"
  123. when: 2 when visible_columns.size - 1
  124. 2 "lui-table__cell--last"
  125. end,
  126. ].compact.join(" ")
  127. 6 then: 6 else: 0 column[:hidable] = true if column[:hidable].nil?
  128. 6 then: 0 else: 0 then: 0 else: 6 column.delete(:filters) if column.key?(:filters) && column.fetch(:filters)&.empty?
  129. 6 then: 0 else: 6 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. 6 else: 2 then: 4 next unless column[:sortable]
  155. 2 column[:sorter] = true
  156. 2 column.delete(:sortable)
  157. 2 column[:sortKey] = column[:sort_key] || column[:dataIndex]
  158. 2 column.delete(:sort_key)
  159. 2 then: 0 else: 2 then: 0 else: 0 column[:defaultSortOrder] = column[:default_sort] == :asc ? "ascend" : "descend" if column[:default_sort]
  160. 2 column.delete(:default_sort)
  161. end
  162. 2 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. 710 @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. 16 actions_count = rows.map { |row| row.actions.count }.max
  184. 4 then: 4 else: 0 then: 4 if actions_count&.positive?
  185. 4 transformed_columns + [action_column(actions_count)]
  186. else: 0 else
  187. transformed_columns
  188. end
  189. end
  190. 1 def unique_dom_id
  191. 138 @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. 6 rows.any? { |row| row.children.any? }
  204. end
  205. 1 def should_show_pagination?
  206. 2 show_pagination
  207. end
  208. 1 def action_zone_enabled?
  209. 2 selectable && action_bar?
  210. end
  211. 1 def data_source_id
  212. 4 "#{id}_data_source"
  213. end
  214. 1 def pagination_id
  215. 2 "#{id}_pagination"
  216. end
  217. 1 def slotting_mode?
  218. 2 rows.any?
  219. end
  220. 1 def data_from_slotting
  221. 2 @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. 2 "lui-table",
  243. 2 then: 0 else: 2 !show_header ? "lui-table--no-header" : nil,
  244. 2 then: 2 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. 2 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. 4 {
  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. 2 then: 0 else: 2 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. 27 option :manageable_rows, default: -> { false }, type: Types::Bool
  316. 33 option :row_data, optional: true, type: Types::Hash, default: -> { {} }
  317. 1 option :table, optional: true
  318. 7 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. 32 <% cells.each do |cell| %>
  327. 96 <%= cell %>
  328. <% end %>
  329. 32 <% actions.each do |action| %>
  330. 32 <%= action %>
  331. <% end %>
  332. 32 <% children.each do |child| %>
  333. 26 <%= child %>
  334. <% end %>
  335. 32 ERB
  336. 1 renders_many :cells, ->(*args, **kwargs, &block) {
  337. 96 LooposUi::V2::Table::Cell.new(*args, row: self, table: table, **kwargs, &block)
  338. }
  339. 1 renders_many :children, ->(*args, **kwargs, &block) {
  340. 26 LooposUi::V2::Table::Row.new(*args,**kwargs, table: table, level: level + 1, &block)
  341. }
  342. 1 renders_many :actions, ->(&block) {
  343. 32 LooposUi::V2::Table::Cell.new(property: "_lui_action_cell", row: self, table: table, &block)
  344. }
  345. 1 def initialize(**kwargs)
  346. 32 then: 0 else: 32 if kwargs[:manageable_rows] && !kwargs[:delete_action]
  347. raise ArgumentError, "row: delete_action is required when manageable_rows is true"
  348. end
  349. 32 super
  350. end
  351. # Inside LooposUi::V2::Table::Row
  352. 1 def data
  353. 32 @data ||= begin
  354. 32 then: 0 else: 32 if error_messages.present? && cells.any?
  355. cells.first.error_messages = error_messages
  356. end
  357. 32 datum = cells.to_h do |cell|
  358. [
  359. 96 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. 32 then: 32 else: 0 if actions.any? || manageable_rows
  365. 32 actions_html = ""
  366. # 1. Append custom actions if they exist
  367. 32 then: 32 else: 0 if actions.any?
  368. 32 actions_html += actions.map do |action|
  369. 32 <<-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. 32 then: 0 else: 32 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. tag_options: {
  386. data: {
  387. row_key: key,
  388. injected_delete_button: true,
  389. },
  390. },
  391. ))}
  392. </div>
  393. HTML
  394. end
  395. # 3. Assign the combined HTML to the actions cell
  396. 32 datum[:_lui_actions] = <<-HTML
  397. <div class="lui-table__actions">
  398. #{actions_html}
  399. </div>
  400. HTML
  401. end
  402. 32 then: 14 else: 18 if children.any?
  403. 14 datum[:children] = children.map(&:data)
  404. end
  405. 32 datum.merge!(non_cell_data)
  406. end
  407. end
  408. 1 def non_cell_data
  409. {
  410. 32 _lui_key: key,
  411. _lui_data: {
  412. delete_action: delete_action,
  413. **row_data,
  414. },
  415. }
  416. end
  417. # Have fun with this one
  418. 1 def tree_render(data,
  419. level = 0,
  420. get_children_fn:,
  421. key_fn:,
  422. render_row_fn:,
  423. parents: [])
  424. 16 then: 6 else: 10 render_row_fn.call(self, data, level) if level.zero?
  425. 16 (get_children_fn.call(data) || []).each do |child|
  426. 26 with_child(key: key_fn.call(child, [*parents, data])) do |child_row|
  427. 26 render_row_fn.call(child_row, child, level + 1)
  428. then: 10 else: 16 child_row.tree_render(
  429. child,
  430. level + 1,
  431. get_children_fn: get_children_fn,
  432. key_fn: key_fn,
  433. render_row_fn: render_row_fn,
  434. parents: [*parents, data],
  435. 26 ) if (get_children_fn.call(child) || []).any?
  436. end
  437. end
  438. end
  439. end
  440. end
  441. end
  442. end

app/components/loopos_ui/v2/table/cell.html.erb

33.33% lines covered

37.5% branches covered

15 relevant lines. 5 lines covered and 10 lines missed.
8 total branches, 3 branches covered and 5 branches missed.
    
  1. 256 <span class="<%= classes %>" style="<%= style %>" id="<%= unique_id %>" data-table-target="cell">
  2. 256 then: 0 else: 256 then: 0 else: 256 <% 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. 256 <div class="lui-table__cell-content_slot">
  22. 256 <%= content_or_dash %>
  23. </div>
  24. <div class="flex flex-row items-center justify-center">
  25. 256 then: 0 else: 256 <%= actions if actions.present? %>
  26. </div>
  27. </span>

app/components/loopos_ui/v2/table/cell.rb

94.44% lines covered

68.18% branches covered

36 relevant lines. 34 lines covered and 2 lines missed.
22 total branches, 15 branches covered and 7 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. 256 "lui-table__cell-content",
  14. 256 then: 0 else: 256 error_messages.present? ? "lui-table__cell-content-error" : nil,
  15. ].compact.join(" ")
  16. end
  17. 1 def content_or_dash
  18. 256 then: 0 else: 256 return content if action_cell?
  19. 256 then: 0 else: 256 return "-" if content.blank? || effectively_empty?
  20. 256 try_typed_columns || content
  21. end
  22. 1 def try_typed_columns
  23. 256 then: 64 else: 192 return if column.blank?
  24. 192 else: 192 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. 256 fragment = Nokogiri::HTML::DocumentFragment.parse(content.to_s)
  31. 256 fragment.children.all? do |c|
  32. 256 text_blank = c.inner_text.strip.empty?
  33. 256 has_visible_elements = c.at_css("i, img, iframe, video, object, embed, [data-react-class], [src], [href]")
  34. 256 text_blank && has_visible_elements.nil?
  35. end
  36. end
  37. 1 def unique_id
  38. 256 then: 128 else: 0 @unique_id ||= "#{table&.unique_dom_id}_cell_#{rand(10**10)}"
  39. end
  40. 1 def column
  41. 704 then: 704 else: 0 then: 704 else: 0 table&.columns_hash&.dig(property)
  42. end
  43. 1 def action_cell?
  44. 256 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. 256 then: 192 else: 64 then: 192 else: 64 then: 192 else: 64 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

88.89% lines covered

50.0% branches covered

9 relevant lines. 8 lines covered and 1 lines missed.
4 total branches, 2 branches covered and 2 branches missed.
    
  1. 2 <div class="lui-table_header" data-controller="table-filters">
  2. 2 then: 2 else: 0 <% if searchbar.present? && searchable %>
  3. 2 <div class="w-80">
  4. 2 <%= searchbar %>
  5. </div>
  6. <% end %>
  7. then: 0 else: 2 <% filters.each do |filter| %>
  8. <%= filter %>
  9. 2 <% end if show_filters %>
  10. 2 <div class="hidden" data-table-filters-target="button">
  11. 2 <%= 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. 2 ) %>
  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. 3 then: 0 else: 0 delegate :t, to: LooposUi::V2::Table
  10. end
  11. end
  12. end
  13. end

app/components/loopos_ui/wysiwyg.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 Wysiwyg < LoopComponent
  3. 1 option :initial_value, optional: true
  4. 13 option :input_name, default: -> { "" }
  5. 9 option :small, default: -> { false }
  6. 11 option :language, default: -> { "en" }
  7. 1 option :app, optional: true
  8. 1 mod :small
  9. 13 mod :neutral_small, condition: -> { app_kind.to_s == "neutral" && small? }
  10. 1 private
  11. 1 def small?
  12. 16 small || false
  13. end
  14. 1 def app_kind
  15. 24 app || LooposUi.config.try(:app_type) || "core"
  16. end
  17. end
  18. end

app/components/loopos_ui/wysiwyg/wysiwyg.html.erb

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. 12 <% dropkiq_preview_id = "wysiwyg-#{input_name}" %>
  2. 24 <%= tag.div(class: classes) do %>
  3. 12 <%= 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. 12 ) %>
  14. <% end %>
  15. 12 <!-- div id="<%= dropkiq_preview_id %>" class='dropkiq-preview'></!-->