labimotion 2.1.0.rc3 → 2.1.0.rc5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1d084fef830351ae2182073c7c42a50abd49bc5bc3f20d256860da683ca6c5c8
4
- data.tar.gz: 24b58889fdc9daeb36267aef35e736c81b56c3fc73dec3a1af06cd1148708875
3
+ metadata.gz: e23b274f29ceb300bbb97f91830dbe2fdc454966d229e88608cd434b6586a3b3
4
+ data.tar.gz: da620e95cb04ce10a69cba25d8cb63f22cb1e21142a1a6a3c93d9fecc33840aa
5
5
  SHA512:
6
- metadata.gz: 1bb720347508e464f7f4a0786a7daaeeb0a9fa185efab31543adc28cb2addf0e57877e36128147755772097d46f8103a45275cc00134c1cff4af3f5b774f0e31
7
- data.tar.gz: 0b9e694ebe9934be89a32147005c314614c3e26ad8f0f1fd79bd7c75207c936fbdc9bd0a594282a0a8c0434d4a4e9e73cf1f4b3d1418a807fc8cfb15c5fecda8
6
+ metadata.gz: db1827e8ad8c56f05c55215f3cd5e3830174f937875c2af49e7899dc4875180364111b1788eecc430b6f3ca8b39e3e0234eb21a3c1036c3b7ab064676f8bb4d1
7
+ data.tar.gz: 3f1667aa18030745ab178330cac70f234d4aa3eab46e3622546aeb4268aaf379e9d8dd0369060324327dd3db67efa2da6e94b6568feb33a88e987bec8030a737
@@ -0,0 +1,269 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labimotion
4
+ # ExporterAPI handles exporting data to various formats (e.g., XLSX)
5
+ # The class length is justified due to the comprehensive helper methods needed for export functionality
6
+ class ExporterAPI < Grape::API
7
+ include Grape::Kaminari
8
+
9
+ helpers Labimotion::ParamHelpers
10
+ helpers Labimotion::ExporterHelpers
11
+
12
+ resource :exporter do
13
+ resource :table_xlsx do
14
+ desc 'Export single table field to XLSX format'
15
+ params do
16
+ use :table_xlsx_params
17
+ end
18
+ route_param :id do
19
+ before do
20
+ validate_and_authorize_request!
21
+ end
22
+
23
+ get do
24
+ layer = fetch_layer
25
+ field = fetch_field(layer)
26
+ validate_table_field!(field)
27
+
28
+ headers, expanded_sub_fields = build_table_headers(field)
29
+ rows = build_table_rows(field, expanded_sub_fields)
30
+
31
+ export_to_xlsx(layer, field, headers, rows)
32
+ rescue StandardError => e
33
+ Labimotion.log_exception(e, current_user)
34
+ error!("500 Internal Server Error: #{e.message}", 500)
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ # rubocop:disable Metrics/BlockLength
41
+ # The helpers block is necessarily large due to the many specialized helper methods
42
+ # for data fetching, validation, formatting, and export functionality
43
+ helpers do
44
+ # Validate and authorize the request, sets @element instance variable
45
+ def validate_and_authorize_request!
46
+ klass = fetch_klass
47
+ @element = klass.find_by(id: params[:id])
48
+
49
+ error!('404 Not Found', 404) if @element.nil?
50
+
51
+ authorize_element_access!
52
+ rescue ActiveRecord::RecordNotFound
53
+ error!('404 Not Found', 404)
54
+ rescue NameError => e
55
+ Labimotion.log_exception(e, current_user)
56
+ error!('400 Bad Request - Invalid type', 400)
57
+ end
58
+
59
+ # Fetch the element or segment class based on params
60
+ def fetch_klass
61
+ Labimotion::Utils.resolve_class(params[:klass])
62
+ end
63
+
64
+ # Authorize element access based on user permissions
65
+ def authorize_element_access!
66
+ element_policy = if params[:klass] == 'Element'
67
+ ElementPolicy.new(current_user,
68
+ @element)
69
+ else
70
+ ElementPolicy.new(current_user,
71
+ @element.element)
72
+ end
73
+ matrix_name = params[:klass] == 'Element' ? 'genericElement' : 'segment'
74
+
75
+ return if current_user.matrix_check_by_name(matrix_name) && element_policy.read?
76
+
77
+ error!('401 Unauthorized', 401)
78
+ end
79
+
80
+ # Fetch layer from element properties
81
+ def fetch_layer
82
+ properties = @element.properties
83
+ layers = properties[Labimotion::Prop::LAYERS] || {}
84
+ layer_key = params[:layer_id].to_s
85
+ layer = layers[layer_key]
86
+
87
+ error!('404 Layer Not Found', 404) if layer.nil?
88
+ layer
89
+ end
90
+
91
+ # Fetch field from layer
92
+ def fetch_field(layer)
93
+ fields = layer[Labimotion::Prop::FIELDS] || []
94
+ field = fields.find { |f| f['field'] == params[:field_id].to_s }
95
+
96
+ error!('404 Field Not Found', 404) if field.nil?
97
+ field
98
+ end
99
+
100
+ # Validate that the field is a table type
101
+ def validate_table_field!(field)
102
+ return if field['type'] == Labimotion::FieldType::TABLE
103
+
104
+ error!('400 Bad Request - Field is not a table type', 400)
105
+ end
106
+
107
+ # Build table headers from field definition, expands DRAG_SAMPLE and DRAG_MOLECULE columns
108
+ def build_table_headers(field)
109
+ sub_fields = field.fetch('sub_fields', [])
110
+ headers = []
111
+ expanded_sub_fields = []
112
+
113
+ sub_fields.each do |sf|
114
+ base_col_name = normalize_column_name(sf['col_name'])
115
+
116
+ if expandable_field?(sf['type'])
117
+ headers, expanded_sub_fields = expand_field_columns(sf, base_col_name, headers, expanded_sub_fields)
118
+ else
119
+ headers << base_col_name
120
+ expanded_sub_fields << sf
121
+ end
122
+ end
123
+
124
+ [headers, expanded_sub_fields]
125
+ end
126
+
127
+ # Expand field columns for DRAG_SAMPLE and DRAG_MOLECULE with sub-headers
128
+ def expand_field_columns(sub_field, base_col_name, headers, expanded_sub_fields)
129
+ sub_headers = sub_field['value'].to_s.split(';').reject(&:empty?)
130
+
131
+ # Always add the base column first (for SVG image link)
132
+ headers << base_col_name
133
+ expanded_sub_fields << sub_field
134
+
135
+ # For DRAG_SAMPLE, add a second base column for short_label
136
+ if sub_field['type'] == Labimotion::FieldType::DRAG_SAMPLE
137
+ headers << 'short_label'
138
+ expanded_sub_fields << {
139
+ 'id' => sub_field['id'],
140
+ 'type' => sub_field['type'],
141
+ 'sub_header' => 'short_label',
142
+ 'original' => sub_field,
143
+ 'is_base_column' => true
144
+ }
145
+ end
146
+
147
+ # Add extra columns for each sub-header
148
+ sub_headers.each do |sub_header|
149
+ headers << sub_header
150
+ expanded_sub_fields << {
151
+ 'id' => sub_field['id'],
152
+ 'type' => sub_field['type'],
153
+ 'sub_header' => sub_header,
154
+ 'original' => sub_field
155
+ }
156
+ end
157
+
158
+ [headers, expanded_sub_fields]
159
+ end
160
+
161
+ # Build table rows from field data
162
+ def build_table_rows(field, expanded_sub_fields)
163
+ sub_values = field.fetch('sub_values', [])
164
+
165
+ sub_values.map do |sub_val|
166
+ expanded_sub_fields.map do |exp_field|
167
+ if exp_field.is_a?(Hash) && exp_field['sub_header']
168
+ format_expanded_cell(sub_val, exp_field)
169
+ else
170
+ format_table_cell(sub_val, exp_field)
171
+ end
172
+ end
173
+ end
174
+ end
175
+
176
+ # Export data to XLSX format
177
+ def export_to_xlsx(layer, field, headers, rows)
178
+ element_name = @element.is_a?(Labimotion::Segment) ? @element.element.name : @element.name
179
+ segment_name = @element.is_a?(Labimotion::Segment) ? @element.segment_klass.label : ''
180
+ selected_layer = "#{layer['label'] || ExporterHelpers::CONST_UNNAMED} (#{layer['key']})"
181
+ selected_field = field['label'] || ExporterHelpers::CONST_UNNAMED
182
+
183
+ filename = generate_filename(element_name, segment_name, selected_layer, selected_field)
184
+ exporter = Labimotion::XlsxExporter.new(filename)
185
+
186
+ worksheet_params = {
187
+ exporter: exporter,
188
+ element_name: element_name,
189
+ layer_name: selected_layer,
190
+ field_name: selected_field,
191
+ headers: headers,
192
+ rows: rows
193
+ }
194
+ worksheet_params[:segment_name] = segment_name unless segment_name.empty?
195
+ populate_worksheet(worksheet_params)
196
+
197
+ # Set response headers and format
198
+ configure_xlsx_response(exporter)
199
+
200
+ exporter.read
201
+ end
202
+
203
+ # Build metadata array for worksheet
204
+ def build_metadata(params)
205
+ metadata = [['Element:', params[:element_name]]]
206
+ metadata << ['Segment:', params[:segment_name]] if params[:segment_name].present?
207
+ metadata.push(
208
+ ['Layer:', params[:layer_name]],
209
+ ['Field:', params[:field_name]],
210
+ ['Exported at:', Time.zone.now.strftime('%Y-%m-%d %H:%M:%S')],
211
+ ['Exported by:', current_user.name]
212
+ )
213
+ metadata
214
+ end
215
+
216
+ # Populate worksheet with data
217
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
218
+ def populate_worksheet(params)
219
+ # Store params in local variables for use in the instance_eval block
220
+ table_headers = params[:headers]
221
+ table_rows = params[:rows]
222
+ metadata = build_metadata(params)
223
+
224
+ params[:exporter].add_worksheet('Table Data') do |sheet|
225
+ # Add metadata section
226
+ sheet.add_section('Table Information', metadata)
227
+
228
+ # Add table data with headers
229
+ header_row_index = sheet.add_header(table_headers)
230
+
231
+ # Add data rows with hyperlink support
232
+ table_rows.each do |row_data|
233
+ # Process row data to handle hyperlinks
234
+ processed_row = row_data.map do |cell_value|
235
+ cell_value.is_a?(Hash) && cell_value[:hyperlink] ? cell_value[:text] : cell_value
236
+ end
237
+
238
+ # Add the row
239
+ row_index = sheet.add_row(processed_row)
240
+
241
+ # Add hyperlinks to cells that need them
242
+ row_data.each_with_index do |cell_value, col_index|
243
+ next unless cell_value.is_a?(Hash) && cell_value[:hyperlink]
244
+
245
+ sheet.add_hyperlink(row_index, col_index, cell_value[:hyperlink])
246
+ end
247
+ end
248
+
249
+ # Auto-fit columns and freeze panes
250
+ sheet.auto_fit_columns
251
+ sheet.freeze_panes(header_row_index + 1, 0) if table_headers.any?
252
+ end
253
+ end
254
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
255
+
256
+ # Set XLSX response headers with proper encoding
257
+ def configure_xlsx_response(exporter)
258
+ env['api.format'] = :binary
259
+ content_type 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
260
+
261
+ # Use both filename and filename* for better browser compatibility
262
+ filename = exporter.filename
263
+ encoded_filename = URI.encode_www_form_component(filename)
264
+ header('Content-Disposition', "attachment; filename=\"#{filename}\"; filename*=UTF-8''#{encoded_filename}")
265
+ end
266
+ end
267
+ # rubocop:enable Metrics/BlockLength
268
+ end
269
+ end
@@ -4,6 +4,7 @@ module Labimotion
4
4
  ## Generic Dataset API
