mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	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:
		| @@ -1,5 +1,5 @@ | ||||
| {{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings labels")}} | ||||
| 				<div class="org-setting-content"> | ||||
| <div class="org-setting-content"> | ||||
| 	<div class="tw-flex tw-items-center"> | ||||
| 		<div class="tw-flex-1"> | ||||
| 			{{ctx.Locale.Tr "org.settings.labels_desc"}} | ||||
| @@ -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" .}} | ||||
| 				</div> | ||||
| {{template "repo/issue/labels/edit_delete_label" .}} | ||||
| 	{{template "repo/issue/labels/label_edit_modal" .}} | ||||
| </div> | ||||
| {{template "org/settings/layout_footer" .}} | ||||
|  | ||||
|   | ||||
| @@ -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> | ||||
| 	{{if and (or .CanWriteIssues .CanWritePulls) (not .Repository.IsArchived)}} | ||||
| 		{{template "repo/issue/labels/label_edit_modal" .}} | ||||
| 	{{end}} | ||||
| </div> | ||||
|  | ||||
| {{if and (or .CanWriteIssues .CanWritePulls) (not .Repository.IsArchived)}} | ||||
| 	{{template "repo/issue/labels/edit_delete_label" .}} | ||||
| {{end}} | ||||
| {{template "base/footer" .}} | ||||
|   | ||||
| @@ -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"> | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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> | ||||
| @@ -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%; | ||||
|   | ||||
| @@ -12,5 +12,5 @@ export function initCommonOrganization() { | ||||
|   }); | ||||
|  | ||||
|   // Labels | ||||
|   initCompLabelEdit('.organization.settings.labels'); | ||||
|   initCompLabelEdit('.page-content.organization.settings.labels'); | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
|   }; | ||||
|  | ||||
|   elModal.addEventListener('input', () => syncModalUi()); | ||||
|  | ||||
|   // 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 elEditLabelButtons = pageContent.querySelectorAll<HTMLElement>('.edit-label-button'); | ||||
|   for (const btn of elEditLabelButtons) { | ||||
|     btn.addEventListener('click', (e) => { | ||||
|       e.preventDefault(); | ||||
|       showLabelEditModal(btn); | ||||
|     }); | ||||
|  | ||||
|   // Edit label | ||||
|   $('.edit-label-button').on('click', function () { | ||||
|     $('#label-modal-id').val($(this).data('id')); | ||||
|  | ||||
|     const $nameInput = $('.edit-label .label-name-input'); | ||||
|     $nameInput.val($(this).data('title')); | ||||
|  | ||||
|     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'); | ||||
|   }); | ||||
| } | ||||
|   | ||||
| @@ -43,7 +43,7 @@ export function initRepository() { | ||||
|   initRepoCommentFormAndSidebar(); | ||||
|  | ||||
|   // Labels | ||||
|   initCompLabelEdit('.repository.labels'); | ||||
|   initCompLabelEdit('.page-content.repository.labels'); | ||||
|   initRepoMilestone(); | ||||
|   initRepoNew(); | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user