Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove jQuery class from the repository topic box #30191

Merged
merged 18 commits into from
Mar 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 11 additions & 12 deletions templates/repo/home.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,21 @@
</div>
</form>
</div>
<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-1" id="repo-topics">
{{range .Topics}}<a class="ui repo-topic large label topic tw-m-0" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}
<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-2 tw-my-2" id="repo-topics">
{{/* it should match the code in issue-home.js */}}
{{range .Topics}}<a class="repo-topic ui large label" href="{{AppSubUrl}}/explore/repos?q={{.Name}}&topic=1">{{.Name}}</a>{{end}}
{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}<button id="manage_topic" class="btn interact-fg tw-text-12">{{ctx.Locale.Tr "repo.topic.manage_topics"}}</button>{{end}}
</div>
{{end}}
{{if and .Permission.IsAdmin (not .Repository.IsArchived)}}
<div class="ui form tw-hidden tw-flex tw-flex-col tw-mt-4" id="topic_edit">
<div class="field tw-flex-1 tw-mb-1">
<div class="ui fluid multiple search selection dropdown tw-flex-wrap" data-text-count-prompt="{{ctx.Locale.Tr "repo.topic.count_prompt"}}" data-text-format-prompt="{{ctx.Locale.Tr "repo.topic.format_prompt"}}">
<input type="hidden" name="topics" value="{{range $i, $v := .Topics}}{{.Name}}{{if Eval $i "+" 1 "<" (len $.Topics)}},{{end}}{{end}}">
{{range .Topics}}
{{/* keey the same layout as Fomantic UI generated labels */}}
<a class="ui label transition visible tw-cursor-default tw-inline-block" data-value="{{.Name}}">{{.Name}}{{svg "octicon-x" 16 "delete icon"}}</a>
{{end}}
<div class="text"></div>
</div>
<div class="ui form tw-hidden tw-flex tw-gap-2 tw-my-2" id="topic_edit">
<div class="ui fluid multiple search selection dropdown tw-flex-wrap tw-flex-1">
<input type="hidden" name="topics" value="{{range $i, $v := .Topics}}{{.Name}}{{if Eval $i "+" 1 "<" (len $.Topics)}},{{end}}{{end}}">
{{range .Topics}}
{{/* keep the same layout as Fomantic UI generated labels */}}
<a class="ui label transition visible tw-cursor-default tw-inline-block" data-value="{{.Name}}">{{.Name}}{{svg "octicon-x" 16 "delete icon"}}</a>
{{end}}
<div class="text"></div>
</div>
<div>
<button class="ui basic button" id="cancel_topic_edit">{{ctx.Locale.Tr "cancel"}}</button>
Expand Down
1 change: 1 addition & 0 deletions web_src/css/repo.css
Original file line number Diff line number Diff line change
Expand Up @@ -2437,6 +2437,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 {
Expand Down
86 changes: 25 additions & 61 deletions web_src/js/features/repo-home.js
Original file line number Diff line number Diff line change
@@ -1,55 +1,53 @@
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';
import {showErrorToast} from '../modules/toast.js';

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 $topicDropdown = $(topicDropdown);
const $topicForm = $(editDiv);
const $topicDropdownSearch = $topicDropdown.find('input.search');
const topicPrompts = {
countPrompt: topicDropdown.getAttribute('data-text-count-prompt') ?? undefined,
formatPrompt: topicDropdown.getAttribute('data-text-format-prompt') ?? undefined,
};
const topicDropdown = editDiv.querySelector('.ui.dropdown');
let lastErrorToast;

mgrBtn.addEventListener('click', () => {
hideElem(viewDiv);
showElem(editDiv);
$topicDropdownSearch.trigger('focus');
topicDropdown.querySelector('input.search').focus();
});

$('#cancel_topic_edit').on('click', () => {
document.querySelector('#cancel_topic_edit').addEventListener('click', () => {
lastErrorToast?.hideToast();
hideElem(editDiv);
showElem(viewDiv);
mgrBtn.focus();
});

saveBtn.addEventListener('click', async () => {
const topics = $('input[name=topics]').val();
document.getElementById('save_topic').addEventListener('click', async (e) => {
lastErrorToast?.hideToast();
const topics = editDiv.querySelector('input[name=topics]').value;

const data = new FormData();
data.append('topics', topics);

const response = await POST(saveBtn.getAttribute('data-link'), {data});
const response = await POST(e.target.getAttribute('data-link'), {data});

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
Expand All @@ -59,27 +57,23 @@ 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();
lastErrorToast = showErrorToast(responseData.message, {duration: 5000});
if (responseData.invalidTopics.length > 0) {
topicPrompts.formatPrompt = responseData.message;

const {invalidTopics} = responseData;
const $topicLabels = $topicDropdown.children('a.ui.label');
const topicLabels = queryElemChildren(topicDropdown, '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 {
topicPrompts.countPrompt = responseData.message;
}
}

// Always validate the form
$topicForm.form('validate form');
});

$topicDropdown.dropdown({
$(topicDropdown).dropdown({
allowAdditions: true,
forceSelection: false,
fullTextSearch: 'exact',
Expand All @@ -102,9 +96,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 queryElemChildren(topicDropdown, 'a.ui.label.visible')) {
current_topics.push(el.getAttribute('data-value'));
});
}

if (res.topics) {
let found = false;
Expand Down Expand Up @@ -146,38 +140,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.children('a.ui.label');
const status = !$topics.length || $topics.last()[0].getAttribute('data-value').match(regExp);
if (!status) {
$topics.last().removeClass('green').addClass('red');
}
return status && !$topicDropdown.children('a.ui.label.red').length;
};

$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);
},
});
}
1 change: 1 addition & 0 deletions web_src/js/modules/toast.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ function showToast(message, level, {gravity, position, duration, useHtmlBody, ..

toast.showToast();
toast.toastElement.querySelector('.toast-close').addEventListener('click', () => toast.hideToast());
return toast;
}

export function showInfoToast(message, opts) {
Expand Down
18 changes: 16 additions & 2 deletions web_src/js/utils/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(el);
}
wxiaoguang marked this conversation as resolved.
Show resolved Hide resolved
}
return elems;
}

export function queryElemSiblings(el, selector = '*', fn) {
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) {
return applyElemsCallback(parent.querySelectorAll(`:scope > ${selector}`), fn);
}

export function onDomReady(cb) {
Expand Down