5
5
  class GenericDatasetAPI < Grape::API
6
6
  include Grape::Kaminari
7
+
7
8
  helpers Labimotion::GenericHelpers
8
9
  helpers Labimotion::DatasetHelpers
9
10
 
@@ -24,7 +25,8 @@ module Labimotion
24
25
  end
25
26
  get do
26
27
  list = klass_list(params[:is_active], params[:displayed_in_list])
27
- serialized_data = Labimotion::DatasetKlassEntity.represent(list, displayed_in_list: params[:displayed_in_list])
28
+ serialized_data = Labimotion::DatasetKlassEntity.represent(list,
29
+ displayed_in_list: params[:displayed_in_list])
28
30
  { mc: 'ss00', data: serialized_data }
29
31
  rescue StandardError => e
30
32
  Labimotion.log_exception(e, current_user)
@@ -32,7 +34,8 @@ module Labimotion
32
34
  end
33
35
  end
34
36
 
35
- # Deprecated: This namespace is no longer used, but kept for backward compatibility. It is replaced by `list_klass`.
37
+ # Deprecated: This namespace is no longer used, but kept for backward compatibility.
38
+ # It is replaced by `list_klass`.
36
39
  namespace :list_dataset_klass do
37
40
  desc 'list Generic Dataset Klass'
