Refactor LabelEdit (#32752)

And fix a regression:
https://github.com/go-gitea/gitea/pull/30053#discussion_r1874405470

Major changes:

* rewrite without jquery
* remove the "delete modal", using "link-action" is good enough
* merge "new modal" and "edit modal"
This commit is contained in:
wxiaoguang 2024-12-08 10:35:28 +08:00 committed by GitHub
parent a78a466383
commit 96d3a03a08
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 110 additions and 193 deletions

View File

@ -7,9 +7,7 @@
<button class="ui small primary new-label button">{{ctx.Locale.Tr "repo.issues.new_label"}}</button>
</div>
<div class="divider"></div>
{{template "repo/issue/labels/label_new" .}}
{{template "repo/issue/labels/label_list" .}}
{{template "repo/issue/labels/label_edit_modal" .}}
</div>
{{template "repo/issue/labels/edit_delete_label" .}}
{{template "org/settings/layout_footer" .}}

View File

@ -8,15 +8,11 @@
<button class="ui small primary new-label button">{{ctx.Locale.Tr "repo.issues.new_label"}}</button>
{{end}}
</div>
{{if and (or .CanWriteIssues .CanWritePulls) (not .Repository.IsArchived)}}
{{template "repo/issue/labels/label_new" .}}
{{end}}
{{template "base/alert" .}}
{{template "repo/issue/labels/label_list" .}}
</div>
</div>
{{if and (or .CanWriteIssues .CanWritePulls) (not .Repository.IsArchived)}}
{{template "repo/issue/labels/edit_delete_label" .}}
{{template "repo/issue/labels/label_edit_modal" .}}
{{end}}
</div>
{{template "base/footer" .}}

View File

@ -1,22 +1,13 @@
<div class="ui g-modal-confirm delete modal">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "repo.issues.label_deletion"}}
</div>
<div class="ui small modal" id="issue-label-edit-modal"
data-current-page-link="{{$.Link}}"{{/*will be used to construct "new label" and "edit label" URLs*/}}
data-text-new-label="{{ctx.Locale.Tr "repo.issues.new_label"}}"
data-text-edit-label="{{ctx.Locale.Tr "repo.issues.label_modify"}}"
>
<div class="header"></div>
<div class="content">
<p>{{ctx.Locale.Tr "repo.issues.label_deletion_desc"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</div>
<div class="ui small edit-label modal">
<div class="header">
{{ctx.Locale.Tr "repo.issues.label_modify"}}
</div>
<div class="content">
<form class="ui edit-label form ignore-dirty" action="{{$.Link}}/edit" method="post">
<form class="ui form ignore-dirty" method="post">
{{.CsrfTokenHtml}}
<input id="label-modal-id" name="id" type="hidden">
<input name="id" type="hidden">
<div class="required field">
<label for="name">{{ctx.Locale.Tr "repo.issues.label_title"}}</label>
<div class="ui small input">

View File

@ -27,6 +27,8 @@
{{end}}
<ul class="issue-label-list">
{{$canEditLabel := and (not $.PageIsOrgSettingsLabels) (not $.Repository.IsArchived) (or $.CanWriteIssues $.CanWritePulls)}}
{{$canEditLabel = or $canEditLabel $.PageIsOrgSettingsLabels}}
{{range .Labels}}
<li class="item">
<div class="label-title">
@ -43,12 +45,16 @@
<div class="label-operation tw-flex">
{{template "repo/issue/labels/label_archived" .}}
<div class="tw-flex tw-ml-auto">
{{if and (not $.PageIsOrgSettingsLabels) (not $.Repository.IsArchived) (or $.CanWriteIssues $.CanWritePulls)}}
<a class="edit-label-button" href="#" data-id="{{.ID}}" data-title="{{.Name}}" {{if .Exclusive}}data-exclusive{{end}} {{if gt .ArchivedUnix 0}}data-is-archived{{end}} data-num-issues="{{.NumIssues}}" data-description="{{.Description}}" data-color={{.Color}}>{{svg "octicon-pencil"}} {{ctx.Locale.Tr "repo.issues.label_edit"}}</a>
<a class="delete-button" href="#" data-url="{{$.Link}}/delete" data-id="{{.ID}}">{{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.issues.label_delete"}}</a>
{{else if $.PageIsOrgSettingsLabels}}
<a class="edit-label-button" href="#" data-id="{{.ID}}" data-title="{{.Name}}" {{if .Exclusive}}data-exclusive{{end}} {{if gt .ArchivedUnix 0}}data-is-archived{{end}} data-num-issues="{{.NumIssues}}" data-description="{{.Description}}" data-color={{.Color}}>{{svg "octicon-pencil"}} {{ctx.Locale.Tr "repo.issues.label_edit"}}</a>
<a class="delete-button" href="#" data-url="{{$.Link}}/delete" data-id="{{.ID}}">{{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.issues.label_delete"}}</a>
{{if $canEditLabel}}
<a class="edit-label-button" href="#"
data-label-id="{{.ID}}" data-label-name="{{.Name}}" data-label-color="{{.Color}}"
data-label-exclusive="{{.Exclusive}}" data-label-is-archived="{{gt .ArchivedUnix 0}}"
data-label-num-issues="{{.NumIssues}}" data-label-description="{{.Description}}"
>{{svg "octicon-pencil"}} {{ctx.Locale.Tr "repo.issues.label_edit"}}</a>
<a class="link-action" href="#" data-url="{{$.Link}}/delete?id={{.ID}}"
data-modal-confirm-header="{{ctx.Locale.Tr "repo.issues.label_deletion"}}"
data-modal-confirm-content="{{ctx.Locale.Tr "repo.issues.label_deletion_desc"}}"
>{{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.issues.label_delete"}}</a>
{{end}}
</div>
</div>

View File

@ -1,48 +0,0 @@
<div class="ui small new-label modal">
<div class="header">
{{ctx.Locale.Tr "repo.issues.new_label"}}
</div>
<div class="content">
<form class="ui new-label form ignore-dirty" action="{{$.Link}}/new" method="post">
{{.CsrfTokenHtml}}
<div class="required field">
<label for="name">{{ctx.Locale.Tr "repo.issues.label_title"}}</label>
<div class="ui small input">
<input class="label-name-input" name="title" placeholder="{{ctx.Locale.Tr "repo.issues.new_label_placeholder"}}" autofocus required maxlength="50">
</div>
</div>
<div class="field label-exclusive-input-field">
<div class="ui checkbox">
<input class="label-exclusive-input" name="exclusive" type="checkbox">
<label>{{ctx.Locale.Tr "repo.issues.label_exclusive"}}</label>
</div>
<br>
<small class="desc">{{ctx.Locale.Tr "repo.issues.label_exclusive_desc"}}</small>
</div>
<div class="field">
<label for="description">{{ctx.Locale.Tr "repo.issues.label_description"}}</label>
<div class="ui small fluid input">
<input class="label-desc-input" name="description" placeholder="{{ctx.Locale.Tr "repo.issues.new_label_desc_placeholder"}}" maxlength="200">
</div>
</div>
<div class="field color-field">
<label for="color">{{ctx.Locale.Tr "repo.issues.label_color"}}</label>
<div class="js-color-picker-input column">
<input name="color" value="#70c24a" placeholder="#c320f6" required maxlength="7">
{{template "repo/issue/label_precolors"}}
</div>
</div>
</form>
</div>
<div class="actions">
<button class="ui cancel button">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "cancel"}}
</button>
<button class="ui primary ok button">
{{svg "octicon-check"}}
{{ctx.Locale.Tr "repo.issues.create_label"}}
</button>
</div>
</div>

View File

@ -2061,17 +2061,6 @@ td .commit-summary {
padding: 1em;
}
.edit-label.modal .form .column,
.new-label.modal .form .column {
padding-right: 0;
}
.edit-label.modal .form .buttons,
.new-label.modal .form .buttons {
margin-left: auto;
padding-top: 15px;
}
.stats-table {
display: table;
width: 100%;

View File

@ -12,5 +12,5 @@ export function initCommonOrganization() {
});
// Labels
initCompLabelEdit('.organization.settings.labels');
initCompLabelEdit('.page-content.organization.settings.labels');
}

View File

@ -1,96 +1,81 @@
import $ from 'jquery';
import {toggleElem} from '../../utils/dom.ts';
import {fomanticQuery} from '../../modules/fomantic/base.ts';
function isExclusiveScopeName(name) {
function nameHasScope(name: string): boolean {
return /.*[^/]\/[^/].*/.test(name);
}
function updateExclusiveLabelEdit(form) {
const nameInput = document.querySelector(`${form} .label-name-input`);
const exclusiveField = document.querySelector(`${form} .label-exclusive-input-field`);
const exclusiveCheckbox = document.querySelector(`${form} .label-exclusive-input`);
const exclusiveWarning = document.querySelector(`${form} .label-exclusive-warning`);
export function initCompLabelEdit(pageSelector: string) {
const pageContent = document.querySelector<HTMLElement>(pageSelector);
if (!pageContent) return;
if (isExclusiveScopeName(nameInput.value)) {
exclusiveField?.classList.remove('muted');
exclusiveField?.removeAttribute('aria-disabled');
if (exclusiveCheckbox.checked && exclusiveCheckbox.getAttribute('data-exclusive-warn')) {
exclusiveWarning?.classList.remove('tw-hidden');
} else {
exclusiveWarning?.classList.add('tw-hidden');
}
} else {
exclusiveField?.classList.add('muted');
exclusiveField?.setAttribute('aria-disabled', 'true');
exclusiveWarning?.classList.add('tw-hidden');
}
}
// for guest view, the modal is not available, the "labels" are read-only
const elModal = pageContent.querySelector<HTMLElement>('#issue-label-edit-modal');
if (!elModal) return;
export function initCompLabelEdit(selector) {
if (!$(selector).length) return;
const elLabelId = elModal.querySelector<HTMLInputElement>('input[name="id"]');
const elNameInput = elModal.querySelector<HTMLInputElement>('.label-name-input');
const elExclusiveField = elModal.querySelector('.label-exclusive-input-field');
const elExclusiveInput = elModal.querySelector<HTMLInputElement>('.label-exclusive-input');
const elExclusiveWarning = elModal.querySelector('.label-exclusive-warning');
const elIsArchivedField = elModal.querySelector('.label-is-archived-input-field');
const elIsArchivedInput = elModal.querySelector<HTMLInputElement>('.label-is-archived-input');
const elDescInput = elModal.querySelector<HTMLInputElement>('.label-desc-input');
const elColorInput = elModal.querySelector<HTMLInputElement>('.js-color-picker-input input');
// Create label
$('.new-label.button').on('click', () => {
updateExclusiveLabelEdit('.new-label');
$('.new-label.modal').modal({
const syncModalUi = () => {
const hasScope = nameHasScope(elNameInput.value);
elExclusiveField.classList.toggle('disabled', !hasScope);
const showExclusiveWarning = hasScope && elExclusiveInput.checked && elModal.hasAttribute('data-need-warn-exclusive');
toggleElem(elExclusiveWarning, showExclusiveWarning);
if (!hasScope) elExclusiveInput.checked = false;
};
const showLabelEditModal = (btn:HTMLElement) => {
// the "btn" should contain the label's attributes by its `data-label-xxx` attributes
const form = elModal.querySelector<HTMLFormElement>('form');
elLabelId.value = btn.getAttribute('data-label-id') || '';
elNameInput.value = btn.getAttribute('data-label-name') || '';
elIsArchivedInput.checked = btn.getAttribute('data-label-is-archived') === 'true';
elExclusiveInput.checked = btn.getAttribute('data-label-exclusive') === 'true';
elDescInput.value = btn.getAttribute('data-label-description') || '';
elColorInput.value = btn.getAttribute('data-label-color') || '';
elColorInput.dispatchEvent(new Event('input', {bubbles: true})); // trigger the color picker
// if label id exists: "edit label" mode; otherwise: "new label" mode
const isEdit = Boolean(elLabelId.value);
// if a label was not exclusive but has issues, then it should warn user if it will become exclusive
const numIssues = parseInt(btn.getAttribute('data-label-num-issues') || '0');
elModal.toggleAttribute('data-need-warn-exclusive', !elExclusiveInput.checked && numIssues > 0);
elModal.querySelector('.header').textContent = isEdit ? elModal.getAttribute('data-text-edit-label') : elModal.getAttribute('data-text-new-label');
const curPageLink = elModal.getAttribute('data-current-page-link');
form.action = isEdit ? `${curPageLink}/edit` : `${curPageLink}/new`;
toggleElem(elIsArchivedField, isEdit);
syncModalUi();
fomanticQuery(elModal).modal({
onApprove() {
const form = document.querySelector('.new-label.form');
if (!form.checkValidity()) {
form.reportValidity();
return false;
}
$('.new-label.form').trigger('submit');
form.submit();
},
}).modal('show');
return false;
});
};
// Edit label
$('.edit-label-button').on('click', function () {
$('#label-modal-id').val($(this).data('id'));
elModal.addEventListener('input', () => syncModalUi());
const $nameInput = $('.edit-label .label-name-input');
$nameInput.val($(this).data('title'));
// theoretically, if the modal exists, the "new label" button should also exist, just in case it doesn't, use "?."
const elNewLabel = pageContent.querySelector<HTMLElement>('.ui.button.new-label');
elNewLabel?.addEventListener('click', () => showLabelEditModal(elNewLabel));
const $isArchivedCheckbox = $('.edit-label .label-is-archived-input');
$isArchivedCheckbox[0].checked = this.hasAttribute('data-is-archived');
const $exclusiveCheckbox = $('.edit-label .label-exclusive-input');
$exclusiveCheckbox[0].checked = this.hasAttribute('data-exclusive');
// Warn when label was previously not exclusive and used in issues
$exclusiveCheckbox.data('exclusive-warn',
$(this).data('num-issues') > 0 &&
(!this.hasAttribute('data-exclusive') || !isExclusiveScopeName($nameInput.val())));
updateExclusiveLabelEdit('.edit-label');
$('.edit-label .label-desc-input').val(this.getAttribute('data-description'));
const colorInput = document.querySelector('.edit-label .js-color-picker-input input');
colorInput.value = this.getAttribute('data-color');
colorInput.dispatchEvent(new Event('input', {bubbles: true}));
$('.edit-label.modal').modal({
onApprove() {
const form = document.querySelector('.edit-label.form');
if (!form.checkValidity()) {
form.reportValidity();
return false;
}
$('.edit-label.form').trigger('submit');
},
}).modal('show');
return false;
});
$('.new-label .label-name-input').on('input', () => {
updateExclusiveLabelEdit('.new-label');
});
$('.new-label .label-exclusive-input').on('change', () => {
updateExclusiveLabelEdit('.new-label');
});
$('.edit-label .label-name-input').on('input', () => {
updateExclusiveLabelEdit('.edit-label');
});
$('.edit-label .label-exclusive-input').on('change', () => {
updateExclusiveLabelEdit('.edit-label');
const elEditLabelButtons = pageContent.querySelectorAll<HTMLElement>('.edit-label-button');
for (const btn of elEditLabelButtons) {
btn.addEventListener('click', (e) => {
e.preventDefault();
showLabelEditModal(btn);
});
}
}

View File

@ -43,7 +43,7 @@ export function initRepository() {
initRepoCommentFormAndSidebar();
// Labels
initCompLabelEdit('.repository.labels');
initCompLabelEdit('.page-content.repository.labels');
initRepoMilestone();
initRepoNew();