@@ -47,6 +47,15 @@ def collect_custom_field_attributes(custom_field_registry, jira_issue)
4747 end
4848 end
4949
50+ def custom_fields_for_issue ( custom_field_registry , jira_issue )
51+ custom_field_registry . filter_map do |entry |
52+ raw_value = jira_issue . payload [ "fields" ] [ entry [ :jira_field ] . jira_field_id ]
53+ next if raw_value . blank?
54+
55+ find_context_for_issue ( entry , jira_issue ) &.dig ( :custom_field )
56+ end . uniq
57+ end
58+
5059 # Builds one OP custom field per (Jira field, context group) combination, before any
5160 # per-project import begins. Context groups describe which (project_key, issuetype_id)
5261 # tuples share an allowedValues set.
@@ -98,59 +107,30 @@ def string_array_field?(jira_field)
98107 schema [ "type" ] == "array" && schema [ "items" ] == "string"
99108 end
100109
101- def build_string_array_registry_entries ( jira_field )
102- string_values = collect_string_values_from_issues ( jira_field )
103- allowed_values = string_values . map { |v | { "value" => v } }
104- groups = jira_field . payload [ "contextGroups" ]
105-
106- contexts = if groups . present?
107- groups . map { |g | build_context_entry ( jira_field , g . merge ( "allowedValues" => allowed_values ) ) }
108- else
109- [
110- build_context_entry (
111- jira_field ,
112- {
113- "projects" => [ ] ,
114- "issuetypes" => [ ] ,
115- "allowedValues" => allowed_values
116- }
117- )
118- ]
119- end
120- [ { jira_field :, contexts : } ]
110+ def option_field? ( jira_field )
111+ schema = jira_field . payload [ "schema" ] || { }
112+ %w[ option option-with-child ] . include? ( schema [ "type" ] ) ||
113+ ( schema [ "type" ] == "array" && schema [ "items" ] == "option" )
121114 end
122115
123- def collect_string_values_from_issues ( jira_field )
124- field_key = jira_field . jira_field_id
125- values = Set . new
126- Import ::JiraIssue . where ( jira_id : @jira_id , jira_project_id : all_jira_import_project_ids ) . find_each do |issue |
127- raw = issue . payload [ "fields" ] [ field_key ]
128- next unless raw . is_a? ( Array )
129-
130- raw . each { |v | values << v . to_s if v . present? }
116+ def build_string_array_registry_entries ( jira_field )
117+ allowed_values = Array ( jira_field . payload [ "contextGroups" ] ) . flat_map { |group | Array ( group [ "allowedValues" ] ) }
118+ allowed_values = issue_string_values ( jira_field ) . reduce ( allowed_values ) do |values , string_value |
119+ merge_allowed_value ( values , { "value" => string_value } )
131120 end
132- values . to_a . sort
121+ contexts = [
122+ build_context_entry ( jira_field , { "projects" => [ ] , "issuetypes" => [ ] , "allowedValues" => allowed_values } )
123+ ]
124+ [ { jira_field :, contexts : } ]
133125 end
134126
135127 def build_multicheckbox_registry_entries ( jira_field )
136- groups = jira_field . payload [ "contextGroups" ]
137-
138- return build_multicheckbox_entries_without_context_groups ( jira_field ) if groups . blank?
128+ groups = augmented_context_groups ( jira_field )
129+ return [ ] if groups . blank?
139130
140131 build_multicheckbox_entries_with_context_groups ( jira_field , groups )
141132 end
142133
143- def build_multicheckbox_entries_without_context_groups ( jira_field )
144- option_values = collect_option_values_from_issues ( jira_field )
145- return [ ] if option_values . empty?
146-
147- if option_values . size == 1
148- [ { jira_field :, contexts : [ build_context_entry ( jira_field , nil , option_value : option_values . first ) ] } ]
149- else
150- [ { jira_field :, contexts : [ build_context_entry ( jira_field , nil ) ] } ]
151- end
152- end
153-
154134 def build_multicheckbox_entries_with_context_groups ( jira_field , groups )
155135 boolean_groups , list_groups = partition_multicheckbox_groups_by_value_count ( groups )
156136
@@ -162,7 +142,7 @@ def partition_multicheckbox_groups_by_value_count(groups)
162142 list_groups = [ ]
163143
164144 groups . each do |group |
165- option_values = Array ( group [ "allowedValues" ] ) . pluck ( "value" ) . compact . uniq
145+ option_values = option_labels ( group [ "allowedValues" ] )
166146 if option_values . size == 1
167147 boolean_groups << { group :, option_value : option_values . first }
168148 elsif option_values . size > 1
@@ -197,19 +177,8 @@ def all_jira_import_project_ids
197177 . pluck ( :id )
198178 end
199179
200- def collect_option_values_from_issues ( jira_field )
201- values = Set . new
202- Import ::JiraIssue . where ( jira_id : @jira_id , jira_project_id : all_jira_import_project_ids ) . find_each do |issue |
203- raw = issue . payload [ "fields" ] [ jira_field . jira_field_id ]
204- next unless raw . is_a? ( Array )
205-
206- raw . each { |v | values << v [ "value" ] if v [ "value" ] . present? }
207- end
208- values . to_a . sort
209- end
210-
211180 def build_contexts_for_field ( jira_field )
212- groups = jira_field . payload [ "contextGroups" ]
181+ groups = augmented_context_groups ( jira_field )
213182 if groups . present?
214183 needs_disambiguation = groups . size > 1
215184 groups . map { |group | build_context_entry ( jira_field , group , needs_disambiguation :) }
@@ -218,6 +187,121 @@ def build_contexts_for_field(jira_field)
218187 end
219188 end
220189
190+ def augmented_context_groups ( jira_field )
191+ groups = dup_context_groups ( jira_field )
192+ return groups unless option_field? ( jira_field )
193+
194+ issue_options = issue_option_values ( jira_field )
195+ return groups if issue_options . empty?
196+
197+ groups << global_context_group if groups . empty?
198+ issue_options . each { |option , *scope | add_option_to_context_group ( groups , option , scope ) }
199+ groups
200+ end
201+
202+ def dup_context_groups ( jira_field )
203+ Array ( jira_field . payload [ "contextGroups" ] ) . map do |group |
204+ group . merge ( "allowedValues" => Array ( group [ "allowedValues" ] ) )
205+ end
206+ end
207+
208+ def global_context_group
209+ { "projects" => [ ] , "issuetypes" => [ ] , "allowedValues" => [ ] }
210+ end
211+
212+ def add_option_to_context_group ( groups , option , scope )
213+ group = groups [ matching_context_group_index ( groups , *scope ) ]
214+ group [ "allowedValues" ] = merge_allowed_value ( group [ "allowedValues" ] , option )
215+ end
216+
217+ def merge_allowed_value ( allowed_values , option )
218+ label = option [ "value" ] . to_s . strip
219+ return allowed_values if label . blank?
220+
221+ existing = allowed_values . find { |av | av [ "value" ] . to_s . strip == label }
222+ return allowed_values + [ build_allowed_value ( label , option [ "child" ] ) ] if existing . nil?
223+
224+ merge_allowed_child ( allowed_values , existing , option [ "child" ] )
225+ end
226+
227+ def merge_allowed_child ( allowed_values , existing , child )
228+ return allowed_values if child . blank?
229+
230+ merged = existing . merge ( "children" => merge_allowed_value ( Array ( existing [ "children" ] ) , child ) )
231+ allowed_values . map { |av | av . equal? ( existing ) ? merged : av }
232+ end
233+
234+ def build_allowed_value ( label , child )
235+ entry = { "value" => label }
236+ entry [ "children" ] = merge_allowed_value ( [ ] , child ) if child . present?
237+ entry
238+ end
239+
240+ def matching_context_group_index ( groups , project_key , issuetype_id )
241+ groups . index do |group |
242+ scope_applies? ( group [ "projects" ] , project_key ) && scope_applies? ( group [ "issuetypes" ] , issuetype_id )
243+ end || 0
244+ end
245+
246+ def option_labels ( allowed_values )
247+ Array ( allowed_values ) . pluck ( "value" ) . compact . map { |value | value . to_s . strip } . compact_blank . uniq
248+ end
249+
250+ def issue_option_values ( jira_field )
251+ issue_field_values_index [ :options ] [ jira_field . jira_field_id ] . values
252+ end
253+
254+ def issue_string_values ( jira_field )
255+ issue_field_values_index [ :strings ] [ jira_field . jira_field_id ] . to_a . sort
256+ end
257+
258+ def issue_field_values_index
259+ @issue_field_values_index ||= build_issue_field_values_index
260+ end
261+
262+ def build_issue_field_values_index
263+ index = { options : Hash . new { |h , k | h [ k ] = { } } , strings : Hash . new { |h , k | h [ k ] = Set . new } }
264+ Import ::JiraIssue . where ( jira_id : @jira_id , jira_project_id : all_jira_import_project_ids ) . find_each do |issue |
265+ scope = issue_context_scope ( issue )
266+ used_custom_field_values ( issue ) . each { |field_key , raw | record_issue_field_values ( index , field_key , raw , scope ) }
267+ end
268+ index
269+ end
270+
271+ def issue_context_scope ( issue )
272+ [ issue . payload . dig ( "fields" , "project" , "key" ) , issue . payload . dig ( "fields" , "issuetype" , "id" ) ]
273+ end
274+
275+ def used_custom_field_values ( issue )
276+ issue . payload [ "fields" ] . select { |field_key , raw | field_key . start_with? ( "customfield_" ) && raw . present? }
277+ end
278+
279+ def record_issue_field_values ( index , field_key , raw , scope )
280+ Array . wrap ( raw ) . each do |value |
281+ if value . is_a? ( Hash )
282+ record_issue_option_value ( index [ :options ] [ field_key ] , value , scope )
283+ elsif raw . is_a? ( Array ) && value . is_a? ( String )
284+ index [ :strings ] [ field_key ] << value . strip if value . strip . present?
285+ end
286+ end
287+ end
288+
289+ def record_issue_option_value ( field_options , option , scope )
290+ return if option [ "value" ] . blank?
291+
292+ field_options [ scope + [ option_chain_signature ( option ) ] ] ||= [ option , *scope ]
293+ end
294+
295+ def option_chain_signature ( option )
296+ labels = [ ]
297+ node = option
298+ while node . is_a? ( Hash ) && node [ "value" ] . present?
299+ labels << node [ "value" ] . to_s . strip
300+ node = node [ "child" ]
301+ end
302+ labels . join ( " / " )
303+ end
304+
221305 def build_context_entry ( jira_field , context_group , option_value : nil , needs_disambiguation : false )
222306 builder = JiraImportCustomFieldBuilder . new (
223307 jira_field ,
@@ -287,11 +371,17 @@ def find_context_for_issue(entry, jira_issue)
287371 end
288372
289373 def context_applies_to_project? ( context , project_key )
290- context [ :projects ] . empty? || context [ :projects ] . include? ( project_key )
374+ scope_applies? ( context [ :projects ] , project_key )
291375 end
292376
293377 def context_applies_to_issuetype? ( context , issuetype_id )
294- context [ :issuetypes ] . empty? || context [ :issuetypes ] . include? ( issuetype_id )
378+ scope_applies? ( context [ :issuetypes ] , issuetype_id )
379+ end
380+
381+ # An empty scope means "applies to all projects" / "applies to all issue types".
382+ def scope_applies? ( scope , value )
383+ scope = Array ( scope )
384+ scope . empty? || scope . include? ( value )
295385 end
296386 end
297387 end
0 commit comments