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"