mirror of https://github.com/go-gitea/gitea.git
Merge branch 'main' into fork-on-edit
This commit is contained in:
commit
2ba118ac69
|
|
@ -831,6 +831,20 @@ type CountUserFilter struct {
|
|||
IsActive optional.Option[bool]
|
||||
}
|
||||
|
||||
// HasUsers checks whether there are any users in the database, or only one user exists.
|
||||
func HasUsers(ctx context.Context) (ret struct {
|
||||
HasAnyUser, HasOnlyOneUser bool
|
||||
}, err error,
|
||||
) {
|
||||
res, err := db.GetEngine(ctx).Table(&User{}).Cols("id").Limit(2).Query()
|
||||
if err != nil {
|
||||
return ret, fmt.Errorf("error checking user existence: %w", err)
|
||||
}
|
||||
ret.HasAnyUser = len(res) != 0
|
||||
ret.HasOnlyOneUser = len(res) == 1
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// CountUsers returns number of users.
|
||||
func CountUsers(ctx context.Context, opts *CountUserFilter) int64 {
|
||||
return countUsers(ctx, opts)
|
||||
|
|
|
|||
|
|
@ -421,6 +421,7 @@ remember_me.compromised = The login token is not valid anymore which may indicat
|
|||
forgot_password_title= Forgot Password
|
||||
forgot_password = Forgot password?
|
||||
need_account = Need an account?
|
||||
sign_up_tip = You are registering the first account in the system, which has administrator privileges. Please carefully remember your username and password. If you forget the username or password, please refer to the Gitea documentation to recover the account.
|
||||
sign_up_now = Register now.
|
||||
sign_up_successful = Account was successfully created. Welcome!
|
||||
confirmation_mail_sent_prompt_ex = A new confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the registration process. If your registration email address is incorrect, you can sign in again and change it.
|
||||
|
|
@ -2817,6 +2818,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.
|
||||
|
|
@ -2838,15 +2840,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.
|
||||
|
|
|
|||
|
|
@ -601,5 +601,7 @@ func SubmitInstall(ctx *context.Context) {
|
|||
// InstallDone shows the "post-install" page, makes it easier to develop the page.
|
||||
// The name is not called as "PostInstall" to avoid misinterpretation as a handler for "POST /install"
|
||||
func InstallDone(ctx *context.Context) { //nolint
|
||||
hasUsers, _ := user_model.HasUsers(ctx)
|
||||
ctx.Data["IsAccountCreated"] = hasUsers.HasAnyUser
|
||||
ctx.HTML(http.StatusOK, tplPostInstall)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -421,9 +421,11 @@ func SignOut(ctx *context.Context) {
|
|||
// SignUp render the register page
|
||||
func SignUp(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("sign_up")
|
||||
|
||||
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
|
||||
|
||||
hasUsers, _ := user_model.HasUsers(ctx)
|
||||
ctx.Data["IsFirstTimeRegistration"] = !hasUsers.HasAnyUser
|
||||
|
||||
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
|
||||
if err != nil {
|
||||
ctx.ServerError("UserSignUp", err)
|
||||
|
|
@ -610,7 +612,13 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any,
|
|||
// sends a confirmation email if required.
|
||||
func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.User) (ok bool) {
|
||||
// Auto-set admin for the only user.
|
||||
if user_model.CountUsers(ctx, nil) == 1 {
|
||||
hasUsers, err := user_model.HasUsers(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("HasUsers", err)
|
||||
return false
|
||||
}
|
||||
if hasUsers.HasOnlyOneUser {
|
||||
// the only user is the one just created, will set it as admin
|
||||
opts := &user_service.UpdateOptions{
|
||||
IsActive: optional.Some(true),
|
||||
IsAdmin: user_service.UpdateOptionFieldFromValue(true),
|
||||
|
|
|
|||
|
|
@ -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" .}}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
<!-- the "cup" has a handler, so move it a little leftward to make it visually in the center -->
|
||||
<div class="tw-ml-[-30px]"><img width="160" src="{{AssetUrlPrefix}}/img/loading.png" alt aria-hidden="true"></div>
|
||||
<div class="tw-my-[2em] tw-text-[18px]">
|
||||
<a id="goto-user-login" href="{{AppSubUrl}}/user/login">{{ctx.Locale.Tr "install.installing_desc"}}</a>
|
||||
<a id="goto-after-install" href="{{AppSubUrl}}{{Iif .IsAccountCreated "/user/login" "/user/sign_up"}}">{{ctx.Locale.Tr "install.installing_desc"}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@
|
|||
{{end}}
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
{{if .IsFirstTimeRegistration}}
|
||||
<p>{{ctx.Locale.Tr "auth.sign_up_tip"}}</p>
|
||||
{{end}}
|
||||
<form class="ui form" action="{{.SignUpLink}}" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
{{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister)}}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -641,7 +641,7 @@ $.fn.modal = function(parameters) {
|
|||
$module
|
||||
.off('mousedown' + elementEventNamespace)
|
||||
;
|
||||
}
|
||||
}
|
||||
$dimmer
|
||||
.off('mousedown' + elementEventNamespace)
|
||||
;
|
||||
|
|
@ -877,7 +877,7 @@ $.fn.modal = function(parameters) {
|
|||
? $(document).scrollTop() + settings.padding
|
||||
: $(document).scrollTop() + (module.cache.contextHeight - module.cache.height - settings.padding),
|
||||
marginLeft: -(module.cache.width / 2)
|
||||
})
|
||||
})
|
||||
;
|
||||
} else {
|
||||
$module
|
||||
|
|
@ -886,7 +886,7 @@ $.fn.modal = function(parameters) {
|
|||
? -(module.cache.height / 2)
|
||||
: settings.padding / 2,
|
||||
marginLeft: -(module.cache.width / 2)
|
||||
})
|
||||
})
|
||||
;
|
||||
}
|
||||
module.verbose('Setting modal offset for legacy mode');
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ function initPreInstall() {
|
|||
}
|
||||
|
||||
function initPostInstall() {
|
||||
const el = document.querySelector('#goto-user-login');
|
||||
const el = document.querySelector('#goto-after-install');
|
||||
if (!el) return;
|
||||
|
||||
const targetUrl = el.getAttribute('href');
|
||||
|
|
|
|||
|
|
@ -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…
Reference in New Issue