mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-04 06:24:11 +01:00 
			
		
		
		
	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:   After:     --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		
							parent
							
								
									4fc626daa1
								
							
						
					
					
						commit
						7de114a332
					
				@ -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 <strong>CANNOT</strong> 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 <strong>CANNOT</strong> be undone!
 | 
			
		||||
settings.name_confirm = Enter the organization name as confirmation:
 | 
			
		||||
settings.delete_notices_1 = This operation <strong>CANNOT</strong> be undone.
 | 
			
		||||
settings.delete_notices_2 = This operation will permanently delete all the <strong>repositories</strong> of <strong>%s</strong> including code, issues, comments, wiki data and collaborator settings.
 | 
			
		||||
settings.delete_notices_3 = This operation will permanently delete all the <strong>packages</strong> of <strong>%s</strong>.
 | 
			
		||||
settings.delete_notices_4 = This operation will permanently delete all the <strong>projects</strong> of <strong>%s</strong>.
 | 
			
		||||
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 <b>%s</b> has been deleted successfully.
 | 
			
		||||
settings.hooks_desc = Add webhooks which will be triggered for <strong>all repositories</strong> under this organization.
 | 
			
		||||
 | 
			
		||||
settings.labels_desc = Add labels which can be used on issues for <strong>all repositories</strong> under this organization.
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
// 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 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)
 | 
			
		||||
			}
 | 
			
		||||
	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.Trace("Organization deleted: %s", ctx.Org.Organization.Name)
 | 
			
		||||
			ctx.Redirect(setting.AppSubURL + "/")
 | 
			
		||||
			log.Error("DeleteOrganization: %v", err)
 | 
			
		||||
			ctx.JSONError(util.Iif(ctx.Doer.IsAdmin, err.Error(), string(ctx.Tr("org.settings.delete_failed"))))
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
 | 
			
		||||
		ctx.ServerError("RenderUserOrgHeader", err)
 | 
			
		||||
		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")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ___________
 | 
			
		||||
// \__    ___/___ _____    _____
 | 
			
		||||
//   |    |_/ __ \\__  \  /     \
 | 
			
		||||
 | 
			
		||||
