mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09: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:
		| @@ -2811,6 +2811,7 @@ team_permission_desc = Permission | |||||||
| team_unit_desc = Allow Access to Repository Sections | team_unit_desc = Allow Access to Repository Sections | ||||||
| team_unit_disabled = (Disabled) | 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_reserved = The organization name "%s" is reserved. | ||||||
| form.name_pattern_not_allowed = The pattern "%s" is not allowed in an organization name. | 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. | 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_settings = Update Settings | ||||||
| settings.update_setting_success = Organization settings have been updated. | 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.update_avatar_success = The organization's avatar has been updated. | ||||||
| settings.delete = Delete Organization | settings.delete = Delete Organization | ||||||
| settings.delete_account = Delete This Organization | settings.delete_account = Delete This Organization | ||||||
| settings.delete_prompt = The organization will be permanently removed. This <strong>CANNOT</strong> be undone! | 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.confirm_delete_account = Confirm Deletion | ||||||
| settings.delete_org_title = Delete Organization | settings.delete_failed = Delete Organization failed because of internal error | ||||||
| settings.delete_org_desc = This organization will be deleted permanently. Continue? | 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.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. | 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" | 	repo_module "code.gitea.io/gitea/modules/repository" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/templates" | 	"code.gitea.io/gitea/modules/templates" | ||||||
|  | 	"code.gitea.io/gitea/modules/util" | ||||||
| 	"code.gitea.io/gitea/modules/web" | 	"code.gitea.io/gitea/modules/web" | ||||||
| 	shared_user "code.gitea.io/gitea/routers/web/shared/user" | 	shared_user "code.gitea.io/gitea/routers/web/shared/user" | ||||||
| 	user_setting "code.gitea.io/gitea/routers/web/user/setting" | 	user_setting "code.gitea.io/gitea/routers/web/user/setting" | ||||||
| @@ -31,8 +32,6 @@ import ( | |||||||
| const ( | const ( | ||||||
| 	// tplSettingsOptions template path for render settings | 	// tplSettingsOptions template path for render settings | ||||||
| 	tplSettingsOptions templates.TplName = "org/settings/options" | 	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 template path for render hook settings | ||||||
| 	tplSettingsHooks templates.TplName = "org/settings/hooks" | 	tplSettingsHooks templates.TplName = "org/settings/hooks" | ||||||
| 	// tplSettingsLabels template path for render labels settings | 	// tplSettingsLabels template path for render labels settings | ||||||
| @@ -71,26 +70,6 @@ func SettingsPost(ctx *context.Context) { | |||||||
|  |  | ||||||
| 	org := ctx.Org.Organization | 	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 form.Email != "" { | ||||||
| 		if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil { | 		if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil { | ||||||
| 			ctx.Data["Err_Email"] = true | 			ctx.Data["Err_Email"] = true | ||||||
| @@ -163,42 +142,27 @@ func SettingsDeleteAvatar(ctx *context.Context) { | |||||||
| 	ctx.JSONRedirect(ctx.Org.OrgLink + "/settings") | 	ctx.JSONRedirect(ctx.Org.OrgLink + "/settings") | ||||||
| } | } | ||||||
|  |  | ||||||
| // SettingsDelete response for deleting an organization | // SettingsDeleteOrgPost response for deleting an organization | ||||||
| func SettingsDelete(ctx *context.Context) { | func SettingsDeleteOrgPost(ctx *context.Context) { | ||||||
| 	ctx.Data["Title"] = ctx.Tr("org.settings") | 	if ctx.Org.Organization.Name != ctx.FormString("org_name") { | ||||||
| 	ctx.Data["PageIsOrgSettings"] = true | 		ctx.JSONError(ctx.Tr("form.enterred_invalid_org_name")) | ||||||
| 	ctx.Data["PageIsSettingsDelete"] = true | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if ctx.Req.Method == http.MethodPost { | 	if err := org_service.DeleteOrganization(ctx, ctx.Org.Organization, false /* no purge */); err != nil { | ||||||
| 		if ctx.Org.Organization.Name != ctx.FormString("org_name") { | 		if repo_model.IsErrUserOwnRepos(err) { | ||||||
| 			ctx.Data["Err_OrgName"] = true | 			ctx.JSONError(ctx.Tr("form.org_still_own_repo")) | ||||||
| 			ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_org_name"), tplSettingsDelete, nil) | 		} else if packages_model.IsErrUserOwnPackages(err) { | ||||||
| 			return | 			ctx.JSONError(ctx.Tr("form.org_still_own_packages")) | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		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 { | 		} else { | ||||||
| 			log.Trace("Organization deleted: %s", ctx.Org.Organization.Name) | 			log.Error("DeleteOrganization: %v", err) | ||||||
| 			ctx.Redirect(setting.AppSubURL + "/") | 			ctx.JSONError(util.Iif(ctx.Doer.IsAdmin, err.Error(), string(ctx.Tr("org.settings.delete_failed")))) | ||||||
| 		} | 		} | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { | 	ctx.Flash.Success(ctx.Tr("org.settings.delete_successful", ctx.Org.Organization.Name)) | ||||||
| 		ctx.ServerError("RenderUserOrgHeader", err) | 	ctx.JSONRedirect(setting.AppSubURL + "/") | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	ctx.HTML(http.StatusOK, tplSettingsDelete) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // Webhooks render webhook list page | // Webhooks render webhook list page | ||||||
| @@ -250,3 +214,40 @@ func Labels(ctx *context.Context) { | |||||||
|  |  | ||||||
| 	ctx.HTML(http.StatusOK, tplSettingsLabels) | 	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() | 					addSettingsVariablesRoutes() | ||||||
| 				}, actions.MustEnableActions) | 				}, 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.Group("/packages", func() { | ||||||
| 					m.Get("", org.Packages) | 					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 | // UpdateOrgSettingForm form for updating organization settings | ||||||
| type UpdateOrgSettingForm struct { | type UpdateOrgSettingForm struct { | ||||||
| 	Name                      string `binding:"Required;Username;MaxSize(40)" locale:"org.org_name_holder"` |  | ||||||
| 	FullName                  string `binding:"MaxSize(100)"` | 	FullName                  string `binding:"MaxSize(100)"` | ||||||
| 	Email                     string `binding:"MaxSize(255)"` | 	Email                     string `binding:"MaxSize(255)"` | ||||||
| 	Description               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) | 	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> | 			</div> | ||||||
| 		</details> | 		</details> | ||||||
| 		{{end}} | 		{{end}} | ||||||
| 		<a class="{{if .PageIsSettingsDelete}}active {{end}}item" href="{{.OrgLink}}/settings/delete"> |  | ||||||
| 			{{ctx.Locale.Tr "org.settings.delete"}} |  | ||||||
| 		</a> |  | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
|   | |||||||
| @@ -1,101 +1,97 @@ | |||||||
| {{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings options")}} | {{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="ui segments org-setting-content"> | ||||||
| 						<div class="field" id="visibility_box"> | 	<h4 class="ui top attached header"> | ||||||
| 							<label for="visibility">{{ctx.Locale.Tr "org.settings.visibility"}}</label> | 		{{ctx.Locale.Tr "org.settings.options"}} | ||||||
| 							<div class="field"> | 	</h4> | ||||||
| 								<div class="ui radio checkbox"> | 	<div class="ui attached segment"> | ||||||
| 									<input class="enable-system-radio" name="visibility" type="radio" value="0" {{if eq .CurrentVisibility 0}}checked{{end}}> | 		<form class="ui form" action="{{.Link}}" method="post"> | ||||||
| 									<label>{{ctx.Locale.Tr "org.settings.visibility.public"}}</label> | 			{{.CsrfTokenHtml}} | ||||||
| 								</div> | 			<div class="field {{if .Err_FullName}}error{{end}}"> | ||||||
| 							</div> | 				<label for="full_name">{{ctx.Locale.Tr "org.org_full_name_holder"}}</label> | ||||||
| 							<div class="field"> | 				<input id="full_name" name="full_name" value="{{.Org.FullName}}" maxlength="100"> | ||||||
| 								<div class="ui radio checkbox"> | 			</div> | ||||||
| 									<input class="enable-system-radio" name="visibility" type="radio" value="1" {{if eq .CurrentVisibility 1}}checked{{end}}> | 			<div class="field {{if .Err_Email}}error{{end}}"> | ||||||
| 									<label>{{ctx.Locale.Tr "org.settings.visibility.limited"}}</label> | 				<label for="email">{{ctx.Locale.Tr "org.settings.email"}}</label> | ||||||
| 								</div> | 				<input id="email" name="email" type="email" value="{{.Org.Email}}" maxlength="255"> | ||||||
| 							</div> | 			</div> | ||||||
| 							<div class="field"> | 			<div class="field {{if .Err_Description}}error{{end}}"> | ||||||
| 								<div class="ui radio checkbox"> | 				{{/* it is rendered as markdown, but the length is limited, so at the moment we do not use the markdown editor here */}} | ||||||
| 									<input class="enable-system-radio" name="visibility" type="radio" value="2" {{if eq .CurrentVisibility 2}}checked{{end}}> | 				<label for="description">{{ctx.Locale.Tr "org.org_desc"}}</label> | ||||||
| 									<label>{{ctx.Locale.Tr "org.settings.visibility.private"}}</label> | 				<textarea id="description" name="description" rows="2" maxlength="255">{{.Org.Description}}</textarea> | ||||||
| 								</div> | 			</div> | ||||||
| 							</div> | 			<div class="field {{if .Err_Website}}error{{end}}"> | ||||||
| 						</div> | 				<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"> | 			<div class="divider"></div> | ||||||
| 							<label>{{ctx.Locale.Tr "org.settings.permission"}}</label> | 			<div class="field" id="visibility_box"> | ||||||
| 							<div class="field"> | 				<label for="visibility">{{ctx.Locale.Tr "org.settings.visibility"}}</label> | ||||||
| 								<div class="ui checkbox"> | 				<div class="field"> | ||||||
| 									<input type="checkbox" name="repo_admin_change_team_access" {{if .RepoAdminChangeTeamAccess}}checked{{end}}> | 					<div class="ui radio checkbox"> | ||||||
| 									<label>{{ctx.Locale.Tr "org.settings.repoadminchangeteam"}}</label> | 						<input class="enable-system-radio" name="visibility" type="radio" value="0" {{if eq .CurrentVisibility 0}}checked{{end}}> | ||||||
| 								</div> | 						<label>{{ctx.Locale.Tr "org.settings.visibility.public"}}</label> | ||||||
| 							</div> | 					</div> | ||||||
| 						</div> | 				</div> | ||||||
|  | 				<div class="field"> | ||||||
| 						{{if .SignedUser.IsAdmin}} | 					<div class="ui radio checkbox"> | ||||||
| 						<div class="divider"></div> | 						<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 class="inline field {{if .Err_MaxRepoCreation}}error{{end}}"> | 					</div> | ||||||
| 							<label for="max_repo_creation">{{ctx.Locale.Tr "admin.users.max_repo_creation"}}</label> | 				</div> | ||||||
| 							<input id="max_repo_creation" name="max_repo_creation" type="number" min="-1" value="{{.Org.MaxRepoCreation}}"> | 				<div class="field"> | ||||||
| 							<p class="help">{{ctx.Locale.Tr "admin.users.max_repo_creation_desc"}}</p> | 					<div class="ui radio checkbox"> | ||||||
| 						</div> | 						<input class="enable-system-radio" name="visibility" type="radio" value="2" {{if eq .CurrentVisibility 2}}checked{{end}}> | ||||||
| 						{{end}} | 						<label>{{ctx.Locale.Tr "org.settings.visibility.private"}}</label> | ||||||
|  | 					</div> | ||||||
| 						<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> | ||||||
| 			</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" .}} | {{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-spacing: 16px; /* space between page elements */ | ||||||
|   --page-margin-x: 32px; /* minimum space on left and right side of page */ |   --page-margin-x: 32px; /* minimum space on left and right side of page */ | ||||||
|   --page-space-bottom: 64px; /* space between last page element and footer */ |   --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) { | @media (min-width: 768px) and (max-width: 1200px) { | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ | |||||||
|   opacity: 1; |   opacity: 1; | ||||||
| } | } | ||||||
|  |  | ||||||
| .ui.dimmer > * { | .ui.dimmer > .ui.modal { | ||||||
|   position: static; |   position: static; | ||||||
|   margin-top: auto !important; |   margin-top: auto !important; | ||||||
|   margin-bottom: auto !important; |   margin-bottom: auto !important; | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ | |||||||
|   position: fixed; |   position: fixed; | ||||||
|   opacity: 0; |   opacity: 0; | ||||||
|   transition: all .2s ease; |   transition: all .2s ease; | ||||||
|   z-index: 500; |   z-index: var(--z-index-toast); | ||||||
|   border-radius: var(--border-radius); |   border-radius: var(--border-radius); | ||||||
|   box-shadow: 0 8px 24px var(--color-shadow); |   box-shadow: 0 8px 24px var(--color-shadow); | ||||||
|   display: flex; |   display: flex; | ||||||
|   | |||||||
| @@ -525,7 +525,7 @@ $.fn.dropdown = function(parameters) { | |||||||
|               return true; |               return true; | ||||||
|             } |             } | ||||||
|             if(settings.onShow.call(element) !== false) { |             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() { |               module.animate.show(function() { | ||||||
|                 if( module.can.click() ) { |                 if( module.can.click() ) { | ||||||
|                   module.bind.intent(); |                   module.bind.intent(); | ||||||
| @@ -753,7 +753,7 @@ $.fn.dropdown = function(parameters) { | |||||||
|               if(module.is.searchSelection() && module.can.show() && module.is.focusedOnSearch() ) { |               if(module.is.searchSelection() && module.can.show() && module.is.focusedOnSearch() ) { | ||||||
|                 module.show(); |                 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()) { |           if(settings.useLabels && module.has.maxSelections()) { | ||||||
| @@ -3994,8 +3994,6 @@ $.fn.dropdown.settings = { | |||||||
|   onShow        : function(){}, |   onShow        : function(){}, | ||||||
|   onHide        : function(){}, |   onHide        : function(){}, | ||||||
|  |  | ||||||
|   onAfterFiltered: function(){}, // GITEA-PATCH: callback to correctly handle the filtered items |  | ||||||
|  |  | ||||||
|   /* Component */ |   /* Component */ | ||||||
|   name           : 'Dropdown', |   name           : 'Dropdown', | ||||||
|   namespace      : 'dropdown', |   namespace      : 'dropdown', | ||||||
|   | |||||||
| @@ -467,7 +467,7 @@ $.fn.modal = function(parameters) { | |||||||
|             ignoreRepeatedEvents = false; |             ignoreRepeatedEvents = false; | ||||||
|             return false; |             return false; | ||||||
|           } |           } | ||||||
|  |           $module.fomanticExt.onModalBeforeHidden.call(element); // GITEA-PATCH: handle more UI updates before hidden | ||||||
|           if( module.is.animating() || module.is.active() ) { |           if( module.is.animating() || module.is.active() ) { | ||||||
|             if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) { |             if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) { | ||||||
|               module.remove.active(); |               module.remove.active(); | ||||||
| @@ -641,7 +641,7 @@ $.fn.modal = function(parameters) { | |||||||
|               $module |               $module | ||||||
|                   .off('mousedown' + elementEventNamespace) |                   .off('mousedown' + elementEventNamespace) | ||||||
|               ; |               ; | ||||||
|             }            |             } | ||||||
|             $dimmer |             $dimmer | ||||||
|               .off('mousedown' + elementEventNamespace) |               .off('mousedown' + elementEventNamespace) | ||||||
|             ; |             ; | ||||||
| @@ -877,7 +877,7 @@ $.fn.modal = function(parameters) { | |||||||
|                       ? $(document).scrollTop() + settings.padding |                       ? $(document).scrollTop() + settings.padding | ||||||
|                       : $(document).scrollTop() + (module.cache.contextHeight - module.cache.height - settings.padding), |                       : $(document).scrollTop() + (module.cache.contextHeight - module.cache.height - settings.padding), | ||||||
|                   marginLeft: -(module.cache.width / 2) |                   marginLeft: -(module.cache.width / 2) | ||||||
|                 })  |                 }) | ||||||
|               ; |               ; | ||||||
|             } else { |             } else { | ||||||
|               $module |               $module | ||||||
| @@ -886,7 +886,7 @@ $.fn.modal = function(parameters) { | |||||||
|                     ? -(module.cache.height / 2) |                     ? -(module.cache.height / 2) | ||||||
|                     : settings.padding / 2, |                     : settings.padding / 2, | ||||||
|                   marginLeft: -(module.cache.width / 2) |                   marginLeft: -(module.cache.width / 2) | ||||||
|                 })  |                 }) | ||||||
|               ; |               ; | ||||||
|             } |             } | ||||||
|             module.verbose('Setting modal offset for legacy mode'); |             module.verbose('Setting modal offset for legacy mode'); | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import {request} from '../modules/fetch.ts'; | 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 {addDelegatedEventListener, submitEventSubmitter} from '../utils/dom.ts'; | ||||||
| import {confirmModal} from './comp/ConfirmModal.ts'; | import {confirmModal} from './comp/ConfirmModal.ts'; | ||||||
| import type {RequestOpts} from '../types.ts'; | import type {RequestOpts} from '../types.ts'; | ||||||
| @@ -24,6 +24,7 @@ function fetchActionDoRedirect(redirect: string) { | |||||||
|  |  | ||||||
| async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: RequestOpts) { | async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: RequestOpts) { | ||||||
|   try { |   try { | ||||||
|  |     hideToastsAll(); | ||||||
|     const resp = await request(url, opt); |     const resp = await request(url, opt); | ||||||
|     if (resp.status === 200) { |     if (resp.status === 200) { | ||||||
|       let {redirect} = await resp.json(); |       let {redirect} = await resp.json(); | ||||||
| @@ -35,7 +36,9 @@ async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: R | |||||||
|         window.location.reload(); |         window.location.reload(); | ||||||
|       } |       } | ||||||
|       return; |       return; | ||||||
|     } else if (resp.status >= 400 && resp.status < 500) { |     } | ||||||
|  |  | ||||||
|  |     if (resp.status >= 400 && resp.status < 500) { | ||||||
|       const data = await resp.json(); |       const data = await resp.json(); | ||||||
|       // the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error" |       // 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. |       // 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 | // use our own `$().dropdown` function to patch Fomantic's dropdown module | ||||||
| export function initAriaDropdownPatch() { | export function initAriaDropdownPatch() { | ||||||
|   if ($.fn.dropdown === ariaDropdownFn) throw new Error('initAriaDropdownPatch could only be called once'); |   if ($.fn.dropdown === ariaDropdownFn) throw new Error('initAriaDropdownPatch could only be called once'); | ||||||
|   $.fn.dropdown.settings.onAfterFiltered = onAfterFiltered; |  | ||||||
|   $.fn.dropdown = ariaDropdownFn; |   $.fn.dropdown = ariaDropdownFn; | ||||||
|   $.fn.fomanticExt.onResponseKeepSelectedItem = onResponseKeepSelectedItem; |   $.fn.fomanticExt.onResponseKeepSelectedItem = onResponseKeepSelectedItem; | ||||||
|  |   $.fn.fomanticExt.onDropdownAfterFiltered = onDropdownAfterFiltered; | ||||||
|   (ariaDropdownFn as FomanticInitFunction).settings = fomanticDropdownFn.settings; |   (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 $dropdown = $(this).closest('.ui.dropdown'); // "this" can be the "ui dropdown" or "<select>" | ||||||
|   const hideEmptyDividers = $dropdown.dropdown('setting', 'hideDividers') === 'empty'; |   const hideEmptyDividers = $dropdown.dropdown('setting', 'hideDividers') === 'empty'; | ||||||
|   const itemsMenu = $dropdown[0].querySelector('.scrolling.menu') || $dropdown[0].querySelector('.menu'); |   const itemsMenu = $dropdown[0].querySelector('.scrolling.menu') || $dropdown[0].querySelector('.menu'); | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| import $ from 'jquery'; | import $ from 'jquery'; | ||||||
| import type {FomanticInitFunction} from '../../types.ts'; | import type {FomanticInitFunction} from '../../types.ts'; | ||||||
|  | import {queryElems} from '../../utils/dom.ts'; | ||||||
|  | import {hideToastsFrom} from '../toast.ts'; | ||||||
|  |  | ||||||
| const fomanticModalFn = $.fn.modal; | const fomanticModalFn = $.fn.modal; | ||||||
|  |  | ||||||
| @@ -7,6 +9,7 @@ const fomanticModalFn = $.fn.modal; | |||||||
| export function initAriaModalPatch() { | export function initAriaModalPatch() { | ||||||
|   if ($.fn.modal === ariaModalFn) throw new Error('initAriaModalPatch could only be called once'); |   if ($.fn.modal === ariaModalFn) throw new Error('initAriaModalPatch could only be called once'); | ||||||
|   $.fn.modal = ariaModalFn; |   $.fn.modal = ariaModalFn; | ||||||
|  |   $.fn.fomanticExt.onModalBeforeHidden = onModalBeforeHidden; | ||||||
|   (ariaModalFn as FomanticInitFunction).settings = fomanticModalFn.settings; |   (ariaModalFn as FomanticInitFunction).settings = fomanticModalFn.settings; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -27,3 +30,10 @@ function ariaModalFn(this: any, ...args: Parameters<FomanticInitFunction>) { | |||||||
|   } |   } | ||||||
|   return ret; |   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 {htmlEscape} from 'escape-goat'; | ||||||
| import {svg} from '../svg.ts'; | 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 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 {Intent} from '../types.ts'; | ||||||
| import type {SvgName} from '../svg.ts'; | import type {SvgName} from '../svg.ts'; | ||||||
| @@ -37,17 +37,20 @@ const levels: ToastLevels = { | |||||||
|  |  | ||||||
| type ToastOpts = { | type ToastOpts = { | ||||||
|   useHtmlBody?: boolean, |   useHtmlBody?: boolean, | ||||||
|   preventDuplicates?: boolean, |   preventDuplicates?: boolean | string, | ||||||
| } & Options; | } & Options; | ||||||
|  |  | ||||||
|  | type ToastifyElement = HTMLElement & {_giteaToastifyInstance?: Toast }; | ||||||
|  |  | ||||||
| // See https://github.com/apvarun/toastify-js#api for options | // 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 { | function showToast(message: string, level: Intent, {gravity, position, duration, useHtmlBody, preventDuplicates = true, ...other}: ToastOpts = {}): Toast { | ||||||
|   const body = useHtmlBody ? String(message) : htmlEscape(message); |   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) { |   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) { |     if (toastEl) { | ||||||
|       const toastDupNumEl = toastEl.querySelector('.toast-duplicate-number'); |       const toastDupNumEl = toastEl.querySelector('.toast-duplicate-number'); | ||||||
|       showElem(toastDupNumEl); |       showElem(toastDupNumEl); | ||||||
| @@ -59,6 +62,7 @@ function showToast(message: string, level: Intent, {gravity, position, duration, | |||||||
|  |  | ||||||
|   const {icon, background, duration: levelDuration} = levels[level ?? 'info']; |   const {icon, background, duration: levelDuration} = levels[level ?? 'info']; | ||||||
|   const toast = Toastify({ |   const toast = Toastify({ | ||||||
|  |     selector: parent, | ||||||
|     text: ` |     text: ` | ||||||
|       <div class='toast-icon'>${svg(icon)}</div> |       <div class='toast-icon'>${svg(icon)}</div> | ||||||
|       <div class='toast-body'><span class="toast-duplicate-number tw-hidden">1</span>${body}</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.showToast(); | ||||||
|   toast.toastElement.querySelector('.toast-close').addEventListener('click', () => toast.hideToast()); |   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; |   return toast; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -89,3 +94,15 @@ export function showWarningToast(message: string, opts?: ToastOpts): Toast { | |||||||
| export function showErrorToast(message: string, opts?: ToastOpts): Toast { | export function showErrorToast(message: string, opts?: ToastOpts): Toast { | ||||||
|   return showToast(message, 'error', opts); |   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); | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user