From c87286b310de2e50f7d4f7415ed781a8f8c35da3 Mon Sep 17 00:00:00 2001 From: Yarden Shoham Date: Sat, 30 Mar 2024 14:15:27 +0000 Subject: [PATCH 01/15] Remove jQuery class from the repository topic box - Switched from jQuery class functions to plain JavaScript `classList` - Tested the repository topic box functionality and it works as before Signed-off-by: Yarden Shoham --- web_src/js/features/repo-home.js | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/web_src/js/features/repo-home.js b/web_src/js/features/repo-home.js index e195c23c37278..af5bced3c0449 100644 --- a/web_src/js/features/repo-home.js +++ b/web_src/js/features/repo-home.js @@ -12,9 +12,11 @@ export function initRepoTopicBar() { const viewDiv = document.getElementById('repo-topics'); const saveBtn = document.getElementById('save_topic'); const topicDropdown = editDiv.querySelector('.dropdown'); - const $topicDropdown = $(topicDropdown); const $topicForm = $(editDiv); - const $topicDropdownSearch = $topicDropdown.find('input.search'); + /** + * @type {HTMLInputElement} + */ + const topicDropdownSearch = topicDropdown.querySelector('input.search'); const topicPrompts = { countPrompt: topicDropdown.getAttribute('data-text-count-prompt') ?? undefined, formatPrompt: topicDropdown.getAttribute('data-text-format-prompt') ?? undefined, @@ -23,7 +25,7 @@ export function initRepoTopicBar() { mgrBtn.addEventListener('click', () => { hideElem(viewDiv); showElem(editDiv); - $topicDropdownSearch.trigger('focus'); + topicDropdownSearch?.focus(); }); $('#cancel_topic_edit').on('click', () => { @@ -64,10 +66,11 @@ export function initRepoTopicBar() { topicPrompts.formatPrompt = responseData.message; const {invalidTopics} = responseData; - const $topicLabels = $topicDropdown.children('a.ui.label'); + const topicLabels = topicDropdown.querySelectorAll(':scope > a.ui.label'); for (const [index, value] of topics.split(',').entries()) { if (invalidTopics.includes(value)) { - $topicLabels.eq(index).removeClass('green').addClass('red'); + topicLabels[index].classList.remove('green'); + topicLabels[index].classList.add('red'); } } } else { @@ -79,7 +82,7 @@ export function initRepoTopicBar() { $topicForm.form('validate form'); }); - $topicDropdown.dropdown({ + $(topicDropdown).dropdown({ allowAdditions: true, forceSelection: false, fullTextSearch: 'exact', @@ -102,9 +105,9 @@ export function initRepoTopicBar() { const query = stripTags(this.urlData.query.trim()); let found_query = false; const current_topics = []; - $topicDropdown.find('a.label.visible').each((_, el) => { + for (const el of topicDropdown.querySelectorAll(':scope > a.label.visible')) { current_topics.push(el.getAttribute('data-value')); - }); + } if (res.topics) { let found = false; @@ -152,12 +155,14 @@ export function initRepoTopicBar() { }); $.fn.form.settings.rules.validateTopic = function (_values, regExp) { - const $topics = $topicDropdown.children('a.ui.label'); - const status = !$topics.length || $topics.last()[0].getAttribute('data-value').match(regExp); + const topics = topicDropdown.querySelectorAll(':scope > a.ui.label'); + const lastTopic = topics[topics.length - 1]; + const status = !topics.length || lastTopic.getAttribute('data-value').match(regExp); if (!status) { - $topics.last().removeClass('green').addClass('red'); + lastTopic.classList.remove('green'); + lastTopic.classList.add('red'); } - return status && !$topicDropdown.children('a.ui.label.red').length; + return status && !topicDropdown.querySelectorAll(':scope > a.ui.label.red').length; }; $topicForm.form({ From 925bf8069a0eb462720975cd7321a8271d2b8a2a Mon Sep 17 00:00:00 2001 From: Yarden Shoham Date: Sun, 31 Mar 2024 08:39:46 +0300 Subject: [PATCH 02/15] Update web_src/js/features/repo-home.js Co-authored-by: silverwind --- web_src/js/features/repo-home.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web_src/js/features/repo-home.js b/web_src/js/features/repo-home.js index af5bced3c0449..3b9faa927cad9 100644 --- a/web_src/js/features/repo-home.js +++ b/web_src/js/features/repo-home.js @@ -156,8 +156,7 @@ export function initRepoTopicBar() { $.fn.form.settings.rules.validateTopic = function (_values, regExp) { const topics = topicDropdown.querySelectorAll(':scope > a.ui.label'); - const lastTopic = topics[topics.length - 1]; - const status = !topics.length || lastTopic.getAttribute('data-value').match(regExp); + const status = !topics.length || (topics[topics.length - 1]).getAttribute('data-value').match(regExp); if (!status) { lastTopic.classList.remove('green'); lastTopic.classList.add('red'); From 76d91b0aa122d6c1b49f54f1323128f26de817db Mon Sep 17 00:00:00 2001 From: Yarden Shoham Date: Sun, 31 Mar 2024 05:42:04 +0000 Subject: [PATCH 03/15] Safer --- web_src/js/features/repo-home.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web_src/js/features/repo-home.js b/web_src/js/features/repo-home.js index 3b9faa927cad9..cabd7d6dd0b26 100644 --- a/web_src/js/features/repo-home.js +++ b/web_src/js/features/repo-home.js @@ -157,9 +157,9 @@ export function initRepoTopicBar() { $.fn.form.settings.rules.validateTopic = function (_values, regExp) { const topics = topicDropdown.querySelectorAll(':scope > a.ui.label'); const status = !topics.length || (topics[topics.length - 1]).getAttribute('data-value').match(regExp); - if (!status) { - lastTopic.classList.remove('green'); - lastTopic.classList.add('red'); + if (!status && topics[topics.length - 1]) { + (topics[topics.length - 1]).classList.remove('green'); + (topics[topics.length - 1]).classList.add('red'); } return status && !topicDropdown.querySelectorAll(':scope > a.ui.label.red').length; }; From de107e9a0c298a59e9284aa0a37fb374fa7dbb1a Mon Sep 17 00:00:00 2001 From: Yarden Shoham Date: Sun, 31 Mar 2024 08:55:44 +0300 Subject: [PATCH 04/15] Apply suggestions from code review Co-authored-by: wxiaoguang --- web_src/js/features/repo-home.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/web_src/js/features/repo-home.js b/web_src/js/features/repo-home.js index cabd7d6dd0b26..9962900dc85b6 100644 --- a/web_src/js/features/repo-home.js +++ b/web_src/js/features/repo-home.js @@ -25,7 +25,7 @@ export function initRepoTopicBar() { mgrBtn.addEventListener('click', () => { hideElem(viewDiv); showElem(editDiv); - topicDropdownSearch?.focus(); + topicDropdownSearch.focus(); }); $('#cancel_topic_edit').on('click', () => { @@ -105,7 +105,7 @@ export function initRepoTopicBar() { const query = stripTags(this.urlData.query.trim()); let found_query = false; const current_topics = []; - for (const el of topicDropdown.querySelectorAll(':scope > a.label.visible')) { + for (const el of topicDropdown.querySelectorAll('a.label.visible')) { current_topics.push(el.getAttribute('data-value')); } @@ -156,12 +156,13 @@ export function initRepoTopicBar() { $.fn.form.settings.rules.validateTopic = function (_values, regExp) { const topics = topicDropdown.querySelectorAll(':scope > a.ui.label'); - const status = !topics.length || (topics[topics.length - 1]).getAttribute('data-value').match(regExp); - if (!status && topics[topics.length - 1]) { - (topics[topics.length - 1]).classList.remove('green'); - (topics[topics.length - 1]).classList.add('red'); + const lastTopic = topics[topics.length - 1]; + const isLastTopicValid = lastTopic?.getAttribute('data-value').match(regExp); + if (lastTopic && !isLastTopicValid) { + lastTopic.classList.remove('green'); + lastTopic.classList.add('red'); } - return status && !topicDropdown.querySelectorAll(':scope > a.ui.label.red').length; + return topicDropdown.querySelectorAll('a.ui.label.red').length === 0; }; $topicForm.form({ From 2cc10ac988defedf20a5d86261ad7ebdb1019ce5 Mon Sep 17 00:00:00 2001 From: Yarden Shoham Date: Sun, 31 Mar 2024 06:00:31 +0000 Subject: [PATCH 05/15] Simplify --- web_src/js/features/repo-home.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/features/repo-home.js b/web_src/js/features/repo-home.js index 9962900dc85b6..d4bfb07280f01 100644 --- a/web_src/js/features/repo-home.js +++ b/web_src/js/features/repo-home.js @@ -66,7 +66,7 @@ export function initRepoTopicBar() { topicPrompts.formatPrompt = responseData.message; const {invalidTopics} = responseData; - const topicLabels = topicDropdown.querySelectorAll(':scope > a.ui.label'); + const topicLabels = topicDropdown.querySelectorAll('a.ui.label'); for (const [index, value] of topics.split(',').entries()) { if (invalidTopics.includes(value)) { topicLabels[index].classList.remove('green'); From bf7319c5bc184e87fedf049c223c45db2677a8ed Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 31 Mar 2024 14:45:20 +0800 Subject: [PATCH 06/15] refactor --- templates/repo/home.tmpl | 23 ++++++----- web_src/css/repo.css | 1 + web_src/js/features/repo-home.js | 68 +++++++------------------------- web_src/js/utils/dom.js | 18 ++++++++- 4 files changed, 43 insertions(+), 67 deletions(-) diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index 4241f77eaddde..59b5d0b0c614c 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -18,22 +18,21 @@ -
- {{range .Topics}}{{.Name}}{{end}} +
+ {{/* it should match the code in issue-home.js */}} + {{range .Topics}}{{.Name}}{{end}} {{if and .Permission.IsAdmin (not .Repository.IsArchived)}}{{end}}
{{end}} {{if and .Permission.IsAdmin (not .Repository.IsArchived)}} -
-
- +
+
diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 780093fb7fc55..d4051eba00cd0 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -2517,6 +2517,7 @@ tbody.commit-list { #repo-topics .repo-topic { font-weight: var(--font-weight-normal); cursor: pointer; + margin: 0; } #new-dependency-drop-list.ui.selection.dropdown { diff --git a/web_src/js/features/repo-home.js b/web_src/js/features/repo-home.js index d4bfb07280f01..5b252cda2b976 100644 --- a/web_src/js/features/repo-home.js +++ b/web_src/js/features/repo-home.js @@ -1,6 +1,6 @@ import $ from 'jquery'; import {stripTags} from '../utils.js'; -import {hideElem, showElem} from '../utils/dom.js'; +import {hideElem, queryElemChildren, showElem} from '../utils/dom.js'; import {POST} from '../modules/fetch.js'; const {appSubUrl} = window.config; @@ -8,34 +8,30 @@ const {appSubUrl} = window.config; export function initRepoTopicBar() { const mgrBtn = document.getElementById('manage_topic'); if (!mgrBtn) return; + const editDiv = document.getElementById('topic_edit'); const viewDiv = document.getElementById('repo-topics'); const saveBtn = document.getElementById('save_topic'); - const topicDropdown = editDiv.querySelector('.dropdown'); - const $topicForm = $(editDiv); - /** - * @type {HTMLInputElement} - */ - const topicDropdownSearch = topicDropdown.querySelector('input.search'); + const topicDropdown = editDiv.querySelector('.ui.dropdown'); const topicPrompts = { - countPrompt: topicDropdown.getAttribute('data-text-count-prompt') ?? undefined, - formatPrompt: topicDropdown.getAttribute('data-text-format-prompt') ?? undefined, + countPrompt: topicDropdown.getAttribute('data-text-count-prompt'), + formatPrompt: topicDropdown.getAttribute('data-text-format-prompt'), }; mgrBtn.addEventListener('click', () => { hideElem(viewDiv); showElem(editDiv); - topicDropdownSearch.focus(); + topicDropdown.querySelector('input.search').focus(); }); - $('#cancel_topic_edit').on('click', () => { + document.querySelector('#cancel_topic_edit').addEventListener('click', () => { hideElem(editDiv); showElem(viewDiv); mgrBtn.focus(); }); saveBtn.addEventListener('click', async () => { - const topics = $('input[name=topics]').val(); + const topics = editDiv.querySelector('input[name=topics]').value; const data = new FormData(); data.append('topics', topics); @@ -45,13 +41,14 @@ export function initRepoTopicBar() { if (response.ok) { const responseData = await response.json(); if (responseData.status === 'ok') { - $(viewDiv).children('.topic').remove(); + queryElemChildren(viewDiv, '.repo-topic', (el) => el.remove()); if (topics.length) { const topicArray = topics.split(','); topicArray.sort(); for (const topic of topicArray) { + // it should match the code in repo/home.tmpl const link = document.createElement('a'); - link.classList.add('ui', 'repo-topic', 'large', 'label', 'topic', 'tw-m-0'); + link.classList.add('repo-topic', 'ui', 'large', 'label'); link.href = `${appSubUrl}/explore/repos?q=${encodeURIComponent(topic)}&topic=1`; link.textContent = topic; mgrBtn.parentNode.insertBefore(link, mgrBtn); // insert all new topics before manage button @@ -66,7 +63,7 @@ export function initRepoTopicBar() { topicPrompts.formatPrompt = responseData.message; const {invalidTopics} = responseData; - const topicLabels = topicDropdown.querySelectorAll('a.ui.label'); + const topicLabels = queryElemChildren(topicDropdown,'a.ui.label'); for (const [index, value] of topics.split(',').entries()) { if (invalidTopics.includes(value)) { topicLabels[index].classList.remove('green'); @@ -77,9 +74,6 @@ export function initRepoTopicBar() { topicPrompts.countPrompt = responseData.message; } } - - // Always validate the form - $topicForm.form('validate form'); }); $(topicDropdown).dropdown({ @@ -105,7 +99,7 @@ export function initRepoTopicBar() { const query = stripTags(this.urlData.query.trim()); let found_query = false; const current_topics = []; - for (const el of topicDropdown.querySelectorAll('a.label.visible')) { + for (const el of queryElemChildren(topicDropdown, 'a.ui.label.visible')) { current_topics.push(el.getAttribute('data-value')); } @@ -149,40 +143,8 @@ export function initRepoTopicBar() { }, onAdd(addedValue, _addedText, $addedChoice) { addedValue = addedValue.toLowerCase().trim(); - $($addedChoice)[0].setAttribute('data-value', addedValue); - $($addedChoice)[0].setAttribute('data-text', addedValue); - }, - }); - - $.fn.form.settings.rules.validateTopic = function (_values, regExp) { - const topics = topicDropdown.querySelectorAll(':scope > a.ui.label'); - const lastTopic = topics[topics.length - 1]; - const isLastTopicValid = lastTopic?.getAttribute('data-value').match(regExp); - if (lastTopic && !isLastTopicValid) { - lastTopic.classList.remove('green'); - lastTopic.classList.add('red'); - } - return topicDropdown.querySelectorAll('a.ui.label.red').length === 0; - }; - - $topicForm.form({ - on: 'change', - inline: true, - fields: { - topics: { - identifier: 'topics', - rules: [ - { - type: 'validateTopic', - value: /^\s*[a-z0-9][-.a-z0-9]{0,35}\s*$/, - prompt: topicPrompts.formatPrompt, - }, - { - type: 'maxCount[25]', - prompt: topicPrompts.countPrompt, - }, - ], - }, + $addedChoice[0].setAttribute('data-value', addedValue); + $addedChoice[0].setAttribute('data-text', addedValue); }, }); } diff --git a/web_src/js/utils/dom.js b/web_src/js/utils/dom.js index fffe9c6109ba5..d6a05230cecb4 100644 --- a/web_src/js/utils/dom.js +++ b/web_src/js/utils/dom.js @@ -51,8 +51,22 @@ export function isElemHidden(el) { return res[0]; } -export function queryElemSiblings(el, selector = '*') { - return Array.from(el.parentNode.children).filter((child) => child !== el && child.matches(selector)); +function applyElemsCallback(elems, fn) { + if (fn) { + for (const el of elems) { + fn.call(el, el); // call the function with the element as "this", and pass the element as the first argument + } + } + return elems; +} + +export function queryElemSiblings(el, selector = '*', fn = null) { + return applyElemsCallback(Array.from(el.parentNode.children).filter((child) => child !== el && child.matches(selector)), fn); +} + +// it works like jQuery.children: only the direct children are selected +export function queryElemChildren(parent, selector, fn = null) { + return applyElemsCallback(parent.querySelectorAll(`:scope > ${selector}`), fn); } export function onDomReady(cb) { From b2dd122956452c5c4284852f57415c64024c484c Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 31 Mar 2024 14:49:45 +0800 Subject: [PATCH 07/15] fix lint --- web_src/js/features/repo-home.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/features/repo-home.js b/web_src/js/features/repo-home.js index 5b252cda2b976..5b6bd9b992bdd 100644 --- a/web_src/js/features/repo-home.js +++ b/web_src/js/features/repo-home.js @@ -63,7 +63,7 @@ export function initRepoTopicBar() { topicPrompts.formatPrompt = responseData.message; const {invalidTopics} = responseData; - const topicLabels = queryElemChildren(topicDropdown,'a.ui.label'); + const topicLabels = queryElemChildren(topicDropdown, 'a.ui.label'); for (const [index, value] of topics.split(',').entries()) { if (invalidTopics.includes(value)) { topicLabels[index].classList.remove('green'); From d539c67796d4b602a092cd83a828e9e2d9f1566d Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 31 Mar 2024 14:55:26 +0800 Subject: [PATCH 08/15] add comment --- web_src/js/features/repo-home.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/features/repo-home.js b/web_src/js/features/repo-home.js index 5b6bd9b992bdd..d161e2a0d3332 100644 --- a/web_src/js/features/repo-home.js +++ b/web_src/js/features/repo-home.js @@ -58,10 +58,10 @@ export function initRepoTopicBar() { showElem(viewDiv); } } else if (response.status === 422) { + // how to test: input topic like " invalid topic " (with spaces), and select it from the list, then "Save" const responseData = await response.json(); if (responseData.invalidTopics.length > 0) { topicPrompts.formatPrompt = responseData.message; - const {invalidTopics} = responseData; const topicLabels = queryElemChildren(topicDropdown, 'a.ui.label'); for (const [index, value] of topics.split(',').entries()) { From 809fd067535baba638b7e78d198e23eb8175fd95 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 31 Mar 2024 15:14:33 +0800 Subject: [PATCH 09/15] use toast to show error message --- templates/repo/home.tmpl | 2 +- web_src/js/features/repo-home.js | 17 +++++++---------- web_src/js/modules/toast.js | 1 + 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index 59b5d0b0c614c..b2e6cf9629951 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -26,7 +26,7 @@ {{end}} {{if and .Permission.IsAdmin (not .Repository.IsArchived)}}
-