diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 123c2da607ec5..e94e0ab9ca9cf 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2811,6 +2811,7 @@ team_permission_desc = Permission team_unit_desc = Allow Access to Repository Sections team_unit_disabled = (Disabled) +form.name_been_taken = The organisation name "%s" has already been taken. form.name_reserved = The organization name "%s" is reserved. form.name_pattern_not_allowed = The pattern "%s" is not allowed in an organization name. form.create_org_not_allowed = You are not allowed to create an organization. @@ -2832,15 +2833,28 @@ settings.visibility.private_shortname = Private settings.update_settings = Update Settings settings.update_setting_success = Organization settings have been updated. -settings.change_orgname_prompt = Note: Changing the organization name will also change your organization's URL and free the old name. -settings.change_orgname_redirect_prompt = The old name will redirect until it is claimed. + +settings.rename = Rename Organization +settings.rename_desc = Changing the organization name will also change your organization's URL and free the old name. +settings.rename_success = Organization %[1]s have been renamed to %[2]s successfully. +settings.rename_no_change = Organization name is no change. +settings.rename_new_org_name = New Organization Name +settings.rename_failed = Rename Organization failed because of internal error +settings.rename_notices_1 = This operation CANNOT be undone. +settings.rename_notices_2 = The old name will redirect until it is claimed. + settings.update_avatar_success = The organization's avatar has been updated. settings.delete = Delete Organization settings.delete_account = Delete This Organization settings.delete_prompt = The organization will be permanently removed. This CANNOT be undone! +settings.name_confirm = Enter the organization name as confirmation: +settings.delete_notices_1 = This operation CANNOT be undone. +settings.delete_notices_2 = This operation will permanently delete all the repositories of %s including code, issues, comments, wiki data and collaborator settings. +settings.delete_notices_3 = This operation will permanently delete all the packages of %s. +settings.delete_notices_4 = This operation will permanently delete all the projects of %s. settings.confirm_delete_account = Confirm Deletion -settings.delete_org_title = Delete Organization -settings.delete_org_desc = This organization will be deleted permanently. Continue? +settings.delete_failed = Delete Organization failed because of internal error +settings.delete_successful = Organization %s has been deleted successfully. settings.hooks_desc = Add webhooks which will be triggered for all repositories under this organization. settings.labels_desc = Add labels which can be used on issues for all repositories under this organization. diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index 9dd0a98160e60..2bc1e8bc43388 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -18,6 +18,7 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" shared_user "code.gitea.io/gitea/routers/web/shared/user" user_setting "code.gitea.io/gitea/routers/web/user/setting" @@ -31,8 +32,6 @@ import ( const ( // tplSettingsOptions template path for render settings tplSettingsOptions templates.TplName = "org/settings/options" - // tplSettingsDelete template path for render delete repository - tplSettingsDelete templates.TplName = "org/settings/delete" // tplSettingsHooks template path for render hook settings tplSettingsHooks templates.TplName = "org/settings/hooks" // tplSettingsLabels template path for render labels settings @@ -71,26 +70,6 @@ func SettingsPost(ctx *context.Context) { org := ctx.Org.Organization - if org.Name != form.Name { - if err := user_service.RenameUser(ctx, org.AsUser(), form.Name); err != nil { - if user_model.IsErrUserAlreadyExist(err) { - ctx.Data["Err_Name"] = true - ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSettingsOptions, &form) - } else if db.IsErrNameReserved(err) { - ctx.Data["Err_Name"] = true - ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form) - } else if db.IsErrNamePatternNotAllowed(err) { - ctx.Data["Err_Name"] = true - ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form) - } else { - ctx.ServerError("RenameUser", err) - } - return - } - - ctx.Org.OrgLink = setting.AppSubURL + "/org/" + url.PathEscape(org.Name) - } - if form.Email != "" { if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil { ctx.Data["Err_Email"] = true @@ -163,42 +142,27 @@ func SettingsDeleteAvatar(ctx *context.Context) { ctx.JSONRedirect(ctx.Org.OrgLink + "/settings") } -// SettingsDelete response for deleting an organization -func SettingsDelete(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("org.settings") - ctx.Data["PageIsOrgSettings"] = true - ctx.Data["PageIsSettingsDelete"] = true - - if ctx.Req.Method == http.MethodPost { - if ctx.Org.Organization.Name != ctx.FormString("org_name") { - ctx.Data["Err_OrgName"] = true - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_org_name"), tplSettingsDelete, nil) - return - } - - if err := org_service.DeleteOrganization(ctx, ctx.Org.Organization, false); err != nil { - if repo_model.IsErrUserOwnRepos(err) { - ctx.Flash.Error(ctx.Tr("form.org_still_own_repo")) - ctx.Redirect(ctx.Org.OrgLink + "/settings/delete") - } else if packages_model.IsErrUserOwnPackages(err) { - ctx.Flash.Error(ctx.Tr("form.org_still_own_packages")) - ctx.Redirect(ctx.Org.OrgLink + "/settings/delete") - } else { - ctx.ServerError("DeleteOrganization", err) - } - } else { - log.Trace("Organization deleted: %s", ctx.Org.Organization.Name) - ctx.Redirect(setting.AppSubURL + "/") - } +// SettingsDeleteOrgPost response for deleting an organization +func SettingsDeleteOrgPost(ctx *context.Context) { + if ctx.Org.Organization.Name != ctx.FormString("org_name") { + ctx.JSONError(ctx.Tr("form.enterred_invalid_org_name")) return } - if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { - ctx.ServerError("RenderUserOrgHeader", err) + if err := org_service.DeleteOrganization(ctx, ctx.Org.Organization, false /* no purge */); err != nil { + if repo_model.IsErrUserOwnRepos(err) { + ctx.JSONError(ctx.Tr("form.org_still_own_repo")) + } else if packages_model.IsErrUserOwnPackages(err) { + ctx.JSONError(ctx.Tr("form.org_still_own_packages")) + } else { + log.Error("DeleteOrganization: %v", err) + ctx.JSONError(util.Iif(ctx.Doer.IsAdmin, err.Error(), string(ctx.Tr("org.settings.delete_failed")))) + } return } - ctx.HTML(http.StatusOK, tplSettingsDelete) + ctx.Flash.Success(ctx.Tr("org.settings.delete_successful", ctx.Org.Organization.Name)) + ctx.JSONRedirect(setting.AppSubURL + "/") } // Webhooks render webhook list page @@ -250,3 +214,40 @@ func Labels(ctx *context.Context) { ctx.HTML(http.StatusOK, tplSettingsLabels) } + +// SettingsRenamePost response for renaming organization +func SettingsRenamePost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.RenameOrgForm) + if ctx.HasError() { + ctx.JSONError(ctx.GetErrMsg()) + return + } + + oldOrgName, newOrgName := ctx.Org.Organization.Name, form.NewOrgName + + if form.OrgName != oldOrgName { + ctx.JSONError(ctx.Tr("form.enterred_invalid_org_name")) + return + } + if newOrgName == oldOrgName { + ctx.JSONError(ctx.Tr("org.settings.rename_no_change")) + return + } + + if err := user_service.RenameUser(ctx, ctx.Org.Organization.AsUser(), newOrgName); err != nil { + if user_model.IsErrUserAlreadyExist(err) { + ctx.JSONError(ctx.Tr("org.form.name_been_taken", newOrgName)) + } else if db.IsErrNameReserved(err) { + ctx.JSONError(ctx.Tr("org.form.name_reserved", newOrgName)) + } else if db.IsErrNamePatternNotAllowed(err) { + ctx.JSONError(ctx.Tr("org.form.name_pattern_not_allowed", newOrgName)) + } else { + log.Error("RenameOrganization: %v", err) + ctx.JSONError(util.Iif(ctx.Doer.IsAdmin, err.Error(), string(ctx.Tr("org.settings.rename_failed")))) + } + return + } + + ctx.Flash.Success(ctx.Tr("org.settings.rename_success", oldOrgName, newOrgName)) + ctx.JSONRedirect(setting.AppSubURL + "/org/" + url.PathEscape(newOrgName) + "/settings") +} diff --git a/routers/web/web.go b/routers/web/web.go index 4012231c4b712..3040375def2c6 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -964,7 +964,8 @@ func registerWebRoutes(m *web.Router) { addSettingsVariablesRoutes() }, actions.MustEnableActions) - m.Methods("GET,POST", "/delete", org.SettingsDelete) + m.Post("/rename", web.Bind(forms.RenameOrgForm{}), org.SettingsRenamePost) + m.Post("/delete", org.SettingsDeleteOrgPost) m.Group("/packages", func() { m.Get("", org.Packages) diff --git a/services/forms/org.go b/services/forms/org.go index db182f7e96b5b..2ac18ef25cc63 100644 --- a/services/forms/org.go +++ b/services/forms/org.go @@ -36,7 +36,6 @@ func (f *CreateOrgForm) Validate(req *http.Request, errs binding.Errors) binding // UpdateOrgSettingForm form for updating organization settings type UpdateOrgSettingForm struct { - Name string `binding:"Required;Username;MaxSize(40)" locale:"org.org_name_holder"` FullName string `binding:"MaxSize(100)"` Email string `binding:"MaxSize(255)"` Description string `binding:"MaxSize(255)"` @@ -53,6 +52,11 @@ func (f *UpdateOrgSettingForm) Validate(req *http.Request, errs binding.Errors) return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } +type RenameOrgForm struct { + OrgName string `binding:"Required"` + NewOrgName string `binding:"Required;Username;MaxSize(40)" locale:"org.org_name_holder"` +} + // ___________ // \__ ___/___ _____ _____ // | |_/ __ \\__ \ / \ diff --git a/templates/org/settings/delete.tmpl b/templates/org/settings/delete.tmpl deleted file mode 100644 index e1ef471e34124..0000000000000 --- a/templates/org/settings/delete.tmpl +++ /dev/null @@ -1,35 +0,0 @@ -{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings delete")}} - -
-

