From c1167709ed1cba035d8c0809a6da6d5d1c8638e5 Mon Sep 17 00:00:00 2001
From: wxiaoguang <wxiaoguang@gmail.com>
Date: Thu, 2 Jan 2025 01:21:13 +0800
Subject: [PATCH] Refactor repo-new.ts (#33070)

1. merge `repo-template.ts` into `repo-new.ts` (they are all for "/repo/create")
2. remove jquery
3. fix an anonying fomantic dropdown bug, see the comment of `onResponseKeepSelectedItem`
---
 web_src/js/features/repo-new.ts         | 49 +++++++++++++++++++++++-
 web_src/js/features/repo-template.ts    | 51 -------------------------
 web_src/js/globals.d.ts                 |  3 +-
 web_src/js/index.ts                     |  2 -
 web_src/js/modules/fomantic.ts          |  3 +-
 web_src/js/modules/fomantic/dropdown.ts | 18 +++++++++
 6 files changed, 69 insertions(+), 57 deletions(-)
 delete mode 100644 web_src/js/features/repo-template.ts

diff --git a/web_src/js/features/repo-new.ts b/web_src/js/features/repo-new.ts
index 101545735f..8a77a77b4a 100644
--- a/web_src/js/features/repo-new.ts
+++ b/web_src/js/features/repo-new.ts
@@ -1,10 +1,53 @@
-import {hideElem, showElem} from '../utils/dom.ts';
+import {hideElem, showElem, toggleElem} from '../utils/dom.ts';
+import {htmlEscape} from 'escape-goat';
+import {fomanticQuery} from '../modules/fomantic/base.ts';
+
+const {appSubUrl} = window.config;
+
+function initRepoNewTemplateSearch(form: HTMLFormElement) {
+  const inputRepoOwnerUid = form.querySelector<HTMLInputElement>('#uid');
+  const elRepoTemplateDropdown = form.querySelector<HTMLInputElement>('#repo_template_search');
+  const inputRepoTemplate = form.querySelector<HTMLInputElement>('#repo_template');
+  const elTemplateUnits = form.querySelector('#template_units');
+  const elNonTemplate = form.querySelector('#non_template');
+  const checkTemplate = function () {
+    const hasSelectedTemplate = inputRepoTemplate.value !== '' && inputRepoTemplate.value !== '0';
+    toggleElem(elTemplateUnits, hasSelectedTemplate);
+    toggleElem(elNonTemplate, !hasSelectedTemplate);
+  };
+  inputRepoTemplate.addEventListener('change', checkTemplate);
+  checkTemplate();
+
+  const $dropdown = fomanticQuery(elRepoTemplateDropdown);
+  const onChangeOwner = function () {
+    $dropdown.dropdown('setting', {
+      apiSettings: {
+        url: `${appSubUrl}/repo/search?q={query}&template=true&priority_owner_id=${inputRepoOwnerUid.value}`,
+        onResponse(response) {
+          const results = [];
+          results.push({name: '', value: ''}); // empty item means not using template
+          for (const tmplRepo of response.data) {
+            results.push({
+              name: htmlEscape(tmplRepo.repository.full_name),
+              value: String(tmplRepo.repository.id),
+            });
+          }
+          $dropdown.fomanticExt.onResponseKeepSelectedItem($dropdown, inputRepoTemplate.value);
+          return {results};
+        },
+        cache: false,
+      },
+    });
+  };
+  inputRepoOwnerUid.addEventListener('change', onChangeOwner);
+  onChangeOwner();
+}
 
 export function initRepoNew() {
   const pageContent = document.querySelector('.page-content.repository.new-repo');
   if (!pageContent) return;
 
-  const form = document.querySelector('.new-repo-form');
+  const form = document.querySelector<HTMLFormElement>('.new-repo-form');
   const inputGitIgnores = form.querySelector<HTMLInputElement>('input[name="gitignores"]');
   const inputLicense = form.querySelector<HTMLInputElement>('input[name="license"]');
   const inputAutoInit = form.querySelector<HTMLInputElement>('input[name="auto_init"]');
@@ -32,4 +75,6 @@ export function initRepoNew() {
   };
   inputRepoName.addEventListener('input', updateUiRepoName);
   updateUiRepoName();
+
+  initRepoNewTemplateSearch(form);
 }
diff --git a/web_src/js/features/repo-template.ts b/web_src/js/features/repo-template.ts
deleted file mode 100644
index fbd7b656ed..0000000000
--- a/web_src/js/features/repo-template.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import $ from 'jquery';
-import {htmlEscape} from 'escape-goat';
-import {hideElem, showElem} from '../utils/dom.ts';
-
-const {appSubUrl} = window.config;
-
-export function initRepoTemplateSearch() {
-  const $repoTemplate = $('#repo_template');
-  const checkTemplate = function () {
-    const $templateUnits = $('#template_units');
-    const $nonTemplate = $('#non_template');
-    if ($repoTemplate.val() !== '' && $repoTemplate.val() !== '0') {
-      showElem($templateUnits);
-      hideElem($nonTemplate);
-    } else {
-      hideElem($templateUnits);
-      showElem($nonTemplate);
-    }
-  };
-  $repoTemplate.on('change', checkTemplate);
-  checkTemplate();
-
-  const changeOwner = function () {
-    $('#repo_template_search')
-      .dropdown({
-        apiSettings: {
-          url: `${appSubUrl}/repo/search?q={query}&template=true&priority_owner_id=${$('#uid').val()}`,
-          onResponse(response) {
-            const filteredResponse = {success: true, results: []};
-            filteredResponse.results.push({
-              name: '',
-              value: '',
-            });
-            // Parse the response from the api to work with our dropdown
-            $.each(response.data, (_r, repo) => {
-              filteredResponse.results.push({
-                name: htmlEscape(repo.repository.full_name),
-                value: repo.repository.id,
-              });
-            });
-            return filteredResponse;
-          },
-          cache: false,
-        },
-
-        fullTextSearch: true,
-      });
-  };
-  $('#uid').on('change', changeOwner);
-  changeOwner();
-}
diff --git a/web_src/js/globals.d.ts b/web_src/js/globals.d.ts
index c08ff9976b..0c540ac296 100644
--- a/web_src/js/globals.d.ts
+++ b/web_src/js/globals.d.ts
@@ -36,8 +36,9 @@ declare module 'swagger-ui-dist/swagger-ui-es-bundle.js' {
 }
 
 interface JQuery {
-  api: any, // fomantic
   areYouSure: any, // jquery.are-you-sure
+  fomanticExt: any; // fomantic extension
+  api: any, // fomantic
   dimmer: any, // fomantic
   dropdown: any; // fomantic
   modal: any; // fomantic
diff --git a/web_src/js/index.ts b/web_src/js/index.ts
index 51d8c96fbd..4d400d3b8f 100644
--- a/web_src/js/index.ts
+++ b/web_src/js/index.ts
@@ -34,7 +34,6 @@ import {
 import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts';
 import {initRepoTopicBar} from './features/repo-home.ts';
 import {initAdminCommon} from './features/admin/common.ts';
-import {initRepoTemplateSearch} from './features/repo-template.ts';
 import {initRepoCodeView} from './features/repo-code.ts';
 import {initSshKeyFormParser} from './features/sshkey-helper.ts';
 import {initUserSettings} from './features/user-settings.ts';
@@ -193,7 +192,6 @@ onDomReady(() => {
     initRepoPullRequestReview,
     initRepoRelease,
     initRepoReleaseNew,
-    initRepoTemplateSearch,
     initRepoTopicBar,
     initRepoWikiForm,
     initRepository,
diff --git a/web_src/js/modules/fomantic.ts b/web_src/js/modules/fomantic.ts
index af47c8fb51..18a3c18c9c 100644
--- a/web_src/js/modules/fomantic.ts
+++ b/web_src/js/modules/fomantic.ts
@@ -11,9 +11,10 @@ import {svg} from '../svg.ts';
 export const fomanticMobileScreen = window.matchMedia('only screen and (max-width: 767.98px)');
 
 export function initGiteaFomantic() {
+  // our extensions
+  $.fn.fomanticExt = {};
   // Silence fomantic's error logging when tabs are used without a target content element
   $.fn.tab.settings.silent = true;
-
   // By default, use "exact match" for full text search
   $.fn.dropdown.settings.fullTextSearch = 'exact';
   // Do not use "cursor: pointer" for dropdown labels
diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts
index 6d0f12cb43..9bdc9bfc33 100644
--- a/web_src/js/modules/fomantic/dropdown.ts
+++ b/web_src/js/modules/fomantic/dropdown.ts
@@ -1,6 +1,7 @@
 import $ from 'jquery';
 import {generateAriaId} from './base.ts';
 import type {FomanticInitFunction} from '../../types.ts';
+import {queryElems} from '../../utils/dom.ts';
 
 const ariaPatchKey = '_giteaAriaPatchDropdown';
 const fomanticDropdownFn = $.fn.dropdown;
@@ -9,6 +10,7 @@ const fomanticDropdownFn = $.fn.dropdown;
 export function initAriaDropdownPatch() {
   if ($.fn.dropdown === ariaDropdownFn) throw new Error('initAriaDropdownPatch could only be called once');
   $.fn.dropdown = ariaDropdownFn;
+  $.fn.fomanticExt.onResponseKeepSelectedItem = onResponseKeepSelectedItem;
   (ariaDropdownFn as FomanticInitFunction).settings = fomanticDropdownFn.settings;
 }
 
@@ -351,3 +353,19 @@ export function hideScopedEmptyDividers(container: Element) {
     if (item.nextElementSibling?.matches('.divider')) hideDivider(item);
   }
 }
+
+function onResponseKeepSelectedItem(dropdown: typeof $|HTMLElement, selectedValue: string) {
+  // There is a bug in fomantic dropdown when using "apiSettings" to fetch data
+  // * when there is a selected item, the dropdown insists on hiding the selected one from the list:
+  // * in the "filter" function: ('[data-value="'+value+'"]').addClass(className.filtered)
+  //
+  // When user selects one item, and click the dropdown again,
+  // then the dropdown only shows other items and will select another (wrong) one.
+  // It can't be easily fix by using setTimeout(patch, 0) in `onResponse` because the `onResponse` is called before another `setTimeout(..., timeLeft)`
+  // Fortunately, the "timeLeft" is controlled by "loadingDuration" which is always zero at the moment, so we can use `setTimeout(..., 10)`
+  const elDropdown = (dropdown instanceof HTMLElement) ? dropdown : dropdown[0];
+  setTimeout(() => {
+    queryElems(elDropdown, `.menu .item[data-value="${CSS.escape(selectedValue)}"].filtered`, (el) => el.classList.remove('filtered'));
+    $(elDropdown).dropdown('set selected', selectedValue ?? '');
+  }, 10);
+}