diff --git a/app/models/custom_actions/actions/base.rb b/app/models/custom_actions/actions/base.rb index 8b64662449b5..4995d3c9371d 100644 --- a/app/models/custom_actions/actions/base.rb +++ b/app/models/custom_actions/actions/base.rb @@ -43,6 +43,12 @@ def allowed_values raise NotImplementedError end + def value_objects + values.map do |value| + allowed_values.find { |v| v[:value] == value } + end + end + def type raise NotImplementedError end diff --git a/app/models/custom_actions/conditions/base.rb b/app/models/custom_actions/conditions/base.rb index 93828f8d3880..ddd0a58fdd30 100644 --- a/app/models/custom_actions/conditions/base.rb +++ b/app/models/custom_actions/conditions/base.rb @@ -45,6 +45,12 @@ def allowed_values .map { |value, label| { value:, label: } } end + def value_objects + values.map do |value| + allowed_values.find { |v| v[:value] == value } + end + end + def human_name WorkPackage.human_attribute_name(self.class.key) end diff --git a/app/views/augmented/_autocomplete_select_decoration.html.erb b/app/views/augmented/_autocomplete_select_decoration.html.erb deleted file mode 100644 index 62f4ad43bb8d..000000000000 --- a/app/views/augmented/_autocomplete_select_decoration.html.erb +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/app/views/custom_actions/_form.html.erb b/app/views/custom_actions/_form.html.erb index 1858ded3b327..2791dd2fdf56 100644 --- a/app/views/custom_actions/_form.html.erb +++ b/app/views/custom_actions/_form.html.erb @@ -32,18 +32,17 @@ } %> <% else %> - <% selected_values = condition.values - select_options = condition.allowed_values.map { |v| { label: v[:label], value: v[:value], selected: selected_values.include?(v[:value]) } } %> - <%= render partial: 'augmented/autocomplete_select_decoration', - locals: { - select_options:, - inputs: { - inputName: input_name, - inputId: "custom_action_conditions_#{condition.key}", - multiple: true, - key: condition.key.to_s - } - } %> + <%= angular_component_tag 'opce-autocompleter', + inputs: { + multiple: true, + defaultData: false, + items: condition.allowed_values.map { |v| { id: v[:value], name: v[:label] } }, + model: condition.value_objects.map { |v| { id: v[:value], name: v[:label] } }, + inputName: input_name, + inputId: "custom_action_conditions_#{condition.key}", + appendTo: "body", + } + %> <% end %> @@ -79,18 +78,17 @@ } %> <% else %> - <% selected_values = action.values - select_options = action.allowed_values.map { |v| { label: v[:label], value: v[:value], selected: selected_values.include?(v[:value]) } } %> - <%= render partial: 'augmented/autocomplete_select_decoration', - locals: { - select_options:, - inputs: { - inputName: input_name, - inputId: "custom_action_actions_#{action.key}", - multiple: action.multi_value?, - key: action.key.to_s - } - } %> + <%= angular_component_tag 'opce-autocompleter', + inputs: { + multiple: true, + defaultData: false, + items: action.allowed_values.map { |v| { id: v[:value], name: v[:label] } }, + model: action.value_objects.map { |v| { id: v[:value], name: v[:label] } }, + inputName: input_name, + inputId: "custom_action_conditions_#{action.key}", + appendTo: "body", + } + %> <% end %> diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 51d140173014..dc2e45c2eea4 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -123,9 +123,6 @@ import { import { ProjectAutocompleterComponent, } from 'core-app/shared/components/autocompleter/project-autocompleter/project-autocompleter.component'; -import { - AutocompleteSelectDecorationComponent, -} from 'core-app/shared/components/autocompleter/autocomplete-select-decoration/autocomplete-select-decoration.component'; import { MembersAutocompleterComponent, } from 'core-app/shared/components/autocompleter/members-autocompleter/members-autocompleter.component'; @@ -426,7 +423,6 @@ export class OpenProjectModule implements DoBootstrap { registerCustomElement('opce-global-search', GlobalSearchInputComponent, { injector }); registerCustomElement('opce-autocompleter', OpAutocompleterComponent, { injector }); registerCustomElement('opce-project-autocompleter', ProjectAutocompleterComponent, { injector }); - registerCustomElement('opce-select-decoration', AutocompleteSelectDecorationComponent, { injector }); registerCustomElement('opce-members-autocompleter', MembersAutocompleterComponent, { injector }); registerCustomElement('opce-user-autocompleter', UserAutocompleterComponent, { injector }); registerCustomElement('opce-macro-attribute-value', AttributeValueMacroComponent, { injector }); diff --git a/frontend/src/app/shared/components/autocompleter/autocomplete-select-decoration/autocomplete-select-decoration.component.ts b/frontend/src/app/shared/components/autocompleter/autocomplete-select-decoration/autocomplete-select-decoration.component.ts deleted file mode 100644 index 897fdbb3deea..000000000000 --- a/frontend/src/app/shared/components/autocompleter/autocomplete-select-decoration/autocomplete-select-decoration.component.ts +++ /dev/null @@ -1,134 +0,0 @@ -//-- copyright -// OpenProject is an open source project management software. -// Copyright (C) the OpenProject GmbH -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License version 3. -// -// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -// Copyright (C) 2006-2013 Jean-Philippe Lang -// Copyright (C) 2010-2013 the ChiliProject Team -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License -// as published by the Free Software Foundation; either version 2 -// of the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -// -// See COPYRIGHT and LICENSE files for more details. -//++ - -import { AfterViewInit, Component, Input, OnInit, ViewChild } from '@angular/core'; -import { NgSelectComponent } from '@ng-select/ng-select'; -import { - IAutocompleteItem, - OpAutocompleterComponent, -} from 'core-app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component'; -import { populateInputsFromDataset } from 'core-app/shared/components/dataset-inputs'; - -type SelectItem = { label:string, value:string, selected?:boolean }; - -export const autocompleteSelectDecorationSelector = 'autocomplete-select-decoration'; - -@Component({ - template: ` - - - {{ item.label }} - - - `, - selector: autocompleteSelectDecorationSelector, -}) -export class AutocompleteSelectDecorationComponent extends OpAutocompleterComponent implements OnInit, AfterViewInit { - @ViewChild(NgSelectComponent) public ngSelectComponent:NgSelectComponent; - - public options:SelectItem[]; - - /** Get the selected options */ - public selected:SelectItem|SelectItem[]; - - /** The field key (e.g. status, type, or project) */ - @Input() key:string; - - text = { - placeholder: this.I18n.t('js.placeholders.selection'), - }; - - ngOnInit():void { - populateInputsFromDataset(this); - const element = this.elementRef.nativeElement as HTMLElement; - - // Add Rails multiple identifier if multiple - if (this.multiple) { - this.inputName += '[]'; - } - - // Prepare and build the options - // Expected array of objects with id, name, select - const data:SelectItem[] = JSON.parse(element.dataset.options!); - - // Set initial selection - this.setInitialSelection(data); - - if (!this.multiple) { - this.selected = (this.selected as SelectItem[])[0]; - } - - this.options = data; - - // Unhide the parent - element.parentElement!.hidden = false; - } - - // eslint-disable-next-line @angular-eslint/no-empty-lifecycle-method - ngAfterViewInit():void { - // do nothing and prevent the parent hook to be called - } - - setInitialSelection(data:SelectItem[]):void { - this.updateSelection(data.filter((element) => element.selected)); - } - - updateSelection(items:SelectItem|SelectItem[]):void { - this.selected = items; - items = _.castArray(items); - - this.removeCurrentSyncedFields(); - items.forEach((el:SelectItem) => { - this.createSyncedField(el.value); - }); - } - - createSyncedField(value:string):void { - const element = jQuery(this.elementRef.nativeElement); - element - .parent() - .append(``); - } - - removeCurrentSyncedFields():void { - const element = jQuery(this.elementRef.nativeElement); - element - .parent() - .find(`input[name="${this.inputName}"]`) - .remove(); - } -} diff --git a/frontend/src/app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component.ts b/frontend/src/app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component.ts index 87a41864d2c5..210d04678aa9 100644 --- a/frontend/src/app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component.ts +++ b/frontend/src/app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component.ts @@ -146,7 +146,7 @@ export class OpAutocompleterComponent(null); @Input() public clearSearchOnAdd?:boolean = true; @@ -314,6 +314,10 @@ export class OpAutocompleterComponent(''); } + if (this.items) { + this.items$.next(this.items as IOPAutocompleterOption[]); + } + if (this.inputValue && !this.model) { this .opAutocompleterService @@ -328,8 +332,7 @@ export class OpAutocompleterComponent <%= content_tag(:div, **@field_wrap_arguments) do %> - <% if decorated_select? %> - <%= render partial: '/augmented/autocomplete_select_decoration', - formats: %i[html], - locals: { - inputs: @autocomplete_options.merge( - classes: "ng-select--primerized #{@input.invalid? ? '-error' : ''}", - inputName: @autocomplete_options.fetch(:inputName) { builder.field_name(@input.name) }, - inputId: @autocomplete_options.fetch(:inputId) { builder.field_id(@input.name) }, - inputValue: @autocomplete_options.fetch(:inputValue) { builder.object.send(@input.name) }, - key: @autocomplete_options.fetch(:resource, ''), - defaultData: @autocomplete_options.fetch(:defaultData) { true } - ), - select_options: select_options.map(&:to_h), - } %> - <% else %> - <%= content_tag(:div, class: ("projects-autocomplete-with-search-icon" if @autocomplete_options.delete(:with_search_icon))) do %> - <%= angular_component_tag @autocomplete_options.fetch(:component), - data: @autocomplete_options.delete(:data) { {} }, - inputs: @autocomplete_options.merge( - classes: "ng-select--primerized #{@input.invalid? ? '-error' : ''}", - inputName: @autocomplete_options.fetch(:inputName) { builder.field_name(@input.name) }, - inputValue: @autocomplete_options.fetch(:inputValue) { builder.object.send(@input.name) }, - defaultData: @autocomplete_options.fetch(:defaultData) { true } - ) - %> - <% end %> + <%= content_tag(:div, class: ("projects-autocomplete-with-search-icon" if @with_search_icon)) do %> + <%= angular_component_tag(@autocomplete_component, + data: @autocomplete_data, + inputs: @autocomplete_inputs) %> <% end %> <% end %> <% end %> diff --git a/lib/primer/open_project/forms/autocompleter.rb b/lib/primer/open_project/forms/autocompleter.rb index f2d35812d62a..c274e9449739 100644 --- a/lib/primer/open_project/forms/autocompleter.rb +++ b/lib/primer/open_project/forms/autocompleter.rb @@ -8,17 +8,37 @@ class Autocompleter < Primer::Forms::BaseComponent include AngularHelper prepend WrappedInput - delegate :builder, :form, :select_options, to: :@input + delegate :builder, :form, to: :@input def initialize(input:, autocomplete_options:, wrapper_data_attributes: {}) super() @input = input - @autocomplete_options = autocomplete_options + @with_search_icon = autocomplete_options.delete(:with_search_icon) { false } + @autocomplete_component = autocomplete_options.delete(:component) { "opce-autocompleter" } + @autocomplete_data = autocomplete_options.delete(:data) { {} } + @autocomplete_inputs = extend_autocomplete_inputs(autocomplete_options) @wrapper_data_attributes = wrapper_data_attributes end - def decorated_select? - @autocomplete_options[:decorated] + def extend_autocomplete_inputs(inputs) # rubocop:disable Metrics/AbcSize + inputs[:classes] = "ng-select--primerized #{@input.invalid? ? '-error' : ''}" + inputs[:inputName] ||= builder.field_name(@input.name) + inputs[:inputValue] ||= builder.object.send(@input.name) + inputs[:defaultData] ||= true + + if inputs.delete(:decorated) + inputs[:items] = @input.select_options.map(&:to_h) + inputs[:model] = selected_options + inputs[:defaultData] = false + end + + inputs + end + + def selected_options + @input.select_options.filter_map do |item| + item.value if item.selected + end end end end diff --git a/lib/primer/open_project/forms/dsl/autocompleter_input.rb b/lib/primer/open_project/forms/dsl/autocompleter_input.rb index 497658f27d91..11cf87d366aa 100644 --- a/lib/primer/open_project/forms/dsl/autocompleter_input.rb +++ b/lib/primer/open_project/forms/dsl/autocompleter_input.rb @@ -18,9 +18,8 @@ def initialize(label:, value:, selected: false) def to_h { - label:, - value:, - selected: + id: value, + name: label } end end diff --git a/spec/support/pages/admin/custom_actions/form.rb b/spec/support/pages/admin/custom_actions/form.rb index 41054956c733..f700f0d551c7 100644 --- a/spec/support/pages/admin/custom_actions/form.rb +++ b/spec/support/pages/admin/custom_actions/form.rb @@ -118,7 +118,7 @@ def set_field_value(field, name, value) if has_selector?(".form--selected-value--container", wait: 0) find(".form--selected-value--container").click autocomplete = true - elsif has_selector?(".autocomplete-select-decoration--wrapper", wait: 0) + elsif has_selector?("op-autocompleter", wait: 0) autocomplete = true end