Browse Source

Feature: Desktop view - Implement sorting by advanced custom single select object attribute columns.

Co-authored-by: Tobias Schäfer <ts@zammad.com>
Co-authored-by: Dominik Klein <dk@zammad.com>
Florian Liebe 1 week ago
parent
commit
da5bb2fb4b

+ 4 - 4
app/frontend/shared/components/ObjectAttributes/__tests__/ObjectAttributes.spec.ts

@@ -122,10 +122,10 @@ describe('common object attributes interface', () => {
       'Display1, Display2',
     )
     expect(getRegion('Single Tree Select Field')).toHaveTextContent(
-      'key1::key1_child1',
+      'key1key1_child1',
     )
     expect(getRegion('Multi Tree Select Field')).toHaveTextContent(
-      'key1, key2, key2::key2_child1',
+      'key1, key2, key2key2_child1',
     )
     expect(getRegion('External Attribute')).toHaveTextContent(
       'Display External',
@@ -227,8 +227,8 @@ describe('common object attributes interface', () => {
     expect(vip).toHaveTextContent('sí')
     expect(singleSelect).toHaveTextContent('Monitor1')
     expect(multiSelect).toHaveTextContent('Monitor1, Monitor2')
-    expect(singleTreeSelect).toHaveTextContent('llave1::llave1_niño1')
-    expect(multiTreeSelect).toHaveTextContent('llave1, llave1::llave1_niño1')
+    expect(singleTreeSelect).toHaveTextContent('llave1llave1_niño1')
+    expect(multiTreeSelect).toHaveTextContent('llave1, llave1llave1_niño1')
   })
 
   it('renders different dates', async () => {

+ 1 - 1
app/frontend/shared/components/ObjectAttributes/attributes/AttributeMultiSelect/AttributeMultiSelect.vue

@@ -18,7 +18,7 @@ const body = computed(() => {
         value
           .split('::')
           .map((option) => translateOption(props.attribute, option))
-          .join('::'),
+          .join(''),
       )
       .join(', ')
   }

+ 1 - 1
app/frontend/shared/components/ObjectAttributes/attributes/AttributeSingleSelect/AttributeSingleSelect.vue

@@ -16,7 +16,7 @@ const body = computed(() => {
     return props.value
       .split('::')
       .map((field) => translateOption(props.attribute, field))
-      .join('::')
+      .join('')
   }
   const value =
     props.attribute.dataOption.historical_options?.[props.value] ?? props.value

+ 4 - 0
app/models/concerns/can_selector/advanced_sorting.rb

@@ -14,6 +14,10 @@ module CanSelector
       [
         TranslatedRelationSort,
         UntranslatedRelationSort,
+        SelectFieldSort,
+        TreeSelectFieldSort,
+        ExternalDataSourceFieldSort,
+        BooleanFieldSort,
       ]
     end
 

+ 86 - 0
app/models/concerns/can_selector/advanced_sorting/base_select_field_sort.rb

@@ -0,0 +1,86 @@
+# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+
+module CanSelector
+  class AdvancedSorting
+    class BaseSelectFieldSort < BaseSort
+      include CanApplyAdvancedSorting
+
+      def calculate_sorting
+        command = case ActiveRecord::Base.connection_db_config.configuration_hash[:adapter]
+                  when 'postgresql'
+                    column_part = cached_sorted_ids.include?("'") ? "CAST(#{adjusted_column} as TEXT)" : adjusted_column
+
+                    "array_position(ARRAY[#{cached_sorted_ids}], #{column_part})"
+                  when 'mysql2'
+                    "FIELD(#{adjusted_column}, #{cached_sorted_ids})"
+                  end
+
+        {
+          order:  "#{meta_value_name} #{input[:direction]}",
+          select: "#{command} as #{meta_value_name}",
+        }
+      end
+
+      private
+
+      def fetch_names_and_ids
+        historical_options.map { |k, v| [k, v] }
+      end
+
+      def cached_sorted_ids
+        Rails.cache.fetch(sorted_ids_cache_key) { calculate_sorted_ids }
+      end
+
+      def sorted_ids_cache_key
+        cache_prefix = self.class.name.demodulize.tableize.tr('_', '-')
+        "#{cache_prefix}-#{object_manager_attribute.cache_key_with_version}-#{Translation.all.cache_key_with_version}"
+      end
+
+      def calculate_sorted_ids
+        names_and_ids = fetch_names_and_ids
+        names_and_ids = translate(names_and_ids)
+        names_and_ids = sort(names_and_ids)
+
+        build_id_string(names_and_ids).join(',')
+      end
+
+      def meta_value_name
+        object.connection.quote_column_name("_advanced_sorting_#{object.name}_#{column}")
+      end
+
+      def adjusted_column
+        raw_selectors_quoted_column(object_manager_attribute.name)
+      end
+
+      def object_manager_attribute
+        @object_manager_attribute ||= ObjectManager::Attribute.get(object: object.class_name, name: self.class.column_name(input, object))
+      end
+
+      def translate(names_and_ids)
+        return names_and_ids if !translate?
+
+        translations = Translation.translate_all locale, *names_and_ids.map(&:second)
+
+        names_and_ids.each do |name_and_id|
+          name_and_id << translations[name_and_id.second]
+        end
+
+        names_and_ids
+      end
+
+      def build_id_string(names_and_ids)
+        names_and_ids.map do |name_and_id|
+          "'#{ApplicationModel.connection.quote_string(name_and_id.first)}'"
+        end
+      end
+
+      def translate?
+        object_manager_attribute.data_option[:translate]
+      end
+
+      def historical_options
+        object_manager_attribute.data_option[:historical_options] || {}
+      end
+    end
+  end
+end

+ 38 - 0
app/models/concerns/can_selector/advanced_sorting/boolean_field_sort.rb

@@ -0,0 +1,38 @@
+# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+
+module CanSelector
+  class AdvancedSorting
+    class BooleanFieldSort < BaseSelectFieldSort
+      data_type 'boolean'
+
+      private
+
+      def sort(names_and_ids)
+        locale_object = Locale.find_by(locale:)
+
+        if !locale_object
+          names_and_ids.sort_by!(&:second)
+          return names_and_ids
+        end
+
+        comparator = TwitterCldr::Collation::Collator.new(locale_object.cldr_language_code)
+
+        if translate?
+          names_and_ids.sort! { |a, b| comparator.compare(a.third, b.third) }
+        else
+          names_and_ids.sort! { |a, b| comparator.compare(a.second, b.second) }
+        end
+
+        names_and_ids
+      end
+
+      def build_id_string(names_and_ids)
+        names_and_ids.map(&:first)
+      end
+
+      def historical_options
+        object_manager_attribute.data_option[:options] || {}
+      end
+    end
+  end
+end

+ 32 - 0
app/models/concerns/can_selector/advanced_sorting/can_apply_advanced_sorting.rb

@@ -0,0 +1,32 @@
+# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+
+module CanSelector
+  class AdvancedSorting
+    module CanApplyAdvancedSorting
+      extend ActiveSupport::Concern
+
+      class_methods do
+        def applicable?(input, locale, object)
+          return false if locale.blank?
+
+          attr = ObjectManager::Attribute.get(object: object.class_name, name: column_name(input, object))
+          return false if attr.nil?
+
+          return true if attr.data_type == data_type
+
+          false
+        end
+
+        def data_type(name = nil)
+          if name.present?
+            @data_type = name
+          elsif defined?(@data_type)
+            @data_type
+          else
+            raise 'data_type not set'
+          end
+        end
+      end
+    end
+  end
+end

+ 21 - 0
app/models/concerns/can_selector/advanced_sorting/external_data_source_field_sort.rb

@@ -0,0 +1,21 @@
+# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+
+module CanSelector
+  class AdvancedSorting
+    class ExternalDataSourceFieldSort < BaseSort
+      include CanApplyAdvancedSorting
+
+      data_type 'autocompletion_ajax_external_data_source'
+
+      def calculate_sorting
+        { order: "#{meta_value_name}->>'value' #{input[:direction]}" }
+      end
+
+      private
+
+      def meta_value_name
+        "#{object.quoted_table_name}.#{object.connection.quote_column_name(column)}"
+      end
+    end
+  end
+end

+ 42 - 0
app/models/concerns/can_selector/advanced_sorting/select_field_sort.rb

@@ -0,0 +1,42 @@
+# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+
+module CanSelector
+  class AdvancedSorting
+    class SelectFieldSort < BaseSelectFieldSort
+      data_type 'select'
+
+      private
+
+      def fetch_names_and_ids
+        return historical_options.map { |k, v| [k, v] } if !custom_sort?
+
+        object_manager_attribute.data_option[:options].map { |h| [h['value'], h['name']] }
+      end
+
+      def sort(names_and_ids)
+        locale_object = Locale.find_by(locale:)
+
+        if !locale_object && !custom_sort?
+          names_and_ids.sort_by!(&:second)
+          return names_and_ids
+        end
+
+        return names_and_ids if custom_sort?
+
+        comparator = TwitterCldr::Collation::Collator.new(locale_object.cldr_language_code)
+
+        if translate?
+          names_and_ids.sort! { |a, b| comparator.compare(a.third, b.third) }
+        else
+          names_and_ids.sort! { |a, b| comparator.compare(a.second, b.second) }
+        end
+
+        names_and_ids
+      end
+
+      def custom_sort?
+        object_manager_attribute.data_option[:customsort].present? && object_manager_attribute.data_option[:customsort] == 'on'
+      end
+    end
+  end
+end

+ 1 - 1
app/models/concerns/can_selector/advanced_sorting/translated_relation_sort.rb

@@ -33,7 +33,7 @@ module CanSelector
       end
 
       def sorted_ids_cache_key
-        "translated-relations-sort-#{locale}-#{assoc.klass.all.cache_key_with_version}"
+        "translated-relations-sort-#{locale}-#{assoc.klass.all.cache_key_with_version}-#{Translation.all.cache_key_with_version}"
       end
 
       def calculate_sorted_ids

Some files were not shown because too many files changed in this diff