mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-04 15:04:00 +01:00 
			
		
		
		
	None of the frontend js/ts files was touched besides these two commands
(edit: no longer true, I touched one file in
61105d0618
because of a deprecation that was not showing before the rename).
`tsc` currently reports 778 errors, so I have disabled it in CI as
planned.
Everything appears to work fine.
		
	
			
		
			
				
	
	
		
			734 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			734 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import $ from 'jquery';
 | 
						|
import {htmlEscape} from 'escape-goat';
 | 
						|
import {createTippy, showTemporaryTooltip} from '../modules/tippy.ts';
 | 
						|
import {hideElem, showElem, toggleElem} from '../utils/dom.ts';
 | 
						|
import {setFileFolding} from './file-fold.ts';
 | 
						|
import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
 | 
						|
import {toAbsoluteUrl} from '../utils.ts';
 | 
						|
import {GET, POST} from '../modules/fetch.ts';
 | 
						|
import {showErrorToast} from '../modules/toast.ts';
 | 
						|
 | 
						|
const {appSubUrl} = window.config;
 | 
						|
 | 
						|
export function initRepoIssueTimeTracking() {
 | 
						|
  $(document).on('click', '.issue-add-time', () => {
 | 
						|
    $('.issue-start-time-modal').modal({
 | 
						|
      duration: 200,
 | 
						|
      onApprove() {
 | 
						|
        $('#add_time_manual_form').trigger('submit');
 | 
						|
      },
 | 
						|
    }).modal('show');
 | 
						|
    $('.issue-start-time-modal input').on('keydown', (e) => {
 | 
						|
      if (e.key === 'Enter') {
 | 
						|
        $('#add_time_manual_form').trigger('submit');
 | 
						|
      }
 | 
						|
    });
 | 
						|
  });
 | 
						|
  $(document).on('click', '.issue-start-time, .issue-stop-time', () => {
 | 
						|
    $('#toggle_stopwatch_form').trigger('submit');
 | 
						|
  });
 | 
						|
  $(document).on('click', '.issue-cancel-time', () => {
 | 
						|
    $('#cancel_stopwatch_form').trigger('submit');
 | 
						|
  });
 | 
						|
  $(document).on('click', 'button.issue-delete-time', function () {
 | 
						|
    const sel = `.issue-delete-time-modal[data-id="${$(this).data('id')}"]`;
 | 
						|
    $(sel).modal({
 | 
						|
      duration: 200,
 | 
						|
      onApprove() {
 | 
						|
        $(`${sel} form`).trigger('submit');
 | 
						|
      },
 | 
						|
    }).modal('show');
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
async function updateDeadline(deadlineString) {
 | 
						|
  hideElem('#deadline-err-invalid-date');
 | 
						|
  document.querySelector('#deadline-loader')?.classList.add('is-loading');
 | 
						|
 | 
						|
  let realDeadline = null;
 | 
						|
  if (deadlineString !== '') {
 | 
						|
    const newDate = Date.parse(deadlineString);
 | 
						|
 | 
						|
    if (Number.isNaN(newDate)) {
 | 
						|
      document.querySelector('#deadline-loader')?.classList.remove('is-loading');
 | 
						|
      showElem('#deadline-err-invalid-date');
 | 
						|
      return false;
 | 
						|
    }
 | 
						|
    realDeadline = new Date(newDate);
 | 
						|
  }
 | 
						|
 | 
						|
  try {
 | 
						|
    const response = await POST(document.querySelector('#update-issue-deadline-form').getAttribute('action'), {
 | 
						|
      data: {due_date: realDeadline},
 | 
						|
    });
 | 
						|
 | 
						|
    if (response.ok) {
 | 
						|
      window.location.reload();
 | 
						|
    } else {
 | 
						|
      throw new Error('Invalid response');
 | 
						|
    }
 | 
						|
  } catch (error) {
 | 
						|
    console.error(error);
 | 
						|
    document.querySelector('#deadline-loader').classList.remove('is-loading');
 | 
						|
    showElem('#deadline-err-invalid-date');
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export function initRepoIssueDue() {
 | 
						|
  $(document).on('click', '.issue-due-edit', () => {
 | 
						|
    toggleElem('#deadlineForm');
 | 
						|
  });
 | 
						|
  $(document).on('click', '.issue-due-remove', () => {
 | 
						|
    updateDeadline('');
 | 
						|
  });
 | 
						|
  $(document).on('submit', '.issue-due-form', () => {
 | 
						|
    updateDeadline($('#deadlineDate').val());
 | 
						|
    return false;
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * @param {HTMLElement} item
 | 
						|
 */
 | 
						|
function excludeLabel(item) {
 | 
						|
  const href = item.getAttribute('href');
 | 
						|
  const id = item.getAttribute('data-label-id');
 | 
						|
 | 
						|
  const regStr = `labels=((?:-?[0-9]+%2c)*)(${id})((?:%2c-?[0-9]+)*)&`;
 | 
						|
  const newStr = 'labels=$1-$2$3&';
 | 
						|
 | 
						|
  window.location = href.replace(new RegExp(regStr), newStr);
 | 
						|
}
 | 
						|
 | 
						|
export function initRepoIssueSidebarList() {
 | 
						|
  const repolink = $('#repolink').val();
 | 
						|
  const repoId = $('#repoId').val();
 | 
						|
  const crossRepoSearch = $('#crossRepoSearch').val();
 | 
						|
  const tp = $('#type').val();
 | 
						|
  let issueSearchUrl = `${appSubUrl}/${repolink}/issues/search?q={query}&type=${tp}`;
 | 
						|
  if (crossRepoSearch === 'true') {
 | 
						|
    issueSearchUrl = `${appSubUrl}/issues/search?q={query}&priority_repo_id=${repoId}&type=${tp}`;
 | 
						|
  }
 | 
						|
  $('#new-dependency-drop-list')
 | 
						|
    .dropdown({
 | 
						|
      apiSettings: {
 | 
						|
        url: issueSearchUrl,
 | 
						|
        onResponse(response) {
 | 
						|
          const filteredResponse = {success: true, results: []};
 | 
						|
          const currIssueId = $('#new-dependency-drop-list').data('issue-id');
 | 
						|
          // Parse the response from the api to work with our dropdown
 | 
						|
          $.each(response, (_i, issue) => {
 | 
						|
            // Don't list current issue in the dependency list.
 | 
						|
            if (issue.id === currIssueId) {
 | 
						|
              return;
 | 
						|
            }
 | 
						|
            filteredResponse.results.push({
 | 
						|
              name: `<div class="gt-ellipsis">#${issue.number} ${htmlEscape(issue.title)}</div>
 | 
						|
<div class="text small tw-break-anywhere">${htmlEscape(issue.repository.full_name)}</div>`,
 | 
						|
              value: issue.id,
 | 
						|
            });
 | 
						|
          });
 | 
						|
          return filteredResponse;
 | 
						|
        },
 | 
						|
        cache: false,
 | 
						|
      },
 | 
						|
 | 
						|
      fullTextSearch: true,
 | 
						|
    });
 | 
						|
 | 
						|
  $('.menu a.label-filter-item').each(function () {
 | 
						|
    $(this).on('click', function (e) {
 | 
						|
      if (e.altKey) {
 | 
						|
        e.preventDefault();
 | 
						|
        excludeLabel(this);
 | 
						|
      }
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  $('.menu .ui.dropdown.label-filter').on('keydown', (e) => {
 | 
						|
    if (e.altKey && e.key === 'Enter') {
 | 
						|
      const selectedItem = document.querySelector('.menu .ui.dropdown.label-filter .menu .item.selected');
 | 
						|
      if (selectedItem) {
 | 
						|
        excludeLabel(selectedItem);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  });
 | 
						|
  $('.ui.dropdown.label-filter, .ui.dropdown.select-label').dropdown('setting', {'hideDividers': 'empty'}).dropdown('refreshItems');
 | 
						|
}
 | 
						|
 | 
						|
export function initRepoIssueCommentDelete() {
 | 
						|
  // Delete comment
 | 
						|
  document.addEventListener('click', async (e) => {
 | 
						|
    if (!e.target.matches('.delete-comment')) return;
 | 
						|
    e.preventDefault();
 | 
						|
 | 
						|
    const deleteButton = e.target;
 | 
						|
    if (window.confirm(deleteButton.getAttribute('data-locale'))) {
 | 
						|
      try {
 | 
						|
        const response = await POST(deleteButton.getAttribute('data-url'));
 | 
						|
        if (!response.ok) throw new Error('Failed to delete comment');
 | 
						|
 | 
						|
        const conversationHolder = deleteButton.closest('.conversation-holder');
 | 
						|
        const parentTimelineItem = deleteButton.closest('.timeline-item');
 | 
						|
        const parentTimelineGroup = deleteButton.closest('.timeline-item-group');
 | 
						|
 | 
						|
        // Check if this was a pending comment.
 | 
						|
        if (conversationHolder?.querySelector('.pending-label')) {
 | 
						|
          const counter = document.querySelector('#review-box .review-comments-counter');
 | 
						|
          let num = parseInt(counter?.getAttribute('data-pending-comment-number')) - 1 || 0;
 | 
						|
          num = Math.max(num, 0);
 | 
						|
          counter.setAttribute('data-pending-comment-number', num);
 | 
						|
          counter.textContent = String(num);
 | 
						|
        }
 | 
						|
 | 
						|
        document.querySelector(`#${deleteButton.getAttribute('data-comment-id')}`)?.remove();
 | 
						|
 | 
						|
        if (conversationHolder && !conversationHolder.querySelector('.comment')) {
 | 
						|
          const path = conversationHolder.getAttribute('data-path');
 | 
						|
          const side = conversationHolder.getAttribute('data-side');
 | 
						|
          const idx = conversationHolder.getAttribute('data-idx');
 | 
						|
          const lineType = conversationHolder.closest('tr').getAttribute('data-line-type');
 | 
						|
 | 
						|
          if (lineType === 'same') {
 | 
						|
            document.querySelector(`[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`).classList.remove('tw-invisible');
 | 
						|
          } else {
 | 
						|
            document.querySelector(`[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`).classList.remove('tw-invisible');
 | 
						|
          }
 | 
						|
 | 
						|
          conversationHolder.remove();
 | 
						|
        }
 | 
						|
 | 
						|
        // Check if there is no review content, move the time avatar upward to avoid overlapping the content below.
 | 
						|
        if (!parentTimelineGroup?.querySelector('.timeline-item.comment') && !parentTimelineItem?.querySelector('.conversation-holder')) {
 | 
						|
          const timelineAvatar = parentTimelineGroup?.querySelector('.timeline-avatar');
 | 
						|
          timelineAvatar?.classList.remove('timeline-avatar-offset');
 | 
						|
        }
 | 
						|
      } catch (error) {
 | 
						|
        console.error(error);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
export function initRepoIssueDependencyDelete() {
 | 
						|
  // Delete Issue dependency
 | 
						|
  $(document).on('click', '.delete-dependency-button', (e) => {
 | 
						|
    const id = e.currentTarget.getAttribute('data-id');
 | 
						|
    const type = e.currentTarget.getAttribute('data-type');
 | 
						|
 | 
						|
    $('.remove-dependency').modal({
 | 
						|
      closable: false,
 | 
						|
      duration: 200,
 | 
						|
      onApprove: () => {
 | 
						|
        $('#removeDependencyID').val(id);
 | 
						|
        $('#dependencyType').val(type);
 | 
						|
        $('#removeDependencyForm').trigger('submit');
 | 
						|
      },
 | 
						|
    }).modal('show');
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
export function initRepoIssueCodeCommentCancel() {
 | 
						|
  // Cancel inline code comment
 | 
						|
  document.addEventListener('click', (e) => {
 | 
						|
    if (!e.target.matches('.cancel-code-comment')) return;
 | 
						|
 | 
						|
    const form = e.target.closest('form');
 | 
						|
    if (form?.classList.contains('comment-form')) {
 | 
						|
      hideElem(form);
 | 
						|
      showElem(form.closest('.comment-code-cloud')?.querySelectorAll('button.comment-form-reply'));
 | 
						|
    } else {
 | 
						|
      form.closest('.comment-code-cloud')?.remove();
 | 
						|
    }
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
export function initRepoPullRequestUpdate() {
 | 
						|
  // Pull Request update button
 | 
						|
  const pullUpdateButton = document.querySelector('.update-button > button');
 | 
						|
  if (!pullUpdateButton) return;
 | 
						|
 | 
						|
  pullUpdateButton.addEventListener('click', async function (e) {
 | 
						|
    e.preventDefault();
 | 
						|
    const redirect = this.getAttribute('data-redirect');
 | 
						|
    this.classList.add('is-loading');
 | 
						|
    let response;
 | 
						|
    try {
 | 
						|
      response = await POST(this.getAttribute('data-do'));
 | 
						|
    } catch (error) {
 | 
						|
      console.error(error);
 | 
						|
    } finally {
 | 
						|
      this.classList.remove('is-loading');
 | 
						|
    }
 | 
						|
    let data;
 | 
						|
    try {
 | 
						|
      data = await response?.json(); // the response is probably not a JSON
 | 
						|
    } catch (error) {
 | 
						|
      console.error(error);
 | 
						|
    }
 | 
						|
    if (data?.redirect) {
 | 
						|
      window.location.href = data.redirect;
 | 
						|
    } else if (redirect) {
 | 
						|
      window.location.href = redirect;
 | 
						|
    } else {
 | 
						|
      window.location.reload();
 | 
						|
    }
 | 
						|
  });
 | 
						|
 | 
						|
  $('.update-button > .dropdown').dropdown({
 | 
						|
    onChange(_text, _value, $choice) {
 | 
						|
      const choiceEl = $choice[0];
 | 
						|
      const url = choiceEl.getAttribute('data-do');
 | 
						|
      if (url) {
 | 
						|
        const buttonText = pullUpdateButton.querySelector('.button-text');
 | 
						|
        if (buttonText) {
 | 
						|
          buttonText.textContent = choiceEl.textContent;
 | 
						|
        }
 | 
						|
        pullUpdateButton.setAttribute('data-do', url);
 | 
						|
      }
 | 
						|
    },
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
export function initRepoPullRequestMergeInstruction() {
 | 
						|
  $('.show-instruction').on('click', () => {
 | 
						|
    toggleElem($('.instruct-content'));
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
export function initRepoPullRequestAllowMaintainerEdit() {
 | 
						|
  const wrapper = document.querySelector('#allow-edits-from-maintainers');
 | 
						|
  if (!wrapper) return;
 | 
						|
  const checkbox = wrapper.querySelector('input[type="checkbox"]');
 | 
						|
  checkbox.addEventListener('input', async () => {
 | 
						|
    const url = `${wrapper.getAttribute('data-url')}/set_allow_maintainer_edit`;
 | 
						|
    wrapper.classList.add('is-loading');
 | 
						|
    try {
 | 
						|
      const resp = await POST(url, {data: new URLSearchParams({allow_maintainer_edit: checkbox.checked})});
 | 
						|
      if (!resp.ok) {
 | 
						|
        throw new Error('Failed to update maintainer edit permission');
 | 
						|
      }
 | 
						|
      const data = await resp.json();
 | 
						|
      checkbox.checked = data.allow_maintainer_edit;
 | 
						|
    } catch (error) {
 | 
						|
      checkbox.checked = !checkbox.checked;
 | 
						|
      console.error(error);
 | 
						|
      showTemporaryTooltip(wrapper, wrapper.getAttribute('data-prompt-error'));
 | 
						|
    } finally {
 | 
						|
      wrapper.classList.remove('is-loading');
 | 
						|
    }
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
export function initRepoIssueReferenceRepositorySearch() {
 | 
						|
  $('.issue_reference_repository_search')
 | 
						|
    .dropdown({
 | 
						|
      apiSettings: {
 | 
						|
        url: `${appSubUrl}/repo/search?q={query}&limit=20`,
 | 
						|
        onResponse(response) {
 | 
						|
          const filteredResponse = {success: true, results: []};
 | 
						|
          $.each(response.data, (_r, repo) => {
 | 
						|
            filteredResponse.results.push({
 | 
						|
              name: htmlEscape(repo.repository.full_name),
 | 
						|
              value: repo.repository.full_name,
 | 
						|
            });
 | 
						|
          });
 | 
						|
          return filteredResponse;
 | 
						|
        },
 | 
						|
        cache: false,
 | 
						|
      },
 | 
						|
      onChange(_value, _text, $choice) {
 | 
						|
        const $form = $choice.closest('form');
 | 
						|
        if (!$form.length) return;
 | 
						|
 | 
						|
        $form[0].setAttribute('action', `${appSubUrl}/${_text}/issues/new`);
 | 
						|
      },
 | 
						|
      fullTextSearch: true,
 | 
						|
    });
 | 
						|
}
 | 
						|
 | 
						|
export function initRepoIssueWipTitle() {
 | 
						|
  $('.title_wip_desc > a').on('click', (e) => {
 | 
						|
    e.preventDefault();
 | 
						|
 | 
						|
    const $issueTitle = $('#issue_title');
 | 
						|
    $issueTitle.trigger('focus');
 | 
						|
    const value = $issueTitle.val().trim().toUpperCase();
 | 
						|
 | 
						|
    const wipPrefixes = $('.title_wip_desc').data('wip-prefixes');
 | 
						|
    for (const prefix of wipPrefixes) {
 | 
						|
      if (value.startsWith(prefix.toUpperCase())) {
 | 
						|
        return;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    $issueTitle.val(`${wipPrefixes[0]} ${$issueTitle.val()}`);
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
export async function updateIssuesMeta(url, action, issue_ids, id) {
 | 
						|
  try {
 | 
						|
    const response = await POST(url, {data: new URLSearchParams({action, issue_ids, id})});
 | 
						|
    if (!response.ok) {
 | 
						|
      throw new Error('Failed to update issues meta');
 | 
						|
    }
 | 
						|
  } catch (error) {
 | 
						|
    console.error(error);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export function initRepoIssueComments() {
 | 
						|
  if (!$('.repository.view.issue .timeline').length) return;
 | 
						|
 | 
						|
  $('.re-request-review').on('click', async function (e) {
 | 
						|
    e.preventDefault();
 | 
						|
    const url = this.getAttribute('data-update-url');
 | 
						|
    const issueId = this.getAttribute('data-issue-id');
 | 
						|
    const id = this.getAttribute('data-id');
 | 
						|
    const isChecked = this.classList.contains('checked');
 | 
						|
 | 
						|
    await updateIssuesMeta(url, isChecked ? 'detach' : 'attach', issueId, id);
 | 
						|
    window.location.reload();
 | 
						|
  });
 | 
						|
 | 
						|
  document.addEventListener('click', (e) => {
 | 
						|
    const urlTarget = document.querySelector(':target');
 | 
						|
    if (!urlTarget) return;
 | 
						|
 | 
						|
    const urlTargetId = urlTarget.id;
 | 
						|
    if (!urlTargetId) return;
 | 
						|
 | 
						|
    if (!/^(issue|pull)(comment)?-\d+$/.test(urlTargetId)) return;
 | 
						|
 | 
						|
    if (!e.target.closest(`#${urlTargetId}`)) {
 | 
						|
      const scrollPosition = $(window).scrollTop();
 | 
						|
      window.location.hash = '';
 | 
						|
      $(window).scrollTop(scrollPosition);
 | 
						|
      window.history.pushState(null, null, ' ');
 | 
						|
    }
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
export async function handleReply(el) {
 | 
						|
  const form = el.closest('.comment-code-cloud').querySelector('.comment-form');
 | 
						|
  const textarea = form.querySelector('textarea');
 | 
						|
 | 
						|
  hideElem(el);
 | 
						|
  showElem(form);
 | 
						|
  const editor = getComboMarkdownEditor(textarea) ?? await initComboMarkdownEditor(form.querySelector('.combo-markdown-editor'));
 | 
						|
  editor.focus();
 | 
						|
  return editor;
 | 
						|
}
 | 
						|
 | 
						|
export function initRepoPullRequestReview() {
 | 
						|
  if (window.location.hash && window.location.hash.startsWith('#issuecomment-')) {
 | 
						|
    // set scrollRestoration to 'manual' when there is a hash in url, so that the scroll position will not be remembered after refreshing
 | 
						|
    if (window.history.scrollRestoration !== 'manual') {
 | 
						|
      window.history.scrollRestoration = 'manual';
 | 
						|
    }
 | 
						|
    const commentDiv = document.querySelector(window.location.hash);
 | 
						|
    if (commentDiv) {
 | 
						|
      // get the name of the parent id
 | 
						|
      const groupID = commentDiv.closest('div[id^="code-comments-"]')?.getAttribute('id');
 | 
						|
      if (groupID && groupID.startsWith('code-comments-')) {
 | 
						|
        const id = groupID.slice(14);
 | 
						|
        const ancestorDiffBox = commentDiv.closest('.diff-file-box');
 | 
						|
        // on pages like conversation, there is no diff header
 | 
						|
        const diffHeader = ancestorDiffBox?.querySelector('.diff-file-header');
 | 
						|
 | 
						|
        // offset is for scrolling
 | 
						|
        let offset = 30;
 | 
						|
        if (diffHeader) {
 | 
						|
          offset += $('.diff-detail-box').outerHeight() + $(diffHeader).outerHeight();
 | 
						|
        }
 | 
						|
 | 
						|
        hideElem(`#show-outdated-${id}`);
 | 
						|
        showElem(`#code-comments-${id}, #code-preview-${id}, #hide-outdated-${id}`);
 | 
						|
        // if the comment box is folded, expand it
 | 
						|
        if (ancestorDiffBox?.getAttribute('data-folded') === 'true') {
 | 
						|
          setFileFolding(ancestorDiffBox, ancestorDiffBox.querySelector('.fold-file'), false);
 | 
						|
        }
 | 
						|
 | 
						|
        window.scrollTo({
 | 
						|
          top: $(commentDiv).offset().top - offset,
 | 
						|
          behavior: 'instant',
 | 
						|
        });
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  $(document).on('click', '.show-outdated', function (e) {
 | 
						|
    e.preventDefault();
 | 
						|
    const id = this.getAttribute('data-comment');
 | 
						|
    hideElem(this);
 | 
						|
    showElem(`#code-comments-${id}`);
 | 
						|
    showElem(`#code-preview-${id}`);
 | 
						|
    showElem(`#hide-outdated-${id}`);
 | 
						|
  });
 | 
						|
 | 
						|
  $(document).on('click', '.hide-outdated', function (e) {
 | 
						|
    e.preventDefault();
 | 
						|
    const id = this.getAttribute('data-comment');
 | 
						|
    hideElem(this);
 | 
						|
    hideElem(`#code-comments-${id}`);
 | 
						|
    hideElem(`#code-preview-${id}`);
 | 
						|
    showElem(`#show-outdated-${id}`);
 | 
						|
  });
 | 
						|
 | 
						|
  $(document).on('click', 'button.comment-form-reply', async function (e) {
 | 
						|
    e.preventDefault();
 | 
						|
    await handleReply(this);
 | 
						|
  });
 | 
						|
 | 
						|
  const $reviewBox = $('.review-box-panel');
 | 
						|
  if ($reviewBox.length === 1) {
 | 
						|
    const _promise = initComboMarkdownEditor($reviewBox.find('.combo-markdown-editor'));
 | 
						|
  }
 | 
						|
 | 
						|
  // The following part is only for diff views
 | 
						|
  if (!$('.repository.pull.diff').length) return;
 | 
						|
 | 
						|
  const $reviewBtn = $('.js-btn-review');
 | 
						|
  const $panel = $reviewBtn.parent().find('.review-box-panel');
 | 
						|
  const $closeBtn = $panel.find('.close');
 | 
						|
 | 
						|
  if ($reviewBtn.length && $panel.length) {
 | 
						|
    const tippy = createTippy($reviewBtn[0], {
 | 
						|
      content: $panel[0],
 | 
						|
      theme: 'default',
 | 
						|
      placement: 'bottom',
 | 
						|
      trigger: 'click',
 | 
						|
      maxWidth: 'none',
 | 
						|
      interactive: true,
 | 
						|
      hideOnClick: true,
 | 
						|
    });
 | 
						|
 | 
						|
    $closeBtn.on('click', (e) => {
 | 
						|
      e.preventDefault();
 | 
						|
      tippy.hide();
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  $(document).on('click', '.add-code-comment', async function (e) {
 | 
						|
    if (e.target.classList.contains('btn-add-single')) return; // https://github.com/go-gitea/gitea/issues/4745
 | 
						|
    e.preventDefault();
 | 
						|
 | 
						|
    const isSplit = this.closest('.code-diff')?.classList.contains('code-diff-split');
 | 
						|
    const side = this.getAttribute('data-side');
 | 
						|
    const idx = this.getAttribute('data-idx');
 | 
						|
    const path = this.closest('[data-path]')?.getAttribute('data-path');
 | 
						|
    const tr = this.closest('tr');
 | 
						|
    const lineType = tr.getAttribute('data-line-type');
 | 
						|
 | 
						|
    const ntr = tr.nextElementSibling;
 | 
						|
    let $ntr = $(ntr);
 | 
						|
    if (!ntr?.classList.contains('add-comment')) {
 | 
						|
      $ntr = $(`
 | 
						|
        <tr class="add-comment" data-line-type="${lineType}">
 | 
						|
          ${isSplit ? `
 | 
						|
            <td class="add-comment-left" colspan="4"></td>
 | 
						|
            <td class="add-comment-right" colspan="4"></td>
 | 
						|
          ` : `
 | 
						|
            <td class="add-comment-left add-comment-right" colspan="5"></td>
 | 
						|
          `}
 | 
						|
        </tr>`);
 | 
						|
      $(tr).after($ntr);
 | 
						|
    }
 | 
						|
 | 
						|
    const $td = $ntr.find(`.add-comment-${side}`);
 | 
						|
    const $commentCloud = $td.find('.comment-code-cloud');
 | 
						|
    if (!$commentCloud.length && !$ntr.find('button[name="pending_review"]').length) {
 | 
						|
      try {
 | 
						|
        const response = await GET(this.closest('[data-new-comment-url]')?.getAttribute('data-new-comment-url'));
 | 
						|
        const html = await response.text();
 | 
						|
        $td.html(html);
 | 
						|
        $td.find("input[name='line']").val(idx);
 | 
						|
        $td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed');
 | 
						|
        $td.find("input[name='path']").val(path);
 | 
						|
        const editor = await initComboMarkdownEditor($td.find('.combo-markdown-editor'));
 | 
						|
        editor.focus();
 | 
						|
      } catch (error) {
 | 
						|
        console.error(error);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
export function initRepoIssueReferenceIssue() {
 | 
						|
  // Reference issue
 | 
						|
  $(document).on('click', '.reference-issue', function (event) {
 | 
						|
    const target = this.getAttribute('data-target');
 | 
						|
    const content = document.querySelector(`#${target}`)?.textContent ?? '';
 | 
						|
    const poster = this.getAttribute('data-poster-username');
 | 
						|
    const reference = toAbsoluteUrl(this.getAttribute('data-reference'));
 | 
						|
    const modalSelector = this.getAttribute('data-modal');
 | 
						|
    const modal = document.querySelector(modalSelector);
 | 
						|
    const textarea = modal.querySelector('textarea[name="content"]');
 | 
						|
    textarea.value = `${content}\n\n_Originally posted by @${poster} in ${reference}_`;
 | 
						|
    $(modal).modal('show');
 | 
						|
    event.preventDefault();
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
export function initRepoIssueWipToggle() {
 | 
						|
  // Toggle WIP
 | 
						|
  $('.toggle-wip a, .toggle-wip button').on('click', async (e) => {
 | 
						|
    e.preventDefault();
 | 
						|
    const toggleWip = e.currentTarget.closest('.toggle-wip');
 | 
						|
    const title = toggleWip.getAttribute('data-title');
 | 
						|
    const wipPrefix = toggleWip.getAttribute('data-wip-prefix');
 | 
						|
    const updateUrl = toggleWip.getAttribute('data-update-url');
 | 
						|
 | 
						|
    try {
 | 
						|
      const params = new URLSearchParams();
 | 
						|
      params.append('title', title?.startsWith(wipPrefix) ? title.slice(wipPrefix.length).trim() : `${wipPrefix.trim()} ${title}`);
 | 
						|
 | 
						|
      const response = await POST(updateUrl, {data: params});
 | 
						|
      if (!response.ok) {
 | 
						|
        throw new Error('Failed to toggle WIP status');
 | 
						|
      }
 | 
						|
      window.location.reload();
 | 
						|
    } catch (error) {
 | 
						|
      console.error(error);
 | 
						|
    }
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
export function initRepoIssueTitleEdit() {
 | 
						|
  const issueTitleDisplay = document.querySelector('#issue-title-display');
 | 
						|
  const issueTitleEditor = document.querySelector('#issue-title-editor');
 | 
						|
  if (!issueTitleEditor) return;
 | 
						|
 | 
						|
  const issueTitleInput = issueTitleEditor.querySelector('input');
 | 
						|
  const oldTitle = issueTitleInput.getAttribute('data-old-title');
 | 
						|
  issueTitleDisplay.querySelector('#issue-title-edit-show').addEventListener('click', () => {
 | 
						|
    hideElem(issueTitleDisplay);
 | 
						|
    hideElem('#pull-desc-display');
 | 
						|
    showElem(issueTitleEditor);
 | 
						|
    showElem('#pull-desc-editor');
 | 
						|
    if (!issueTitleInput.value.trim()) {
 | 
						|
      issueTitleInput.value = oldTitle;
 | 
						|
    }
 | 
						|
    issueTitleInput.focus();
 | 
						|
  });
 | 
						|
  issueTitleEditor.querySelector('.ui.cancel.button').addEventListener('click', () => {
 | 
						|
    hideElem(issueTitleEditor);
 | 
						|
    hideElem('#pull-desc-editor');
 | 
						|
    showElem(issueTitleDisplay);
 | 
						|
    showElem('#pull-desc-display');
 | 
						|
  });
 | 
						|
 | 
						|
  const pullDescEditor = document.querySelector('#pull-desc-editor'); // it may not exist for a merged PR
 | 
						|
  const prTargetUpdateUrl = pullDescEditor?.getAttribute('data-target-update-url');
 | 
						|
 | 
						|
  const editSaveButton = issueTitleEditor.querySelector('.ui.primary.button');
 | 
						|
  editSaveButton.addEventListener('click', async () => {
 | 
						|
    const newTitle = issueTitleInput.value.trim();
 | 
						|
    try {
 | 
						|
      if (newTitle && newTitle !== oldTitle) {
 | 
						|
        const resp = await POST(editSaveButton.getAttribute('data-update-url'), {data: new URLSearchParams({title: newTitle})});
 | 
						|
        if (!resp.ok) {
 | 
						|
          throw new Error(`Failed to update issue title: ${resp.statusText}`);
 | 
						|
        }
 | 
						|
      }
 | 
						|
      if (prTargetUpdateUrl) {
 | 
						|
        const newTargetBranch = document.querySelector('#pull-target-branch').getAttribute('data-branch');
 | 
						|
        const oldTargetBranch = document.querySelector('#branch_target').textContent;
 | 
						|
        if (newTargetBranch !== oldTargetBranch) {
 | 
						|
          const resp = await POST(prTargetUpdateUrl, {data: new URLSearchParams({target_branch: newTargetBranch})});
 | 
						|
          if (!resp.ok) {
 | 
						|
            throw new Error(`Failed to update PR target branch: ${resp.statusText}`);
 | 
						|
          }
 | 
						|
        }
 | 
						|
      }
 | 
						|
      window.location.reload();
 | 
						|
    } catch (error) {
 | 
						|
      console.error(error);
 | 
						|
      showErrorToast(error.message);
 | 
						|
    }
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
export function initRepoIssueBranchSelect() {
 | 
						|
  document.querySelector('#branch-select')?.addEventListener('click', (e) => {
 | 
						|
    const el = e.target.closest('.item[data-branch]');
 | 
						|
    if (!el) return;
 | 
						|
    const pullTargetBranch = document.querySelector('#pull-target-branch');
 | 
						|
    const baseName = pullTargetBranch.getAttribute('data-basename');
 | 
						|
    const branchNameNew = el.getAttribute('data-branch');
 | 
						|
    const branchNameOld = pullTargetBranch.getAttribute('data-branch');
 | 
						|
    pullTargetBranch.textContent = pullTargetBranch.textContent.replace(`${baseName}:${branchNameOld}`, `${baseName}:${branchNameNew}`);
 | 
						|
    pullTargetBranch.setAttribute('data-branch', branchNameNew);
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
export async function initSingleCommentEditor($commentForm) {
 | 
						|
  // pages:
 | 
						|
  // * normal new issue/pr page: no status-button, no comment-button (there is only a normal submit button which can submit empty content)
 | 
						|
  // * issue/pr view page: with comment form, has status-button and comment-button
 | 
						|
  const opts = {};
 | 
						|
  const statusButton = document.querySelector('#status-button');
 | 
						|
  const commentButton = document.querySelector('#comment-button');
 | 
						|
  opts.onContentChanged = (editor) => {
 | 
						|
    const editorText = editor.value().trim();
 | 
						|
    if (statusButton) {
 | 
						|
      statusButton.textContent = statusButton.getAttribute(editorText ? 'data-status-and-comment' : 'data-status');
 | 
						|
    }
 | 
						|
    if (commentButton) {
 | 
						|
      commentButton.disabled = !editorText;
 | 
						|
    }
 | 
						|
  };
 | 
						|
  const editor = await initComboMarkdownEditor($commentForm.find('.combo-markdown-editor'), opts);
 | 
						|
  opts.onContentChanged(editor); // sync state of buttons with the initial content
 | 
						|
}
 | 
						|
 | 
						|
export function initIssueTemplateCommentEditors($commentForm) {
 | 
						|
  // pages:
 | 
						|
  // * new issue with issue template
 | 
						|
  const $comboFields = $commentForm.find('.combo-editor-dropzone');
 | 
						|
 | 
						|
  const initCombo = async ($combo) => {
 | 
						|
    const $dropzoneContainer = $combo.find('.form-field-dropzone');
 | 
						|
    const $formField = $combo.find('.form-field-real');
 | 
						|
    const $markdownEditor = $combo.find('.combo-markdown-editor');
 | 
						|
 | 
						|
    const editor = await initComboMarkdownEditor($markdownEditor, {
 | 
						|
      onContentChanged: (editor) => {
 | 
						|
        $formField.val(editor.value());
 | 
						|
      },
 | 
						|
    });
 | 
						|
 | 
						|
    $formField.on('focus', async () => {
 | 
						|
      // deactivate all markdown editors
 | 
						|
      showElem($commentForm.find('.combo-editor-dropzone .form-field-real'));
 | 
						|
      hideElem($commentForm.find('.combo-editor-dropzone .combo-markdown-editor'));
 | 
						|
      hideElem($commentForm.find('.combo-editor-dropzone .form-field-dropzone'));
 | 
						|
 | 
						|
      // activate this markdown editor
 | 
						|
      hideElem($formField);
 | 
						|
      showElem($markdownEditor);
 | 
						|
      showElem($dropzoneContainer);
 | 
						|
 | 
						|
      await editor.switchToUserPreference();
 | 
						|
      editor.focus();
 | 
						|
    });
 | 
						|
  };
 | 
						|
 | 
						|
  for (const el of $comboFields) {
 | 
						|
    initCombo($(el));
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
// This function used to show and hide archived label on issue/pr
 | 
						|
//  page in the sidebar where we select the labels
 | 
						|
//  If we have any archived label tagged to issue and pr. We will show that
 | 
						|
//  archived label with checked classed otherwise we will hide it
 | 
						|
//  with the help of this function.
 | 
						|
//  This function runs globally.
 | 
						|
export function initArchivedLabelHandler() {
 | 
						|
  if (!document.querySelector('.archived-label-hint')) return;
 | 
						|
  for (const label of document.querySelectorAll('[data-is-archived]')) {
 | 
						|
    toggleElem(label, label.classList.contains('checked'));
 | 
						|
  }
 | 
						|
}
 |