- {{ctx.Locale.Tr "org.settings.delete_account"}} -

-
-
-

{{svg "octicon-alert"}} {{ctx.Locale.Tr "org.settings.delete_prompt"}}

-
-
- {{.CsrfTokenHtml}} -
- - -
- -
-
-
- - - -{{template "org/settings/layout_footer" .}} diff --git a/templates/org/settings/navbar.tmpl b/templates/org/settings/navbar.tmpl index ce792f667c4f9..58475de7e7a31 100644 --- a/templates/org/settings/navbar.tmpl +++ b/templates/org/settings/navbar.tmpl @@ -41,8 +41,5 @@ {{end}} - - {{ctx.Locale.Tr "org.settings.delete"}} - diff --git a/templates/org/settings/options.tmpl b/templates/org/settings/options.tmpl index f4583bbe366b6..d94bb4c62b2e5 100644 --- a/templates/org/settings/options.tmpl +++ b/templates/org/settings/options.tmpl @@ -1,101 +1,97 @@ {{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings options")}} -
-

- {{ctx.Locale.Tr "org.settings.options"}} -

-
-
- {{.CsrfTokenHtml}} -
- - -
-
- - -
-
- - -
-
- {{/* it is rendered as markdown, but the length is limited, so at the moment we do not use the markdown editor here */}} - - -
-
- - -
-
- - -
-
-
- -
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
+
+

+ {{ctx.Locale.Tr "org.settings.options"}} +

+
+ + {{.CsrfTokenHtml}} +
+ + +
+
+ + +
+
+ {{/* it is rendered as markdown, but the length is limited, so at the moment we do not use the markdown editor here */}} + + +
+
+ + +
+
+ + +
+ +
+
+ +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
-
- -
-
- - -
-
-
+
+ +
+
+ + +
+
+
- {{if .SignedUser.IsAdmin}} -
+ {{if .SignedUser.IsAdmin}} +
-
- - -

{{ctx.Locale.Tr "admin.users.max_repo_creation_desc"}}

-
- {{end}} +
+ + +

{{ctx.Locale.Tr "admin.users.max_repo_creation_desc"}}

+
+ {{end}} -
- -
- +
+ +
+ -
+
-
- {{.CsrfTokenHtml}} -
- {{template "shared/avatar_upload_crop" dict "LabelText" (ctx.Locale.Tr "settings.choose_new_avatar")}} -
-
- - -
-
-
+
+ {{.CsrfTokenHtml}} +
+ {{template "shared/avatar_upload_crop" dict "LabelText" (ctx.Locale.Tr "settings.choose_new_avatar")}} +
+
+ +
+
+
+
+ +{{template "org/settings/options_dangerzone" .}} + {{template "org/settings/layout_footer" .}} diff --git a/templates/org/settings/options_dangerzone.tmpl b/templates/org/settings/options_dangerzone.tmpl new file mode 100644 index 0000000000000..01cf3fd4051dc --- /dev/null +++ b/templates/org/settings/options_dangerzone.tmpl @@ -0,0 +1,93 @@ +

+ {{ctx.Locale.Tr "repo.settings.danger_zone"}} +

+
+
+
+
+
{{ctx.Locale.Tr "org.settings.rename"}}
+
{{ctx.Locale.Tr "org.settings.rename_desc"}}
+
+
+ +
+
+ +
+
+
{{ctx.Locale.Tr "org.settings.delete_account"}}
+
{{ctx.Locale.Tr "org.settings.delete_prompt"}}
+
+
+ +
+
+
+
+ + + + diff --git a/web_src/css/base.css b/web_src/css/base.css index b50abf79f1d2c..dc58fb850a134 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/modules/dimmer.css b/web_src/css/modules/dimmer.css index 89248213707e3..7d1ca6171aa9e 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 1145f3b1b58be..330d3b176eb39 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/fomantic/build/components/dropdown.js b/web_src/fomantic/build/components/dropdown.js index 85530c79912ee..3ad098486516b 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 420ecc250baa1..3f578ccfccc7d 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/features/common-fetch-action.ts b/web_src/js/features/common-fetch-action.ts index 5b6430779fd06..93597134540d8 100644 --- a/web_src/js/features/common-fetch-action.ts +++ b/web_src/js/features/common-fetch-action.ts @@ -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. diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts index 02fee5a267d85..ccc22073d7fe5 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 "