Rework delete org and rename org UI (#34762)

# What's the problem of the original implementation

Renaming organization will mix with organization's information change
make the operation difficult to keep consistent.

This PR created a danger zone like what's repository setting. It also
moved organization's `rename` and `delete` operations to this zone. The
original updating repository will not change the name any more.

This is also a step to extract the `updaterepository` function
completely.

Before:


![image](https://github.com/user-attachments/assets/d097dfdf-07be-4d79-8fcf-e78822515575)

![image](https://github.com/user-attachments/assets/42ee832c-cb44-41ec-9fe3-92a1c94747d2)

After:


![image](https://github.com/user-attachments/assets/f7700ed7-f104-4302-a924-09e118f24be3)

![image](https://github.com/user-attachments/assets/4c49952a-578e-4d14-bd01-4a68c9e02412)

![image](https://github.com/user-attachments/assets/814829d3-00fe-4e87-ae05-625c129170d2)

![image](https://github.com/user-attachments/assets/b067b263-c909-4b48-b23c-73481c32d350)

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Lunny Xiao
2025-06-22 02:21:48 +08:00
committed by GitHub
parent 4fc626daa1
commit 7de114a332
17 changed files with 314 additions and 211 deletions

View File

@@ -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) {

View File

@@ -20,7 +20,7 @@
opacity: 1;
}
.ui.dimmer > * {
.ui.dimmer > .ui.modal {
position: static;
margin-top: auto !important;
margin-bottom: auto !important;

View File

@@ -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;

View File

@@ -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',

View File

@@ -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');

View File

@@ -1,5 +1,5 @@
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';
@@ -24,6 +24,7 @@ function fetchActionDoRedirect(redirect: string) {
async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: RequestOpts) {
try {
hideToastsAll();
const resp = await request(url, opt);
if (resp.status === 200) {
let {redirect} = await resp.json();
@@ -35,7 +36,9 @@ async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: R
window.location.reload();
}
return;
} else if (resp.status >= 400 && resp.status < 500) {
}
if (resp.status >= 400 && resp.status < 500) {
const data = await resp.json();
// 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.

View File

@@ -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');

View File

@@ -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);
}

View File

@@ -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);
}