38
41
  params do
@@ -65,6 +68,20 @@ module Labimotion
65
68
  { error: e.message }
66
69
  end
67
70
  end
71
+
72
+ namespace :find_template do
73
+ desc 'Find best matching template for given OLS term ID'
74
+ params do
75
+ requires :ols_term_id, type: String, desc: 'OLS Term ID (e.g., CHMO:0000470)'
76
+ end
77
+ get do
78
+ result = find_best_match_template(params[:ols_term_id])
79
+ result
80
+ rescue StandardError => e
81
+ Labimotion.log_exception(e, current_user)
82
+ raise e
83
+ end
84
+ end
68
85
  end
69
86
  end
70
87
  end
@@ -59,6 +59,65 @@ module Labimotion
59
59
  end
60
60
  end
61
61
 
62
+ namespace :search_by_like do
63
+ desc 'Search elements by name (case-insensitive like search)'
64
+ params do
65
+ requires :name, type: String, desc: 'Search query for element name'
66
+ requires :short_label, type: String, desc: 'Search query for element short label'
67
+ requires :klass_id, type: Integer, desc: 'Filter by element klass id'
68
+ optional :limit, type: Integer, desc: 'Maximum number of results', default: 20
69
+ end
70
+ get do
71
+ scope = Labimotion::Element.fetch_for_user(
72
+ current_user.id,
73
+ name: params[:name],
74
+ short_label: params[:short_label],
75
+ klass_id: params[:klass_id],
76
+ limit: params[:limit]
77
+ )
78
+
79
+ results = scope.map do |element|
80
+ Labimotion::ElementLookupEntity.represent(element)
81
+ end
82
+
83
+ { elements: results, total_count: results.count }
84
+ rescue StandardError => e
85
+ Labimotion.log_exception(e, current_user)
86
+ { elements: [], total_count: 0, error: e.message }
87
+ end
88
+ end
89
+
90
+ namespace :search_basic_by_like do
91
+ desc 'Search basic elements by name and short label (case-insensitive like search)'
92
+ params do
93
+ requires :klass_name, type: String, desc: 'Class name (device_description or wellplate or ...etc.)', default: 'device_description'
94
+ requires :name, type: String, desc: 'Search query for basic element name'
95
+ requires :short_label, type: String, desc: 'Search query for basic element short label'
96
+ optional :limit, type: Integer, desc: 'Maximum number of results', default: 20
97
+ end
98
+ get do
99
+ # Convert snake_case to PascalCase (e.g. device_description -> DeviceDescription)
100
+ klass_name = params[:klass_name].camelize
101
+ klass = "Labimotion::#{klass_name}".constantize
102
+
103
+ scope = klass.fetch_for_user(
104
+ current_user.id,
105
+ name: params[:name],
106
+ short_label: params[:short_label],
107
+ limit: params[:limit]
108
+ )
109
+
110
+ results = scope.map do |record|
111
+ Labimotion::ElementLookupEntity.represent(record)
112
+ end
113
+
114
+ { elements: results, total_count: results.count }
115
+ rescue StandardError => e
116
+ Labimotion.log_exception(e, current_user)
117
+ { elements: [], total_count: 0, error: e.message }
118
+ end
119
+ end
120
+
62
121
  namespace :export do
