From a1475fad3c160e3ddd7d4b9ff1526c10e1ad4a0f Mon Sep 17 00:00:00 2001 From: Sascha Karnatz <122262394+sascha-karnatz@users.noreply.github.com> Date: Mon, 26 Feb 2024 12:56:26 +0100 Subject: [PATCH 1/2] Transform the tag selector into a web component it is still a Select2 select until we find a better alternative, but it is now abstracted into a web component and a View Component to make it easier usable and avoid initialization issues. --- .../alchemy/admin/tags_autocomplete.rb | 25 ++++++++ app/javascript/alchemy_admin.js | 1 + .../components/element_editor.js | 2 - .../components/tags_autocomplete.js | 57 +++++++++++++++++++ app/javascript/alchemy_admin/gui.js | 3 - .../alchemy_admin/tags_autocomplete.js | 46 --------------- .../alchemy/admin/attachments/edit.html.erb | 7 +-- .../alchemy/admin/elements/_element.html.erb | 6 +- .../alchemy/admin/layoutpages/edit.html.erb | 7 +-- app/views/alchemy/admin/pages/_form.html.erb | 7 +-- .../partials/_autocomplete_tag_list.html.erb | 1 - .../alchemy/admin/pictures/_form.html.erb | 6 +- .../admin/pictures/edit_multiple.html.erb | 6 +- .../alchemy/admin/resources/_form.html.erb | 7 +-- config/locales/alchemy.en.yml | 1 + .../alchemy/admin/tags_autocomplete_spec.rb | 36 ++++++++++++ .../admin/resources_integration_spec.rb | 2 +- .../components/element_editor.spec.js | 14 ----- .../components/tags_autocomplete.spec.js | 38 +++++++++++++ 19 files changed, 180 insertions(+), 92 deletions(-) create mode 100644 app/components/alchemy/admin/tags_autocomplete.rb create mode 100644 app/javascript/alchemy_admin/components/tags_autocomplete.js delete mode 100644 app/javascript/alchemy_admin/tags_autocomplete.js delete mode 100644 app/views/alchemy/admin/partials/_autocomplete_tag_list.html.erb create mode 100644 spec/components/alchemy/admin/tags_autocomplete_spec.rb create mode 100644 spec/javascript/alchemy_admin/components/tags_autocomplete.spec.js diff --git a/app/components/alchemy/admin/tags_autocomplete.rb b/app/components/alchemy/admin/tags_autocomplete.rb new file mode 100644 index 0000000000..d2066e627c --- /dev/null +++ b/app/components/alchemy/admin/tags_autocomplete.rb @@ -0,0 +1,25 @@ +module Alchemy + module Admin + class TagsAutocomplete < ViewComponent::Base + delegate :alchemy, to: :helpers + + def initialize(additional_class: nil) + @additional_class = additional_class + end + + def call + content_tag("alchemy-tags-autocomplete", content, attributes) + end + + private + + def attributes + { + placeholder: Alchemy.t(:search_tag), + url: alchemy.autocomplete_admin_tags_path, + class: @additional_class + } + end + end + end +end diff --git a/app/javascript/alchemy_admin.js b/app/javascript/alchemy_admin.js index 268ca46c3b..d88ae45d87 100644 --- a/app/javascript/alchemy_admin.js +++ b/app/javascript/alchemy_admin.js @@ -34,6 +34,7 @@ import "alchemy_admin/components/overlay" import "alchemy_admin/components/page_select" import "alchemy_admin/components/select" import "alchemy_admin/components/spinner" +import "alchemy_admin/components/tags_autocomplete" import "alchemy_admin/components/tinymce" import { setDefaultAnimation } from "shoelace" diff --git a/app/javascript/alchemy_admin/components/element_editor.js b/app/javascript/alchemy_admin/components/element_editor.js index d3bb5627bc..ac398cd157 100644 --- a/app/javascript/alchemy_admin/components/element_editor.js +++ b/app/javascript/alchemy_admin/components/element_editor.js @@ -1,4 +1,3 @@ -import TagsAutocomplete from "alchemy_admin/tags_autocomplete" import ImageLoader from "alchemy_admin/image_loader" import fileEditors from "alchemy_admin/file_editors" import pictureEditors from "alchemy_admin/picture_editors" @@ -44,7 +43,6 @@ export class ElementEditor extends HTMLElement { `#${this.id} .ingredient-editor.file, #${this.id} .ingredient-editor.audio, #${this.id} .ingredient-editor.video` ) pictureEditors(`#${this.id} .ingredient-editor.picture`) - TagsAutocomplete(this) } handleEvent(event) { diff --git a/app/javascript/alchemy_admin/components/tags_autocomplete.js b/app/javascript/alchemy_admin/components/tags_autocomplete.js new file mode 100644 index 0000000000..647a3341a0 --- /dev/null +++ b/app/javascript/alchemy_admin/components/tags_autocomplete.js @@ -0,0 +1,57 @@ +class TagsAutocomplete extends HTMLElement { + connectedCallback() { + this.classList.add("autocomplete_tag_list") + $(this.input).select2(this.select2Config) + } + + get input() { + return this.getElementsByTagName("input")[0] + } + + get select2Config() { + return { + tags: true, + tokenSeparators: [","], + openOnEnter: false, + minimumInputLength: 1, + createSearchChoice: this.#createSearchChoice, + ajax: { + url: this.getAttribute("url"), + dataType: "json", + data: (term) => { + return { term } + }, + results: (data) => { + return { results: data } + } + }, + initSelection: this.#initSelection + } + } + + #createSearchChoice(term, data) { + if ( + $(data).filter(function () { + return this.text.localeCompare(term) === 0 + }).length === 0 + ) { + return { + id: term, + text: term + } + } + } + + #initSelection(element, callback) { + const data = [] + $(element.val().split(",")).each(function () { + data.push({ + id: this.trim(), + text: this + }) + }) + callback(data) + } +} + +customElements.define("alchemy-tags-autocomplete", TagsAutocomplete) diff --git a/app/javascript/alchemy_admin/gui.js b/app/javascript/alchemy_admin/gui.js index 47d1f72dcc..83591deff3 100644 --- a/app/javascript/alchemy_admin/gui.js +++ b/app/javascript/alchemy_admin/gui.js @@ -1,12 +1,9 @@ -import TagsAutocomplete from "alchemy_admin/tags_autocomplete" - function init(scope) { if (!scope) { Alchemy.watchForDialogs() } Alchemy.Hotkeys(scope) Alchemy.ListFilter(scope) - TagsAutocomplete(scope) } export default { diff --git a/app/javascript/alchemy_admin/tags_autocomplete.js b/app/javascript/alchemy_admin/tags_autocomplete.js deleted file mode 100644 index 32b495a7a2..0000000000 --- a/app/javascript/alchemy_admin/tags_autocomplete.js +++ /dev/null @@ -1,46 +0,0 @@ -function createSearchChoice(term, data) { - if ( - $(data).filter(function () { - return this.text.localeCompare(term) === 0 - }).length === 0 - ) { - return { - id: term, - text: term - } - } -} - -function initSelection(element, callback) { - const data = [] - $(element.val().split(",")).each(function () { - data.push({ - id: this.trim(), - text: this - }) - }) - callback(data) -} - -export default function TagsAutocomplete(scope) { - const field = $("[data-autocomplete]", scope) - const url = field.data("autocomplete") - field.select2({ - tags: true, - tokenSeparators: [","], - minimumInputLength: 1, - openOnEnter: false, - createSearchChoice, - ajax: { - url, - dataType: "json", - data: (term) => { - return { term } - }, - results: (data) => { - return { results: data } - } - }, - initSelection - }) -} diff --git a/app/views/alchemy/admin/attachments/edit.html.erb b/app/views/alchemy/admin/attachments/edit.html.erb index f5eb0d1d79..c7856ab47f 100644 --- a/app/views/alchemy/admin/attachments/edit.html.erb +++ b/app/views/alchemy/admin/attachments/edit.html.erb @@ -2,9 +2,8 @@ url: {action: :update, q: search_filter_params[:q], page: params[:page]}) do |f| -%> <%= f.input :name, input_html: {autofocus: true} %> <%= f.input :file_name, input_html: {autofocus: true}, hint: Alchemy.t(:attachment_filename_notice) %> -
- <%= f.label :tag_list %> - <%= render 'alchemy/admin/partials/autocomplete_tag_list', f: f %> -
+ <%= render Alchemy::Admin::TagsAutocomplete.new do %> + <%= f.input :tag_list, input_html: { value: f.object.tag_list.join(",") } %> + <% end %> <%= f.submit Alchemy.t(:save) %> <% end %> diff --git a/app/views/alchemy/admin/elements/_element.html.erb b/app/views/alchemy/admin/elements/_element.html.erb index 59947d55da..a73bd5261b 100644 --- a/app/views/alchemy/admin/elements/_element.html.erb +++ b/app/views/alchemy/admin/elements/_element.html.erb @@ -53,10 +53,10 @@ <% end %> <% if element.taggable? %> -
+ <%= render Alchemy::Admin::TagsAutocomplete.new do %> <%= f.label :tag_list %> - <%= render 'alchemy/admin/partials/autocomplete_tag_list', f: f %> -
+ <%= f.text_field :tag_list, value: f.object.tag_list.join(",") %> + <% end %> <% end %> <% end %> diff --git a/app/views/alchemy/admin/layoutpages/edit.html.erb b/app/views/alchemy/admin/layoutpages/edit.html.erb index 8e07640b16..2f9fd8cd05 100644 --- a/app/views/alchemy/admin/layoutpages/edit.html.erb +++ b/app/views/alchemy/admin/layoutpages/edit.html.erb @@ -1,8 +1,7 @@ <%= alchemy_form_for [:admin, @page], class: 'edit_page' do |f| %> <%= f.input :name, autofocus: true %> -
- <%= f.label :tag_list %> - <%= render 'alchemy/admin/partials/autocomplete_tag_list', f: f %> -
+ <%= render Alchemy::Admin::TagsAutocomplete.new do %> + <%= f.input :tag_list, input_html: { value: f.object.tag_list.join(",") } %> + <% end %> <%= f.submit Alchemy.t(:save) %> <% end %> diff --git a/app/views/alchemy/admin/pages/_form.html.erb b/app/views/alchemy/admin/pages/_form.html.erb index 28809bbaf3..1a75b640bc 100644 --- a/app/views/alchemy/admin/pages/_form.html.erb +++ b/app/views/alchemy/admin/pages/_form.html.erb @@ -47,10 +47,9 @@ as: 'text', hint: Alchemy.t('pages.update.comma_seperated') %> -
- <%= f.label :tag_list %> - <%= render 'alchemy/admin/partials/autocomplete_tag_list', f: f %> -
+ <%= render Alchemy::Admin::TagsAutocomplete.new do %> + <%= f.input :tag_list, input_html: { value: f.object.tag_list.join(",") } %> + <% end %> <%= f.submit Alchemy.t(:save) %> <% end %> diff --git a/app/views/alchemy/admin/partials/_autocomplete_tag_list.html.erb b/app/views/alchemy/admin/partials/_autocomplete_tag_list.html.erb deleted file mode 100644 index 92511c498d..0000000000 --- a/app/views/alchemy/admin/partials/_autocomplete_tag_list.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= f.text_field :tag_list, value: f.object.tag_list.join(","), data: {autocomplete: alchemy.autocomplete_admin_tags_path} %> diff --git a/app/views/alchemy/admin/pictures/_form.html.erb b/app/views/alchemy/admin/pictures/_form.html.erb index d4cb16b42d..b249636e30 100644 --- a/app/views/alchemy/admin/pictures/_form.html.erb +++ b/app/views/alchemy/admin/pictures/_form.html.erb @@ -1,11 +1,11 @@ <%= alchemy_form_for [:admin, @picture] do |f| %> <%= f.input :name %> <%= render "picture_description_field", f: f %> -
+ <%= render Alchemy::Admin::TagsAutocomplete.new(additional_class: "input") do %> <%= f.label :tag_list %> - <%= render 'alchemy/admin/partials/autocomplete_tag_list', f: f %> + <%= f.text_field :tag_list, value: f.object.tag_list.join(",") %> <%= Alchemy.t('Please seperate the tags with commata') %> -
+ <% end %> <%= hidden_field_tag :q, search_filter_params[:q] %> <%= hidden_field_tag :size, @size %> <%= hidden_field_tag :tagged_with, search_filter_params[:tagged_with] %> diff --git a/app/views/alchemy/admin/pictures/edit_multiple.html.erb b/app/views/alchemy/admin/pictures/edit_multiple.html.erb index 0c2d07d955..6f17b5e0de 100644 --- a/app/views/alchemy/admin/pictures/edit_multiple.html.erb +++ b/app/views/alchemy/admin/pictures/edit_multiple.html.erb @@ -17,11 +17,11 @@ <%= text_field_tag :pictures_name %> -
+ <%= render Alchemy::Admin::TagsAutocomplete.new(additional_class: "input") do %> <%= label_tag :pictures_tag_list, Alchemy.t(:tags), class: 'control-label' %> - <%= text_field_tag :pictures_tag_list, @tags, data: {autocomplete: alchemy.autocomplete_admin_tags_path} %> + <%= text_field_tag :pictures_tag_list, @tags %> <%= Alchemy.t('Please seperate the tags with commata') %> -
+ <% end %>
<%= button_tag Alchemy.t(:save), name: nil, class: 'button' %> diff --git a/app/views/alchemy/admin/resources/_form.html.erb b/app/views/alchemy/admin/resources/_form.html.erb index f1519e1a5d..8435e59135 100644 --- a/app/views/alchemy/admin/resources/_form.html.erb +++ b/app/views/alchemy/admin/resources/_form.html.erb @@ -18,10 +18,9 @@ <% end %> <% end %> <% if f.object.respond_to?(:tag_list) %> -
- <%= f.label :tag_list %> - <%= render 'alchemy/admin/partials/autocomplete_tag_list', f: f %> -
+ <%= render Alchemy::Admin::TagsAutocomplete.new do %> + <%= f.input :tag_list, input_html: { value: f.object.tag_list.join(",") } %> + <% end %> <% end %> <%= f.submit Alchemy.t(:save) %> <% end %> diff --git a/config/locales/alchemy.en.yml b/config/locales/alchemy.en.yml index 888ca58819..855e44ccf6 100644 --- a/config/locales/alchemy.en.yml +++ b/config/locales/alchemy.en.yml @@ -414,6 +414,7 @@ en: delete_page: "Delete this page" delete_tag: "Delete tag" search_node: "Search menu node" + search_tag: "Search tag" document: "File" download_csv: "Download CSV" download_file: "Download file '%{filename}'" diff --git a/spec/components/alchemy/admin/tags_autocomplete_spec.rb b/spec/components/alchemy/admin/tags_autocomplete_spec.rb new file mode 100644 index 0000000000..7468ce1a84 --- /dev/null +++ b/spec/components/alchemy/admin/tags_autocomplete_spec.rb @@ -0,0 +1,36 @@ +require "rails_helper" + +RSpec.describe Alchemy::Admin::TagsAutocomplete, type: :component do + before do + render + end + + context "without parameters" do + subject(:render) do + render_inline(described_class.new) { "Page Select Content" } + end + + it "should render the component and render given block content" do + expect(page).to have_selector("alchemy-tags-autocomplete") + expect(page).to have_text("Page Select Content") + end + + it "should have the default placeholder" do + expect(page).to have_selector("alchemy-tags-autocomplete[placeholder='Search tag']") + end + + it "should have the default tags autocomplete - url" do + expect(page).to have_selector("alchemy-tags-autocomplete[url='/admin/tags/autocomplete']") + end + end + + context "with additional class" do + subject(:render) do + render_inline(described_class.new(additional_class: "foooo")) + end + + it "should have these class" do + expect(page).to have_selector("alchemy-tags-autocomplete.foooo") + end + end +end diff --git a/spec/features/admin/resources_integration_spec.rb b/spec/features/admin/resources_integration_spec.rb index de43c713e1..4c43664fef 100644 --- a/spec/features/admin/resources_integration_spec.rb +++ b/spec/features/admin/resources_integration_spec.rb @@ -290,7 +290,7 @@ context "with event that acts_as_taggable" do it "shows an autocomplete tag list in the form" do visit "/admin/events/new" - expect(page).to have_selector('input#event_tag_list[type="text"][data-autocomplete="/admin/tags/autocomplete"]') + expect(page).to have_selector('alchemy-tags-autocomplete input#event_tag_list[type="text"]') end context "with tagged events in the index view" do diff --git a/spec/javascript/alchemy_admin/components/element_editor.spec.js b/spec/javascript/alchemy_admin/components/element_editor.spec.js index 6f7c7c3bc7..8335bed7a4 100644 --- a/spec/javascript/alchemy_admin/components/element_editor.spec.js +++ b/spec/javascript/alchemy_admin/components/element_editor.spec.js @@ -1,4 +1,3 @@ -import TagsAutocomplete from "alchemy_admin/tags_autocomplete" import ImageLoader from "alchemy_admin/image_loader" import fileEditors from "alchemy_admin/file_editors" import pictureEditors from "alchemy_admin/picture_editors" @@ -6,13 +5,6 @@ import IngredientAnchorLink from "alchemy_admin/ingredient_anchor_link" import { ElementEditor } from "alchemy_admin/components/element_editor" import { renderComponent } from "./component.helper" -jest.mock("alchemy_admin/tags_autocomplete", () => { - return { - __esModule: true, - default: jest.fn() - } -}) - jest.mock("alchemy_admin/image_loader", () => { return { __esModule: true, @@ -141,7 +133,6 @@ describe("alchemy-element-editor", () => { describe("connectedCallback", () => { beforeEach(() => { - TagsAutocomplete.mockClear() ImageLoader.init.mockClear() fileEditors.mockClear() pictureEditors.mockClear() @@ -171,11 +162,6 @@ describe("alchemy-element-editor", () => { getComponent(html) expect(pictureEditors).toHaveBeenCalled() }) - - it("initializes tags autocomplete", () => { - getComponent(html) - expect(TagsAutocomplete).toHaveBeenCalled() - }) }) describe("on click", () => { diff --git a/spec/javascript/alchemy_admin/components/tags_autocomplete.spec.js b/spec/javascript/alchemy_admin/components/tags_autocomplete.spec.js new file mode 100644 index 0000000000..a3ccd07967 --- /dev/null +++ b/spec/javascript/alchemy_admin/components/tags_autocomplete.spec.js @@ -0,0 +1,38 @@ +import { renderComponent } from "./component.helper" + +// import jquery and append it to the window object +import jQuery from "jquery" +globalThis.$ = jQuery +globalThis.jQuery = jQuery + +import "alchemy_admin/components/tags_autocomplete" +import("assets/jquery_plugins/select2") + +describe("alchemy-tags-autocomplete", () => { + /** + * + * @type {HTMLElement | undefined} + */ + let component = undefined + + beforeEach(() => { + const html = ` + + + + ` + component = renderComponent("alchemy-tags-autocomplete", html) + }) + + it("should render the input field", () => { + expect(component.getElementsByTagName("input")[0]).toBeInstanceOf( + HTMLElement + ) + }) + + it("should initialize Select2", () => { + expect( + component.getElementsByClassName("select2-container").length + ).toEqual(1) + }) +}) From 56c74d1e5c29105b8692f786e7b0467c0a334398 Mon Sep 17 00:00:00 2001 From: Sascha Karnatz <122262394+sascha-karnatz@users.noreply.github.com> Date: Tue, 27 Feb 2024 10:15:53 +0100 Subject: [PATCH 2/2] Add missing translation The select page - translation for the PageSelect View Component was missing. --- config/locales/alchemy.en.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/locales/alchemy.en.yml b/config/locales/alchemy.en.yml index 855e44ccf6..1c010eb1b6 100644 --- a/config/locales/alchemy.en.yml +++ b/config/locales/alchemy.en.yml @@ -413,6 +413,7 @@ en: delete_node: "Delete this menu node" delete_page: "Delete this page" delete_tag: "Delete tag" + search_page: "Search page" search_node: "Search menu node" search_tag: "Search tag" document: "File"