aboutsummaryrefslogtreecommitdiffstats
path: root/web_src
diff options
context:
space:
mode:
Diffstat (limited to 'web_src')
-rw-r--r--web_src/css/base.css4
-rw-r--r--web_src/css/markup/content.css4
-rw-r--r--web_src/css/modules/breadcrumb.css6
-rw-r--r--web_src/css/modules/dimmer.css2
-rw-r--r--web_src/css/modules/toast.css2
-rw-r--r--web_src/css/repo.css6
-rw-r--r--web_src/css/repo/wiki.css4
-rw-r--r--web_src/fomantic/build/components/dropdown.js6
-rw-r--r--web_src/fomantic/build/components/modal.js8
-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
25 files changed, 143 insertions, 117 deletions
diff --git a/web_src/css/base.css b/web_src/css/base.css
index b50abf79f1..dc58fb850a 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -30,6 +30,10 @@
--page-spacing: 16px; /* space between page elements */
--page-margin-x: 32px; /* minimum space on left and right side of page */
--page-space-bottom: 64px; /* space between last page element and footer */
+
+ /* z-index */
+ --z-index-modal: 1001; /* modal dialog, hard-coded from Fomantic modal.css */
+ --z-index-toast: 1002; /* should be larger than modal */
}
@media (min-width: 768px) and (max-width: 1200px) {
diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css
index 8f92a51749..c6a89edf25 100644
--- a/web_src/css/markup/content.css
+++ b/web_src/css/markup/content.css
@@ -134,7 +134,9 @@
margin-bottom: 16px;
}
-/* override p:last-child from base.css */
+/* override p:last-child from base.css.
+Fomantic assumes that <p>/<hX> elements only have margins between elements, but not for the first's top or last's bottom.
+In markup content, we always use bottom margin for all elements */
.markup p:last-child {
margin-bottom: 16px;
}
diff --git a/web_src/css/modules/breadcrumb.css b/web_src/css/modules/breadcrumb.css
index ca488c2150..77e31ef627 100644
--- a/web_src/css/modules/breadcrumb.css
+++ b/web_src/css/modules/breadcrumb.css
@@ -1,14 +1,10 @@
.breadcrumb {
display: flex;
- flex-wrap: wrap;
align-items: center;
gap: 3px;
+ overflow-wrap: anywhere;
}
.breadcrumb .breadcrumb-divider {
color: var(--color-text-light-2);
}
-
-.breadcrumb > * {
- display: inline;
-}
diff --git a/web_src/css/modules/dimmer.css b/web_src/css/modules/dimmer.css
index 8924821370..7d1ca6171a 100644
--- a/web_src/css/modules/dimmer.css
+++ b/web_src/css/modules/dimmer.css
@@ -20,7 +20,7 @@
opacity: 1;
}
-.ui.dimmer > * {
+.ui.dimmer > .ui.modal {
position: static;
margin-top: auto !important;
margin-bottom: auto !important;
diff --git a/web_src/css/modules/toast.css b/web_src/css/modules/toast.css
index 1145f3b1b5..330d3b176e 100644
--- a/web_src/css/modules/toast.css
+++ b/web_src/css/modules/toast.css
@@ -3,7 +3,7 @@
position: fixed;
opacity: 0;
transition: all .2s ease;
- z-index: 500;
+ z-index: var(--z-index-toast);
border-radius: var(--border-radius);
box-shadow: 0 8px 24px var(--color-shadow);
display: flex;
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index cbc890e356..1a05b68dd4 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -139,11 +139,6 @@ td .commit-summary {
}
}
-.repo-path {
- display: flex;
- overflow-wrap: anywhere;
-}
-
.repository.file.list .non-diff-file-content .header .icon {
font-size: 1em;
}
@@ -1839,6 +1834,7 @@ tbody.commit-list {
border-radius: 0;
display: flex;
flex-direction: column;
+ gap: 0.5em;
}
/* fomantic's last-child selector does not work with hidden last child */
diff --git a/web_src/css/repo/wiki.css b/web_src/css/repo/wiki.css
index ca59dadb9c..144cb1206c 100644
--- a/web_src/css/repo/wiki.css
+++ b/web_src/css/repo/wiki.css
@@ -39,10 +39,6 @@
min-width: 150px;
}
-.repository.wiki .wiki-content-sidebar .ui.message.unicode-escape-prompt p {
- display: none;
-}
-
.repository.wiki .wiki-content-footer {
margin-top: 1em;
}
diff --git a/web_src/fomantic/build/components/dropdown.js b/web_src/fomantic/build/components/dropdown.js
index 85530c7991..3ad0984865 100644
--- a/web_src/fomantic/build/components/dropdown.js
+++ b/web_src/fomantic/build/components/dropdown.js
@@ -525,7 +525,7 @@ $.fn.dropdown = function(parameters) {
return true;
}
if(settings.onShow.call(element) !== false) {
- settings.onAfterFiltered.call(element); // GITEA-PATCH: callback to correctly handle the filtered items
+ $module.fomanticExt.onDropdownAfterFiltered.call(element); // GITEA-PATCH: callback to correctly handle the filtered items
module.animate.show(function() {
if( module.can.click() ) {
module.bind.intent();
@@ -753,7 +753,7 @@ $.fn.dropdown = function(parameters) {
if(module.is.searchSelection() && module.can.show() && module.is.focusedOnSearch() ) {
module.show();
}
- settings.onAfterFiltered.call(element); // GITEA-PATCH: callback to correctly handle the filtered items
+ $module.fomanticExt.onDropdownAfterFiltered.call(element); // GITEA-PATCH: callback to correctly handle the filtered items
}
;
if(settings.useLabels && module.has.maxSelections()) {
@@ -3994,8 +3994,6 @@ $.fn.dropdown.settings = {
onShow : function(){},
onHide : function(){},
- onAfterFiltered: function(){}, // GITEA-PATCH: callback to correctly handle the filtered items
-
/* Component */
name : 'Dropdown',
namespace : 'dropdown',
diff --git a/web_src/fomantic/build/components/modal.js b/web_src/fomantic/build/components/modal.js
index 420ecc250b..3f578ccfcc 100644
--- a/web_src/fomantic/build/components/modal.js
+++ b/web_src/fomantic/build/components/modal.js
@@ -467,7 +467,7 @@ $.fn.modal = function(parameters) {
ignoreRepeatedEvents = false;
return false;
}
-
+ $module.fomanticExt.onModalBeforeHidden.call(element); // GITEA-PATCH: handle more UI updates before hidden
if( module.is.animating() || module.is.active() ) {
if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) {
module.remove.active();
@@ -641,7 +641,7 @@ $.fn.modal = function(parameters) {
$module
.off('mousedown' + elementEventNamespace)
;
- }
+ }
$dimmer
.off('mousedown' + elementEventNamespace)
;
@@ -877,7 +877,7 @@ $.fn.modal = function(parameters) {
? $(document).scrollTop() + settings.padding
: $(document).scrollTop() + (module.cache.contextHeight - module.cache.height - settings.padding),
marginLeft: -(module.cache.width / 2)
- })
+ })
;
} else {
$module
@@ -886,7 +886,7 @@ $.fn.modal = function(parameters) {
? -(module.cache.height / 2)
: settings.padding / 2,
marginLeft: -(module.cache.width / 2)
- })
+ })
;
}
module.verbose('Setting modal offset for legacy mode');
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);
+}