63
122
  desc 'export element'
64
123
  params do
@@ -225,12 +284,13 @@ module Labimotion
225
284
  desc 'list Generic Element Klass'
226
285
  params do
227
286
  optional :is_generic, type: Boolean, desc: 'Is Generic or Non-Generic Element'
228
- optional :is_active, type: Boolean, desc: 'Active or Inactive Dataset'
287
+ optional :is_active, type: Boolean, desc: 'Active or Inactive'
229
288
  optional :displayed_in_list, type: Boolean, desc: 'Display in list format', default: true
230
289
  end
231
290
  get do
232
291
  scope = params[:displayed_in_list] ? Labimotion::ElementKlass.for_list_display : Labimotion::ElementKlass.all
233
292
  scope = scope.where(is_generic: params[:is_generic]) if params.key?(:is_generic)
293
+ scope = scope.where(is_active: params[:is_active]) if params.key?(:is_active)
234
294
 
235
295
  list = scope.sort_by(&:place)
236
296
  present list, with: Labimotion::ElementKlassEntity, root: 'klass', displayed_in_list: params[:displayed_in_list]
@@ -465,4 +525,25 @@ module Labimotion
465
525
  end
466
526
  end
467
527
  end
528
+
529
+ # Entity for element lookup by name response
530
+ class ElementLookupEntity < Grape::Entity
531
+ expose :id
532
+ expose :name
533
+ expose :short_label do |element|
534
+ element.respond_to?(:short_label) ? element.short_label : nil
535
+ end
536
+ expose :element_klass_id, as: :element_klass_id do |element|
537
+ element.element_klass&.id
538
+ end
539
+ expose :klass_label, as: :klass_label do |element|
540
+ element.element_klass&.label
541
+ end
542
+ expose :klass_name, as: :klass_name do |element|
543
+ element.element_klass&.name
544
+ end
545
+ expose :klass_icon, as: :klass_icon do |element|
546
+ element.element_klass&.icon_name
547
+ end
548
+ end
468
549
  end
