aboutsummaryrefslogtreecommitdiffstats
path: root/web_src/js
diff options
context:
space:
mode:
Diffstat (limited to 'web_src/js')
-rw-r--r--web_src/js/components/ActivityHeatmap.vue4
-rw-r--r--web_src/js/components/ContextPopup.vue12
-rw-r--r--web_src/js/components/DiffFileTreeItem.vue4
-rw-r--r--web_src/js/components/PullRequestMergeForm.vue40
-rw-r--r--web_src/js/components/RepoActivityTopAuthors.vue8
-rw-r--r--web_src/js/components/RepoCodeFrequency.vue12
-rw-r--r--web_src/js/components/RepoContributors.vue2
-rw-r--r--web_src/js/components/RepoRecentCommits.vue10
-rw-r--r--web_src/js/components/ViewFileTree.vue4
-rw-r--r--web_src/js/components/ViewFileTreeItem.vue8
-rw-r--r--web_src/js/features/common-fetch-action.ts41
-rw-r--r--web_src/js/features/install.ts2
-rw-r--r--web_src/js/features/repo-editor.ts28
-rw-r--r--web_src/js/modules/fomantic/dropdown.ts4
-rw-r--r--web_src/js/modules/fomantic/modal.ts10
-rw-r--r--web_src/js/modules/toast.ts29
16 files changed, 126 insertions, 92 deletions
diff --git a/web_src/js/components/ActivityHeatmap.vue b/web_src/js/components/ActivityHeatmap.vue
index eaa9b0ffb1..296cb61cff 100644
--- a/web_src/js/components/ActivityHeatmap.vue
+++ b/web_src/js/components/ActivityHeatmap.vue
@@ -1,7 +1,7 @@
<script lang="ts" setup>
// TODO: Switch to upstream after https://github.com/razorness/vue3-calendar-heatmap/pull/34 is merged
import {CalendarHeatmap} from '@silverwind/vue3-calendar-heatmap';
-import {onMounted, ref} from 'vue';
+import {onMounted, shallowRef} from 'vue';
import type {Value as HeatmapValue, Locale as HeatmapLocale} from '@silverwind/vue3-calendar-heatmap';
defineProps<{
@@ -24,7 +24,7 @@ const colorRange = [
'var(--color-primary-dark-4)',
];
-const endDate = ref(new Date());
+const endDate = shallowRef(new Date());
onMounted(() => {
// work around issue with first legend color being rendered twice and legend cut off
diff --git a/web_src/js/components/ContextPopup.vue b/web_src/js/components/ContextPopup.vue
index 0aae202d42..5ec4499e48 100644
--- a/web_src/js/components/ContextPopup.vue
+++ b/web_src/js/components/ContextPopup.vue
@@ -2,16 +2,16 @@
import {SvgIcon} from '../svg.ts';
import {GET} from '../modules/fetch.ts';
import {getIssueColor, getIssueIcon} from '../features/issue.ts';
-import {computed, onMounted, ref} from 'vue';
+import {computed, onMounted, shallowRef, useTemplateRef} from 'vue';
import type {IssuePathInfo} from '../types.ts';
const {appSubUrl, i18n} = window.config;
-const loading = ref(false);
-const issue = ref(null);
-const renderedLabels = ref('');
+const loading = shallowRef(false);
+const issue = shallowRef(null);
+const renderedLabels = shallowRef('');
const i18nErrorOccurred = i18n.error_occurred;
-const i18nErrorMessage = ref(null);
+const i18nErrorMessage = shallowRef(null);
const createdAt = computed(() => new Date(issue.value.created_at).toLocaleDateString(undefined, {year: 'numeric', month: 'short', day: 'numeric'}));
const body = computed(() => {
@@ -22,7 +22,7 @@ const body = computed(() => {
return body;
});
-const root = ref<HTMLElement | null>(null);
+const root = useTemplateRef('root');
onMounted(() => {
root.value.addEventListener('ce-load-context-popup', (e: CustomEventInit<IssuePathInfo>) => {
diff --git a/web_src/js/components/DiffFileTreeItem.vue b/web_src/js/components/DiffFileTreeItem.vue
index 24bf590082..f15f093ff8 100644
--- a/web_src/js/components/DiffFileTreeItem.vue
+++ b/web_src/js/components/DiffFileTreeItem.vue
@@ -1,6 +1,6 @@
<script lang="ts" setup>
import {SvgIcon, type SvgName} from '../svg.ts';
-import {ref} from 'vue';
+import {shallowRef} from 'vue';
import {type DiffStatus, type DiffTreeEntry, diffTreeStore} from '../modules/diff-file.ts';
const props = defineProps<{
@@ -8,7 +8,7 @@ const props = defineProps<{
}>();
const store = diffTreeStore();
-const collapsed = ref(props.item.IsViewed);
+const collapsed = shallowRef(props.item.IsViewed);
function getIconForDiffStatus(pType: DiffStatus) {
const diffTypes: Record<DiffStatus, { name: SvgName, classes: Array<string> }> = {
diff --git a/web_src/js/components/PullRequestMergeForm.vue b/web_src/js/components/PullRequestMergeForm.vue
index 4f291f5ca1..b2c28414c0 100644
--- a/web_src/js/components/PullRequestMergeForm.vue
+++ b/web_src/js/components/PullRequestMergeForm.vue
@@ -1,19 +1,19 @@
<script lang="ts" setup>
-import {computed, onMounted, onUnmounted, ref, watch} from 'vue';
+import {computed, onMounted, onUnmounted, shallowRef, watch} from 'vue';
import {SvgIcon} from '../svg.ts';
import {toggleElem} from '../utils/dom.ts';
const {csrfToken, pageData} = window.config;
-const mergeForm = ref(pageData.pullRequestMergeForm);
+const mergeForm = pageData.pullRequestMergeForm;
-const mergeTitleFieldValue = ref('');
-const mergeMessageFieldValue = ref('');
-const deleteBranchAfterMerge = ref(false);
-const autoMergeWhenSucceed = ref(false);
+const mergeTitleFieldValue = shallowRef('');
+const mergeMessageFieldValue = shallowRef('');
+const deleteBranchAfterMerge = shallowRef(false);
+const autoMergeWhenSucceed = shallowRef(false);
-const mergeStyle = ref('');
-const mergeStyleDetail = ref({
+const mergeStyle = shallowRef('');
+const mergeStyleDetail = shallowRef({
hideMergeMessageTexts: false,
textDoMerge: '',
mergeTitleFieldText: '',
@@ -21,33 +21,33 @@ const mergeStyleDetail = ref({
hideAutoMerge: false,
});
-const mergeStyleAllowedCount = ref(0);
+const mergeStyleAllowedCount = shallowRef(0);
-const showMergeStyleMenu = ref(false);
-const showActionForm = ref(false);
+const showMergeStyleMenu = shallowRef(false);
+const showActionForm = shallowRef(false);
const mergeButtonStyleClass = computed(() => {
- if (mergeForm.value.allOverridableChecksOk) return 'primary';
+ if (mergeForm.allOverridableChecksOk) return 'primary';
return autoMergeWhenSucceed.value ? 'primary' : 'red';
});
const forceMerge = computed(() => {
- return mergeForm.value.canMergeNow && !mergeForm.value.allOverridableChecksOk;
+ return mergeForm.canMergeNow && !mergeForm.allOverridableChecksOk;
});
watch(mergeStyle, (val) => {
- mergeStyleDetail.value = mergeForm.value.mergeStyles.find((e: any) => e.name === val);
+ mergeStyleDetail.value = mergeForm.mergeStyles.find((e: any) => e.name === val);
for (const elem of document.querySelectorAll('[data-pull-merge-style]')) {
toggleElem(elem, elem.getAttribute('data-pull-merge-style') === val);
}
});
onMounted(() => {
- mergeStyleAllowedCount.value = mergeForm.value.mergeStyles.reduce((v: any, msd: any) => v + (msd.allowed ? 1 : 0), 0);
+ mergeStyleAllowedCount.value = mergeForm.mergeStyles.reduce((v: any, msd: any) => v + (msd.allowed ? 1 : 0), 0);
- let mergeStyle = mergeForm.value.mergeStyles.find((e: any) => e.allowed && e.name === mergeForm.value.defaultMergeStyle)?.name;
- if (!mergeStyle) mergeStyle = mergeForm.value.mergeStyles.find((e: any) => e.allowed)?.name;
- switchMergeStyle(mergeStyle, !mergeForm.value.canMergeNow);
+ let mergeStyle = mergeForm.mergeStyles.find((e: any) => e.allowed && e.name === mergeForm.defaultMergeStyle)?.name;
+ if (!mergeStyle) mergeStyle = mergeForm.mergeStyles.find((e: any) => e.allowed)?.name;
+ switchMergeStyle(mergeStyle, !mergeForm.canMergeNow);
document.addEventListener('mouseup', hideMergeStyleMenu);
});
@@ -63,7 +63,7 @@ function hideMergeStyleMenu() {
function toggleActionForm(show: boolean) {
showActionForm.value = show;
if (!show) return;
- deleteBranchAfterMerge.value = mergeForm.value.defaultDeleteBranchAfterMerge;
+ deleteBranchAfterMerge.value = mergeForm.defaultDeleteBranchAfterMerge;
mergeTitleFieldValue.value = mergeStyleDetail.value.mergeTitleFieldText;
mergeMessageFieldValue.value = mergeStyleDetail.value.mergeMessageFieldText;
}
@@ -74,7 +74,7 @@ function switchMergeStyle(name: string, autoMerge = false) {
}
function clearMergeMessage() {
- mergeMessageFieldValue.value = mergeForm.value.defaultMergeMessage;
+ mergeMessageFieldValue.value = mergeForm.defaultMergeMessage;
}
</script>
diff --git a/web_src/js/components/RepoActivityTopAuthors.vue b/web_src/js/components/RepoActivityTopAuthors.vue
index 77b85bd7e2..bbdfda41d0 100644
--- a/web_src/js/components/RepoActivityTopAuthors.vue
+++ b/web_src/js/components/RepoActivityTopAuthors.vue
@@ -1,9 +1,9 @@
<script lang="ts" setup>
// @ts-expect-error - module exports no types
import {VueBarGraph} from 'vue-bar-graph';
-import {computed, onMounted, ref} from 'vue';
+import {computed, onMounted, shallowRef, useTemplateRef} from 'vue';
-const colors = ref({
+const colors = shallowRef({
barColor: 'green',
textColor: 'black',
textAltColor: 'white',
@@ -41,8 +41,8 @@ const graphWidth = computed(() => {
return activityTopAuthors.length * 40;
});
-const styleElement = ref<HTMLElement | null>(null);
-const altStyleElement = ref<HTMLElement | null>(null);
+const styleElement = useTemplateRef('styleElement');
+const altStyleElement = useTemplateRef('altStyleElement');
onMounted(() => {
const refStyle = window.getComputedStyle(styleElement.value);
diff --git a/web_src/js/components/RepoCodeFrequency.vue b/web_src/js/components/RepoCodeFrequency.vue
index f04fc065b6..f331a26fe9 100644
--- a/web_src/js/components/RepoCodeFrequency.vue
+++ b/web_src/js/components/RepoCodeFrequency.vue
@@ -23,7 +23,7 @@ import {
import {chartJsColors} from '../utils/color.ts';
import {sleep} from '../utils.ts';
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
-import {onMounted, ref} from 'vue';
+import {onMounted, shallowRef} from 'vue';
const {pageData} = window.config;
@@ -47,10 +47,10 @@ defineProps<{
};
}>();
-const isLoading = ref(false);
-const errorText = ref('');
-const repoLink = ref(pageData.repoLink || []);
-const data = ref<DayData[]>([]);
+const isLoading = shallowRef(false);
+const errorText = shallowRef('');
+const repoLink = pageData.repoLink;
+const data = shallowRef<DayData[]>([]);
onMounted(() => {
fetchGraphData();
@@ -61,7 +61,7 @@ async function fetchGraphData() {
try {
let response: Response;
do {
- response = await GET(`${repoLink.value}/activity/code-frequency/data`);
+ response = await GET(`${repoLink}/activity/code-frequency/data`);
if (response.status === 202) {
await sleep(1000); // wait for 1 second before retrying
}
diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue
index 1006ea30bb..754acb997d 100644
--- a/web_src/js/components/RepoContributors.vue
+++ b/web_src/js/components/RepoContributors.vue
@@ -397,7 +397,7 @@ export default defineComponent({
<div class="ui top attached header tw-flex tw-flex-1">
<b class="ui right">#{{ index + 1 }}</b>
<a :href="contributor.home_link">
- <img class="ui avatar tw-align-middle" height="40" width="40" :src="contributor.avatar_link" alt="">
+ <img loading="lazy" class="ui avatar tw-align-middle" height="40" width="40" :src="contributor.avatar_link" alt="">
</a>
<div class="tw-ml-2">
<a v-if="contributor.home_link !== ''" :href="contributor.home_link"><h4>{{ contributor.name }}</h4></a>
diff --git a/web_src/js/components/RepoRecentCommits.vue b/web_src/js/components/RepoRecentCommits.vue
index 1b3d8fd459..27aa27dfc3 100644
--- a/web_src/js/components/RepoRecentCommits.vue
+++ b/web_src/js/components/RepoRecentCommits.vue
@@ -21,7 +21,7 @@ import {
import {chartJsColors} from '../utils/color.ts';
import {sleep} from '../utils.ts';
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
-import {onMounted, ref} from 'vue';
+import {onMounted, ref, shallowRef} from 'vue';
const {pageData} = window.config;
@@ -43,9 +43,9 @@ defineProps<{
};
}>();
-const isLoading = ref(false);
-const errorText = ref('');
-const repoLink = ref(pageData.repoLink || []);
+const isLoading = shallowRef(false);
+const errorText = shallowRef('');
+const repoLink = pageData.repoLink;
const data = ref<DayData[]>([]);
onMounted(() => {
@@ -57,7 +57,7 @@ async function fetchGraphData() {
try {
let response: Response;
do {
- response = await GET(`${repoLink.value}/activity/recent-commits/data`);
+ response = await GET(`${repoLink}/activity/recent-commits/data`);
if (response.status === 202) {
await sleep(1000); // wait for 1 second before retrying
}
diff --git a/web_src/js/components/ViewFileTree.vue b/web_src/js/components/ViewFileTree.vue
index d560824159..1f90f92586 100644
--- a/web_src/js/components/ViewFileTree.vue
+++ b/web_src/js/components/ViewFileTree.vue
@@ -1,9 +1,9 @@
<script lang="ts" setup>
import ViewFileTreeItem from './ViewFileTreeItem.vue';
-import {onMounted, ref} from 'vue';
+import {onMounted, useTemplateRef} from 'vue';
import {createViewFileTreeStore} from './ViewFileTreeStore.ts';
-const elRoot = ref<HTMLElement | null>(null);
+const elRoot = useTemplateRef('elRoot');
const props = defineProps({
repoLink: {type: String, required: true},
diff --git a/web_src/js/components/ViewFileTreeItem.vue b/web_src/js/components/ViewFileTreeItem.vue
index 4a7569e921..5173c7eb46 100644
--- a/web_src/js/components/ViewFileTreeItem.vue
+++ b/web_src/js/components/ViewFileTreeItem.vue
@@ -1,7 +1,7 @@
<script lang="ts" setup>
import {SvgIcon} from '../svg.ts';
import {isPlainClick} from '../utils/dom.ts';
-import {ref} from 'vue';
+import {shallowRef} from 'vue';
import {type createViewFileTreeStore} from './ViewFileTreeStore.ts';
type Item = {
@@ -20,9 +20,9 @@ const props = defineProps<{
}>();
const store = props.store;
-const isLoading = ref(false);
-const children = ref(props.item.children);
-const collapsed = ref(!props.item.children);
+const isLoading = shallowRef(false);
+const children = shallowRef(props.item.children);
+const collapsed = shallowRef(!props.item.children);
const doLoadChildren = async () => {
collapsed.value = !collapsed.value;
diff --git a/web_src/js/features/common-fetch-action.ts b/web_src/js/features/common-fetch-action.ts
index a4a69540a8..a372216ae6 100644
--- a/web_src/js/features/common-fetch-action.ts
+++ b/web_src/js/features/common-fetch-action.ts
@@ -1,11 +1,11 @@
import {request} from '../modules/fetch.ts';
-import {showErrorToast} from '../modules/toast.ts';
+import {hideToastsAll, 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;
+const {appSubUrl} = window.config;
// fetchActionDoRedirect does real redirection to bypass the browser's limitations of "location"
// more details are in the backend's fetch-redirect handler
@@ -23,10 +23,20 @@ function fetchActionDoRedirect(redirect: string) {
}
async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: RequestOpts) {
+ const showErrorForResponse = (code: number, message: string) => {
+ showErrorToast(`Error ${code || 'request'}: ${message}`);
+ };
+
+ let respStatus = 0;
+ let respText = '';
try {
+ hideToastsAll();
const resp = await request(url, opt);
- if (resp.status === 200) {
- let {redirect} = await resp.json();
+ respStatus = resp.status;
+ respText = await resp.text();
+ const respJson = JSON.parse(respText);
+ if (respStatus === 200) {
+ let {redirect} = respJson;
redirect = redirect || actionElem.getAttribute('data-redirect');
ignoreAreYouSure(actionElem); // ignore the areYouSure check before reloading
if (redirect) {
@@ -35,22 +45,21 @@ async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: R
window.location.reload();
}
return;
- } else if (resp.status >= 400 && resp.status < 500) {
- const data = await resp.json();
+ }
+
+ if (respStatus >= 400 && respStatus < 500 && respJson?.errorMessage) {
// the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error"
// but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond.
- if (data.errorMessage) {
- showErrorToast(data.errorMessage, {useHtmlBody: data.renderFormat === 'html'});
- } else {
- showErrorToast(`server error: ${resp.status}`);
- }
+ showErrorToast(respJson.errorMessage, {useHtmlBody: respJson.renderFormat === 'html'});
} else {
- showErrorToast(`server error: ${resp.status}`);
+ showErrorForResponse(respStatus, respText);
}
} catch (e) {
- if (e.name !== 'AbortError') {
- console.error('error when doRequest', e);
- showErrorToast(`${i18n.network_error} ${e}`);
+ if (e.name === 'SyntaxError') {
+ showErrorForResponse(respStatus, (respText || '').substring(0, 100));
+ } else if (e.name !== 'AbortError') {
+ console.error('fetchActionDoRequest error', e);
+ showErrorForResponse(respStatus, `${e}`);
}
}
actionElem.classList.remove('is-loading', 'loading-icon-2px');
@@ -70,7 +79,7 @@ export async function submitFormFetchAction(formEl: HTMLFormElement, formSubmitt
}
const formMethod = formEl.getAttribute('method') || 'get';
- const formActionUrl = formEl.getAttribute('action');
+ const formActionUrl = formEl.getAttribute('action') || window.location.href;
const formData = new FormData(formEl);
const [submitterName, submitterValue] = [formSubmitter?.getAttribute('name'), formSubmitter?.getAttribute('value')];
if (submitterName) {
diff --git a/web_src/js/features/install.ts b/web_src/js/features/install.ts
index 34df4757f9..ca4bcce881 100644
--- a/web_src/js/features/install.ts
+++ b/web_src/js/features/install.ts
@@ -104,7 +104,7 @@ function initPreInstall() {
}
function initPostInstall() {
- const el = document.querySelector('#goto-user-login');
+ const el = document.querySelector('#goto-after-install');
if (!el) return;
const targetUrl = el.getAttribute('href');
diff --git a/web_src/js/features/repo-editor.ts b/web_src/js/features/repo-editor.ts
index acf4127399..c6b5cccd54 100644
--- a/web_src/js/features/repo-editor.ts
+++ b/web_src/js/features/repo-editor.ts
@@ -7,6 +7,7 @@ import {initDropzone} from './dropzone.ts';
import {confirmModal} from './comp/ConfirmModal.ts';
import {applyAreYouSure, ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
+import {submitFormFetchAction} from './common-fetch-action.ts';
function initEditPreviewTab(elForm: HTMLFormElement) {
const elTabMenu = elForm.querySelector('.repo-editor-menu');
@@ -143,31 +144,28 @@ export function initRepoEditor() {
const elForm = document.querySelector<HTMLFormElement>('.repository.editor .edit.form');
+ // on the upload page, there is no editor(textarea)
+ const editArea = document.querySelector<HTMLTextAreaElement>('.page-content.repository.editor textarea#edit_area');
+ if (!editArea) return;
+
// Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage
// to enable or disable the commit button
const commitButton = document.querySelector<HTMLButtonElement>('#commit-button');
const dirtyFileClass = 'dirty-file';
- // Enabling the button at the start if the page has posted
- if (document.querySelector<HTMLInputElement>('input[name="page_has_posted"]')?.value === 'true') {
- commitButton.disabled = false;
- }
-
+ const syncCommitButtonState = () => {
+ const dirty = elForm.classList.contains(dirtyFileClass);
+ commitButton.disabled = !dirty;
+ };
// Registering a custom listener for the file path and the file content
// FIXME: it is not quite right here (old bug), it causes double-init, the global areYouSure "dirty" class will also be added
applyAreYouSure(elForm, {
silent: true,
dirtyClass: dirtyFileClass,
fieldSelector: ':input:not(.commit-form-wrapper :input)',
- change($form: any) {
- const dirty = $form[0]?.classList.contains(dirtyFileClass);
- commitButton.disabled = !dirty;
- },
+ change: syncCommitButtonState,
});
-
- // on the upload page, there is no editor(textarea)
- const editArea = document.querySelector<HTMLTextAreaElement>('.page-content.repository.editor textarea#edit_area');
- if (!editArea) return;
+ syncCommitButtonState(); // disable the "commit" button when no content changes
initEditPreviewTab(elForm);
@@ -182,7 +180,7 @@ export function initRepoEditor() {
editor.setValue(value);
}
- commitButton?.addEventListener('click', async (e) => {
+ commitButton.addEventListener('click', async (e) => {
// A modal which asks if an empty file should be committed
if (!editArea.value) {
e.preventDefault();
@@ -191,7 +189,7 @@ export function initRepoEditor() {
content: elForm.getAttribute('data-text-empty-confirm-content'),
})) {
ignoreAreYouSure(elForm);
- elForm.submit();
+ submitFormFetchAction(elForm);
}
}
});
diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts
index 02fee5a267..ccc22073d7 100644
--- a/web_src/js/modules/fomantic/dropdown.ts
+++ b/web_src/js/modules/fomantic/dropdown.ts
@@ -9,9 +9,9 @@ const fomanticDropdownFn = $.fn.dropdown;
// use our own `$().dropdown` function to patch Fomantic's dropdown module
export function initAriaDropdownPatch() {
if ($.fn.dropdown === ariaDropdownFn) throw new Error('initAriaDropdownPatch could only be called once');
- $.fn.dropdown.settings.onAfterFiltered = onAfterFiltered;
$.fn.dropdown = ariaDropdownFn;
$.fn.fomanticExt.onResponseKeepSelectedItem = onResponseKeepSelectedItem;
+ $.fn.fomanticExt.onDropdownAfterFiltered = onDropdownAfterFiltered;
(ariaDropdownFn as FomanticInitFunction).settings = fomanticDropdownFn.settings;
}
@@ -71,7 +71,7 @@ function updateSelectionLabel(label: HTMLElement) {
}
}
-function onAfterFiltered(this: any) {
+function onDropdownAfterFiltered(this: any) {
const $dropdown = $(this).closest('.ui.dropdown'); // "this" can be the "ui dropdown" or "<select>"
const hideEmptyDividers = $dropdown.dropdown('setting', 'hideDividers') === 'empty';
const itemsMenu = $dropdown[0].querySelector('.scrolling.menu') || $dropdown[0].querySelector('.menu');
diff --git a/web_src/js/modules/fomantic/modal.ts b/web_src/js/modules/fomantic/modal.ts
index 6a2c558890..b07b941590 100644
--- a/web_src/js/modules/fomantic/modal.ts
+++ b/web_src/js/modules/fomantic/modal.ts
@@ -1,5 +1,7 @@
import $ from 'jquery';
import type {FomanticInitFunction} from '../../types.ts';
+import {queryElems} from '../../utils/dom.ts';
+import {hideToastsFrom} from '../toast.ts';
const fomanticModalFn = $.fn.modal;
@@ -7,6 +9,7 @@ const fomanticModalFn = $.fn.modal;
export function initAriaModalPatch() {
if ($.fn.modal === ariaModalFn) throw new Error('initAriaModalPatch could only be called once');
$.fn.modal = ariaModalFn;
+ $.fn.fomanticExt.onModalBeforeHidden = onModalBeforeHidden;
(ariaModalFn as FomanticInitFunction).settings = fomanticModalFn.settings;
}
@@ -27,3 +30,10 @@ function ariaModalFn(this: any, ...args: Parameters<FomanticInitFunction>) {
}
return ret;
}
+
+function onModalBeforeHidden(this: any) {
+ const $modal = $(this);
+ const elModal = $modal[0];
+ queryElems(elModal, 'form', (form: HTMLFormElement) => form.reset());
+ hideToastsFrom(elModal.closest('.ui.dimmer') ?? document.body);
+}
diff --git a/web_src/js/modules/toast.ts b/web_src/js/modules/toast.ts
index 36e2321743..b0afc343c3 100644
--- a/web_src/js/modules/toast.ts
+++ b/web_src/js/modules/toast.ts
@@ -1,6 +1,6 @@
import {htmlEscape} from 'escape-goat';
import {svg} from '../svg.ts';
-import {animateOnce, showElem} from '../utils/dom.ts';
+import {animateOnce, queryElems, showElem} from '../utils/dom.ts';
import Toastify from 'toastify-js'; // don't use "async import", because when network error occurs, the "async import" also fails and nothing is shown
import type {Intent} from '../types.ts';
import type {SvgName} from '../svg.ts';
@@ -37,17 +37,20 @@ const levels: ToastLevels = {
type ToastOpts = {
useHtmlBody?: boolean,
- preventDuplicates?: boolean,
+ preventDuplicates?: boolean | string,
} & Options;
+type ToastifyElement = HTMLElement & {_giteaToastifyInstance?: Toast };
+
// See https://github.com/apvarun/toastify-js#api for options
function showToast(message: string, level: Intent, {gravity, position, duration, useHtmlBody, preventDuplicates = true, ...other}: ToastOpts = {}): Toast {
const body = useHtmlBody ? String(message) : htmlEscape(message);
- const key = `${level}-${body}`;
+ const parent = document.querySelector('.ui.dimmer.active') ?? document.body;
+ const duplicateKey = preventDuplicates ? (preventDuplicates === true ? `${level}-${body}` : preventDuplicates) : '';
- // prevent showing duplicate toasts with same level and message, and give a visual feedback for end users
+ // prevent showing duplicate toasts with the same level and message, and give visual feedback for end users
if (preventDuplicates) {
- const toastEl = document.querySelector(`.toastify[data-toast-unique-key="${CSS.escape(key)}"]`);
+ const toastEl = parent.querySelector(`:scope > .toastify.on[data-toast-unique-key="${CSS.escape(duplicateKey)}"]`);
if (toastEl) {
const toastDupNumEl = toastEl.querySelector('.toast-duplicate-number');
showElem(toastDupNumEl);
@@ -59,6 +62,7 @@ function showToast(message: string, level: Intent, {gravity, position, duration,
const {icon, background, duration: levelDuration} = levels[level ?? 'info'];
const toast = Toastify({
+ selector: parent,
text: `
<div class='toast-icon'>${svg(icon)}</div>
<div class='toast-body'><span class="toast-duplicate-number tw-hidden">1</span>${body}</div>
@@ -74,7 +78,8 @@ function showToast(message: string, level: Intent, {gravity, position, duration,
toast.showToast();
toast.toastElement.querySelector('.toast-close').addEventListener('click', () => toast.hideToast());
- toast.toastElement.setAttribute('data-toast-unique-key', key);
+ toast.toastElement.setAttribute('data-toast-unique-key', duplicateKey);
+ (toast.toastElement as ToastifyElement)._giteaToastifyInstance = toast;
return toast;
}
@@ -89,3 +94,15 @@ export function showWarningToast(message: string, opts?: ToastOpts): Toast {
export function showErrorToast(message: string, opts?: ToastOpts): Toast {
return showToast(message, 'error', opts);
}
+
+function hideToastByElement(el: Element): void {
+ (el as ToastifyElement)?._giteaToastifyInstance?.hideToast();
+}
+
+export function hideToastsFrom(parent: Element): void {
+ queryElems(parent, ':scope > .toastify.on', hideToastByElement);
+}
+
+export function hideToastsAll(): void {
+ queryElems(document, '.toastify.on', hideToastByElement);
+}