Merge branch 'main' into add-file-tree-to-file-view-page

This commit is contained in:
Kerwin Bryant
2024-12-23 10:56:23 +08:00
committed by GitHub
329 changed files with 5412 additions and 5413 deletions
+5 -4
View File
@@ -6,24 +6,25 @@
import {SvgIcon} from '../svg.ts';
withDefaults(defineProps<{
status: '',
status: 'success' | 'skipped' | 'waiting' | 'blocked' | 'running' | 'failure' | 'cancelled' | 'unknown',
size?: number,
className?: string,
localeStatus?: string,
}>(), {
size: 16,
className: undefined,
className: '',
localeStatus: undefined,
});
</script>
<template>
<span class="tw-flex tw-items-center" :data-tooltip-content="localeStatus" v-if="status">
<span :data-tooltip-content="localeStatus ?? status" v-if="status">
<SvgIcon name="octicon-check-circle-fill" class="text green" :size="size" :class-name="className" v-if="status === 'success'"/>
<SvgIcon name="octicon-skip" class="text grey" :size="size" :class-name="className" v-else-if="status === 'skipped'"/>
<SvgIcon name="octicon-stop" class="text yellow" :size="size" :class-name="className" v-else-if="status === 'cancelled'"/>
<SvgIcon name="octicon-clock" class="text yellow" :size="size" :class-name="className" v-else-if="status === 'waiting'"/>
<SvgIcon name="octicon-blocked" class="text yellow" :size="size" :class-name="className" v-else-if="status === 'blocked'"/>
<SvgIcon name="octicon-meter" class="text yellow" :size="size" :class-name="'job-status-rotate ' + className" v-else-if="status === 'running'"/>
<SvgIcon name="octicon-x-circle-fill" class="text red" :size="size" v-else-if="['failure', 'cancelled', 'unknown'].includes(status)"/>
<SvgIcon name="octicon-x-circle-fill" class="text red" :size="size" v-else/><!-- failure, unknown -->
</span>
</template>
+2 -3
View File
@@ -25,10 +25,9 @@ const body = computed(() => {
const root = ref<HTMLElement | null>(null);
onMounted(() => {
root.value.addEventListener('ce-load-context-popup', (e: CustomEvent) => {
const data: IssuePathInfo = e.detail;
root.value.addEventListener('ce-load-context-popup', (e: CustomEventInit<IssuePathInfo>) => {
if (!loading.value && issue.value === null) {
load(data);
load(e.detail);
}
});
});
+89 -6
View File
@@ -38,6 +38,25 @@ function parseLineCommand(line: LogLine): LogLineCommand | null {
return null;
}
function isLogElementInViewport(el: HTMLElement): boolean {
const rect = el.getBoundingClientRect();
return rect.top >= 0 && rect.bottom <= window.innerHeight; // only check height but not width
}
type LocaleStorageOptions = {
autoScroll: boolean;
expandRunning: boolean;
};
function getLocaleStorageOptions(): LocaleStorageOptions {
try {
const optsJson = localStorage.getItem('actions-view-options');
if (optsJson) return JSON.parse(optsJson);
} catch {}
// if no options in localStorage, or failed to parse, return default options
return {autoScroll: true, expandRunning: false};
}
const sfc = {
name: 'RepoActionView',
components: {
@@ -51,7 +70,17 @@ const sfc = {
locale: Object,
},
watch: {
optionAlwaysAutoScroll() {
this.saveLocaleStorageOptions();
},
optionAlwaysExpandRunning() {
this.saveLocaleStorageOptions();
},
},
data() {
const {autoScroll, expandRunning} = getLocaleStorageOptions();
return {
// internal state
loadingAbortController: null,
@@ -65,6 +94,8 @@ const sfc = {
'log-time-stamp': false,
'log-time-seconds': false,
},
optionAlwaysAutoScroll: autoScroll ?? false,
optionAlwaysExpandRunning: expandRunning ?? false,
// provided by backend
run: {
@@ -142,9 +173,19 @@ const sfc = {
},
methods: {
// get the active container element, either the `job-step-logs` or the `job-log-list` in the `job-log-group`
getLogsContainer(stepIndex: number) {
const el = this.$refs.logs[stepIndex];
saveLocaleStorageOptions() {
const opts: LocaleStorageOptions = {autoScroll: this.optionAlwaysAutoScroll, expandRunning: this.optionAlwaysExpandRunning};
localStorage.setItem('actions-view-options', JSON.stringify(opts));
},
// get the job step logs container ('.job-step-logs')
getJobStepLogsContainer(stepIndex: number): HTMLElement {
return this.$refs.logs[stepIndex];
},
// get the active logs container element, either the `job-step-logs` or the `job-log-list` in the `job-log-group`
getActiveLogsContainer(stepIndex: number): HTMLElement {
const el = this.getJobStepLogsContainer(stepIndex);
return el._stepLogsActiveContainer ?? el;
},
// begin a log group
@@ -217,9 +258,17 @@ const sfc = {
);
},
shouldAutoScroll(stepIndex: number): boolean {
if (!this.optionAlwaysAutoScroll) return false;
const el = this.getJobStepLogsContainer(stepIndex);
// if the logs container is empty, then auto-scroll if the step is expanded
if (!el.lastChild) return this.currentJobStepsStates[stepIndex].expanded;
return isLogElementInViewport(el.lastChild);
},
appendLogs(stepIndex: number, startTime: number, logLines: LogLine[]) {
for (const line of logLines) {
const el = this.getLogsContainer(stepIndex);
const el = this.getActiveLogsContainer(stepIndex);
const cmd = parseLineCommand(line);
if (cmd?.name === 'group') {
this.beginLogGroup(stepIndex, startTime, line, cmd);
@@ -264,6 +313,7 @@ const sfc = {
const abortController = new AbortController();
this.loadingAbortController = abortController;
try {
const isFirstLoad = !this.run.status;
const job = await this.fetchJobData(abortController);
if (this.loadingAbortController !== abortController) return;
@@ -273,11 +323,20 @@ const sfc = {
// sync the currentJobStepsStates to store the job step states
for (let i = 0; i < this.currentJob.steps.length; i++) {
const expanded = isFirstLoad && this.optionAlwaysExpandRunning && this.currentJob.steps[i].status === 'running';
if (!this.currentJobStepsStates[i]) {
// initial states for job steps
this.currentJobStepsStates[i] = {cursor: null, expanded: false};
this.currentJobStepsStates[i] = {cursor: null, expanded};
}
}
// find the step indexes that need to auto-scroll
const autoScrollStepIndexes = new Map<number, boolean>();
for (const logs of job.logs.stepsLog ?? []) {
if (autoScrollStepIndexes.has(logs.step)) continue;
autoScrollStepIndexes.set(logs.step, this.shouldAutoScroll(logs.step));
}
// append logs to the UI
for (const logs of job.logs.stepsLog ?? []) {
// save the cursor, it will be passed to backend next time
@@ -285,6 +344,15 @@ const sfc = {
this.appendLogs(logs.step, logs.started, logs.lines);
}
// auto-scroll to the last log line of the last step
let autoScrollJobStepElement: HTMLElement;
for (let stepIndex = 0; stepIndex < this.currentJob.steps.length; stepIndex++) {
if (!autoScrollStepIndexes.get(stepIndex)) continue;
autoScrollJobStepElement = this.getJobStepLogsContainer(stepIndex);
}
autoScrollJobStepElement?.lastElementChild.scrollIntoView({behavior: 'smooth', block: 'nearest'});
// clear the interval timer if the job is done
if (this.run.done && this.intervalID) {
clearInterval(this.intervalID);
this.intervalID = null;
@@ -393,6 +461,8 @@ export function initRepositoryActionView() {
skipped: el.getAttribute('data-locale-status-skipped'),
blocked: el.getAttribute('data-locale-status-blocked'),
},
logsAlwaysAutoScroll: el.getAttribute('data-locale-logs-always-auto-scroll'),
logsAlwaysExpandRunning: el.getAttribute('data-locale-logs-always-expand-running'),
},
});
view.mount(el);
@@ -495,6 +565,17 @@ export function initRepositoryActionView() {
<i class="icon"><SvgIcon :name="isFullScreen ? 'octicon-check' : 'gitea-empty-checkbox'"/></i>
{{ locale.showFullScreen }}
</a>
<div class="divider"/>
<a class="item" @click="optionAlwaysAutoScroll = !optionAlwaysAutoScroll">
<i class="icon"><SvgIcon :name="optionAlwaysAutoScroll ? 'octicon-check' : 'gitea-empty-checkbox'"/></i>
{{ locale.logsAlwaysAutoScroll }}
</a>
<a class="item" @click="optionAlwaysExpandRunning = !optionAlwaysExpandRunning">
<i class="icon"><SvgIcon :name="optionAlwaysExpandRunning ? 'octicon-check' : 'gitea-empty-checkbox'"/></i>
{{ locale.logsAlwaysExpandRunning }}
</a>
<div class="divider"/>
<a :class="['item', !currentJob.steps.length ? 'disabled' : '']" :href="run.link+'/jobs/'+jobIndex+'/logs'" target="_blank">
<i class="icon"><SvgIcon name="octicon-download"/></i>
@@ -551,11 +632,13 @@ export function initRepositoryActionView() {
.action-info-summary-title {
display: flex;
align-items: center;
gap: 0.5em;
}
.action-info-summary-title-text {
font-size: 20px;
margin: 0 0 0 8px;
margin: 0;
flex: 1;
overflow-wrap: anywhere;
}
+40 -45
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import {createApp, nextTick} from 'vue';
import {nextTick} from 'vue';
import {SvgIcon} from '../svg.ts';
import {showErrorToast} from '../modules/toast.ts';
import {GET} from '../modules/fetch.ts';
@@ -17,11 +17,11 @@ type SelectedTab = 'branches' | 'tags';
type TabLoadingStates = Record<SelectedTab, '' | 'loading' | 'done'>
let currentElRoot: HTMLElement;
const sfc = {
components: {SvgIcon},
props: {
elRoot: HTMLElement,
},
computed: {
searchFieldPlaceholder() {
return this.selectedTab === 'branches' ? this.textFilterBranch : this.textFilterTag;
@@ -55,18 +55,15 @@ const sfc = {
return `${this.currentRepoLink}/branches/_new/${this.currentRefType}/${pathEscapeSegments(this.currentRefShortName)}`;
},
},
watch: {
menuVisible(visible) {
menuVisible(visible: boolean) {
if (!visible) return;
this.focusSearchField();
this.loadTabItems();
},
},
data() {
const elRoot = currentElRoot;
const shouldShowTabBranches = elRoot.getAttribute('data-show-tab-branches') === 'true';
const shouldShowTabBranches = this.elRoot.getAttribute('data-show-tab-branches') === 'true';
return {
csrfToken: window.config.csrfToken,
allItems: [] as ListItem[],
@@ -76,43 +73,51 @@ const sfc = {
activeItemIndex: 0,
tabLoadingStates: {} as TabLoadingStates,
textReleaseCompare: elRoot.getAttribute('data-text-release-compare'),
textBranches: elRoot.getAttribute('data-text-branches'),
textTags: elRoot.getAttribute('data-text-tags'),
textFilterBranch: elRoot.getAttribute('data-text-filter-branch'),
textFilterTag: elRoot.getAttribute('data-text-filter-tag'),
textDefaultBranchLabel: elRoot.getAttribute('data-text-default-branch-label'),
textCreateTag: elRoot.getAttribute('data-text-create-tag'),
textCreateBranch: elRoot.getAttribute('data-text-create-branch'),
textCreateRefFrom: elRoot.getAttribute('data-text-create-ref-from'),
textNoResults: elRoot.getAttribute('data-text-no-results'),
textViewAllBranches: elRoot.getAttribute('data-text-view-all-branches'),
textViewAllTags: elRoot.getAttribute('data-text-view-all-tags'),
textReleaseCompare: this.elRoot.getAttribute('data-text-release-compare'),
textBranches: this.elRoot.getAttribute('data-text-branches'),
textTags: this.elRoot.getAttribute('data-text-tags'),
textFilterBranch: this.elRoot.getAttribute('data-text-filter-branch'),
textFilterTag: this.elRoot.getAttribute('data-text-filter-tag'),
textDefaultBranchLabel: this.elRoot.getAttribute('data-text-default-branch-label'),
textCreateTag: this.elRoot.getAttribute('data-text-create-tag'),
textCreateBranch: this.elRoot.getAttribute('data-text-create-branch'),
textCreateRefFrom: this.elRoot.getAttribute('data-text-create-ref-from'),
textNoResults: this.elRoot.getAttribute('data-text-no-results'),
textViewAllBranches: this.elRoot.getAttribute('data-text-view-all-branches'),
textViewAllTags: this.elRoot.getAttribute('data-text-view-all-tags'),
currentRepoDefaultBranch: elRoot.getAttribute('data-current-repo-default-branch'),
currentRepoLink: elRoot.getAttribute('data-current-repo-link'),
currentTreePath: elRoot.getAttribute('data-current-tree-path'),
currentRefType: elRoot.getAttribute('data-current-ref-type'),
currentRefShortName: elRoot.getAttribute('data-current-ref-short-name'),
currentRepoDefaultBranch: this.elRoot.getAttribute('data-current-repo-default-branch'),
currentRepoLink: this.elRoot.getAttribute('data-current-repo-link'),
currentTreePath: this.elRoot.getAttribute('data-current-tree-path'),
currentRefType: this.elRoot.getAttribute('data-current-ref-type'),
currentRefShortName: this.elRoot.getAttribute('data-current-ref-short-name'),
refLinkTemplate: elRoot.getAttribute('data-ref-link-template'),
refFormActionTemplate: elRoot.getAttribute('data-ref-form-action-template'),
dropdownFixedText: elRoot.getAttribute('data-dropdown-fixed-text'),
refLinkTemplate: this.elRoot.getAttribute('data-ref-link-template'),
refFormActionTemplate: this.elRoot.getAttribute('data-ref-form-action-template'),
dropdownFixedText: this.elRoot.getAttribute('data-dropdown-fixed-text'),
showTabBranches: shouldShowTabBranches,
showTabTags: elRoot.getAttribute('data-show-tab-tags') === 'true',
allowCreateNewRef: elRoot.getAttribute('data-allow-create-new-ref') === 'true',
showViewAllRefsEntry: elRoot.getAttribute('data-show-view-all-refs-entry') === 'true',
enableFeed: elRoot.getAttribute('data-enable-feed') === 'true',
showTabTags: this.elRoot.getAttribute('data-show-tab-tags') === 'true',
allowCreateNewRef: this.elRoot.getAttribute('data-allow-create-new-ref') === 'true',
showViewAllRefsEntry: this.elRoot.getAttribute('data-show-view-all-refs-entry') === 'true',
enableFeed: this.elRoot.getAttribute('data-enable-feed') === 'true',
};
},
beforeMount() {
document.body.addEventListener('click', (e) => {
if (this.$el.contains(e.target)) return;
if (this.menuVisible) this.menuVisible = false;
});
},
mounted() {
if (this.refFormActionTemplate) {
// if the selector is used in a form and needs to change the form action,
// make a mock item and select it to update the form action
const item: ListItem = {selected: true, refType: this.currentRefType, refShortName: this.currentRefShortName, rssFeedLink: ''};
this.selectItem(item);
}
},
methods: {
selectItem(item: ListItem) {
this.menuVisible = false;
@@ -209,16 +214,6 @@ const sfc = {
},
};
export function initRepoBranchTagSelector(selector) {
for (const elRoot of document.querySelectorAll(selector)) {
// it is very hacky, but it is the only way to pass the elRoot to the "data()" function
// it could be improved in the future to do more rewriting.
currentElRoot = elRoot;
const comp = {...sfc};
createApp(comp).mount(elRoot);
}
}
export default sfc; // activate IDE's Vue plugin
</script>
<template>
+2 -1
View File
@@ -1,6 +1,7 @@
import {showTemporaryTooltip} from '../modules/tippy.ts';
import {toAbsoluteUrl} from '../utils.ts';
import {clippie} from 'clippie';
import type {DOMEvent} from '../utils/dom.ts';
const {copy_success, copy_error} = window.config.i18n;
@@ -9,7 +10,7 @@ const {copy_success, copy_error} = window.config.i18n;
// - data-clipboard-target: Holds a selector for a <input> or <textarea> whose content is copied
// - data-clipboard-text-type: When set to 'url' will convert relative to absolute urls
export function initGlobalCopyToClipboardListener() {
document.addEventListener('click', async (e: MouseEvent & {target: HTMLElement}) => {
document.addEventListener('click', async (e: DOMEvent<MouseEvent>) => {
const target = e.target.closest('[data-clipboard-text], [data-clipboard-target]');
if (!target) return;
+4 -3
View File
@@ -1,4 +1,5 @@
import {createTippy} from '../modules/tippy.ts';
import type {DOMEvent} from '../utils/dom.ts';
export async function initColorPickers() {
const els = document.querySelectorAll<HTMLElement>('.js-color-picker-input');
@@ -37,7 +38,7 @@ function initPicker(el: HTMLElement): void {
updateSquare(square, e.detail.value);
});
input.addEventListener('input', (e: Event & {target: HTMLInputElement}) => {
input.addEventListener('input', (e: DOMEvent<Event, HTMLInputElement>) => {
updateSquare(square, e.target.value);
updatePicker(picker, e.target.value);
});
@@ -55,8 +56,8 @@ function initPicker(el: HTMLElement): void {
});
// init precolors
for (const colorEl of el.querySelectorAll('.precolors .color')) {
colorEl.addEventListener('click', (e: MouseEvent & {target: HTMLAnchorElement}) => {
for (const colorEl of el.querySelectorAll<HTMLElement>('.precolors .color')) {
colorEl.addEventListener('click', (e: DOMEvent<MouseEvent, HTMLAnchorElement>) => {
const newValue = e.target.getAttribute('data-color-hex');
input.value = newValue;
input.dispatchEvent(new Event('input', {bubbles: true}));
+3 -2
View File
@@ -3,6 +3,7 @@ import {showErrorToast} from '../modules/toast.ts';
import {addDelegatedEventListener, submitEventSubmitter} from '../utils/dom.ts';
import {confirmModal} from './comp/ConfirmModal.ts';
import type {RequestOpts} from '../types.ts';
import {ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
const {appSubUrl, i18n} = window.config;
@@ -27,7 +28,7 @@ async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: R
if (resp.status === 200) {
let {redirect} = await resp.json();
redirect = redirect || actionElem.getAttribute('data-redirect');
actionElem.classList.remove('dirty'); // remove the areYouSure check before reloading
ignoreAreYouSure(actionElem); // ignore the areYouSure check before reloading
if (redirect) {
fetchActionDoRedirect(redirect);
} else {
@@ -100,7 +101,7 @@ async function linkAction(el: HTMLElement, e: Event) {
const url = el.getAttribute('data-url');
const doRequest = async () => {
if ('disabled' in el) el.disabled = true; // el could be A or BUTTON, but A doesn't have disabled attribute
await fetchActionDoRequest(el, url, {method: 'POST'});
await fetchActionDoRequest(el, url, {method: el.getAttribute('data-link-action-method') || 'POST'});
if ('disabled' in el) el.disabled = false;
};
+2 -2
View File
@@ -1,6 +1,6 @@
import {applyAreYouSure, initAreYouSure} from '../vendor/jquery.are-you-sure.ts';
import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.ts';
import {queryElems} from '../utils/dom.ts';
import {queryElems, type DOMEvent} from '../utils/dom.ts';
import {initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
export function initGlobalFormDirtyLeaveConfirm() {
@@ -13,7 +13,7 @@ export function initGlobalFormDirtyLeaveConfirm() {
}
export function initGlobalEnterQuickSubmit() {
document.addEventListener('keydown', (e: KeyboardEvent & {target: HTMLElement}) => {
document.addEventListener('keydown', (e: DOMEvent<KeyboardEvent>) => {
if (e.key !== 'Enter') return;
const hasCtrlOrMeta = ((e.ctrlKey || e.metaKey) && !e.altKey);
if (hasCtrlOrMeta && e.target.matches('textarea')) {
+2 -2
View File
@@ -1,4 +1,4 @@
import {showElem} from '../../utils/dom.ts';
import {showElem, type DOMEvent} from '../../utils/dom.ts';
type CropperOpts = {
container: HTMLElement,
@@ -26,7 +26,7 @@ export async function initCompCropper({container, fileInput, imageSource}: Cropp
},
});
fileInput.addEventListener('input', (e: Event & {target: HTMLInputElement}) => {
fileInput.addEventListener('input', (e: DOMEvent<Event, HTMLInputElement>) => {
const files = e.target.files;
if (files?.length > 0) {
currentFileName = files[0].name;
+3 -2
View File
@@ -1,9 +1,10 @@
import {POST} from '../../modules/fetch.ts';
import {fomanticQuery} from '../../modules/fomantic/base.ts';
import type {DOMEvent} from '../../utils/dom.ts';
export function initCompReactionSelector(parent: ParentNode = document) {
for (const container of parent.querySelectorAll('.issue-content, .diff-file-body')) {
container.addEventListener('click', async (e: MouseEvent & {target: HTMLElement}) => {
for (const container of parent.querySelectorAll<HTMLElement>('.issue-content, .diff-file-body')) {
container.addEventListener('click', async (e: DOMEvent<MouseEvent>) => {
// there are 2 places for the "reaction" buttons, one is the top-right reaction menu, one is the bottom of the comment
const target = e.target.closest('.comment-reaction-button');
if (!target) return;
+25 -24
View File
@@ -1,13 +1,10 @@
const sourcesByUrl = {};
const sourcesByPort = {};
class Source {
url: string;
eventSource: EventSource;
listening: Record<string, any>;
clients: Array<any>;
listening: Record<string, boolean>;
clients: Array<MessagePort>;
constructor(url) {
constructor(url: string) {
this.url = url;
this.eventSource = new EventSource(url);
this.listening = {};
@@ -20,7 +17,7 @@ class Source {
this.listen('error');
}
register(port) {
register(port: MessagePort) {
if (this.clients.includes(port)) return;
this.clients.push(port);
@@ -31,7 +28,7 @@ class Source {
});
}
deregister(port) {
deregister(port: MessagePort) {
const portIdx = this.clients.indexOf(port);
if (portIdx < 0) {
return this.clients.length;
@@ -47,7 +44,7 @@ class Source {
this.eventSource = null;
}
listen(eventType) {
listen(eventType: string) {
if (this.listening[eventType]) return;
this.listening[eventType] = true;
this.eventSource.addEventListener(eventType, (event) => {
@@ -58,13 +55,13 @@ class Source {
});
}
notifyClients(event) {
notifyClients(event: {type: string, data: any}) {
for (const client of this.clients) {
client.postMessage(event);
}
}
status(port) {
status(port: MessagePort) {
port.postMessage({
type: 'status',
message: `url: ${this.url} readyState: ${this.eventSource.readyState}`,
@@ -72,7 +69,11 @@ class Source {
}
}
self.addEventListener('connect', (e: Event & {ports: Array<any>}) => {
const sourcesByUrl: Map<string, Source | null> = new Map();
const sourcesByPort: Map<MessagePort, Source | null> = new Map();
// @ts-expect-error: typescript bug?
self.addEventListener('connect', (e: MessageEvent) => {
for (const port of e.ports) {
port.addEventListener('message', (event) => {
if (!self.EventSource) {
@@ -84,14 +85,14 @@ self.addEventListener('connect', (e: Event & {ports: Array<any>}) => {
}
if (event.data.type === 'start') {
const url = event.data.url;
if (sourcesByUrl[url]) {
if (sourcesByUrl.get(url)) {
// we have a Source registered to this url
const source = sourcesByUrl[url];
const source = sourcesByUrl.get(url);
source.register(port);
sourcesByPort[port] = source;
sourcesByPort.set(port, source);
return;
}
let source = sourcesByPort[port];
let source = sourcesByPort.get(port);
if (source) {
if (source.eventSource && source.url === url) return;
@@ -101,30 +102,30 @@ self.addEventListener('connect', (e: Event & {ports: Array<any>}) => {
// Clean-up
if (count === 0) {
source.close();
sourcesByUrl[source.url] = null;
sourcesByUrl.set(source.url, null);
}
}
// Create a new Source
source = new Source(url);
source.register(port);
sourcesByUrl[url] = source;
sourcesByPort[port] = source;
sourcesByUrl.set(url, source);
sourcesByPort.set(port, source);
} else if (event.data.type === 'listen') {
const source = sourcesByPort[port];
const source = sourcesByPort.get(port);
source.listen(event.data.eventType);
} else if (event.data.type === 'close') {
const source = sourcesByPort[port];
const source = sourcesByPort.get(port);
if (!source) return;
const count = source.deregister(port);
if (count === 0) {
source.close();
sourcesByUrl[source.url] = null;
sourcesByPort[port] = null;
sourcesByUrl.set(source.url, null);
sourcesByPort.set(port, null);
}
} else if (event.data.type === 'status') {
const source = sourcesByPort[port];
const source = sourcesByPort.get(port);
if (!source) {
port.postMessage({
type: 'status',
+3 -3
View File
@@ -1,6 +1,6 @@
import $ from 'jquery';
import {GET} from '../modules/fetch.ts';
import {toggleElem} from '../utils/dom.ts';
import {toggleElem, type DOMEvent} from '../utils/dom.ts';
import {logoutFromWorker} from '../modules/worker.ts';
const {appSubUrl, notificationSettings, assetVersionEncoded} = window.config;
@@ -25,8 +25,8 @@ export function initNotificationsTable() {
});
// mark clicked unread links for deletion on bfcache restore
for (const link of table.querySelectorAll('.notifications-item[data-status="1"] .notifications-link')) {
link.addEventListener('click', (e : MouseEvent & {target: HTMLElement}) => {
for (const link of table.querySelectorAll<HTMLAnchorElement>('.notifications-item[data-status="1"] .notifications-link')) {
link.addEventListener('click', (e: DOMEvent<MouseEvent>) => {
e.target.closest('.notifications-item').setAttribute('data-remove', 'true');
});
}
+4 -2
View File
@@ -1,6 +1,8 @@
import type {DOMEvent} from '../utils/dom.ts';
export function initOAuth2SettingsDisableCheckbox() {
for (const el of document.querySelectorAll('.disable-setting')) {
el.addEventListener('change', (e: Event & {target: HTMLInputElement}) => {
for (const el of document.querySelectorAll<HTMLInputElement>('.disable-setting')) {
el.addEventListener('change', (e: DOMEvent<Event, HTMLInputElement>) => {
document.querySelector(e.target.getAttribute('data-target')).classList.toggle('disabled', e.target.checked);
});
}
+2 -2
View File
@@ -6,7 +6,7 @@ import {attachRefIssueContextPopup} from './contextpopup.ts';
import {POST} from '../modules/fetch.ts';
import {initDropzone} from './dropzone.ts';
import {confirmModal} from './comp/ConfirmModal.ts';
import {applyAreYouSure} from '../vendor/jquery.are-you-sure.ts';
import {applyAreYouSure, ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
function initEditPreviewTab(elForm: HTMLFormElement) {
@@ -188,7 +188,7 @@ export function initRepoEditor() {
header: elForm.getAttribute('data-text-empty-confirm-header'),
content: elForm.getAttribute('data-text-empty-confirm-content'),
})) {
elForm.classList.remove('dirty');
ignoreAreYouSure(elForm);
elForm.submit();
}
}
+6 -6
View File
@@ -1,9 +1,9 @@
import {hideElem, showElem} from '../utils/dom.ts';
import {hideElem, showElem, type DOMEvent} from '../utils/dom.ts';
import {GET} from '../modules/fetch.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
export function initRepoGraphGit() {
const graphContainer = document.querySelector('#git-graph-container');
const graphContainer = document.querySelector<HTMLElement>('#git-graph-container');
if (!graphContainer) return;
document.querySelector('#flow-color-monochrome')?.addEventListener('click', () => {
@@ -87,7 +87,7 @@ export function initRepoGraphGit() {
fomanticQuery(flowSelectRefsDropdown).dropdown({
clearable: true,
fullTextSeach: 'exact',
onRemove(toRemove) {
onRemove(toRemove: string) {
if (toRemove === '...flow-hide-pr-refs') {
params.delete('hide-pr-refs');
} else {
@@ -101,7 +101,7 @@ export function initRepoGraphGit() {
}
updateGraph();
},
onAdd(toAdd) {
onAdd(toAdd: string) {
if (toAdd === '...flow-hide-pr-refs') {
params.set('hide-pr-refs', 'true');
} else {
@@ -111,7 +111,7 @@ export function initRepoGraphGit() {
},
});
graphContainer.addEventListener('mouseenter', (e: MouseEvent & {target: HTMLElement}) => {
graphContainer.addEventListener('mouseenter', (e: DOMEvent<MouseEvent>) => {
if (e.target.matches('#rev-list li')) {
const flow = e.target.getAttribute('data-flow');
if (flow === '0') return;
@@ -132,7 +132,7 @@ export function initRepoGraphGit() {
}
});
graphContainer.addEventListener('mouseleave', (e: MouseEvent & {target: HTMLElement}) => {
graphContainer.addEventListener('mouseleave', (e: DOMEvent<MouseEvent>) => {
if (e.target.matches('#rev-list li')) {
const flow = e.target.getAttribute('data-flow');
if (flow === '0') return;
+2 -2
View File
@@ -1,5 +1,5 @@
import {stripTags} from '../utils.ts';
import {hideElem, queryElemChildren, showElem} from '../utils/dom.ts';
import {hideElem, queryElemChildren, showElem, type DOMEvent} from '../utils/dom.ts';
import {POST} from '../modules/fetch.ts';
import {showErrorToast, type Toast} from '../modules/toast.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
@@ -28,7 +28,7 @@ export function initRepoTopicBar() {
mgrBtn.focus();
});
document.querySelector('#save_topic').addEventListener('click', async (e: MouseEvent & {target: HTMLButtonElement}) => {
document.querySelector<HTMLButtonElement>('#save_topic').addEventListener('click', async (e: DOMEvent<MouseEvent, HTMLButtonElement>) => {
lastErrorToast?.hideToast();
const topics = editDiv.querySelector<HTMLInputElement>('input[name=topics]').value;
+5 -1
View File
@@ -7,6 +7,7 @@ import {attachRefIssueContextPopup} from './contextpopup.ts';
import {initCommentContent, initMarkupContent} from '../markup/content.ts';
import {triggerUploadStateChanged} from './comp/EditorUpload.ts';
import {convertHtmlToMarkdown} from '../markup/html2markdown.ts';
import {applyAreYouSure, reinitializeAreYouSure} from '../vendor/jquery.are-you-sure.ts';
async function tryOnEditContent(e) {
const clickTarget = e.target.closest('.edit-content');
@@ -48,6 +49,7 @@ async function tryOnEditContent(e) {
showErrorToast(data.errorMessage);
return;
}
reinitializeAreYouSure(editContentZone.querySelector('form')); // the form is no longer dirty
editContentZone.setAttribute('data-content-version', data.contentVersion);
if (!data.content) {
renderContent.innerHTML = document.querySelector('#no-content').innerHTML;
@@ -86,13 +88,15 @@ async function tryOnEditContent(e) {
comboMarkdownEditor = getComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
if (!comboMarkdownEditor) {
editContentZone.innerHTML = document.querySelector('#issue-comment-editor-template').innerHTML;
const form = editContentZone.querySelector('form');
applyAreYouSure(form);
const saveButton = querySingleVisibleElem<HTMLButtonElement>(editContentZone, '.ui.primary.button');
const cancelButton = querySingleVisibleElem<HTMLButtonElement>(editContentZone, '.ui.cancel.button');
comboMarkdownEditor = await initComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor'));
const syncUiState = () => saveButton.disabled = comboMarkdownEditor.isUploading();
comboMarkdownEditor.container.addEventListener(ComboMarkdownEditor.EventUploadStateChanged, syncUiState);
cancelButton.addEventListener('click', cancelAndReset);
saveButton.addEventListener('click', saveAndRefresh);
form.addEventListener('submit', saveAndRefresh);
}
// FIXME: ideally here should reload content and attachment list from backend for existing editor, to avoid losing data
+16 -12
View File
@@ -8,6 +8,7 @@ import {
queryElems,
showElem,
toggleElem,
type DOMEvent,
} from '../utils/dom.ts';
import {setFileFolding} from './file-fold.ts';
import {ComboMarkdownEditor, getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
@@ -16,6 +17,7 @@ import {GET, POST} from '../modules/fetch.ts';
import {showErrorToast} from '../modules/toast.ts';
import {initRepoIssueSidebar} from './repo-issue-sidebar.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
import {ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
const {appSubUrl} = window.config;
@@ -52,7 +54,7 @@ export function initRepoIssueSidebarList() {
});
}
function initRepoIssueLabelFilter(elDropdown: Element) {
function initRepoIssueLabelFilter(elDropdown: HTMLElement) {
const url = new URL(window.location.href);
const showArchivedLabels = url.searchParams.get('archived_labels') === 'true';
const queryLabels = url.searchParams.get('labels') || '';
@@ -124,7 +126,7 @@ export function initRepoIssueFilterItemLabel() {
export function initRepoIssueCommentDelete() {
// Delete comment
document.addEventListener('click', async (e: MouseEvent & {target: HTMLElement}) => {
document.addEventListener('click', async (e: DOMEvent<MouseEvent>) => {
if (!e.target.matches('.delete-comment')) return;
e.preventDefault();
@@ -199,7 +201,7 @@ export function initRepoIssueDependencyDelete() {
export function initRepoIssueCodeCommentCancel() {
// Cancel inline code comment
document.addEventListener('click', (e: MouseEvent & {target: HTMLElement}) => {
document.addEventListener('click', (e: DOMEvent<MouseEvent>) => {
if (!e.target.matches('.cancel-code-comment')) return;
const form = e.target.closest('form');
@@ -221,7 +223,7 @@ export function initRepoPullRequestUpdate() {
e.preventDefault();
const redirect = this.getAttribute('data-redirect');
this.classList.add('is-loading');
let response;
let response: Response;
try {
response = await POST(this.getAttribute('data-do'));
} catch (error) {
@@ -229,7 +231,7 @@ export function initRepoPullRequestUpdate() {
} finally {
this.classList.remove('is-loading');
}
let data;
let data: Record<string, any>;
try {
data = await response?.json(); // the response is probably not a JSON
} catch (error) {
@@ -340,7 +342,7 @@ export function initRepoIssueWipTitle() {
export function initRepoIssueComments() {
if (!$('.repository.view.issue .timeline').length) return;
document.addEventListener('click', (e: MouseEvent & {target: HTMLElement}) => {
document.addEventListener('click', (e: DOMEvent<MouseEvent>) => {
const urlTarget = document.querySelector(':target');
if (!urlTarget) return;
@@ -532,7 +534,7 @@ export function initRepoIssueWipToggle() {
export function initRepoIssueTitleEdit() {
const issueTitleDisplay = document.querySelector('#issue-title-display');
const issueTitleEditor = document.querySelector('#issue-title-editor');
const issueTitleEditor = document.querySelector<HTMLFormElement>('#issue-title-editor');
if (!issueTitleEditor) return;
const issueTitleInput = issueTitleEditor.querySelector('input');
@@ -558,7 +560,8 @@ export function initRepoIssueTitleEdit() {
const prTargetUpdateUrl = pullDescEditor?.getAttribute('data-target-update-url');
const editSaveButton = issueTitleEditor.querySelector('.ui.primary.button');
editSaveButton.addEventListener('click', async () => {
issueTitleEditor.addEventListener('submit', async (e) => {
e.preventDefault();
const newTitle = issueTitleInput.value.trim();
try {
if (newTitle && newTitle !== oldTitle) {
@@ -577,6 +580,7 @@ export function initRepoIssueTitleEdit() {
}
}
}
ignoreAreYouSure(issueTitleEditor);
window.location.reload();
} catch (error) {
console.error(error);
@@ -586,7 +590,7 @@ export function initRepoIssueTitleEdit() {
}
export function initRepoIssueBranchSelect() {
document.querySelector('#branch-select')?.addEventListener('click', (e: MouseEvent & {target: HTMLElement}) => {
document.querySelector<HTMLElement>('#branch-select')?.addEventListener('click', (e: DOMEvent<MouseEvent>) => {
const el = e.target.closest('.item[data-branch]');
if (!el) return;
const pullTargetBranch = document.querySelector('#pull-target-branch');
@@ -625,10 +629,10 @@ function initIssueTemplateCommentEditors($commentForm) {
// * new issue with issue template
const $comboFields = $commentForm.find('.combo-editor-dropzone');
const initCombo = async (elCombo) => {
const initCombo = async (elCombo: HTMLElement) => {
const $formField = $(elCombo.querySelector('.form-field-real'));
const dropzoneContainer = elCombo.querySelector('.form-field-dropzone');
const markdownEditor = elCombo.querySelector('.combo-markdown-editor');
const dropzoneContainer = elCombo.querySelector<HTMLElement>('.form-field-dropzone');
const markdownEditor = elCombo.querySelector<HTMLElement>('.combo-markdown-editor');
const editor = await initComboMarkdownEditor(markdownEditor);
editor.container.addEventListener(ComboMarkdownEditor.EventEditorContentChanged, () => $formField.val(editor.value()));
+8 -1
View File
@@ -7,7 +7,6 @@ import {
initRepoPullRequestUpdate,
} from './repo-issue.ts';
import {initUnicodeEscapeButton} from './repo-unicode-escape.ts';
import {initRepoBranchTagSelector} from '../components/RepoBranchTagSelector.vue';
import {initRepoCloneButtons} from './repo-common.ts';
import {initCitationFileCopyContent} from './citation.ts';
import {initCompLabelEdit} from './comp/LabelEdit.ts';
@@ -20,6 +19,14 @@ import {hideElem, queryElemChildren, showElem} from '../utils/dom.ts';
import {initRepoIssueCommentEdit} from './repo-issue-edit.ts';
import {initRepoMilestone} from './repo-milestone.ts';
import {initRepoNew} from './repo-new.ts';
import {createApp} from 'vue';
import RepoBranchTagSelector from '../components/RepoBranchTagSelector.vue';
function initRepoBranchTagSelector(selector: string) {
for (const elRoot of document.querySelectorAll(selector)) {
createApp(RepoBranchTagSelector, {elRoot}).mount(elRoot);
}
}
export function initBranchSelectorTabs() {
const elSelectBranch = document.querySelector('.ui.dropdown.select-branch');
+75 -115
View File
@@ -1,31 +1,17 @@
import $ from 'jquery';
import {contrastColor} from '../utils/color.ts';
import {createSortable} from '../modules/sortable.ts';
import {POST, DELETE, PUT} from '../modules/fetch.ts';
import {POST, request} from '../modules/fetch.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
import {queryElemChildren, queryElems} from '../utils/dom.ts';
import type {SortableEvent} from 'sortablejs';
function updateIssueCount(cards) {
const parent = cards.parentElement;
const cnt = parent.querySelectorAll('.issue-card').length;
parent.querySelectorAll('.project-column-issue-count')[0].textContent = cnt;
function updateIssueCount(card: HTMLElement): void {
const parent = card.parentElement;
const count = parent.querySelectorAll('.issue-card').length;
parent.querySelector('.project-column-issue-count').textContent = String(count);
}
async function createNewColumn(url, columnTitle, projectColorInput) {
try {
await POST(url, {
data: {
title: columnTitle.val(),
color: projectColorInput.val(),
},
});
} catch (error) {
console.error(error);
} finally {
columnTitle.closest('form').removeClass('dirty');
window.location.reload();
}
}
async function moveIssue({item, from, to, oldIndex}: {item: HTMLElement, from: HTMLElement, to: HTMLElement, oldIndex: number}) {
async function moveIssue({item, from, to, oldIndex}: SortableEvent): Promise<void> {
const columnCards = to.querySelectorAll('.issue-card');
updateIssueCount(from);
updateIssueCount(to);
@@ -47,13 +33,10 @@ async function moveIssue({item, from, to, oldIndex}: {item: HTMLElement, from: H
}
}
async function initRepoProjectSortable() {
const els = document.querySelectorAll('#project-board > .board.sortable');
if (!els.length) return;
async function initRepoProjectSortable(): Promise<void> {
// the HTML layout is: #project-board > .board > .project-column .cards > .issue-card
const mainBoard = els[0];
let boardColumns = mainBoard.querySelectorAll('.project-column');
const mainBoard = document.querySelector('#project-board > .board.sortable');
let boardColumns = mainBoard.querySelectorAll<HTMLElement>('.project-column');
createSortable(mainBoard, {
group: 'project-column',
draggable: '.project-column',
@@ -61,7 +44,7 @@ async function initRepoProjectSortable() {
delayOnTouchOnly: true,
delay: 500,
onSort: async () => { // eslint-disable-line @typescript-eslint/no-misused-promises
boardColumns = mainBoard.querySelectorAll('.project-column');
boardColumns = mainBoard.querySelectorAll<HTMLElement>('.project-column');
const columnSorting = {
columns: Array.from(boardColumns, (column, i) => ({
@@ -81,7 +64,7 @@ async function initRepoProjectSortable() {
});
for (const boardColumn of boardColumns) {
const boardCardList = boardColumn.querySelectorAll('.cards')[0];
const boardCardList = boardColumn.querySelector('.cards');
createSortable(boardCardList, {
group: 'shared',
onAdd: moveIssue, // eslint-disable-line @typescript-eslint/no-misused-promises
@@ -92,97 +75,74 @@ async function initRepoProjectSortable() {
}
}
export function initRepoProject() {
if (!document.querySelector('.repository.projects')) {
return;
}
function initRepoProjectColumnEdit(writableProjectBoard: Element): void {
const elModal = document.querySelector<HTMLElement>('.ui.modal#project-column-modal-edit');
const elForm = elModal.querySelector<HTMLFormElement>('form');
initRepoProjectSortable(); // no await
const elColumnId = elForm.querySelector<HTMLInputElement>('input[name="id"]');
const elColumnTitle = elForm.querySelector<HTMLInputElement>('input[name="title"]');
const elColumnColor = elForm.querySelector<HTMLInputElement>('input[name="color"]');
for (const modal of document.querySelectorAll('.edit-project-column-modal')) {
const projectHeader = modal.closest<HTMLElement>('.project-column-header');
const projectTitleLabel = projectHeader?.querySelector<HTMLElement>('.project-column-title-label');
const projectTitleInput = modal.querySelector<HTMLInputElement>('.project-column-title-input');
const projectColorInput = modal.querySelector<HTMLInputElement>('#new_project_column_color');
const boardColumn = modal.closest<HTMLElement>('.project-column');
modal.querySelector('.edit-project-column-button')?.addEventListener('click', async function (e) {
e.preventDefault();
try {
await PUT(this.getAttribute('data-url'), {
data: {
title: projectTitleInput?.value,
color: projectColorInput?.value,
},
});
} catch (error) {
console.error(error);
} finally {
projectTitleLabel.textContent = projectTitleInput?.value;
projectTitleInput.closest('form')?.classList.remove('dirty');
const dividers = boardColumn.querySelectorAll<HTMLElement>(':scope > .divider');
if (projectColorInput.value) {
const color = contrastColor(projectColorInput.value);
boardColumn.style.setProperty('background', projectColorInput.value, 'important');
boardColumn.style.setProperty('color', color, 'important');
for (const divider of dividers) {
divider.style.setProperty('color', color);
}
} else {
boardColumn.style.removeProperty('background');
boardColumn.style.removeProperty('color');
for (const divider of dividers) {
divider.style.removeProperty('color');
}
}
$('.ui.modal').modal('hide');
}
});
}
const attrDataColumnId = 'data-modal-project-column-id';
const attrDataColumnTitle = 'data-modal-project-column-title-input';
const attrDataColumnColor = 'data-modal-project-column-color-input';
$('.default-project-column-modal').each(function () {
const $boardColumn = $(this).closest('.project-column');
const $showButton = $($boardColumn).find('.default-project-column-show');
const $commitButton = $(this).find('.actions > .ok.button');
$($commitButton).on('click', async (e) => {
e.preventDefault();
try {
await POST($($showButton).data('url'));
} catch (error) {
console.error(error);
} finally {
window.location.reload();
}
// the "new" button is not in project board, so need to query from document
queryElems(document, '.show-project-column-modal-edit', (el) => {
el.addEventListener('click', () => {
elColumnId.value = el.getAttribute(attrDataColumnId);
elColumnTitle.value = el.getAttribute(attrDataColumnTitle);
elColumnColor.value = el.getAttribute(attrDataColumnColor);
elColumnColor.dispatchEvent(new Event('input', {bubbles: true})); // trigger the color picker
});
});
$('.show-delete-project-column-modal').each(function () {
const $deleteColumnModal = $(`${this.getAttribute('data-modal')}`);
const $deleteColumnButton = $deleteColumnModal.find('.actions > .ok.button');
const deleteUrl = this.getAttribute('data-url');
$deleteColumnButton.on('click', async (e) => {
e.preventDefault();
try {
await DELETE(deleteUrl);
} catch (error) {
console.error(error);
} finally {
window.location.reload();
}
});
});
$('#new_project_column_submit').on('click', (e) => {
elForm.addEventListener('submit', async (e) => {
e.preventDefault();
const $columnTitle = $('#new_project_column');
const $projectColorInput = $('#new_project_column_color_picker');
if (!$columnTitle.val()) {
return;
const columnId = elColumnId.value;
const actionBaseLink = elForm.getAttribute('data-action-base-link');
const formData = new FormData(elForm);
const formLink = columnId ? `${actionBaseLink}/${columnId}` : `${actionBaseLink}/columns/new`;
const formMethod = columnId ? 'PUT' : 'POST';
try {
elForm.classList.add('is-loading');
await request(formLink, {method: formMethod, data: formData});
if (!columnId) {
window.location.reload(); // newly added column, need to reload the page
return;
}
fomanticQuery(elModal).modal('hide');
// update the newly saved column title and color in the project board (to avoid reload)
const elEditButton = writableProjectBoard.querySelector<HTMLButtonElement>(`.show-project-column-modal-edit[${attrDataColumnId}="${columnId}"]`);
elEditButton.setAttribute(attrDataColumnTitle, elColumnTitle.value);
elEditButton.setAttribute(attrDataColumnColor, elColumnColor.value);
const elBoardColumn = writableProjectBoard.querySelector<HTMLElement>(`.project-column[data-id="${columnId}"]`);
const elBoardColumnTitle = elBoardColumn.querySelector<HTMLElement>(`.project-column-title-text`);
elBoardColumnTitle.textContent = elColumnTitle.value;
if (elColumnColor.value) {
const textColor = contrastColor(elColumnColor.value);
elBoardColumn.style.setProperty('background', elColumnColor.value, 'important');
elBoardColumn.style.setProperty('color', textColor, 'important');
queryElemChildren<HTMLElement>(elBoardColumn, '.divider', (divider) => divider.style.color = textColor);
} else {
elBoardColumn.style.removeProperty('background');
elBoardColumn.style.removeProperty('color');
queryElemChildren<HTMLElement>(elBoardColumn, '.divider', (divider) => divider.style.removeProperty('color'));
}
} finally {
elForm.classList.remove('is-loading');
}
const url = e.target.getAttribute('data-url');
createNewColumn(url, $columnTitle, $projectColorInput);
});
}
export function initRepoProject(): void {
const writableProjectBoard = document.querySelector('#project-board[data-project-borad-writable="true"]');
if (!writableProjectBoard) return;
initRepoProjectSortable(); // no await
initRepoProjectColumnEdit(writableProjectBoard);
}
+4 -4
View File
@@ -1,7 +1,7 @@
import {hideElem, showElem} from '../utils/dom.ts';
import {hideElem, showElem, type DOMEvent} from '../utils/dom.ts';
export function initRepoRelease() {
document.addEventListener('click', (e: MouseEvent & {target: HTMLElement}) => {
document.addEventListener('click', (e: DOMEvent<MouseEvent>) => {
if (e.target.matches('.remove-rel-attach')) {
const uuid = e.target.getAttribute('data-uuid');
const id = e.target.getAttribute('data-id');
@@ -42,7 +42,7 @@ function initTagNameEditor() {
}
};
hideTargetInput(tagNameInput); // update on page load because the input may have a value
tagNameInput.addEventListener('input', (e: InputEvent & {target: HTMLInputElement}) => {
hideTargetInput(e.target);
tagNameInput.addEventListener('input', (e) => {
hideTargetInput(e.target as HTMLInputElement);
});
}
+3 -1
View File
@@ -1,8 +1,10 @@
import type {DOMEvent} from '../utils/dom.ts';
export function initRepositorySearch() {
const repositorySearchForm = document.querySelector<HTMLFormElement>('#repo-search-form');
if (!repositorySearchForm) return;
repositorySearchForm.addEventListener('change', (e: Event & {target: HTMLFormElement}) => {
repositorySearchForm.addEventListener('change', (e: DOMEvent<Event, HTMLInputElement>) => {
e.preventDefault();
const params = new URLSearchParams();
@@ -2,7 +2,8 @@ import {beforeEach, describe, expect, test, vi} from 'vitest';
import {initRepoSettingsBranchesDrag} from './repo-settings-branches.ts';
import {POST} from '../modules/fetch.ts';
import {createSortable} from '../modules/sortable.ts';
import type {SortableEvent} from 'sortablejs';
import type {SortableEvent, SortableOptions} from 'sortablejs';
import type Sortable from 'sortablejs';
vi.mock('../modules/fetch.ts', () => ({
POST: vi.fn(),
@@ -55,9 +56,10 @@ describe('Repository Branch Settings', () => {
vi.mocked(POST).mockResolvedValue({ok: true} as Response);
// Mock createSortable to capture and execute the onEnd callback
vi.mocked(createSortable).mockImplementation(async (_el: Element, options) => {
vi.mocked(createSortable).mockImplementation(async (_el: Element, options: SortableOptions) => {
options.onEnd(new Event('SortableEvent') as SortableEvent);
return {destroy: vi.fn()};
// @ts-expect-error: mock is incomplete
return {destroy: vi.fn()} as Sortable;
});
initRepoSettingsBranchesDrag();
+2 -2
View File
@@ -122,7 +122,7 @@ function initRepoSettingsBranches() {
function initRepoSettingsOptions() {
if ($('.repository.settings.options').length > 0) {
// Enable or select internal/external wiki system and issue tracker.
$('.enable-system').on('change', function (this: HTMLInputElement) {
$('.enable-system').on('change', function (this: HTMLInputElement) { // eslint-disable-line @typescript-eslint/no-deprecated
if (this.checked) {
$($(this).data('target')).removeClass('disabled');
if (!$(this).data('context')) $($(this).data('context')).addClass('disabled');
@@ -131,7 +131,7 @@ function initRepoSettingsOptions() {
if (!$(this).data('context')) $($(this).data('context')).removeClass('disabled');
}
});
$('.enable-system-radio').on('change', function (this: HTMLInputElement) {
$('.enable-system-radio').on('change', function (this: HTMLInputElement) { // eslint-disable-line @typescript-eslint/no-deprecated
if (this.value === 'false') {
$($(this).data('target')).addClass('disabled');
if ($(this).data('context') !== undefined) $($(this).data('context')).removeClass('disabled');
+1 -1
View File
@@ -13,7 +13,7 @@ async function initRepoWikiFormEditor() {
let editor: ComboMarkdownEditor;
let renderRequesting = false;
let lastContent;
let lastContent: string = '';
const renderEasyMDEPreview = async function () {
if (renderRequesting) return;
-1
View File
@@ -14,7 +14,6 @@ declare module '*.vue' {
export default component;
// List of named exports from vue components, used to make `tsc` output clean.
// To actually lint .vue files, `vue-tsc` is used because `tsc` can not parse them.
export function initRepoBranchTagSelector(selector: string): void;
export function initDashboardRepoList(): void;
export function initRepositoryActionView(): void;
}
+2 -2
View File
@@ -9,13 +9,13 @@ window.htmx.config.requestClass = 'is-loading';
window.htmx.config.scrollIntoViewOnBoost = false;
// https://htmx.org/events/#htmx:sendError
document.body.addEventListener('htmx:sendError', (event: HtmxEvent) => {
document.body.addEventListener('htmx:sendError', (event: Partial<HtmxEvent>) => {
// TODO: add translations
showErrorToast(`Network error when calling ${event.detail.requestConfig.path}`);
});
// https://htmx.org/events/#htmx:responseError
document.body.addEventListener('htmx:responseError', (event: HtmxEvent) => {
document.body.addEventListener('htmx:responseError', (event: Partial<HtmxEvent>) => {
// TODO: add translations
showErrorToast(`Error ${event.detail.xhr.status} when calling ${event.detail.requestConfig.path}`);
});
+8 -8
View File
@@ -12,7 +12,7 @@ type ProcessorContext = {
function prepareProcessors(ctx:ProcessorContext): Processors {
const processors = {
H1(el: HTMLHeadingElement) {
H1(el: HTMLElement) {
const level = parseInt(el.tagName.slice(1));
el.textContent = `${'#'.repeat(level)} ${el.textContent.trim()}`;
},
@@ -25,7 +25,7 @@ function prepareProcessors(ctx:ProcessorContext): Processors {
DEL(el: HTMLElement) {
return `~~${el.textContent}~~`;
},
A(el: HTMLAnchorElement) {
A(el: HTMLElement) {
const text = el.textContent || 'link';
const href = el.getAttribute('href');
if (/^https?:/.test(text) && text === href) {
@@ -33,7 +33,7 @@ function prepareProcessors(ctx:ProcessorContext): Processors {
}
return href ? `[${text}](${href})` : text;
},
IMG(el: HTMLImageElement) {
IMG(el: HTMLElement) {
const alt = el.getAttribute('alt') || 'image';
const src = el.getAttribute('src');
const widthAttr = el.hasAttribute('width') ? ` width="${htmlEscape(el.getAttribute('width') || '')}"` : '';
@@ -43,7 +43,7 @@ function prepareProcessors(ctx:ProcessorContext): Processors {
}
return `![${alt}](${src})`;
},
P(el: HTMLParagraphElement) {
P(el: HTMLElement) {
el.textContent = `${el.textContent}\n`;
},
BLOCKQUOTE(el: HTMLElement) {
@@ -54,14 +54,14 @@ function prepareProcessors(ctx:ProcessorContext): Processors {
el.textContent = `${preNewLine}${el.textContent}\n`;
},
LI(el: HTMLElement) {
const parent = el.parentNode;
const bullet = (parent as HTMLElement).tagName === 'OL' ? `1. ` : '* ';
const parent = el.parentNode as HTMLElement;
const bullet = parent.tagName === 'OL' ? `1. ` : '* ';
const nestingIdentLevel = Math.max(0, ctx.listNestingLevel - 1);
el.textContent = `${' '.repeat(nestingIdentLevel * 4)}${bullet}${el.textContent}${ctx.elementIsLast ? '' : '\n'}`;
return el;
},
INPUT(el: HTMLInputElement) {
return el.checked ? '[x] ' : '[ ] ';
INPUT(el: HTMLElement) {
return (el as HTMLInputElement).checked ? '[x] ' : '[ ] ';
},
CODE(el: HTMLElement) {
const text = el.textContent;
+2 -1
View File
@@ -1,6 +1,7 @@
import type {SortableOptions, SortableEvent} from 'sortablejs';
import type SortableType from 'sortablejs';
export async function createSortable(el: Element, opts: {handle?: string} & SortableOptions = {}) {
export async function createSortable(el: Element, opts: {handle?: string} & SortableOptions = {}): Promise<SortableType> {
// @ts-expect-error: wrong type derived by typescript
const {Sortable} = await import(/* webpackChunkName: "sortablejs" */'sortablejs');
+5 -5
View File
@@ -121,14 +121,14 @@ function switchTitleToTooltip(target: Element): void {
* Some browsers like PaleMoon don't support "addEventListener('mouseenter', capture)"
* The tippy by default uses "mouseenter" event to show, so we use "mouseover" event to switch to tippy
*/
function lazyTooltipOnMouseHover(e: MouseEvent): void {
function lazyTooltipOnMouseHover(e: Event): void {
e.target.removeEventListener('mouseover', lazyTooltipOnMouseHover, true);
attachTooltip(this);
}
// Activate the tooltip for current element.
// If the element has no aria-label, use the tooltip content as aria-label.
function attachLazyTooltip(el: Element): void {
function attachLazyTooltip(el: HTMLElement): void {
el.addEventListener('mouseover', lazyTooltipOnMouseHover, {capture: true});
// meanwhile, if the element has no aria-label, use the tooltip content as aria-label
@@ -141,8 +141,8 @@ function attachLazyTooltip(el: Element): void {
}
// Activate the tooltip for all children elements.
function attachChildrenLazyTooltip(target: Element): void {
for (const el of target.querySelectorAll<Element>('[data-tooltip-content]')) {
function attachChildrenLazyTooltip(target: HTMLElement): void {
for (const el of target.querySelectorAll<HTMLElement>('[data-tooltip-content]')) {
attachLazyTooltip(el);
}
}
@@ -160,7 +160,7 @@ export function initGlobalTooltips(): void {
for (const mutation of [...mutationList, ...pending]) {
if (mutation.type === 'childList') {
// mainly for Vue components and AJAX rendered elements
for (const el of mutation.addedNodes as NodeListOf<Element>) {
for (const el of mutation.addedNodes as NodeListOf<HTMLElement>) {
if (!isDocumentFragmentOrElementNode(el)) continue;
attachChildrenLazyTooltip(el);
if (el.hasAttribute('data-tooltip-content')) {
+2
View File
@@ -65,6 +65,7 @@ import octiconSidebarCollapse from '../../public/assets/img/svg/octicon-sidebar-
import octiconSidebarExpand from '../../public/assets/img/svg/octicon-sidebar-expand.svg';
import octiconSkip from '../../public/assets/img/svg/octicon-skip.svg';
import octiconStar from '../../public/assets/img/svg/octicon-star.svg';
import octiconStop from '../../public/assets/img/svg/octicon-stop.svg';
import octiconStrikethrough from '../../public/assets/img/svg/octicon-strikethrough.svg';
import octiconSync from '../../public/assets/img/svg/octicon-sync.svg';
import octiconTable from '../../public/assets/img/svg/octicon-table.svg';
@@ -140,6 +141,7 @@ const svgs = {
'octicon-sidebar-expand': octiconSidebarExpand,
'octicon-skip': octiconSkip,
'octicon-star': octiconStar,
'octicon-stop': octiconStop,
'octicon-strikethrough': octiconStrikethrough,
'octicon-sync': octiconSync,
'octicon-table': octiconTable,
+2 -1
View File
@@ -7,6 +7,7 @@ type ArrayLikeIterable<T> = ArrayLike<T> & Iterable<T>; // for NodeListOf and Ar
type ElementArg = Element | string | ArrayLikeIterable<Element> | ReturnType<typeof $>;
type ElementsCallback<T extends Element> = (el: T) => Promisable<any>;
type ElementsCallbackWithArgs = (el: Element, ...args: any[]) => Promisable<any>;
export type DOMEvent<E extends Event, T extends Element = HTMLElement> = E & { target: Partial<T>; };
function elementsCall(el: ElementArg, func: ElementsCallbackWithArgs, ...args: any[]) {
if (typeof el === 'string' || el instanceof String) {
@@ -87,7 +88,7 @@ export function queryElemChildren<T extends Element>(parent: Element | ParentNod
// it works like parent.querySelectorAll: all descendants are selected
// in the future, all "queryElems(document, ...)" should be refactored to use a more specific parent
export function queryElems<T extends Element>(parent: Element | ParentNode, selector: string, fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
export function queryElems<T extends HTMLElement>(parent: Element | ParentNode, selector: string, fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
return applyElemsCallback<T>(parent.querySelectorAll(selector), fn);
}
+15 -4
View File
@@ -2,6 +2,7 @@
// Fork of the upstream module. The only changes are:
// * use export to make it work with ES6 modules.
// * the addition of `const` to make it strict mode compatible.
// * ignore forms with "ignore-dirty" class, ignore hidden forms (closest('.tw-hidden'))
/*!
* jQuery Plugin: Are-You-Sure (Dirty Form Detection)
@@ -161,10 +162,10 @@ export function initAreYouSure($) {
if (!settings.silent && !window.aysUnloadSet) {
window.aysUnloadSet = true;
$(window).bind('beforeunload', function() {
const $dirtyForms = $("form").filter('.' + settings.dirtyClass);
if ($dirtyForms.length == 0) {
return;
}
const $forms = $("form:not(.ignore-dirty)").filter('.' + settings.dirtyClass);
const dirtyFormCount = Array.from($forms).reduce((res, form) => form.closest('.tw-hidden') ? res : res + 1, 0);
if (dirtyFormCount === 0) return;
// Prevent multiple prompts - seen on Chrome and IE
if (navigator.userAgent.toLowerCase().match(/msie|chrome/)) {
if (window.aysHasPrompted) {
@@ -199,3 +200,13 @@ export function initAreYouSure($) {
export function applyAreYouSure(selectorOrEl: string|Element|$, opts = {}) {
$(selectorOrEl).areYouSure(opts);
}
export function reinitializeAreYouSure(selectorOrEl: string|Element|$) {
$(selectorOrEl).trigger('reinitialize.areYouSure');
}
export function ignoreAreYouSure(selectorOrEl: string|Element|$) {
// here we should only add "ignore-dirty" but not remove "dirty".
// because when using "enter" to submit a form, the "dirty" class will appear again before reloading.
$(selectorOrEl).addClass('ignore-dirty');
}