@@ -2,6 +2,7 @@
2
2
  module Labimotion
3
3
  class LabimotionAPI < Grape::API
4
4
  mount Labimotion::ConverterAPI
5
+ mount Labimotion::ExporterAPI
5
6
  mount Labimotion::GenericKlassAPI
6
7
  mount Labimotion::GenericElementAPI
7
8
  mount Labimotion::GenericDatasetAPI
@@ -33,18 +33,41 @@ module Labimotion
33
33
  ds = Labimotion::DatasetKlass.find_by(ols_term_id: attributes['ols_term_id'])
34
34
  ds.update!(attributes)
35
35
  ds.create_klasses_revision(current_user)
36
- { status: 'success', message: "This dataset: [#{attributes['label']}] has been upgraded to the version: #{attributes['version']}!" }
36
+ { status: 'success',
37
+ message: "This dataset: [#{attributes['label']}] has been upgraded to the version: #{attributes['version']}!" }
37
38
  end
38
39
  else
39
40
  attributes['created_by'] = current_user.id
40
41
  ds = Labimotion::DatasetKlass.create!(attributes)
41
42
  ds.create_klasses_revision(current_user)
42
- { status: 'success', message: "The dataset: #{attributes['label']} has been created using version: #{attributes['version']}!" }
43
+ { status: 'success',
44
+ message: "The dataset: #{attributes['label']} has been created using version: #{attributes['version']}!" }
43
45
  end
44
46
  rescue StandardError => e
45
47
  Labimotion.log_exception(e, current_user)
46
48
  # { error: e.message }
47
49
  raise e
48
50
  end
51
+
52
+ def find_best_match_template(ols_term_id)
53
+ result = Labimotion::TemplateMatcher.find_best_match(ols_term_id)
54
+ if result[:template]
55
+ build_template_response(result[:template], result[:match_type], result[:info_messages])
56
+ else
57
+ { error: '', data: {}, info: "No matching template found for term id [#{ols_term_id}]" }
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def build_template_response(template, match_type, info_messages)
64
+ response = {
65
+ error: '',
66
+ data: Labimotion::DatasetKlassEntity.represent(template, displayed_in_list: false),
67
+ match_type: match_type
68
+ }
69
+ response[:info] = info_messages.join('; ') if info_messages.any?
70
+ response
71
+ end
49
72
  end
