mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-03 03:23:25 +09:00
refactor: improve types in the frontend, misc fixes (#39142)
This commit is contained in:
@@ -25,7 +25,7 @@ function initSystemConfigAutoCheckbox(el: HTMLInputElement) {
|
||||
value: String(collectCheckboxBooleanValue(el)),
|
||||
});
|
||||
const resp = await POST(`${appSubUrl}/-/admin/config`, {data});
|
||||
const json: Record<string, any> = await resp.json();
|
||||
const json: {errorMessage?: string} = await resp.json();
|
||||
if (json.errorMessage) throw new Error(json.errorMessage);
|
||||
} catch (ex) {
|
||||
showTemporaryTooltip(el, errorMessage(ex));
|
||||
@@ -112,7 +112,7 @@ export class ConfigFormValueMapper {
|
||||
}
|
||||
|
||||
collectConfigValueFromElement(el: GeneralFormFieldElement) {
|
||||
let val: any;
|
||||
let val: boolean | number | string;
|
||||
const valType = this.presetValueTypes[el.name];
|
||||
if (el.matches('[type="checkbox"]')) {
|
||||
// TODO: if it needs to support array values in the future,
|
||||
|
||||
@@ -16,7 +16,7 @@ export async function initAdminSelfCheck() {
|
||||
now: String(Date.now()), // TODO: check time difference between server and client
|
||||
}),
|
||||
});
|
||||
const json: Record<string, any> = await resp.json();
|
||||
const json: {problems: string[] | null} = await resp.json();
|
||||
toggleElem(elCheckByFrontend, Boolean(json.problems?.length));
|
||||
for (const problem of json.problems ?? []) {
|
||||
const elProblem = document.createElement('div');
|
||||
|
||||
@@ -47,7 +47,7 @@ export type ElementWithAssignableProperties = {
|
||||
nodeName: string;
|
||||
getAttribute: (name: string) => string | null;
|
||||
setAttribute: (name: string, value: string) => void;
|
||||
} & Record<string, any>;
|
||||
};
|
||||
|
||||
export function assignElementProperty(el: ElementWithAssignableProperties, kebabName: string, val: string) {
|
||||
if (el.nodeName === 'FORM') {
|
||||
@@ -59,14 +59,15 @@ export function assignElementProperty(el: ElementWithAssignableProperties, kebab
|
||||
if (kebabName === 'url') kebabName = 'action';
|
||||
}
|
||||
const camelizedName = camelize(kebabName);
|
||||
const old = el[camelizedName];
|
||||
const properties: Record<string, unknown> = el;
|
||||
const old = properties[camelizedName];
|
||||
if (typeof old === 'boolean') {
|
||||
el[camelizedName] = val === 'true';
|
||||
properties[camelizedName] = val === 'true';
|
||||
} else if (typeof old === 'number') {
|
||||
el[camelizedName] = parseFloat(val);
|
||||
properties[camelizedName] = parseFloat(val);
|
||||
} else if (typeof old === 'string') {
|
||||
el[camelizedName] = val;
|
||||
} else if (old?.nodeName) {
|
||||
properties[camelizedName] = val;
|
||||
} else if (old && typeof old === 'object' && 'nodeName' in old) {
|
||||
// "form" has an edge case: its "<input name=action>" element overwrites the "action" property, we can only set attribute
|
||||
el.setAttribute(kebabName, val);
|
||||
} else {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import {handleGlobalEnterQuickSubmit} from './QuickSubmit.ts';
|
||||
import {renderPreviewPanelContent} from '../repo-editor.ts';
|
||||
import {toggleTasklistCheckbox} from '../../markup/tasklist.ts';
|
||||
import {easyMDEToolbarActions} from './EasyMDEToolbarActions.ts';
|
||||
import {easyMDEToolbarActions, type EasyMdeToolbarAction} from './EasyMDEToolbarActions.ts';
|
||||
import {initTextExpander} from './TextExpander.ts';
|
||||
import {showErrorToast} from '../../modules/toast.ts';
|
||||
import {POST} from '../../modules/fetch.ts';
|
||||
@@ -25,6 +25,7 @@ import {DropzoneCustomEventReloadFiles, initDropzone} from '../dropzone.ts';
|
||||
import {createTippy} from '../../modules/tippy.ts';
|
||||
import {initTabSwitcher} from '../../modules/fomantic/tab.ts';
|
||||
import type EasyMDE from 'easymde';
|
||||
import type Dropzone from '@deltablot/dropzone';
|
||||
import {localUserSettings} from '../../modules/user-settings.ts';
|
||||
|
||||
/**
|
||||
@@ -57,11 +58,11 @@ type Heights = {
|
||||
|
||||
type ComboMarkdownEditorOptions = {
|
||||
editorHeights?: Heights,
|
||||
easyMDEOptions?: EasyMDE.Options,
|
||||
easyMDEOptions?: Omit<EasyMDE.Options, 'toolbar'> & {toolbar?: ReadonlyArray<string>},
|
||||
};
|
||||
|
||||
type ComboMarkdownEditorTextarea = HTMLTextAreaElement & {_giteaComboMarkdownEditor: any};
|
||||
type ComboMarkdownEditorContainer = HTMLElement & {_giteaComboMarkdownEditor?: any};
|
||||
type ComboMarkdownEditorTextarea = HTMLTextAreaElement & {_giteaComboMarkdownEditor: ComboMarkdownEditor};
|
||||
type ComboMarkdownEditorContainer = HTMLElement & {_giteaComboMarkdownEditor?: ComboMarkdownEditor};
|
||||
|
||||
export class ComboMarkdownEditor {
|
||||
static EventEditorContentChanged = EventEditorContentChanged;
|
||||
@@ -75,18 +76,18 @@ export class ComboMarkdownEditor {
|
||||
tabPreviewer?: HTMLElement;
|
||||
|
||||
supportEasyMDE!: boolean;
|
||||
easyMDE: any;
|
||||
easyMDEToolbarActions: any;
|
||||
easyMDEToolbarDefault: any;
|
||||
easyMDE: EasyMDE | null = null;
|
||||
easyMDEToolbarActions?: Record<string, EasyMdeToolbarAction>;
|
||||
easyMDEToolbarDefault!: string[];
|
||||
|
||||
textarea!: ComboMarkdownEditorTextarea;
|
||||
textareaMarkdownToolbar!: HTMLElement;
|
||||
textareaAutosize: any;
|
||||
textareaAutosize?: ReturnType<typeof autosize>;
|
||||
|
||||
buttonMonospace!: HTMLButtonElement;
|
||||
|
||||
dropzone: HTMLElement | null = null;
|
||||
attachedDropzoneInst: any;
|
||||
attachedDropzoneInst?: Dropzone;
|
||||
|
||||
previewMode!: string;
|
||||
previewUrl!: string;
|
||||
@@ -188,19 +189,19 @@ export class ComboMarkdownEditor {
|
||||
}
|
||||
|
||||
dropzoneReloadFiles() {
|
||||
if (!this.dropzone) return;
|
||||
if (!this.attachedDropzoneInst) return;
|
||||
this.attachedDropzoneInst.emit(DropzoneCustomEventReloadFiles);
|
||||
}
|
||||
|
||||
dropzoneSubmitReload() {
|
||||
if (!this.dropzone) return;
|
||||
if (!this.attachedDropzoneInst) return;
|
||||
this.attachedDropzoneInst.emit('submit');
|
||||
this.attachedDropzoneInst.emit(DropzoneCustomEventReloadFiles);
|
||||
}
|
||||
|
||||
isUploading() {
|
||||
if (!this.dropzone) return false;
|
||||
return this.attachedDropzoneInst.getQueuedFiles().length || this.attachedDropzoneInst.getUploadingFiles().length;
|
||||
if (!this.attachedDropzoneInst) return false;
|
||||
return Boolean(this.attachedDropzoneInst.getQueuedFiles().length || this.attachedDropzoneInst.getUploadingFiles().length);
|
||||
}
|
||||
|
||||
setupTab() {
|
||||
@@ -300,9 +301,9 @@ export class ComboMarkdownEditor {
|
||||
];
|
||||
}
|
||||
|
||||
parseEasyMDEToolbar(easyMde: typeof EasyMDE, actions: any) {
|
||||
parseEasyMDEToolbar(easyMde: typeof EasyMDE, actions: ReadonlyArray<string>) {
|
||||
this.easyMDEToolbarActions = this.easyMDEToolbarActions || easyMDEToolbarActions(easyMde, this);
|
||||
const processed = [];
|
||||
const processed: EasyMdeToolbarAction[] = [];
|
||||
for (const action of actions) {
|
||||
const actionButton = this.easyMDEToolbarActions[action];
|
||||
if (!actionButton) throw new Error(`Unknown EasyMDE toolbar action ${action}`);
|
||||
@@ -345,27 +346,27 @@ export class ComboMarkdownEditor {
|
||||
inputStyle: 'contenteditable', // nativeSpellcheck requires contenteditable
|
||||
nativeSpellcheck: true,
|
||||
...this.options.easyMDEOptions,
|
||||
toolbar: this.parseEasyMDEToolbar(EasyMDE, this.options.easyMDEOptions?.toolbar ?? this.easyMDEToolbarDefault) as EasyMDE.Options['toolbar'],
|
||||
};
|
||||
easyMDEOpt.toolbar = this.parseEasyMDEToolbar(EasyMDE, easyMDEOpt.toolbar ?? this.easyMDEToolbarDefault);
|
||||
|
||||
this.easyMDE = new EasyMDE(easyMDEOpt);
|
||||
this.easyMDE.codemirror.on('change', () => triggerEditorContentChanged(this.container));
|
||||
this.easyMDE.codemirror.setOption('extraKeys', {
|
||||
'Cmd-Enter': (cm: any) => handleGlobalEnterQuickSubmit(cm.getTextArea()),
|
||||
'Ctrl-Enter': (cm: any) => handleGlobalEnterQuickSubmit(cm.getTextArea()),
|
||||
Enter: (cm: any) => {
|
||||
'Cmd-Enter': () => { handleGlobalEnterQuickSubmit(this.textarea) },
|
||||
'Ctrl-Enter': () => { handleGlobalEnterQuickSubmit(this.textarea) },
|
||||
Enter: (cm) => {
|
||||
const tributeContainer = document.querySelector<HTMLElement>('.tribute-container');
|
||||
if (!tributeContainer || tributeContainer.style.display === 'none') {
|
||||
cm.execCommand('newlineAndIndent');
|
||||
}
|
||||
},
|
||||
Up: (cm: any) => {
|
||||
Up: (cm) => {
|
||||
const tributeContainer = document.querySelector<HTMLElement>('.tribute-container');
|
||||
if (!tributeContainer || tributeContainer.style.display === 'none') {
|
||||
return cm.execCommand('goLineUp');
|
||||
}
|
||||
},
|
||||
Down: (cm: any) => {
|
||||
Down: (cm) => {
|
||||
const tributeContainer = document.querySelector<HTMLElement>('.tribute-container');
|
||||
if (!tributeContainer || tributeContainer.style.display === 'none') {
|
||||
return cm.execCommand('goLineDown');
|
||||
@@ -380,20 +381,16 @@ export class ComboMarkdownEditor {
|
||||
hideElem(this.textareaMarkdownToolbar);
|
||||
}
|
||||
|
||||
value(v?: any) {
|
||||
if (v === undefined) {
|
||||
value(v?: string): string {
|
||||
if (v !== undefined) {
|
||||
if (this.easyMDE) {
|
||||
return this.easyMDE.value();
|
||||
this.easyMDE.value(v);
|
||||
} else {
|
||||
this.textarea.value = v;
|
||||
}
|
||||
return this.textarea.value;
|
||||
this.textareaAutosize?.resizeToFit();
|
||||
}
|
||||
|
||||
if (this.easyMDE) {
|
||||
this.easyMDE.value(v);
|
||||
} else {
|
||||
this.textarea.value = v;
|
||||
}
|
||||
this.textareaAutosize?.resizeToFit();
|
||||
return this.easyMDE ? this.easyMDE.value() : this.textarea.value;
|
||||
}
|
||||
|
||||
focus() {
|
||||
@@ -438,10 +435,9 @@ function applyMonospaceToAllEditors() {
|
||||
}
|
||||
}
|
||||
|
||||
export function getComboMarkdownEditor(el: any): ComboMarkdownEditor | null {
|
||||
export function getComboMarkdownEditor(el: Element | null): ComboMarkdownEditor | null {
|
||||
if (!el) return null;
|
||||
if (el.length) el = el[0];
|
||||
return el._giteaComboMarkdownEditor;
|
||||
return (el as ComboMarkdownEditorContainer)._giteaComboMarkdownEditor ?? null;
|
||||
}
|
||||
|
||||
export async function initComboMarkdownEditor(container: HTMLElement, options:ComboMarkdownEditorOptions = {}) {
|
||||
|
||||
@@ -2,8 +2,15 @@ import {svg} from '../../svg.ts';
|
||||
import type EasyMDE from 'easymde';
|
||||
import type {ComboMarkdownEditor} from './ComboMarkdownEditor.ts';
|
||||
|
||||
export function easyMDEToolbarActions(easyMde: typeof EasyMDE, editor: ComboMarkdownEditor): Record<string, Partial<EasyMDE.ToolbarIcon | string>> {
|
||||
const actions: Record<string, Partial<EasyMDE.ToolbarIcon> | string> = {
|
||||
export type EasyMdeToolbarAction = {
|
||||
name?: string,
|
||||
action: EasyMDE.ToolbarIcon['action'],
|
||||
icon: string,
|
||||
title: string,
|
||||
} | '|';
|
||||
|
||||
export function easyMDEToolbarActions(easyMde: typeof EasyMDE, editor: ComboMarkdownEditor): Record<string, EasyMdeToolbarAction> {
|
||||
const actions: Record<string, EasyMdeToolbarAction> = {
|
||||
'|': '|',
|
||||
'heading-1': {
|
||||
action: easyMde.toggleHeading1,
|
||||
|
||||
@@ -12,18 +12,20 @@ import type Dropzone from '@deltablot/dropzone';
|
||||
|
||||
let uploadIdCounter = 0;
|
||||
|
||||
type UploadFile = File & {_giteaUploadId?: number, uuid?: string};
|
||||
|
||||
export const EventUploadStateChanged = 'ce-upload-state-changed';
|
||||
|
||||
export function triggerUploadStateChanged(target: HTMLElement) {
|
||||
target.dispatchEvent(new CustomEvent(EventUploadStateChanged, {bubbles: true}));
|
||||
}
|
||||
|
||||
function uploadFile(dropzoneEl: HTMLElement, file: File) {
|
||||
return new Promise((resolve) => {
|
||||
function uploadFile(dropzoneEl: HTMLElement, file: UploadFile) {
|
||||
return new Promise<UploadFile>((resolve) => {
|
||||
const curUploadId = uploadIdCounter++;
|
||||
(file as any)._giteaUploadId = curUploadId;
|
||||
file._giteaUploadId = curUploadId;
|
||||
const dropzoneInst = dropzoneEl.dropzone;
|
||||
const onUploadDone = ({file}: {file: any}) => {
|
||||
const onUploadDone = ({file}: {file: UploadFile}) => {
|
||||
if (file._giteaUploadId === curUploadId) {
|
||||
dropzoneInst.off(DropzoneCustomEventUploadDone, onUploadDone);
|
||||
resolve(file);
|
||||
@@ -131,7 +133,7 @@ function getPastedImages(e: ClipboardEvent) {
|
||||
}
|
||||
|
||||
export function initEasyMDEPaste(easyMDE: EasyMDE, dropzoneEl: HTMLElement) {
|
||||
const editor = new CodeMirrorEditor(easyMDE.codemirror as any);
|
||||
const editor = new CodeMirrorEditor(easyMDE.codemirror as CodeMirror.EditorFromTextArea);
|
||||
easyMDE.codemirror.on('paste', (_, e) => {
|
||||
const images = getPastedImages(e);
|
||||
if (!images.length) return;
|
||||
|
||||
@@ -10,16 +10,12 @@ import type TextExpanderElement from '@github/text-expander-element';
|
||||
import type {TextExpanderChangeEvent, TextExpanderResult} from '@github/text-expander-element';
|
||||
|
||||
async function fetchIssueSuggestions(key: string, text: string, signal: AbortSignal): Promise<TextExpanderResult> {
|
||||
const issuePathInfo = parseIssueHref(window.location.href);
|
||||
if (!issuePathInfo.ownerName) {
|
||||
const repoOwnerPathInfo = parseRepoOwnerPathInfo(window.location.pathname);
|
||||
issuePathInfo.ownerName = repoOwnerPathInfo.ownerName;
|
||||
issuePathInfo.repoName = repoOwnerPathInfo.repoName;
|
||||
// then no issuePathInfo.indexString here, it is only used to exclude the current issue when "matchIssue"
|
||||
}
|
||||
if (!issuePathInfo.ownerName) return {matched: false};
|
||||
const hrefPathInfo = parseIssueHref(window.location.href);
|
||||
// the fallback has no indexString, it is only used to exclude the current issue when "matchIssue"
|
||||
const pathInfo = hrefPathInfo ?? parseRepoOwnerPathInfo(window.location.pathname);
|
||||
if (!pathInfo) return {matched: false};
|
||||
|
||||
const matches = await matchIssue(issuePathInfo.ownerName, issuePathInfo.repoName, issuePathInfo.indexString, text, signal);
|
||||
const matches = await matchIssue(pathInfo.ownerName, pathInfo.repoName, hrefPathInfo?.indexString, text, signal);
|
||||
if (!matches.length) return {matched: false};
|
||||
|
||||
const ul = createElementFromAttrs('ul', {class: 'suggestions'});
|
||||
@@ -131,7 +127,8 @@ export function initTextExpander(expander: TextExpanderElement) {
|
||||
}
|
||||
});
|
||||
|
||||
expander.addEventListener('text-expander-value', ({detail}: Record<string, any>) => {
|
||||
expander.addEventListener('text-expander-value', (event) => {
|
||||
const {detail} = event as CustomEvent<{item: HTMLElement, key: string, value: string}>;
|
||||
if (detail?.item) {
|
||||
// add a space after @mentions and #issue as it's likely the user wants one
|
||||
const suffix = ['@', '#'].includes(detail.key) ? ' ' : '';
|
||||
|
||||
@@ -9,6 +9,7 @@ import {isImageFile, isVideoFile} from '../utils.ts';
|
||||
import type Dropzone from '@deltablot/dropzone';
|
||||
|
||||
type CustomDropzoneFile = Dropzone.DropzoneFile & {uuid: string};
|
||||
type UploadResponse = {uuid: string};
|
||||
|
||||
// dropzone has its owner event dispatcher (emitter)
|
||||
export const DropzoneCustomEventReloadFiles = 'dropzone-custom-reload-files';
|
||||
@@ -69,19 +70,20 @@ export async function initDropzone(dropzoneEl: HTMLElement) {
|
||||
|
||||
let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event
|
||||
let fileUuidDict: FileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone
|
||||
const opts: Record<string, any> = {
|
||||
url: dropzoneEl.getAttribute('data-upload-url'),
|
||||
acceptedFiles: ['*/*', ''].includes(dropzoneEl.getAttribute('data-accepts')!) ? null : dropzoneEl.getAttribute('data-accepts'),
|
||||
const opts: Dropzone.DropzoneOptions = {
|
||||
url: dropzoneEl.getAttribute('data-upload-url')!,
|
||||
addRemoveLinks: true,
|
||||
dictDefaultMessage: dropzoneEl.getAttribute('data-default-message'),
|
||||
dictInvalidFileType: dropzoneEl.getAttribute('data-invalid-input-type'),
|
||||
dictFileTooBig: dropzoneEl.getAttribute('data-file-too-big'),
|
||||
dictRemoveFile: dropzoneEl.getAttribute('data-remove-file'),
|
||||
dictDefaultMessage: dropzoneEl.getAttribute('data-default-message')!,
|
||||
dictInvalidFileType: dropzoneEl.getAttribute('data-invalid-input-type')!,
|
||||
dictFileTooBig: dropzoneEl.getAttribute('data-file-too-big')!,
|
||||
dictRemoveFile: dropzoneEl.getAttribute('data-remove-file')!,
|
||||
timeout: 0,
|
||||
thumbnailMethod: 'contain',
|
||||
thumbnailWidth: 480,
|
||||
thumbnailHeight: 480,
|
||||
};
|
||||
const accepts = dropzoneEl.getAttribute('data-accepts')!;
|
||||
if (!['*/*', ''].includes(accepts)) opts.acceptedFiles = accepts;
|
||||
if (dropzoneEl.hasAttribute('data-max-file')) opts.maxFiles = Number(dropzoneEl.getAttribute('data-max-file'));
|
||||
if (dropzoneEl.hasAttribute('data-max-size')) opts.maxFilesize = Number(dropzoneEl.getAttribute('data-max-size'));
|
||||
|
||||
@@ -89,7 +91,7 @@ export async function initDropzone(dropzoneEl: HTMLElement) {
|
||||
// "http://localhost:3000/owner/repo/issues/[object%20Event]"
|
||||
// the reason is that the preview "callback(dataURL)" is assign to "img.onerror" then "thumbnail" uses the error object as the dataURL and generates '<img src="[object Event]">'
|
||||
const dzInst = await createDropzone(dropzoneEl, opts);
|
||||
dzInst.on('success', (file: CustomDropzoneFile, resp: any) => {
|
||||
dzInst.on('success', (file: CustomDropzoneFile, resp: UploadResponse) => {
|
||||
file.uuid = resp.uuid;
|
||||
fileUuidDict[file.uuid] = {submitted: false};
|
||||
const input = createElementFromAttrs('input', {name: 'files', type: 'hidden', id: `dropzone-file-${resp.uuid}`, value: resp.uuid});
|
||||
|
||||
@@ -50,7 +50,7 @@ export function initDiffFileViewedForm(el: Element) {
|
||||
// Unfortunately, actual forms cause too many problems, hence another approach is needed
|
||||
const files: Record<string, boolean> = {};
|
||||
files[fileName] = this.checked;
|
||||
const data: Record<string, any> = {files};
|
||||
const data: {files: Record<string, boolean>, headCommitSHA?: string} = {files};
|
||||
const headCommitSHA = el.getAttribute('data-headcommit');
|
||||
if (headCommitSHA) data.headCommitSHA = headCommitSHA;
|
||||
POST(el.getAttribute('data-link')!, {data});
|
||||
|
||||
@@ -49,7 +49,7 @@ async function showRefIssuePopup(link: HTMLAnchorElement) {
|
||||
export function initRefIssueContextPopup() {
|
||||
const selector = 'a[href]:not([data-ref-issue-popup]):not(.ref-external-issue)';
|
||||
addDelegatedEventListener<HTMLAnchorElement, MouseEvent>(document, 'mouseover', selector, (link) => {
|
||||
if (!parseIssueHref(link.getAttribute('href')!).ownerName) return;
|
||||
if (!parseIssueHref(link.getAttribute('href')!)) return;
|
||||
if (!link.classList.contains('ref-issue') && !link.closest('[data-ref-issue-container]')) return;
|
||||
if (getAttachedTippyInstance(link)) return;
|
||||
link.setAttribute('data-ref-issue-popup', '');
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import {hideElem, showElem, toggleElem} from '../utils/dom.ts';
|
||||
import {GET} from '../modules/fetch.ts';
|
||||
|
||||
type GitRef = {name: string, web_link: string};
|
||||
type RefsResponse = {tags: GitRef[], branches: GitRef[], default_branch: string};
|
||||
|
||||
async function loadBranchesAndTags(area: Element, loadingButton: Element) {
|
||||
loadingButton.classList.add('disabled');
|
||||
try {
|
||||
const res = await GET(loadingButton.getAttribute('data-url')!);
|
||||
const data = await res.json();
|
||||
const data: RefsResponse = await res.json();
|
||||
hideElem(loadingButton);
|
||||
addTags(area, data.tags);
|
||||
addBranches(area, data.branches, data.default_branch);
|
||||
@@ -15,7 +18,7 @@ async function loadBranchesAndTags(area: Element, loadingButton: Element) {
|
||||
}
|
||||
}
|
||||
|
||||
function addTags(area: Element, tags: Array<Record<string, any>>) {
|
||||
function addTags(area: Element, tags: GitRef[]) {
|
||||
const tagArea = area.querySelector('.tag-area')!;
|
||||
toggleElem(tagArea.parentElement!, tags.length > 0);
|
||||
for (const tag of tags) {
|
||||
@@ -23,7 +26,7 @@ function addTags(area: Element, tags: Array<Record<string, any>>) {
|
||||
}
|
||||
}
|
||||
|
||||
function addBranches(area: Element, branches: Array<Record<string, any>>, defaultBranch: string) {
|
||||
function addBranches(area: Element, branches: GitRef[], defaultBranch: string) {
|
||||
const defaultBranchTooltip = area.getAttribute('data-text-default-branch-tooltip');
|
||||
const branchArea = area.querySelector('.branch-area')!;
|
||||
toggleElem(branchArea.parentElement!, branches.length > 0);
|
||||
|
||||
@@ -34,7 +34,7 @@ export function strSubMatch(full: string, subLower: string) {
|
||||
return res;
|
||||
}
|
||||
|
||||
export function calcMatchedWeight(matchResult: Array<any>) {
|
||||
export function calcMatchedWeight(matchResult: string[]) {
|
||||
let weight = 0;
|
||||
for (let i = 0; i < matchResult.length; i++) {
|
||||
if (i % 2 === 1) { // matches are on odd indices, see strSubMatch
|
||||
|
||||
@@ -3,9 +3,13 @@ import {hideElem, queryElemChildren, showElem} 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';
|
||||
import type {FomanticApiResponse, JQueryElem} from '../types.ts';
|
||||
|
||||
const {appSubUrl} = window.config;
|
||||
|
||||
type TopicSearchResponse = {topics: Array<{topic_name: string}>};
|
||||
type TopicSearchResult = {description: string, 'data-value': string};
|
||||
|
||||
export function initRepoTopicBar() {
|
||||
const mgrBtn = document.querySelector<HTMLButtonElement>('#manage_topic');
|
||||
if (!mgrBtn) return;
|
||||
@@ -87,10 +91,10 @@ export function initRepoTopicBar() {
|
||||
apiSettings: {
|
||||
url: `${appSubUrl}/explore/topics/search?q={query}`,
|
||||
throttle: 500,
|
||||
onResponse(this: any, res: any) {
|
||||
const formattedResponse = {
|
||||
onResponse(this: {urlData: {query: string}}, res: TopicSearchResponse) {
|
||||
const formattedResponse: FomanticApiResponse<TopicSearchResult> = {
|
||||
success: false,
|
||||
results: [] as Array<Record<string, any>>,
|
||||
results: [],
|
||||
};
|
||||
const query = stripTags(this.urlData.query.trim());
|
||||
let found_query = false;
|
||||
@@ -134,7 +138,7 @@ export function initRepoTopicBar() {
|
||||
this.attr('data-value', value).contents().first().replaceWith(value);
|
||||
return fomanticQuery(this);
|
||||
},
|
||||
onAdd(addedValue: string, _addedText: any, $addedChoice: any) {
|
||||
onAdd(addedValue: string, _addedText: any, $addedChoice: JQueryElem) {
|
||||
addedValue = addedValue.toLowerCase().trim();
|
||||
$addedChoice[0].setAttribute('data-value', addedValue);
|
||||
$addedChoice[0].setAttribute('data-text', addedValue);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {parseIssuePageInfo} from '../utils.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
import {hideFomanticModal, showFomanticModal} from '../modules/fomantic/modal.ts';
|
||||
import {html, htmlRaw} from '../utils/html.ts';
|
||||
import type {JQueryElem} from '../types.ts';
|
||||
|
||||
let i18nTextEdited: string;
|
||||
let i18nTextOptions: string;
|
||||
@@ -35,7 +36,7 @@ function showContentHistoryDetail(issueBaseUrl: string, commentId: string, histo
|
||||
$fomanticDropdownOptions.dropdown({
|
||||
showOnFocus: false,
|
||||
allowReselection: true,
|
||||
async onChange(_value: string, _text: string, $item: any) {
|
||||
async onChange(_value: string, _text: string, $item: JQueryElem) {
|
||||
const optionItem = $item.data('option-item');
|
||||
if (optionItem === 'delete') {
|
||||
if (window.confirm(i18nTextDeleteFromHistoryConfirm)) {
|
||||
@@ -116,7 +117,7 @@ function showContentHistoryMenu(issueBaseUrl: string, elCommentItem: Element, co
|
||||
onHide() {
|
||||
$fomanticDropdown.dropdown('change values', null);
|
||||
},
|
||||
onChange(value: string, itemHtml: string, $item: any) {
|
||||
onChange(value: string, itemHtml: string, $item: JQueryElem) {
|
||||
if (value && !$item.find('[data-history-is-deleted=1]').length) {
|
||||
showContentHistoryDetail(issueBaseUrl, commentId, value, itemHtml);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
import {performFetchAction} from '../modules/fetch-action.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
|
||||
@@ -100,7 +104,7 @@ function initDropdownUserRemoteSearch(el: Element) {
|
||||
elMenu.querySelector(`.item[data-value="${CSS.escape(username)}"]`)?.classList.add('selected');
|
||||
};
|
||||
|
||||
const processedResults: Record<string, string>[] = []; // to be used by dropdown to generate menu items
|
||||
const processedResults: ProcessedIssuePoster[] = []; // to be used by dropdown to generate menu items
|
||||
const syncItemFromInput = () => {
|
||||
const inputVal = elSearchInput.value.trim();
|
||||
elItemFromInput.setAttribute('data-value', inputVal);
|
||||
@@ -121,7 +125,7 @@ function initDropdownUserRemoteSearch(el: Element) {
|
||||
onMenuUpdated: () => syncItemFromInput(),
|
||||
apiSettings: {
|
||||
url: `${searchUrl}&q={query}`,
|
||||
onResponse(resp: any) {
|
||||
onResponse(resp: IssuePosterResponse) {
|
||||
// the content is provided by backend IssuePosters handler
|
||||
processedResults.length = 0;
|
||||
for (const item of resp.results) {
|
||||
@@ -132,8 +136,7 @@ function initDropdownUserRemoteSearch(el: Element) {
|
||||
const htmlItem = html`<div class="item" data-value="${item.username}">${htmlRaw(htmlItemInner)}</div>`;
|
||||
processedResults.push({type: 'html', html: htmlItem});
|
||||
}
|
||||
resp.results = processedResults;
|
||||
return resp;
|
||||
return {results: processedResults};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import {GET} from '../modules/fetch.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
import {createElementFromHTML, activePageTimerRefresh} from '../utils/dom.ts';
|
||||
import {registerGlobalEventFunc} from '../modules/observer.ts';
|
||||
import type {JQueryElem} from '../types.ts';
|
||||
|
||||
export function initRepoPullRequestUpdate(el: HTMLElement) {
|
||||
const elDropdown = el.querySelector(':scope > .ui.dropdown');
|
||||
@@ -10,10 +11,10 @@ export function initRepoPullRequestUpdate(el: HTMLElement) {
|
||||
const elButton = el.querySelector<HTMLButtonElement>(':scope > button')!;
|
||||
|
||||
fomanticQuery(elDropdown).dropdown({
|
||||
onChange(_text: string, _value: string, $choice: any) {
|
||||
onChange(_text: string, _value: string, $choice: JQueryElem) {
|
||||
const choiceEl = $choice[0];
|
||||
elButton.textContent = choiceEl.textContent;
|
||||
elButton.setAttribute('data-url', choiceEl.getAttribute('data-update-url'));
|
||||
elButton.setAttribute('data-url', choiceEl.getAttribute('data-update-url')!);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {parseIssuePageInfo} from '../utils.ts';
|
||||
import {html} from '../utils/html.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
import {showTemporaryTooltip} from '../modules/tippy.ts';
|
||||
import type {FomanticApiResponse, Issue} from '../types.ts';
|
||||
|
||||
const {appSubUrl} = window.config;
|
||||
|
||||
@@ -64,8 +65,8 @@ export function initRepoIssueSidebarDependency(elSidebar: HTMLElement) {
|
||||
apiSettings: {
|
||||
url: issueSearchUrl,
|
||||
rawResponse: true, // backend responds an array, prevent fomantic api from converting it to an object
|
||||
onResponse(response: any) {
|
||||
const filteredResponse = {success: true, results: [] as Array<Record<string, any>>};
|
||||
onResponse(response: Issue[]) {
|
||||
const filteredResponse: FomanticApiResponse<{value: number, name: string}> = {success: true, results: []};
|
||||
const currIssueId = elDropdown.getAttribute('data-issue-id');
|
||||
// Parse the response from the api to work with our dropdown
|
||||
for (const issue of response) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
import {showFomanticModal} from '../modules/fomantic/modal.ts';
|
||||
import {ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
|
||||
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
||||
import type {FomanticApiResponse} from '../types.ts';
|
||||
|
||||
const {appSubUrl} = window.config;
|
||||
|
||||
@@ -305,8 +306,8 @@ export function initRepoIssueReferenceIssue() {
|
||||
fullTextSearch: true,
|
||||
apiSettings: {
|
||||
url: `${appSubUrl}/repo/search?q={query}&limit=20`,
|
||||
onResponse(response: any) {
|
||||
const filteredResponse = {success: true, results: [] as Array<Record<string, any>>};
|
||||
onResponse(response: {data: Array<{repository: {full_name: string}}>}) {
|
||||
const filteredResponse: FomanticApiResponse<{name: string, value: string}> = {success: true, results: []};
|
||||
for (const repo of response.data) {
|
||||
filteredResponse.results.push({
|
||||
name: htmlEscape(repo.repository.full_name),
|
||||
|
||||
@@ -38,7 +38,7 @@ function initRepoNewTemplateSearch(form: HTMLFormElement) {
|
||||
$repoTemplateDropdown.dropdown('setting', {
|
||||
apiSettings: {
|
||||
url: `${appSubUrl}/repo/search?q={query}&template=true&priority_owner_id=${ownerId}`,
|
||||
onResponse(response: any) {
|
||||
onResponse(response: {data: Array<{repository: {full_name: string, id: number}}>}) {
|
||||
const results = [];
|
||||
results.push({name: '', value: ''}); // empty item means not using template
|
||||
for (const tmplRepo of response.data) {
|
||||
|
||||
@@ -55,7 +55,7 @@ async function initRepoWikiForm(form: HTMLFormElement) {
|
||||
'unordered-list', 'ordered-list', '|',
|
||||
'link', 'image', 'table', 'horizontal-rule', '|',
|
||||
'preview', 'fullscreen', 'side-by-side', '|', 'gitea-switch-to-textarea',
|
||||
] as any, // to use custom toolbar buttons
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user