@ -1,35 +0,0 @@
 | 
			
		||||
{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings delete")}}
 | 
			
		||||
 | 
			
		||||
			<div class="org-setting-content">
 | 
			
		||||
				<h4 class="ui top attached error header">
 | 
			
		||||
					{{ctx.Locale.Tr "org.settings.delete_account"}}
 | 
			
		||||
				</h4>
 | 
			
		||||
				<div class="ui attached error segment">
 | 
			
		||||
					<div class="ui red message">
 | 
			
		||||
						<p class="text left">{{svg "octicon-alert"}} {{ctx.Locale.Tr "org.settings.delete_prompt"}}</p>
 | 
			
		||||
					</div>
 | 
			
		||||
					<form class="ui form ignore-dirty" id="delete-form" action="{{.Link}}" method="post">
 | 
			
		||||
						{{.CsrfTokenHtml}}
 | 
			
		||||
						<div class="inline required field {{if .Err_OrgName}}error{{end}}">
 | 
			
		||||
							<label for="org_name">{{ctx.Locale.Tr "org.org_name_holder"}}</label>
 | 
			
		||||
							<input id="org_name" name="org_name" value="" autocomplete="off" autofocus required>
 | 
			
		||||
						</div>
 | 
			
		||||
						<button class="ui red button delete-button" data-type="form" data-form="#delete-form">
 | 
			
		||||
							{{ctx.Locale.Tr "org.settings.confirm_delete_account"}}
 | 
			
		||||
						</button>
 | 
			
		||||
					</form>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
<div class="ui g-modal-confirm delete modal">
 | 
			
		||||
	<div class="header">
 | 
			
		||||
		{{svg "octicon-trash"}}
 | 
			
		||||
		{{ctx.Locale.Tr "org.settings.delete_org_title"}}
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="content">
 | 
			
		||||
		<p>{{ctx.Locale.Tr "org.settings.delete_org_desc"}}</p>
 | 
			
		||||
	</div>
 | 
			
		||||
	{{template "base/modal_actions_confirm" .}}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{{template "org/settings/layout_footer" .}}
 | 
			
		||||
@ -41,8 +41,5 @@
 | 
			
		||||
			</div>
 | 
			
		||||
		</details>
 | 
			
		||||
		{{end}}
 | 
			
		||||
		<a class="{{if .PageIsSettingsDelete}}active {{end}}item" href="{{.OrgLink}}/settings/delete">
 | 
			
		||||
			{{ctx.Locale.Tr "org.settings.delete"}}
 | 
			
		||||
		</a>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -1,101 +1,97 @@
 | 
			
		||||
{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings options")}}
 | 
			
		||||
			<div class="org-setting-content">
 | 
			
		||||
				<h4 class="ui top attached header">
 | 
			
		||||
					{{ctx.Locale.Tr "org.settings.options"}}
 | 
			
		||||
				</h4>
 | 
			
		||||
				<div class="ui attached segment">
 | 
			
		||||
					<form class="ui form" action="{{.Link}}" method="post">
 | 
			
		||||
						{{.CsrfTokenHtml}}
 | 
			
		||||
						<div class="required field {{if .Err_Name}}error{{end}}">
 | 
			
		||||
							<label for="org_name">{{ctx.Locale.Tr "org.org_name_holder"}}
 | 
			
		||||
								<span class="text red tw-hidden" id="org-name-change-prompt">
 | 
			
		||||
									<br>{{ctx.Locale.Tr "org.settings.change_orgname_prompt"}}<br>{{ctx.Locale.Tr "org.settings.change_orgname_redirect_prompt"}}
 | 
			
		||||
								</span>
 | 
			
		||||
							</label>
 | 
			
		||||
							<input id="org_name" name="name" value="{{.Org.Name}}" data-org-name="{{.Org.Name}}" required maxlength="40">
 | 
			
		||||
						</div>
 | 
			
		||||
						<div class="field {{if .Err_FullName}}error{{end}}">
 | 
			
		||||
							<label for="full_name">{{ctx.Locale.Tr "org.org_full_name_holder"}}</label>
 | 
			
		||||
							<input id="full_name" name="full_name" value="{{.Org.FullName}}" maxlength="100">
 | 
			
		||||
						</div>
 | 
			
		||||
						<div class="field {{if .Err_Email}}error{{end}}">
 | 
			
		||||
							<label for="email">{{ctx.Locale.Tr "org.settings.email"}}</label>
 | 
			
		||||
							<input id="email" name="email" type="email" value="{{.Org.Email}}" maxlength="255">
 | 
			
		||||
						</div>
 | 
			
		||||
						<div class="field {{if .Err_Description}}error{{end}}">
 | 
			
		||||
							{{/* it is rendered as markdown, but the length is limited, so at the moment we do not use the markdown editor here */}}
 | 
			
		||||
							<label for="description">{{ctx.Locale.Tr "org.org_desc"}}</label>
 | 
			
		||||
							<textarea id="description" name="description" rows="2" maxlength="255">{{.Org.Description}}</textarea>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div class="field {{if .Err_Website}}error{{end}}">
 | 
			
		||||
							<label for="website">{{ctx.Locale.Tr "org.settings.website"}}</label>
 | 
			
		||||
							<input id="website" name="website" type="url" value="{{.Org.Website}}" maxlength="255">
 | 
			
		||||
						</div>
 | 
			
		||||
						<div class="field">
 | 
			
		||||
							<label for="location">{{ctx.Locale.Tr "org.settings.location"}}</label>
 | 
			
		||||
							<input id="location" name="location"  value="{{.Org.Location}}" maxlength="50">
 | 
			
		||||
						</div>
 | 
			
		||||
 | 
			
		||||
						<div class="divider"></div>
 | 
			
		||||
						<div class="field" id="visibility_box">
 | 
			
		||||
							<label for="visibility">{{ctx.Locale.Tr "org.settings.visibility"}}</label>
 | 
			
		||||
							<div class="field">
 | 
			
		||||
								<div class="ui radio checkbox">
 | 
			
		||||
									<input class="enable-system-radio" name="visibility" type="radio" value="0" {{if eq .CurrentVisibility 0}}checked{{end}}>
 | 
			
		||||
									<label>{{ctx.Locale.Tr "org.settings.visibility.public"}}</label>
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
							<div class="field">
 | 
			
		||||
								<div class="ui radio checkbox">
 | 
			
		||||
									<input class="enable-system-radio" name="visibility" type="radio" value="1" {{if eq .CurrentVisibility 1}}checked{{end}}>
 | 
			
		||||
									<label>{{ctx.Locale.Tr "org.settings.visibility.limited"}}</label>
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
							<div class="field">
 | 
			
		||||
								<div class="ui radio checkbox">
 | 
			
		||||
									<input class="enable-system-radio" name="visibility" type="radio" value="2" {{if eq .CurrentVisibility 2}}checked{{end}}>
 | 
			
		||||
									<label>{{ctx.Locale.Tr "org.settings.visibility.private"}}</label>
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
<div class="ui segments org-setting-content">
 | 
			
		||||
	<h4 class="ui top attached header">
 | 
			
		||||
		{{ctx.Locale.Tr "org.settings.options"}}
 | 
			
		||||
	</h4>
 | 
			
		||||
	<div class="ui attached segment">
 | 
			
		||||
		<form class="ui form" action="{{.Link}}" method="post">
 | 
			
		||||
			{{.CsrfTokenHtml}}
 | 
			
		||||
			<div class="field {{if .Err_FullName}}error{{end}}">
 | 
			
		||||
				<label for="full_name">{{ctx.Locale.Tr "org.org_full_name_holder"}}</label>
 | 
			
		||||
				<input id="full_name" name="full_name" value="{{.Org.FullName}}" maxlength="100">
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="field {{if .Err_Email}}error{{end}}">
 | 
			
		||||
				<label for="email">{{ctx.Locale.Tr "org.settings.email"}}</label>
 | 
			
		||||
				<input id="email" name="email" type="email" value="{{.Org.Email}}" maxlength="255">
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="field {{if .Err_Description}}error{{end}}">
 | 
			
		||||
				{{/* it is rendered as markdown, but the length is limited, so at the moment we do not use the markdown editor here */}}
 | 
			
		||||
				<label for="description">{{ctx.Locale.Tr "org.org_desc"}}</label>
 | 
			
		||||
				<textarea id="description" name="description" rows="2" maxlength="255">{{.Org.Description}}</textarea>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="field {{if .Err_Website}}error{{end}}">
 | 
			
		||||
				<label for="website">{{ctx.Locale.Tr "org.settings.website"}}</label>
 | 
			
		||||
				<input id="website" name="website" type="url" value="{{.Org.Website}}" maxlength="255">
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="field">
 | 
			
		||||
				<label for="location">{{ctx.Locale.Tr "org.settings.location"}}</label>
 | 
			
		||||
				<input id="location" name="location"  value="{{.Org.Location}}" maxlength="50">
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
						<div class="field" id="permission_box">
 | 
			
		||||
							<label>{{ctx.Locale.Tr "org.settings.permission"}}</label>
 | 
			
		||||
							<div class="field">
 | 
			
		||||
								<div class="ui checkbox">
 | 
			
		||||
									<input type="checkbox" name="repo_admin_change_team_access" {{if .RepoAdminChangeTeamAccess}}checked{{end}}>
 | 
			
		||||
									<label>{{ctx.Locale.Tr "org.settings.repoadminchangeteam"}}</label>
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
 | 
			
		||||
						{{if .SignedUser.IsAdmin}}
 | 
			
		||||
						<div class="divider"></div>
 | 
			
		||||
 | 
			
		||||
						<div class="inline field {{if .Err_MaxRepoCreation}}error{{end}}">
 | 
			
		||||
							<label for="max_repo_creation">{{ctx.Locale.Tr "admin.users.max_repo_creation"}}</label>
 | 
			
		||||
							<input id="max_repo_creation" name="max_repo_creation" type="number" min="-1" value="{{.Org.MaxRepoCreation}}">
 | 
			
		||||
							<p class="help">{{ctx.Locale.Tr "admin.users.max_repo_creation_desc"}}</p>
 | 
			
		||||
						</div>
 | 
			
		||||
						{{end}}
 | 
			
		||||
 | 
			
		||||
						<div class="field">
 | 
			
		||||
							<button class="ui primary button">{{ctx.Locale.Tr "org.settings.update_settings"}}</button>
 | 
			
		||||
						</div>
 | 
			
		||||
					</form>
 | 
			
		||||
 | 
			
		||||
					<div class="divider"></div>
 | 
			
		||||
 | 
			
		||||
					<form class="ui form" action="{{.Link}}/avatar" method="post" enctype="multipart/form-data">
 | 
			
		||||
						{{.CsrfTokenHtml}}
 | 
			
		||||
						<div class="inline field">
 | 
			
		||||
							{{template "shared/avatar_upload_crop" dict "LabelText" (ctx.Locale.Tr "settings.choose_new_avatar")}}
 | 
			
		||||
						</div>
 | 
			
		||||
						<div class="field">
 | 
			
		||||
							<button class="ui primary button">{{ctx.Locale.Tr "settings.update_avatar"}}</button>
 | 
			
		||||
							<button class="ui red button link-action" data-url="{{.Link}}/avatar/delete">{{ctx.Locale.Tr "settings.delete_current_avatar"}}</button>
 | 
			
		||||
						</div>
 | 
			
		||||
					</form>
 | 
			
		||||
			<div class="divider"></div>
 | 
			
		||||
			<div class="field" id="visibility_box">
 | 
			
		||||
				<label for="visibility">{{ctx.Locale.Tr "org.settings.visibility"}}</label>
 | 
			
		||||
				<div class="field">
 | 
			
		||||
					<div class="ui radio checkbox">
 | 
			
		||||
						<input class="enable-system-radio" name="visibility" type="radio" value="0" {{if eq .CurrentVisibility 0}}checked{{end}}>
 | 
			
		||||
						<label>{{ctx.Locale.Tr "org.settings.visibility.public"}}</label>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="field">
 | 
			
		||||
					<div class="ui radio checkbox">
 | 
			
		||||
						<input class="enable-system-radio" name="visibility" type="radio" value="1" {{if eq .CurrentVisibility 1}}checked{{end}}>
 | 
			
		||||
						<label>{{ctx.Locale.Tr "org.settings.visibility.limited"}}</label>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="field">
 | 
			
		||||
					<div class="ui radio checkbox">
 | 
			
		||||
						<input class="enable-system-radio" name="visibility" type="radio" value="2" {{if eq .CurrentVisibility 2}}checked{{end}}>
 | 
			
		||||
						<label>{{ctx.Locale.Tr "org.settings.visibility.private"}}</label>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<div class="field" id="permission_box">
 | 
			
		||||
				<label>{{ctx.Locale.Tr "org.settings.permission"}}</label>
 | 
			
		||||
				<div class="field">
 | 
			
		||||
					<div class="ui checkbox">
 | 
			
		||||
						<input type="checkbox" name="repo_admin_change_team_access" {{if .RepoAdminChangeTeamAccess}}checked{{end}}>
 | 
			
		||||
						<label>{{ctx.Locale.Tr "org.settings.repoadminchangeteam"}}</label>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			{{if .SignedUser.IsAdmin}}
 | 
			
		||||
			<div class="divider"></div>
 | 
			
		||||
 | 
			
		||||
			<div class="inline field {{if .Err_MaxRepoCreation}}error{{end}}">
 | 
			
		||||
				<label for="max_repo_creation">{{ctx.Locale.Tr "admin.users.max_repo_creation"}}</label>
 | 
			
		||||
				<input id="max_repo_creation" name="max_repo_creation" type="number" min="-1" value="{{.Org.MaxRepoCreation}}">
 | 
			
		||||
				<p class="help">{{ctx.Locale.Tr "admin.users.max_repo_creation_desc"}}</p>
 | 
			
		||||
			</div>
 | 
			
		||||
			{{end}}
 | 
			
		||||
 | 
			
		||||
			<div class="field">
 | 
			
		||||
				<button class="ui primary button">{{ctx.Locale.Tr "org.settings.update_settings"}}</button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</form>
 | 
			
		||||
 | 
			
		||||
		<div class="divider"></div>
 | 
			
		||||
 | 
			
		||||
		<form class="ui form" action="{{.Link}}/avatar" method="post" enctype="multipart/form-data">
 | 
			
		||||
			{{.CsrfTokenHtml}}
 | 
			
		||||
			<div class="inline field">
 | 
			
		||||
				{{template "shared/avatar_upload_crop" dict "LabelText" (ctx.Locale.Tr "settings.choose_new_avatar")}}
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="field">
 | 
			
		||||
				<button class="ui primary button">{{ctx.Locale.Tr "settings.update_avatar"}}</button>
 | 
			
		||||
				<button class="ui red button link-action" data-url="{{.Link}}/avatar/delete">{{ctx.Locale.Tr "settings.delete_current_avatar"}}</button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</form>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{{template "org/settings/options_dangerzone" .}}
 | 
			
		||||
 | 
			
		||||
{{template "org/settings/layout_footer" .}}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										93
									
								
								templates/org/settings/options_dangerzone.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								templates/org/settings/options_dangerzone.tmpl
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,93 @@
 | 
			
		||||
<h4 class="ui top attached error header">
 | 
			
		||||
	{{ctx.Locale.Tr "repo.settings.danger_zone"}}
 | 
			
		||||
</h4>
 | 
			
		||||
<div class="ui attached error danger segment">
 | 
			
		||||
	<div class="flex-list">
 | 
			
		||||
		<div class="flex-item tw-items-center">
 | 
			
		||||
			<div class="flex-item-main">
 | 
			
		||||
				<div class="flex-item-title">{{ctx.Locale.Tr "org.settings.rename"}}</div>
 | 
			
		||||
				<div class="flex-item-body">{{ctx.Locale.Tr "org.settings.rename_desc"}}</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="flex-item-trailing">
 | 
			
		||||
				<button class="ui basic red show-modal button" data-modal="#rename-org-modal">{{ctx.Locale.Tr "org.settings.rename"}}</button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<div class="flex-item">
 | 
			
		||||
			<div class="flex-item-main">
 | 
			
		||||
				<div class="flex-item-title">{{ctx.Locale.Tr "org.settings.delete_account"}}</div>
 | 
			
		||||
				<div class="flex-item-body">{{ctx.Locale.Tr "org.settings.delete_prompt"}}</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="flex-item-trailing">
 | 
			
		||||
				<button class="ui basic red show-modal button" data-modal="#delete-org-modal">{{ctx.Locale.Tr "org.settings.delete_account"}}</button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="ui small modal" id="rename-org-modal">
 | 
			
		||||
	<div class="header">
 | 
			
		||||
		{{ctx.Locale.Tr "org.settings.rename"}}
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="content">
 | 
			
		||||
		<ul class="ui warning message">
 | 
			
		||||
			<li>{{ctx.Locale.Tr "org.settings.rename_notices_1"}}</li>
 | 
			
		||||
			<li>{{ctx.Locale.Tr "org.settings.rename_notices_2"}}</li>
 | 
			
		||||
		</ul>
 | 
			
		||||
		<form class="ui form form-fetch-action" action="{{.Link}}/rename" method="post">
 | 
			
		||||
			{{.CsrfTokenHtml}}
 | 
			
		||||
			<div class="field">
 | 
			
		||||
				<label>
 | 
			
		||||
					{{ctx.Locale.Tr "org.settings.name_confirm"}}
 | 
			
		||||
					<span class="text red">{{.Org.Name}}</span>
 | 
			
		||||
				</label>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="required field">
 | 
			
		||||
				<label for="org_name_to_rename">{{ctx.Locale.Tr "org.org_name_holder"}}</label>
 | 
			
		||||
				<input id="org_name_to_rename" name="org_name" required>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<div class="required field">
 | 
			
		||||
				<label>{{ctx.Locale.Tr "org.settings.rename_new_org_name"}}</label>
 | 
			
		||||
				<input name="new_org_name" required>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<div class="actions">
 | 
			
		||||
				<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
 | 
			
		||||
				<button class="ui red button">{{ctx.Locale.Tr "org.settings.rename"}}</button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</form>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="ui small modal" id="delete-org-modal">
 | 
			
		||||
	<div class="header">
 | 
			
		||||
		{{ctx.Locale.Tr "org.settings.delete_account"}}
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="content">
 | 
			
		||||
		<ul class="ui warning message">
 | 
			
		||||
			<li>{{ctx.Locale.Tr "org.settings.delete_notices_1"}}</li>
 | 
			
		||||
			<li>{{ctx.Locale.Tr "org.settings.delete_notices_2" .Org.Name}}</li>
 | 
			
		||||
			<li>{{ctx.Locale.Tr "org.settings.delete_notices_3" .Org.Name}}</li>
 | 
			
		||||
			<li>{{ctx.Locale.Tr "org.settings.delete_notices_4" .Org.Name}}</li>
 | 
			
		||||
		</ul>
 | 
			
		||||
		<form class="ui form form-fetch-action" action="{{.Link}}/delete" method="post">
 | 
			
		||||
			{{.CsrfTokenHtml}}
 | 
			
		||||
			<div class="field">
 | 
			
		||||
				<label>
 | 
			
		||||
					{{ctx.Locale.Tr "org.settings.name_confirm"}}
 | 
			
		||||
					<span class="text red">{{.Org.Name}}</span>
 | 
			
		||||
				</label>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="required field">
 | 
			
		||||
				<label>{{ctx.Locale.Tr "org.org_name_holder"}}</label>
 | 
			
		||||
				<input name="org_name" required>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<div class="actions">
 | 
			
		||||
				<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
 | 
			
		||||
				<button class="ui red button">{{ctx.Locale.Tr "org.settings.delete_account"}}</button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</form>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
@ -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) {
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@
 | 
			
		||||
  opacity: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ui.dimmer > * {
 | 
			
		||||
.ui.dimmer > .ui.modal {
 | 
			
		||||
  position: static;
 | 
			
		||||
  margin-top: auto !important;
 | 
			
		||||
  margin-bottom: auto !important;
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -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',
 | 
			
		||||
 | 
			
		||||
@ -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();
 | 
			
		||||
 | 
			
		||||
@ -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.
 | 
			
		||||
 | 
			
		||||
@ -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');
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user