50
73
  end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labimotion
4
+ # ExporterHelpers provides utility methods for formatting and processing export data
5
+ module ExporterHelpers
6
+ # Constants
7
+ CONST_UNNAMED = '(Unnamed)'
8
+
9
+ # Normalize column name, returns CONST_UNNAMED for blank values
10
+ def normalize_column_name(col_name)
11
+ return CONST_UNNAMED if col_name.nil? || col_name.to_s.strip.empty?
12
+
13
+ col_name
14
+ end
15
+
16
+ # Check if field type should be expanded (DRAG_SAMPLE or DRAG_MOLECULE)
17
+ def expandable_field?(field_type)
18
+ [Labimotion::FieldType::DRAG_SAMPLE, Labimotion::FieldType::DRAG_MOLECULE].include?(field_type)
19
+ end
20
+
21
+ # Generate sanitized filename for export
22
+ def generate_filename(element_name, segment_name, layer_name, field_name)
23
+ timestamp = Time.zone.now.strftime('%Y%m%d_%H%M%S')
24
+ parts = [element_name]
25
+ parts << segment_name unless segment_name.empty?
26
+ parts.push(layer_name, field_name, timestamp)
27
+ parts.join('_').gsub(/\s+/, '_').gsub(/[()]/, '(' => '[', ')' => ']')
28
+ end
29
+
30
+ # Format table cell value based on field type
31
+ def format_table_cell(sub_val, sub_field)
32
+ return '' if sub_field.fetch('id', nil).nil? || sub_val[sub_field['id']].nil?
33
+
34
+ case sub_field['type']
35
+ when Labimotion::FieldType::DRAG_SAMPLE, Labimotion::FieldType::DRAG_MOLECULE
36
+ format_drag_field_cell(sub_val, sub_field)
37
+ when Labimotion::FieldType::SELECT
38
+ format_select_cell(sub_val, sub_field)
39
+ when Labimotion::FieldType::SYSTEM_DEFINED
40
+ format_system_defined_cell(sub_val, sub_field)
41
+ else
42
+ format_default_cell(sub_val, sub_field)
43
+ end
44
+ rescue StandardError => e
45
+ Labimotion.log_exception(e)
46
+ ''
47
+ end
48
+
49
+ # Format expanded cell for DRAG_SAMPLE or DRAG_MOLECULE with sub-headers
50
+ def format_expanded_cell(sub_val, exp_field)
51
+ field_id = exp_field['id']
52
+ sub_header = exp_field['sub_header']
53
+
54
+ return '' if field_id.nil? || sub_val[field_id].nil?
55
+
56
+ val = sub_val[field_id]['value'] || {}
57
+ return '' if val.blank? || !val.is_a?(Hash)
58
+
59
+ property_key = map_sub_header_to_property(sub_header)
60
+ val[property_key].to_s
61
+ rescue StandardError => e
62
+ Labimotion.log_exception(e)
63
+ ''
64
+ end
65
+
66
+ # Map sub-header to property key
67
+ def map_sub_header_to_property(sub_header)
68
+ property_map = {
69
+ 'name' => 'el_name',
70
+ 'label' => 'el_label',
71
+ 'short_label' => 'el_short_label',
72
+ 'external_label' => 'el_external_label',
73
+ 'molecular_weight' => 'el_molecular_weight',
74
+ 'smiles' => 'el_smiles',
75
+ 'inchikey' => 'el_inchikey',
76
+ 'iupac' => 'el_iupac',
77
+ 'sum_formula' => 'el_sum_formula',
78
+ 'decoupled' => 'el_decoupled'
79
+ }
80
+
81
+ property_map[sub_header] || "el_#{sub_header}"
82
+ end
83
+
84
+ # Format drag field cell (DRAG_SAMPLE or DRAG_MOLECULE), returns hyperlink or label
85
+ def format_drag_field_cell(sub_val, sub_field)
86
+ val = sub_val[sub_field['id']]['value'] || {}
87
+ return '' if val.blank?
88
+
89
+ svg_url = val['el_svg'].to_s
90
+ if svg_url.present?
91
+ full_url = URI.join(Rails.application.config.root_url, svg_url).to_s
92
+ { hyperlink: full_url, text: '[image link]' }
93
+ else
94
+ val['el_label'].to_s
95
+ end
96
+ end
97
+
98
+ # Format select cell
99
+ def format_select_cell(sub_val, sub_field)
100
+ sub_val[sub_field['id']]['value'].to_s
101
+ end
102
+
103
+ # Format system-defined cell with unit
104
+ def format_system_defined_cell(sub_val, sub_field)
105
+ value = sub_val[sub_field['id']]['value'].to_s
106
+ unit = find_unit_label(sub_val, sub_field)
107
+
108
+ unit.present? ? "#{value} #{unit}" : value
109
+ end
110
+
111
+ # Find unit label for system-defined field
112
+ def find_unit_label(sub_val, sub_field)
113
+ value_system = extract_value_system(sub_val, sub_field)
114
+ find_unit_by_key(sub_field['option_layers'], value_system)
115
+ end
116
+
117
+ # Extract value_system from sub_val or sub_field
118
+ def extract_value_system(sub_val, sub_field)
119
+ field_data = sub_val[sub_field['id']]
120
+ field_data.is_a?(Hash) ? field_data['value_system'] : sub_field['value_system']
121
+ end
122
+
123
+ # Find unit label by field and key
124
+ def find_unit_by_key(option_layers, value_system)
125
+ field_config = Labimotion::Units::FIELDS.find { |o| o[:field] == option_layers }
126
+ return '' unless field_config
127
+
128
+ units = field_config.fetch(:units, [])
129
+ unit = units.find { |u| u[:key] == value_system }
130
+ unit ? unit.fetch(:label, '') : ''
131
+ end
132
+
133
+ # Format default cell
134
+ def format_default_cell(sub_val, sub_field)
135
+ cell_data = sub_val[sub_field['id']]
136
+ cell_data.is_a?(Hash) ? cell_data['value'].to_s : cell_data.to_s
137
+ end
138
+ end
139
+ end
@@ -134,5 +134,11 @@ module Labimotion
134
134
  optional :select_options, type: Hash, desc: 'selections'
