mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-19 16:18:11 +00:00
chore: refactor legacy JS init code to use "data-global-init" (#39836)
Move the remaining legacy init code, which probed page classes with `querySelector`, to `data-global-init`, so each feature initializes from its own element. `hashchange` listeners now go through one module that runs them in priority order, because independent listeners broke when mount order changed. - Convert issue list, diff, repo create, settings and org team inits to `data-global-init` - Run `hashchange` listeners by priority, the diff scroll handler last - Make the devtest mock's failing log requests deterministic - Add e2e tests for diff tree navigation to folded files and job log permalinks --------- Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
@@ -6,7 +6,6 @@ package devtest
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
mathRand "math/rand/v2"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
@@ -597,7 +596,7 @@ func fillViewRunResponseCurrentJob(ctx *context.Context, resp *actions.ViewRespo
|
||||
mockLogOptions = append(mockLogOptions, generateMockStepsLogOptions{mockCountFirst: 30, mockCountGeneral: 3, groupRepeat: 20})
|
||||
|
||||
resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &actions.ViewJobStep{
|
||||
Summary: "step 2 (mock error)",
|
||||
Summary: "step 2",
|
||||
Duration: time.Hour.String(),
|
||||
Status: actions_model.StatusRunning.String(),
|
||||
})
|
||||
@@ -609,20 +608,16 @@ func fillViewRunResponseCurrentJob(ctx *context.Context, resp *actions.ViewRespo
|
||||
|
||||
resp.Logs.StepsLog = []*actions.ViewStepLog{}
|
||||
doSlowResponse := false
|
||||
doErrorResponse := false
|
||||
for _, logCur := range req.LogCursors {
|
||||
if !logCur.Expanded {
|
||||
continue
|
||||
}
|
||||
doSlowResponse = doSlowResponse || logCur.Step == 0
|
||||
doErrorResponse = doErrorResponse || logCur.Step == 2
|
||||
resp.Logs.StepsLog = append(resp.Logs.StepsLog, generateMockStepsLog(logCur, mockLogOptions[logCur.Step])...)
|
||||
}
|
||||
if doErrorResponse {
|
||||
if mathRand.Float64() > 0.5 {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "devtest mock error response")
|
||||
return
|
||||
}
|
||||
if ctx.PathParamInt64("run") == 50 && len(resp.Logs.StepsLog) > 0 {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "devtest mock error response")
|
||||
return
|
||||
}
|
||||
if doSlowResponse {
|
||||
time.Sleep(time.Duration(3000) * time.Millisecond)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<a href="{{AppSubUrl}}/devtest/repo-action-view/runs/30">Run:CanRerunLatest</a>
|
||||
<a href="{{AppSubUrl}}/devtest/repo-action-view/runs/10/attempts/2">Run:PreviousAttempt</a>
|
||||
<a href="{{AppSubUrl}}/devtest/repo-action-view/runs/40">Run:ReusableCaller</a>
|
||||
<a href="{{AppSubUrl}}/devtest/repo-action-view/runs/50/jobs/500">Run:LogsAlwaysFail</a>
|
||||
</div>
|
||||
{{template "repo/actions/view_component" (dict
|
||||
"JobID" (or .JobID 0)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization new team">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization new team" data-global-init="initOrgTeamSettings">
|
||||
{{template "org/header" .}}
|
||||
<div class="ui container">
|
||||
<div class="ui grid">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="ui attached segment">
|
||||
{{template "base/alert" .}}
|
||||
{{template "repo/create_helper" .}}
|
||||
<form class="ui form left-right-form new-repo-form" action="{{.Link}}" method="post">
|
||||
<form class="ui form left-right-form" action="{{.Link}}" method="post" data-global-init="initRepoNew">
|
||||
<div id="create-repo-error-message" class="ui negative message tw-text-center tw-hidden"></div>
|
||||
<div class="inline required field {{if .Err_Owner}}error{{end}}">
|
||||
<label>{{ctx.Locale.Tr "repo.owner"}}</label>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
{{template "repo/diff/whitespace_dropdown" .}}
|
||||
{{template "repo/diff/options_dropdown" .}}
|
||||
{{if .PageIsPullFiles}}
|
||||
<div id="diff-commit-select" data-merge-base="{{$.CompareInfo.CompareBase}}" data-issuelink="{{$.Issue.Link}}"
|
||||
<div id="diff-commit-select" data-global-init="initDiffCommitSelect" data-merge-base="{{$.CompareInfo.CompareBase}}" data-issuelink="{{$.Issue.Link}}"
|
||||
data-queryparams="?style={{if $.IsSplitStyle}}split{{else}}unified{{end}}&whitespace={{$.WhitespaceBehavior}}&show-outdated={{$.ShowOutdatedComments}}"
|
||||
data-text-filter-changes-by-commit="{{ctx.Locale.Tr "repo.pulls.filter_changes_by_commit"}}">
|
||||
{{/* the following will be replaced by vue component, but this avoids any loading artifacts till the vue component is initialized */}}
|
||||
@@ -63,7 +63,7 @@
|
||||
<div id="diff-container">
|
||||
{{if $showFileTree}}
|
||||
{{$.FileIconPoolHTML}}
|
||||
<div id="diff-file-tree" class="tw-hidden not-mobile" data-locale="{{JsonUtils.EncodeToString (dict
|
||||
<div id="diff-file-tree" data-global-init="initDiffFileTree" class="tw-hidden not-mobile" data-locale="{{JsonUtils.EncodeToString (dict
|
||||
"filterFiles" (ctx.Locale.Tr "repo.diff.filter_files")
|
||||
"filterFilesClear" (ctx.Locale.Tr "repo.diff.filter_files_clear")
|
||||
"filterByFileExtension" (ctx.Locale.Tr "repo.diff.filter_by_file_extension")
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* TextFilterTitle
|
||||
*/}}
|
||||
{{$queryLink := .QueryLink}}
|
||||
<div class="item ui dropdown custom user-remote-search" data-tooltip-content="{{ctx.Locale.Tr "repo.user_search_tooltip"}}"
|
||||
<div class="item ui dropdown custom" data-tooltip-content="{{ctx.Locale.Tr "repo.user_search_tooltip"}}" data-global-init="initDropdownUserRemoteSearch"
|
||||
data-search-url="{{$.UserSearchUrl}}"
|
||||
data-selected-username="{{$.SelectedUsername}}"
|
||||
data-action-jump-url="{{QueryBuild $queryLink $.QueryParamKey NIL}}&{{$.QueryParamKey}}={username}"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div id="issue-filters" class="issue-list-toolbar">
|
||||
<div class="issue-list-toolbar-left">
|
||||
{{if and $.CanWriteIssuesOrPulls .Issues}}
|
||||
<input type="checkbox" autocomplete="off" class="issue-checkbox-all tw-mr-4" title="{{ctx.Locale.Tr "repo.issues.action_check_all"}}">
|
||||
<input type="checkbox" autocomplete="off" class="issue-checkbox-all tw-mr-4" title="{{ctx.Locale.Tr "repo.issues.action_check_all"}}" data-global-init="initRepoIssueListCheckboxes">
|
||||
{{end}}
|
||||
{{template "repo/issue/openclose" .}}
|
||||
<!-- Total Tracked Time -->
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
{{template "repo/code/recently_pushed_new_branches" dict "RecentBranchesPromptData" .RecentBranchesPromptData}}
|
||||
|
||||
{{if .PinnedIssues}}
|
||||
<div id="issue-pins" {{if .IsRepoAdmin}}data-is-repo-admin{{end}}>
|
||||
<div id="issue-pins" {{if .IsRepoAdmin}}data-global-init="initIssuePinSort"{{end}}>
|
||||
{{range .PinnedIssues}}
|
||||
<div class="issue-card tw-break-anywhere {{if $.IsRepoAdmin}}tw-cursor-grab{{end}}" data-move-url="{{$.Link}}/move_pin" data-issue-id="{{.ID}}">
|
||||
{{template "repo/issue/card" (dict "Issue" . "Page" $ "isPinnedIssueCard" true)}}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{{template "repo/settings/layout_head" (dict "pageClass" "repository settings edit githook")}}
|
||||
<div class="repo-setting-content">
|
||||
<form class="ui form" action="{{.Link}}" method="post">
|
||||
<form class="ui form" action="{{.Link}}" method="post" data-global-init="initRepoSettingsGitHook">
|
||||
<h4 class="ui top attached header flex-left-right">
|
||||
{{.Hook.Name}}
|
||||
<div class="tw-font-normal tw-font-sans tw-text-base">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<p>{{ctx.Locale.Tr "settings.profile_desc"}}</p>
|
||||
<form class="ui form" action="{{.Link}}" method="post">
|
||||
<form class="ui form" action="{{.Link}}" method="post" data-global-init="initUserSettingsForm">
|
||||
<div class="required field {{if .Err_Name}}error{{end}}">
|
||||
<label for="username">{{ctx.Locale.Tr "username"}}
|
||||
<span class="tw-text-red tw-hidden" id="name-change-prompt"> {{ctx.Locale.Tr "settings.change_username_prompt"}}</span>
|
||||
|
||||
@@ -27,3 +27,8 @@ test('job queue refreshes with filter open', async ({page, request}) => {
|
||||
await expect(page.getByRole('link', {name: 'queued #2'})).toBeVisible();
|
||||
await expect(waitingFilter).toBeVisible();
|
||||
});
|
||||
|
||||
test('job log line permalink expands its step', async ({page}) => {
|
||||
await page.goto('/devtest/repo-action-view/runs/10/jobs/1#jobstep-1-23');
|
||||
await expect(page.getByRole('link', {name: '23', exact: true})).toBeInViewport();
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {apiCreateFiles, apiCreatePR, apiCreateRepo, apiCreateUser, apiUserHeaders, loginUser, randomString} from './utils.ts';
|
||||
|
||||
test('diff sidebar filtering', async ({page, request}) => {
|
||||
test('diff sidebar filtering and folded file navigation', async ({page, request}) => {
|
||||
const user = `df-${randomString(8)}`;
|
||||
await apiCreateUser(request, user);
|
||||
const headers = apiUserHeaders(user);
|
||||
@@ -12,7 +12,7 @@ test('diff sidebar filtering', async ({page, request}) => {
|
||||
{path: 'src/a.ts', content: 'a\n'},
|
||||
{path: 'src/b.ts', content: 'b\n'},
|
||||
{path: 'src/.eslintrc', content: 'e\n'}, // dotfiles count as "No extension"
|
||||
{path: 'styles/x.css', content: 'x\n'},
|
||||
{path: 'styles/x.css', content: 'x\n'.repeat(150)},
|
||||
{path: 'docs/intro.md', content: 'r\n'},
|
||||
{path: 'Makefile', content: 'm\n'},
|
||||
], {branch: 'main', newBranch: 'feat', headers});
|
||||
@@ -34,6 +34,12 @@ test('diff sidebar filtering', async ({page, request}) => {
|
||||
const treeWidth = (await tree.boundingBox())!.width;
|
||||
expect(boxesWidth).toBeGreaterThan(treeWidth * 2);
|
||||
|
||||
await page.getByLabel('Diff Options').click();
|
||||
await page.getByRole('menuitem', {name: 'Collapse all files'}).click();
|
||||
await tree.getByRole('link', {name: 'x.css'}).click();
|
||||
const cssFileHeader = page.getByRole('link', {name: 'styles/x.css', exact: true});
|
||||
await expect.poll(async () => (await cssFileHeader.boundingBox())!.y).toBeLessThan(page.viewportSize()!.height / 2);
|
||||
|
||||
// search filters tree and file boxes
|
||||
await search.fill('a.ts');
|
||||
await expect(items).toHaveText([/a\.ts/]);
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {ActionsArtifact, ActionsJob, ActionsRun, ActionsStatus} from '../mo
|
||||
import {AnsiLineRenderer} from '../render/ansi.ts';
|
||||
import {createLogLineMessage, parseLogLineCommand} from './ActionRunView.ts';
|
||||
import type {ActionRunViewStore, LogLine, LogLineCommand} from './ActionRunView.ts';
|
||||
import {registerAsyncHashChangeListener, removeAsyncHashChangeListener} from '../modules/hashchange.ts';
|
||||
|
||||
function isLogElementInViewport(el: Element, {extraViewPortHeight} = {extraViewPortHeight: 0}): boolean {
|
||||
const rect = el.getBoundingClientRect();
|
||||
@@ -176,13 +177,12 @@ onMounted(async () => {
|
||||
});
|
||||
|
||||
intervalID = setInterval(() => loadJob(), 1000);
|
||||
hashChangeListener();
|
||||
window.addEventListener('hashchange', hashChangeListener);
|
||||
registerAsyncHashChangeListener(hashChangeListener);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
menuTippy.destroy();
|
||||
window.removeEventListener('hashchange', hashChangeListener);
|
||||
removeAsyncHashChangeListener(hashChangeListener);
|
||||
// clear the interval timer when the component is unmounted
|
||||
// even our page is rendered once, not spa style
|
||||
if (intervalID) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {diffTreeStore, filterDiffTree, applyFiltersToFileBoxes, extensionFilterT
|
||||
import {setFileFolding} from '../features/file-fold.ts';
|
||||
import {onMounted, onUnmounted, computed, watch} from 'vue';
|
||||
import {localUserSettings} from '../modules/user-settings.ts';
|
||||
import {registerAsyncHashChangeListener, removeAsyncHashChangeListener} from '../modules/hashchange.ts';
|
||||
|
||||
const LOCAL_STORAGE_KEY = 'diff_file_tree_visible';
|
||||
|
||||
@@ -28,13 +29,12 @@ onMounted(() => {
|
||||
// while the tree is hidden there is no control to clear a filter restored from the URL
|
||||
if (store.fileTreeIsVisible) applyFiltersToFileBoxes(store); else store.activeExtensions = 'all';
|
||||
document.querySelector('.diff-toggle-file-tree-button')!.addEventListener('click', toggleVisibility);
|
||||
hashChangeListener();
|
||||
window.addEventListener('hashchange', hashChangeListener);
|
||||
registerAsyncHashChangeListener(hashChangeListener);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.querySelector('.diff-toggle-file-tree-button')!.removeEventListener('click', toggleVisibility);
|
||||
window.removeEventListener('hashchange', hashChangeListener);
|
||||
removeAsyncHashChangeListener(hashChangeListener);
|
||||
});
|
||||
|
||||
function hashChangeListener() {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import {queryElems, toggleElem} from '../utils/dom.ts';
|
||||
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
||||
|
||||
function initOrgTeamSettings() {
|
||||
// on the page "page-content organization new team"
|
||||
const pageContent = document.querySelector('.page-content.organization.new.team');
|
||||
if (!pageContent) return;
|
||||
function initOrgTeamSettings(pageContent: HTMLElement) {
|
||||
queryElems(pageContent, 'input[name=permission]', (el) => el.addEventListener('change', () => {
|
||||
// Change team access mode
|
||||
const val = pageContent.querySelector<HTMLInputElement>('input[name=permission]:checked')?.value;
|
||||
@@ -12,6 +10,5 @@ function initOrgTeamSettings() {
|
||||
}
|
||||
|
||||
export function initOrgTeam() {
|
||||
if (!document.querySelector('.page-content.organization')) return;
|
||||
initOrgTeamSettings();
|
||||
registerGlobalInitFunc('initOrgTeamSettings', initOrgTeamSettings);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {svg} from '../svg.ts';
|
||||
import {createTippy} from '../modules/tippy.ts';
|
||||
import {addDelegatedEventListener} from '../utils/dom.ts';
|
||||
import {registerAsyncHashChangeListener} from '../modules/hashchange.ts';
|
||||
|
||||
function changeHash(hash: string) {
|
||||
if (window.history.pushState) {
|
||||
@@ -142,6 +143,5 @@ export function initRepoCodeView() {
|
||||
showLineButton();
|
||||
}
|
||||
};
|
||||
onHashChange();
|
||||
window.addEventListener('hashchange', onHashChange);
|
||||
registerAsyncHashChangeListener(onHashChange);
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import {createApp} from 'vue';
|
||||
import DiffCommitSelector from '../components/DiffCommitSelector.vue';
|
||||
|
||||
export function initDiffCommitSelect() {
|
||||
const el = document.querySelector('#diff-commit-select');
|
||||
if (!el) return;
|
||||
|
||||
const commitSelect = createApp(DiffCommitSelector);
|
||||
commitSelect.mount(el);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import {createApp} from 'vue';
|
||||
import DiffFileTree from '../components/DiffFileTree.vue';
|
||||
import type {DiffFileTreeLocale} from '../modules/diff-file.ts';
|
||||
|
||||
export function initDiffFileTree() {
|
||||
const el = document.querySelector('#diff-file-tree');
|
||||
if (!el) return;
|
||||
|
||||
const locale = JSON.parse(el.getAttribute('data-locale')!) as DiffFileTreeLocale;
|
||||
createApp(DiffFileTree, {locale}).mount(el);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import {createApp} from 'vue';
|
||||
import DiffFileTree from '../components/DiffFileTree.vue';
|
||||
import {initRepoIssueContentHistory} from './repo-issue-content.ts';
|
||||
import {initDiffFileTree} from './repo-diff-filetree.ts';
|
||||
import {initDiffCommitSelect} from './repo-diff-commitselect.ts';
|
||||
import {validateTextareaNonEmpty} from './comp/ComboMarkdownEditor.ts';
|
||||
import {initExpandAndCollapseFilesButton, initDiffFileViewedForm} from './pull-view-file.ts';
|
||||
import {showErrorToast} from '../modules/toast.ts';
|
||||
@@ -12,8 +12,10 @@ import {invertFileFolding} from './file-fold.ts';
|
||||
import {parseDom} from '../utils.ts';
|
||||
import {registerGlobalEventFunc, registerGlobalInitFunc} from '../modules/observer.ts';
|
||||
import {performFetchActionTrigger} from '../modules/fetch-action.ts';
|
||||
import {applyFiltersToFileBoxes, diffTreeStore} from '../modules/diff-file.ts';
|
||||
import {applyFiltersToFileBoxes, type DiffFileTreeLocale, diffTreeStore} from '../modules/diff-file.ts';
|
||||
import {initImageDiff} from './imagediff.ts';
|
||||
import DiffCommitSelector from '../components/DiffCommitSelector.vue';
|
||||
import {registerAsyncHashChangeListener} from '../modules/hashchange.ts';
|
||||
|
||||
function initDiffFileViewToggle(el: HTMLElement) {
|
||||
// switch between "rendered" and "source", for image and CSV files
|
||||
@@ -245,6 +247,15 @@ async function onLocationHashChange() {
|
||||
}
|
||||
}
|
||||
|
||||
function initDiffFileTree(el: HTMLElement) {
|
||||
const locale = JSON.parse(el.getAttribute('data-locale')!) as DiffFileTreeLocale;
|
||||
createApp(DiffFileTree, {locale}).mount(el);
|
||||
}
|
||||
|
||||
function initDiffCommitSelect(el: HTMLElement) {
|
||||
createApp(DiffCommitSelector).mount(el);
|
||||
}
|
||||
|
||||
export function initRepoDiffView() {
|
||||
initRepoDiffConversationForm(); // such form appears on the "conversation" page and "diff" page
|
||||
registerGlobalEventFunc('click', 'diffLoadMoreFiles', (el) => { diffLoadMoreFiles(el) });
|
||||
@@ -254,13 +265,14 @@ export function initRepoDiffView() {
|
||||
registerGlobalInitFunc('initDiffFileViewedForm', initDiffFileViewedForm);
|
||||
registerGlobalInitFunc('initDiffFileImageDiff', initImageDiff);
|
||||
registerGlobalInitFunc('initDiffFileViewToggle', initDiffFileViewToggle);
|
||||
registerGlobalInitFunc('initDiffFileTree', initDiffFileTree);
|
||||
registerGlobalInitFunc('initDiffCommitSelect', initDiffCommitSelect);
|
||||
|
||||
if (!document.querySelector('#diff-file-boxes')) return;
|
||||
initRepoDiffConversationNav(); // "previous" and "next" buttons only appear on "diff" page
|
||||
initDiffFileTree();
|
||||
initDiffCommitSelect();
|
||||
initExpandAndCollapseFilesButton();
|
||||
|
||||
window.addEventListener('hashchange', onLocationHashChange);
|
||||
onLocationHashChange();
|
||||
// onLocationHashChange does very complicated things (e.g.: load incomplete content and scroll), and it might change the URL hash
|
||||
// so make it run after all other hash change listeners
|
||||
registerAsyncHashChangeListener(onLocationHashChange, 999);
|
||||
}
|
||||
|
||||
@@ -1,42 +1,35 @@
|
||||
import {toggleElem, queryElems, isElemVisible} from '../utils/dom.ts';
|
||||
import {toggleElem, queryElems} from '../utils/dom.ts';
|
||||
import {html, htmlRaw} from '../utils/html.ts';
|
||||
import {confirmModal} from './comp/ConfirmModal.ts';
|
||||
import {createSortable} from '../modules/sortable.ts';
|
||||
import {DELETE, POST} from '../modules/fetch.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
import {performFetchAction} from '../modules/fetch-action.ts';
|
||||
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
||||
import type {SortableEvent} from 'sortablejs';
|
||||
|
||||
type IssuePoster = {avatar_link: string, full_name: string, username: string};
|
||||
type ProcessedIssuePoster = {type: 'html', html: string};
|
||||
type IssuePosterResponse = {results: IssuePoster[]};
|
||||
|
||||
function initRepoIssueListCheckboxes() {
|
||||
const issueSelectAll = document.querySelector<HTMLInputElement>('.issue-checkbox-all');
|
||||
if (!issueSelectAll) return; // logged out state
|
||||
const issueCheckboxes = document.querySelectorAll<HTMLInputElement>('.issue-checkbox');
|
||||
function initRepoIssueListCheckboxes(issueSelectAll: HTMLInputElement) {
|
||||
const container = issueSelectAll.closest('.page-content')!;
|
||||
const issueCheckboxes = container.querySelectorAll<HTMLInputElement>('.issue-checkbox');
|
||||
const issueFilters = container.querySelector<HTMLElement>('#issue-filters')!;
|
||||
const issueActions = container.querySelector<HTMLElement>('#issue-actions')!;
|
||||
|
||||
const syncIssueSelectionState = () => {
|
||||
const checkedCheckboxes = Array.from(issueCheckboxes).filter((el) => el.checked);
|
||||
const anyChecked = Boolean(checkedCheckboxes.length);
|
||||
const allChecked = anyChecked && checkedCheckboxes.length === issueCheckboxes.length;
|
||||
|
||||
if (allChecked) {
|
||||
issueSelectAll.checked = true;
|
||||
issueSelectAll.indeterminate = false;
|
||||
} else if (anyChecked) {
|
||||
issueSelectAll.checked = false;
|
||||
issueSelectAll.indeterminate = true;
|
||||
} else {
|
||||
issueSelectAll.checked = false;
|
||||
issueSelectAll.indeterminate = false;
|
||||
}
|
||||
issueSelectAll.checked = allChecked;
|
||||
issueSelectAll.indeterminate = anyChecked && !allChecked;
|
||||
// if any issue is selected, show the action panel, otherwise show the filter panel
|
||||
toggleElem('#issue-filters', !anyChecked);
|
||||
toggleElem('#issue-actions', anyChecked);
|
||||
toggleElem(issueFilters, !anyChecked);
|
||||
toggleElem(issueActions, anyChecked);
|
||||
// there are two panels but only one select-all checkbox, so move the checkbox to the visible panel
|
||||
const panels = document.querySelectorAll<HTMLElement>('#issue-filters, #issue-actions');
|
||||
const visiblePanel = Array.from(panels).find((el) => isElemVisible(el))!;
|
||||
const visiblePanel = anyChecked ? issueActions : issueFilters;
|
||||
const toolbarLeft = visiblePanel.querySelector('.issue-list-toolbar-left')!;
|
||||
toolbarLeft.prepend(issueSelectAll);
|
||||
};
|
||||
@@ -52,15 +45,14 @@ function initRepoIssueListCheckboxes() {
|
||||
syncIssueSelectionState();
|
||||
});
|
||||
|
||||
queryElems(document, '.issue-action', (el) => el.addEventListener('click',
|
||||
queryElems(container, '.issue-action', (el) => el.addEventListener('click',
|
||||
async (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const url = el.getAttribute('data-url')!;
|
||||
let action = el.getAttribute('data-action')!;
|
||||
const elementId = el.getAttribute('data-element-id')!;
|
||||
const issueIDList: string[] = Array.from(document.querySelectorAll('.issue-checkbox:checked'), (el) => (el.getAttribute('data-issue-id')!));
|
||||
const issueIDs = issueIDList.join(',');
|
||||
const issueIDs = Array.from(container.querySelectorAll('.issue-checkbox:checked'), (el) => el.getAttribute('data-issue-id')!).join(',');
|
||||
if (!issueIDs) return;
|
||||
|
||||
// for label toggle
|
||||
@@ -166,14 +158,7 @@ async function pinMoveEnd(e: SortableEvent) {
|
||||
await POST(url, {data: {id, position: e.newIndex! + 1}});
|
||||
}
|
||||
|
||||
function initIssuePinSort() {
|
||||
const pinDiv = document.querySelector<HTMLElement>('#issue-pins');
|
||||
|
||||
if (pinDiv === null) return;
|
||||
|
||||
// If the User is not a Repo Admin, we don't need to proceed
|
||||
if (!pinDiv.hasAttribute('data-is-repo-admin')) return;
|
||||
|
||||
function initIssuePinSort(pinDiv: HTMLElement) {
|
||||
initPinRemoveButton();
|
||||
|
||||
// If only one issue pinned, we don't need to make this Sortable
|
||||
@@ -190,12 +175,7 @@ function initIssuePinSort() {
|
||||
}
|
||||
|
||||
export function initRepoIssueList() {
|
||||
if (document.querySelector('.page-content.repository.issue-list, .page-content.repository.milestone-issue-list')) {
|
||||
initRepoIssueListCheckboxes();
|
||||
queryElems(document, '.ui.dropdown.user-remote-search', (el) => initDropdownUserRemoteSearch(el));
|
||||
initIssuePinSort();
|
||||
} else if (document.querySelector('.page-content.dashboard.issues')) {
|
||||
// user or org home: issue list, pull request list
|
||||
queryElems(document, '.ui.dropdown.user-remote-search', (el) => initDropdownUserRemoteSearch(el));
|
||||
}
|
||||
registerGlobalInitFunc('initRepoIssueListCheckboxes', initRepoIssueListCheckboxes);
|
||||
registerGlobalInitFunc('initDropdownUserRemoteSearch', initDropdownUserRemoteSearch);
|
||||
registerGlobalInitFunc('initIssuePinSort', initIssuePinSort);
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ export function initRepository() {
|
||||
registerGlobalInitFunc('initRepoPullMergeBox', initRepoPullMergeBox);
|
||||
registerGlobalInitFunc('initRepoPullRequestUpdate', initRepoPullRequestUpdate);
|
||||
registerGlobalInitFunc('initRepoCitationPanel', initRepoCitationPanel);
|
||||
registerGlobalInitFunc('initRepoNew', initRepoNew);
|
||||
|
||||
// TODO: need to refactor more legacy if-querySelector-based "init" code to "data-global-xxx" framework in the future.
|
||||
const pageContent = document.querySelector('.page-content.repository');
|
||||
@@ -51,7 +52,6 @@ export function initRepository() {
|
||||
initRepoCommentForm();
|
||||
|
||||
initRepoMilestone();
|
||||
initRepoNew();
|
||||
|
||||
initRepoCloneButtons();
|
||||
initRepoSettings();
|
||||
|
||||
@@ -56,11 +56,7 @@ function initRepoNewTemplateSearch(form: HTMLFormElement) {
|
||||
onChangeOwner();
|
||||
}
|
||||
|
||||
export function initRepoNew() {
|
||||
const pageContent = document.querySelector('.page-content.repository.new-repo');
|
||||
if (!pageContent) return;
|
||||
|
||||
const form = document.querySelector<HTMLFormElement>('.new-repo-form')!;
|
||||
export function initRepoNew(form: HTMLFormElement) {
|
||||
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"]')!;
|
||||
|
||||
@@ -5,6 +5,7 @@ import {initRepoSettingsBranchesDrag} from './repo-settings-branches.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
import {attachSearchBox} from '../modules/search.ts';
|
||||
import {globMatch} from '../utils/glob.ts';
|
||||
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
||||
|
||||
const {appSubUrl} = window.config;
|
||||
|
||||
@@ -64,9 +65,8 @@ function initRepoSettingsSearchTeamBox() {
|
||||
})));
|
||||
}
|
||||
|
||||
function initRepoSettingsGitHook() {
|
||||
if (!document.querySelector('.page-content.repository.settings.edit.githook')) return;
|
||||
createCodeEditor(document.querySelector<HTMLTextAreaElement>('#content')!);
|
||||
function initRepoSettingsGitHook(form: HTMLFormElement) {
|
||||
createCodeEditor(form.querySelector<HTMLTextAreaElement>('textarea[name=content]')!);
|
||||
}
|
||||
|
||||
function initRepoSettingsBranches() {
|
||||
@@ -137,11 +137,11 @@ function initRepoSettingsOptions() {
|
||||
}
|
||||
|
||||
export function initRepoSettings() {
|
||||
registerGlobalInitFunc('initRepoSettingsGitHook', initRepoSettingsGitHook);
|
||||
if (!document.querySelector('.page-content.repository.settings')) return;
|
||||
initRepoSettingsOptions();
|
||||
initRepoSettingsBranches();
|
||||
initRepoSettingsCollaboration();
|
||||
initRepoSettingsSearchTeamBox();
|
||||
initRepoSettingsGitHook();
|
||||
initRepoSettingsBranchesDrag();
|
||||
}
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import {hideElem, showElem} from '../utils/dom.ts';
|
||||
import {toggleElem} from '../utils/dom.ts';
|
||||
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
||||
|
||||
export function initUserSettings() {
|
||||
if (!document.querySelector('.user.settings.profile')) return;
|
||||
|
||||
const usernameInput = document.querySelector<HTMLInputElement>('#username');
|
||||
if (!usernameInput) return;
|
||||
usernameInput.addEventListener('input', function () {
|
||||
const prompt = document.querySelector('#name-change-prompt')!;
|
||||
const promptRedirect = document.querySelector('#name-change-redirect-prompt')!;
|
||||
if (this.value.toLowerCase() !== this.getAttribute('data-name')!.toLowerCase()) {
|
||||
showElem(prompt);
|
||||
showElem(promptRedirect);
|
||||
} else {
|
||||
hideElem(prompt);
|
||||
hideElem(promptRedirect);
|
||||
}
|
||||
registerGlobalInitFunc('initUserSettingsForm', (elForm: HTMLFormElement) => {
|
||||
const usernameInput = elForm.querySelector<HTMLInputElement>('input[name=name]')!;
|
||||
usernameInput.addEventListener('input', () => {
|
||||
const prompt = elForm.querySelector('#name-change-prompt')!;
|
||||
const promptRedirect = elForm.querySelector('#name-change-redirect-prompt')!;
|
||||
const showPrompt = usernameInput.value.toLowerCase() !== usernameInput.getAttribute('data-name')!.toLowerCase();
|
||||
toggleElem(prompt, showPrompt);
|
||||
toggleElem(promptRedirect, showPrompt);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ import {initGlobalShortcut} from './modules/shortcut.ts';
|
||||
import {initDevtest} from './modules/devtest.ts';
|
||||
import {initRepoWatch} from './features/repo-watch.ts';
|
||||
import {initPackagesView} from './features/packages.ts';
|
||||
import {initAsyncHashChangeListeners} from './modules/hashchange.ts';
|
||||
|
||||
const initStartTime = performance.now();
|
||||
const initPerformanceTracer = callInitFunctions([
|
||||
@@ -177,3 +178,7 @@ if (initDur > 500) {
|
||||
}
|
||||
|
||||
window.config.frontendInited = true;
|
||||
|
||||
// then init the async hash change handlers, then they can do async content loading by URL hash
|
||||
// e.g.: expand hidden comments or load incomplete diff files
|
||||
initAsyncHashChangeListeners();
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
type HashChangeListener = (e: HashChangeEvent) => void | Promise<void>;
|
||||
|
||||
const hashChangeListeners: Array<{listener: HashChangeListener, priority: number}> = [];
|
||||
let hashChangeListenersInited = false;
|
||||
|
||||
export function registerAsyncHashChangeListener(listener: HashChangeListener, priority = 0): void {
|
||||
hashChangeListeners.push({listener, priority});
|
||||
hashChangeListeners.sort((a, b) => a.priority - b.priority);
|
||||
if (hashChangeListenersInited) listener(new HashChangeEvent('hashchange')); // late registrants missed the initial run
|
||||
}
|
||||
|
||||
export function removeAsyncHashChangeListener(listener: HashChangeListener): void {
|
||||
const index = hashChangeListeners.findIndex((item) => item.listener === listener);
|
||||
if (index !== -1) hashChangeListeners.splice(index, 1);
|
||||
}
|
||||
|
||||
async function runHashChangeListeners(e: HashChangeEvent) {
|
||||
for (const {listener} of hashChangeListeners) await listener(e);
|
||||
}
|
||||
|
||||
export async function initAsyncHashChangeListeners() {
|
||||
hashChangeListenersInited = true;
|
||||
window.addEventListener('hashchange', runHashChangeListeners);
|
||||
await runHashChangeListeners(new HashChangeEvent('hashchange'));
|
||||
}
|
||||
Reference in New Issue
Block a user