diff --git a/backend/app/representations/counts_version_repr.rb b/backend/app/representations/counts_version_repr.rb index 273e9c4..92f2d25 100644 --- a/backend/app/representations/counts_version_repr.rb +++ b/backend/app/representations/counts_version_repr.rb @@ -84,6 +84,15 @@ class CountsVersionRepr def locale_names(values) @name_columns.to_h do |field| locale = field.fetch(:name_locale) + if field[:multiple_values] + entries = values.select { _1[:language_code] == locale[:language_code] } + aliases = entries.flat_map do |entry| + entry[:aliases].select do |value| + value['script_code'].to_s.downcase == locale[:script_code].to_s.downcase + end.map { _1.merge('language_code' => entry[:language_code]) } + end + next [field.fetch(:key).to_sym, aliases] + end entry = values.find do |value| value[:language_code] == locale[:language_code] && value[:script_code].to_s.downcase == locale[:script_code].to_s.downcase diff --git a/backend/app/services/counts_condition.rb b/backend/app/services/counts_condition.rb index 280d58b..41b4ccb 100644 --- a/backend/app/services/counts_condition.rb +++ b/backend/app/services/counts_condition.rb @@ -30,10 +30,11 @@ class CountsCondition nullable = prefix.present? || (physical ? physical[:nullable] : key != 'latest') nullable = false if field[:children] entry = { key:, type:, labels: path_labels, nullable:, - multiple: prefix.present? || field[:children].present?, + multiple: prefix.present? || field[:children].present? || field[:multiple_values], collection: field[:children].present? || type == :interval, operators: CountsResourceDefinition::OPERATORS.fetch(type) } entry[:label_values] = field[:label_values] if field[:label_values] + entry[:name_locale] = field[:name_locale] if field[:name_locale] result.reject! { _1[:key] == key } result << entry visit.call(field[:children] || [], key, path_labels) @@ -344,6 +345,7 @@ class CountsCondition def source_for(key) root, *parts = key.split('.') if @definition.resource == 'tags' && @definition.name_column(root) + return @definition.alias_source(root) if root.start_with?('aliases_') return [nil, @definition.name_expression(root)] end return [nil, "(#{ @population.latest_sql })"] if root == 'latest' @@ -357,6 +359,10 @@ class CountsCondition if root == 'parents' source, id = parent_source if @definition.resource == 'tags' && @definition.name_column(parts.first) + if parts.first.start_with?('aliases_') + aliases, value = @definition.alias_source(parts.first, 'parent_version') + return ["#{ source } JOIN #{ aliases }", value] + end return [source, @definition.name_expression(parts.first, 'parent_version')] end if parts.first == 'names' @@ -402,6 +408,10 @@ class CountsCondition source = post_tag_source document = 'tag_snapshot.item' if @definition.name_column(parts.first) + if parts.first.start_with?('aliases_') + aliases, value = @definition.alias_source(parts.first, 'referenced_tag') + return ["#{ source } JOIN #{ aliases }", value] + end return [source, @definition.name_expression(parts.first, 'referenced_tag')] end case parts.first diff --git a/backend/app/services/counts_defaults.rb b/backend/app/services/counts_defaults.rb index 3e87606..e084a58 100644 --- a/backend/app/services/counts_defaults.rb +++ b/backend/app/services/counts_defaults.rb @@ -14,12 +14,17 @@ class CountsDefaults details = detail_params(resource) if details name = CountsResourceDefinition.name_key(locale) + aliases = CountsResourceDefinition.aliases_key(locale) details = details.transform_values do |value| - value.split(' ').map do |token| + value.split(' ').flat_map do |token| if resource == 'tags' && ['name', 'parents.name'].include?(token) - token == 'name' ? name : "parents.#{ name }" + token == 'name' ? name : ["parents.#{ name }", "parents.#{ aliases }"] + elsif resource == 'tags' && token == 'aliases' + aliases + elsif resource == 'tags' && token.start_with?('aliases.') + [] elsif resource == 'posts' && token == 'tags.name' - "tags.#{ name }" + ["tags.#{ name }", "tags.#{ aliases }"] else token end end.join(' ') diff --git a/backend/app/services/counts_export_projection.rb b/backend/app/services/counts_export_projection.rb index 7a8dab2..87d5efe 100644 --- a/backend/app/services/counts_export_projection.rb +++ b/backend/app/services/counts_export_projection.rb @@ -139,6 +139,7 @@ class CountsExportProjection if @complete_names && ((!@summary && @resource == 'tags') || row.key?(:names)) result['names'] = row[:names] || [] end + result['aliases'] = row[:aliases] if @complete_names && row.key?(:aliases) result end @@ -155,6 +156,9 @@ class CountsExportProjection end def csv_display(value, key) + if key.start_with?('aliases_') && value.is_a?(Array) + return value.map { fetch(_1, 'name') }.compact.join("\n") + end return @labels.fetch('bot', '') if key == 'created_by_user' && value.nil? return @labels.fetch("event_type:#{ value }", value) if key == 'event_type' return value ? @labels.fetch('latest', 'true') : '' if key == 'latest' @@ -272,6 +276,8 @@ class CountsExportProjection end if @complete_names && ['tags', 'parents'].include?(prefix) result['names'] = fetch(row, 'names') || [] + aliases = fetch(row, 'aliases') + result['aliases'] = aliases if aliases end result end diff --git a/backend/app/services/counts_resource_definition.rb b/backend/app/services/counts_resource_definition.rb index 274f925..872e379 100644 --- a/backend/app/services/counts_resource_definition.rb +++ b/backend/app/services/counts_resource_definition.rb @@ -112,19 +112,29 @@ class CountsResourceDefinition def field_label(key) = LABELS.fetch(key, "counts:attributes.#{ key }") def self.name_key(code) = "name_#{ code.to_s.downcase.tr('-', '_') }" + def self.aliases_key(code) = "aliases_#{ code.to_s.downcase.tr('-', '_') }" def name_columns - @name_columns ||= Locale.order(:code).map do |locale| - { key: self.class.name_key(locale.code), label: 'counts:localeName', - label_values: { locale: locale.name }, - name_locale: { code: locale.code, language_code: locale.language_code, - script_code: locale.script_code } } + @name_columns ||= Locale.select(:code, :language_code, :script_code).order(:code) + .flat_map do |locale| + codes = { code: locale.code, language_code: locale.language_code, + script_code: locale.script_code } + [{ key: self.class.name_key(locale.code), label: 'counts:localeName', name_locale: codes }, + { key: self.class.aliases_key(locale.code), label: 'counts:localeAliases', + name_locale: codes, multiple_values: true }] end end def name_column(key) = name_columns.find { _1[:key] == key } def name_expression(key, row = 'v') + if key.start_with?('aliases_') + source, value = alias_source(key, row) + return "CAST(COALESCE((SELECT JSON_ARRAYAGG(#{ value }) OVER (" + + 'ORDER BY locale_alias.ordinal ROWS BETWEEN UNBOUNDED PRECEDING ' + + "AND UNBOUNDED FOLLOWING) FROM #{ source } WHERE #{ value } IS NOT NULL " + + 'LIMIT 1), JSON_ARRAY()) AS CHAR) COLLATE utf8mb4_bin' + end locale = name_column(key).fetch(:name_locale) connection = model.connection path = connection.quote("$.#{ locale[:language_code].to_json }") @@ -135,6 +145,19 @@ class CountsResourceDefinition "CONCAT(#{ path }, '.name')), CAST('null' AS JSON))) END" end + def alias_source(key, row = 'v') + locale = name_column(key).fetch(:name_locale) + connection = model.connection + path = connection.quote("$.#{ locale[:language_code].to_json }.aliases[*]") + script = connection.quote(locale[:script_code]) + source = "JSON_TABLE(COALESCE(#{ row }.names, JSON_OBJECT()), #{ path } " + + "COLUMNS (ordinal FOR ORDINALITY, name TEXT PATH '$.name', " + + "script TEXT PATH '$.script_code')) locale_alias" + value = "CASE WHEN LOWER(locale_alias.script) = LOWER(#{ script }) " + + 'THEN locale_alias.name END' + [source, value] + end + def output_fields content = case resource when 'posts' @@ -149,6 +172,7 @@ class CountsResourceDefinition keys.flat_map do |key| next name_columns if key == 'name' && resource == 'tags' next [] if key == 'names' && resource == 'tags' + next [] if key == 'aliases' && resource == 'tags' label = if key == 'parents' resource == 'tags' ? 'tags:parents' : 'shared:labels.parentPosts' else field_label(key) @@ -174,6 +198,7 @@ class CountsResourceDefinition else [] end keys.flat_map do |child| + next [] if child == 'aliases' && key == 'tags' if child == 'name' && ['tags', 'parents'].include?(key) next [{ key: child, label: CHILD_LABELS.fetch(child) }, *name_columns] end @@ -194,14 +219,16 @@ class CountsResourceDefinition fields = output_fields if resource == 'tags' fields += [{ key: 'name', label: 'counts:columns.name' }, - { key: 'names', label: 'counts:columns.names', children: child_fields('names') }] + { key: 'names', label: 'counts:columns.names', children: child_fields('names') }, + { key: 'aliases', label: 'tags:aliases', children: child_fields('aliases') }] end if resource == 'posts' fields = fields.map do |field| next field unless field[:key] == 'tags' field.merge(children: field[:children] + [ - { key: 'names', label: 'counts:columns.names', children: child_fields('names') }]) + { key: 'names', label: 'counts:columns.names', children: child_fields('names') }, + { key: 'aliases', label: 'tags:aliases', children: child_fields('aliases') }]) end end fields diff --git a/frontend/src/components/counts/CountsAdditionalConditions.tsx b/frontend/src/components/counts/CountsAdditionalConditions.tsx index 9b9562f..26630f3 100644 --- a/frontend/src/components/counts/CountsAdditionalConditions.tsx +++ b/frontend/src/components/counts/CountsAdditionalConditions.tsx @@ -40,7 +40,7 @@ const ConditionEditor = ( controls.setActions ([{ label: label (initial == null ? 'add' : 'shared:actions.edit'), onSelect: () => save (condition) }]) }, [controls, condition, initial, label, save]) - return
+ return