135
135
  optional :option_layers, type: String, desc: 'option'
136
136
  end
137
+
138
+ params :table_xlsx_params do
139
+ requires :klass, type: String, desc: 'Generic Type', values: %w[Element Segment]
140
+ requires :layer_id, type: String, desc: 'layer identifier'
141
+ requires :field_id, type: String, desc: 'field identifier'
142
+ end
137
143
  end
138
144
  end
@@ -29,7 +29,8 @@ module Labimotion
29
29
  end
30
30
 
31
31
  def self.find_dataset_klass(ols_term_id)
32
- Labimotion::DatasetKlass.find_by(ols_term_id: ols_term_id)
32
+ result = Labimotion::TemplateMatcher.find_best_match(ols_term_id)
33
+ result[:template]
33
34
  end
34
35
 
35
36
  def self.create_dataset(container, klass)
@@ -113,11 +113,12 @@ module Labimotion
113
113
  Labimotion::ElementsElement.find_or_create_by(parent_id: element.id, element_id: el.id)
114
114
  els << el.id
115
115
  end
116
-
117
116
  end
118
117
  if element.present?
119
- es_list = Labimotion::ElementsSample.where(element_id: element.id).where.not(sample_id: sds)
120
- ee_list = Labimotion::ElementsElement.where(parent_id: element.id).where.not(element_id: els&.flatten)
118
+ sds = sds.flatten.uniq
119
+ els = els.flatten.uniq
120
+ es_list = sds.present? ? Labimotion::ElementsSample.where(element_id: element.id).where.not(sample_id: sds) : Labimotion::ElementsSample.where(element_id: element.id)
121
+ ee_list = els.present? ? Labimotion::ElementsElement.where(parent_id: element.id).where.not(element_id: els) : Labimotion::ElementsElement.where(parent_id: element.id)
121
122
  es_list.destroy_all if es_list.present?
122
123
  ee_list.destroy_all if ee_list.present?
123
124
  end