feat: migrate show-modal to native dialogs (#10287)

Test coverage:

|Modal|Test|
|-|-|
|admin: adopt unadopted|missing, not needed|
|admin: delete unadopted|missing, not needed|
|admin: delete user|e2e added: `Admin: delete a user`|
|delete package|missing|
|new project|?|
|edit project col|?|
|default project col|?|
|delete project col|?|
|commit cherry-pick|?|
|commit delete note|?|
|fork redirect|?|
|lock/unlock issue|?|
|dismiss PR review|?|
|migration delete|?|
|migration cancel|?|
|lfs delete|?|
|convert mirror|?|
|convert fork|?|
|transfer repo|?|
|delete repo|?|
|archive repo|integration present, selectors adjusted|
|delete wiki|?|
|rename wiki branch|?|
|push mirror edit|?|
|mde: new table|e2e present, selectors adjusted|
|mde: new link|e2e present, selectors adjusted|
|actions: add secret|?|
|actions: edit variable|?|

Co-authored-by: 0ko <0ko@noreply.codeberg.org>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10287
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
This commit is contained in:
Gusted 2026-05-03 06:42:14 +02:00 committed by 0ko
commit 555d88070d
30 changed files with 776 additions and 710 deletions

View file

@ -24,37 +24,37 @@
<span class="tw-flex-1"> {{svg "octicon-file-directory-fill"}} {{$dir}}</span>
<div>
<button class="ui button primary show-modal tw-p-2" data-modal="#adopt-unadopted-modal-{{$dirI}}">{{svg "octicon-plus"}} {{ctx.Locale.Tr "repo.adopt_preexisting_label"}}</button>
<div class="ui g-modal-confirm modal" id="adopt-unadopted-modal-{{$dirI}}">
<div class="header">
<span class="label">{{ctx.Locale.Tr "repo.adopt_preexisting"}}</span>
</div>
<div class="content">
<p>{{ctx.Locale.Tr "repo.adopt_preexisting_content" $dir}}</p>
</div>
<form class="ui form" method="post" action="{{AppSubUrl}}/admin/repos/unadopted">
<input type="hidden" name="id" value="{{$dir}}">
<input type="hidden" name="action" value="adopt">
<input type="hidden" name="q" value="{{$.Keyword}}">
<input type="hidden" name="page" value="{{$.CurrentPage}}">
{{template "base/modal_actions_confirm"}}
</form>
</div>
<dialog id="adopt-unadopted-modal-{{$dirI}}">
<article>
<header>{{ctx.Locale.Tr "repo.adopt_preexisting"}}</header>
<div class="content">
<p>{{ctx.Locale.Tr "repo.adopt_preexisting_content" $dir}}</p>
</div>
<form class="ui form" method="post" action="{{AppSubUrl}}/admin/repos/unadopted">
<input type="hidden" name="id" value="{{$dir}}">
<input type="hidden" name="action" value="adopt">
<input type="hidden" name="q" value="{{$.Keyword}}">
<input type="hidden" name="page" value="{{$.CurrentPage}}">
{{template "base/modal_actions_confirm"}}
</form>
</article>
</dialog>
<button class="ui button red show-modal tw-p-2" data-modal="#delete-unadopted-modal-{{$dirI}}">{{svg "octicon-x"}} {{ctx.Locale.Tr "repo.delete_preexisting_label"}}</button>
<div class="ui g-modal-confirm modal" id="delete-unadopted-modal-{{$dirI}}">
<div class="header">
<span class="label">{{ctx.Locale.Tr "repo.delete_preexisting"}}</span>
</div>
<div class="content">
<p>{{ctx.Locale.Tr "repo.delete_preexisting_content" $dir}}</p>
</div>
<form class="ui form" method="post" action="{{AppSubUrl}}/admin/repos/unadopted">
<input type="hidden" name="id" value="{{$dir}}">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="q" value="{{$.Keyword}}">
<input type="hidden" name="page" value="{{$.CurrentPage}}">
{{template "base/modal_actions_confirm"}}
</form>
</div>
<dialog id="delete-unadopted-modal-{{$dirI}}">
<article>
<header>{{ctx.Locale.Tr "repo.delete_preexisting"}}</header>
<div class="content">
<p>{{ctx.Locale.Tr "repo.delete_preexisting_content" $dir}}</p>
</div>
<form class="ui form" method="post" action="{{AppSubUrl}}/admin/repos/unadopted">
<input type="hidden" name="id" value="{{$dir}}">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="q" value="{{$.Keyword}}">
<input type="hidden" name="page" value="{{$.CurrentPage}}">
{{template "base/modal_actions_confirm"}}
</form>
</article>
</dialog>
</div>
</div>
{{end}}

View file

@ -224,24 +224,23 @@
</div>
</div>
<div class="ui g-modal-confirm delete modal" id="delete-user-modal">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "settings.delete_account_title"}}
</div>
<form class="ui form" method="post" action="./delete">
<div class="content">
<p>{{ctx.Locale.Tr "settings.delete_account_desc"}}</p>
<div class="field">
<div class="ui checkbox">
<label for="purge">{{ctx.Locale.Tr "admin.users.purge"}}</label>
<input name="purge" type="checkbox">
<dialog id="delete-user-modal">
<article>
<header>{{svg "octicon-trash"}} {{ctx.Locale.Tr "settings.delete_account_title"}}</header>
<form class="ui form" method="post" action="./delete">
<div class="content">
<p>{{ctx.Locale.Tr "settings.delete_account_desc"}}</p>
<div class="field">
<div class="ui checkbox">
<label for="purge">{{ctx.Locale.Tr "admin.users.purge"}}</label>
<input name="purge" type="checkbox">
</div>
<p class="help">{{ctx.Locale.Tr "admin.users.purge_help"}}</p>
</div>
<p class="help">{{ctx.Locale.Tr "admin.users.purge_help"}}</p>
</div>
</div>
{{template "base/modal_actions_confirm" .}}
</form>
</div>
{{template "base/modal_actions_confirm" .}}
</form>
</article>
</dialog>
{{template "admin/layout_footer" .}}

View file

@ -53,20 +53,20 @@
<div class="flex-item-trailing">
<button class="ui basic red show-modal button" data-modal="#delete-package-modal">{{ctx.Locale.Tr "packages.settings.delete"}}</button>
</div>
<div class="ui tiny modal" id="delete-package-modal">
<div class="header">
{{ctx.Locale.Tr "packages.settings.delete"}}
</div>
<div class="content">
<div class="ui warning message tw-break-anywhere">
{{ctx.Locale.Tr "packages.settings.delete.notice" .PackageDescriptor.Package.Name .PackageDescriptor.Version.Version}}
<dialog id="delete-package-modal">
<article>
<header>{{ctx.Locale.Tr "packages.settings.delete"}}</header>
<div class="content">
<div class="ui warning message tw-break-anywhere">
{{ctx.Locale.Tr "packages.settings.delete.notice" .PackageDescriptor.Package.Name .PackageDescriptor.Version.Version}}
</div>
</div>
<form class="ui form" action="{{.Link}}" method="post">
<input type="hidden" name="action" value="delete">
{{template "base/modal_actions_confirm" .}}
</form>
</div>
</div>
</article>
</dialog>
</div>
</div>
</div>

View file

@ -29,32 +29,31 @@
{{ctx.Locale.Tr "new_project_column"}}
</button>
</div>
<div class="ui small modal new-project-column-modal" id="new-project-column-item">
<div class="header">
{{ctx.Locale.Tr "repo.projects.column.new"}}
</div>
<div class="content">
<dialog id="new-project-column-item">
<article>
<header>{{ctx.Locale.Tr "repo.projects.column.new"}}</header>
<form class="ui form">
<div class="required field">
<label for="new_project_column">{{ctx.Locale.Tr "repo.projects.column.new_title"}}</label>
<input class="new-project-column" id="new_project_column" name="title" required>
</div>
<div class="content">
<div class="required field">
<label for="new_project_column">{{ctx.Locale.Tr "repo.projects.column.new_title"}}</label>
<input class="new-project-column" id="new_project_column" name="title" required>
</div>
<div class="field color-field">
<label for="new_project_column_color_picker">{{ctx.Locale.Tr "repo.projects.column.color"}}</label>
<div class="js-color-picker-input column">
<input maxlength="7" placeholder="#c320f6" id="new_project_column_color_picker" name="color">
{{template "repo/issue/label_precolors"}}
<div class="field color-field">
<label for="new_project_column_color_picker">{{ctx.Locale.Tr "repo.projects.column.color"}}</label>
<div class="js-color-picker-input column">
<input maxlength="7" placeholder="#c320f6" id="new_project_column_color_picker" name="color">
{{template "repo/issue/label_precolors"}}
</div>
</div>
</div>
<div class="text right actions">
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<div class="actions">
<button type="button" class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button data-url="{{$.Link}}" class="ui primary button" id="new_project_column_submit">{{ctx.Locale.Tr "repo.projects.column.new_submit"}}</button>
</div>
</form>
</div>
</div>
</article>
</dialog>
{{end}}
</div>
@ -101,54 +100,54 @@
</a>
{{end}}
<div class="ui small modal edit-project-column-modal" id="edit-project-column-modal-{{.ID}}">
<div class="header">
{{ctx.Locale.Tr "repo.projects.column.edit"}}
</div>
<div class="content">
<dialog class="edit-project-column-modal" id="edit-project-column-modal-{{.ID}}">
<article>
<header>{{ctx.Locale.Tr "repo.projects.column.edit"}}</header>
<form class="ui form">
<div class="required field">
<label for="new_project_column_title">{{ctx.Locale.Tr "repo.projects.column.edit_title"}}</label>
<input class="project-column-title-input" id="new_project_column_title" name="title" value="{{.Title}}" required>
</div>
<div class="field color-field">
<label for="new_project_column_color">{{ctx.Locale.Tr "repo.projects.column.color"}}</label>
<div class="js-color-picker-input column">
<input maxlength="7" placeholder="#c320f6" id="new_project_column_color" name="color" value="{{.Color}}">
{{template "repo/issue/label_precolors"}}
<div class="content">
<div class="required field">
<label for="new_project_column_title">{{ctx.Locale.Tr "repo.projects.column.edit_title"}}</label>
<input class="project-column-title-input" id="new_project_column_title" name="title" value="{{.Title}}" required>
</div>
</div>
<div class="text right actions">
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<div class="field color-field">
<label for="new_project_column_color">{{ctx.Locale.Tr "repo.projects.column.color"}}</label>
<div class="js-color-picker-input column">
<input maxlength="7" placeholder="#c320f6" id="new_project_column_color" name="color" value="{{.Color}}">
{{template "repo/issue/label_precolors"}}
</div>
</div>
</div>
<div class="actions">
<button type="button" class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button data-url="{{$.Link}}/{{.ID}}" class="ui primary button edit-project-column-button">{{ctx.Locale.Tr "repo.projects.column.edit"}}</button>
</div>
</form>
</div>
</div>
</article>
</dialog>
<div class="ui g-modal-confirm modal default-project-column-modal" id="default-project-column-modal-{{.ID}}">
<div class="header">
<span id="default-project-column-header"></span>
</div>
<div class="content">
<label id="default-project-column-content"></label>
</div>
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</div>
<dialog class="default-project-column-modal" id="default-project-column-modal-{{.ID}}">
<article>
<header><span id="default-project-column-header"></span></header>
<div class="content">
<label id="default-project-column-content"></label>
</div>
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</article>
</dialog>
<div class="ui g-modal-confirm modal" id="delete-project-column-modal-{{.ID}}">
<div class="header">
{{ctx.Locale.Tr "repo.projects.column.delete"}}
</div>
<div class="content">
<label>
{{ctx.Locale.Tr "repo.projects.column.deletion_desc"}}
</label>
</div>
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</div>
<dialog id="delete-project-column-modal-{{.ID}}">
<article>
<header>{{ctx.Locale.Tr "repo.projects.column.delete"}}</header>
<div class="content">
<label>
{{ctx.Locale.Tr "repo.projects.column.deletion_desc"}}
</label>
</div>
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</article>
</dialog>
</div>
</div>
{{end}}

View file

@ -55,25 +55,28 @@
data-modal-cherry-pick-header="{{ctx.Locale.Tr "repo.commit.cherry-pick-header" (ShortSha .CommitID)}}"
data-modal-cherry-pick-content="{{ctx.Locale.Tr "repo.commit.cherry-pick-content"}}"
data-modal-cherry-pick-submit="{{ctx.Locale.Tr "repo.commit.cherry-pick"}}">{{ctx.Locale.Tr "repo.commit.cherry-pick"}}</div>
<div class="ui g-modal-confirm modal" id="cherry-pick-modal">
<div class="header">
<span id="cherry-pick-header"></span>
</div>
<div class="content">
<p id="cherry-pick-content" class="branch-dropdown"></p>
{{template "repo/branch_dropdown" dict "root" .
"noTag" true "disableCreateBranch" true
"branchForm" "branch-dropdown-form"
"branchURLPrefix" (printf "%s/_cherrypick/%s/" $.RepoLink .CommitID) "branchURLSuffix" ""
"setAction" true "submitForm" true}}
<dialog id="cherry-pick-modal">
<article>
<header><span id="cherry-pick-header"></span></header>
<div class="content">
<p id="cherry-pick-content" class="branch-dropdown"></p>
{{template "repo/branch_dropdown" dict "root" .
"noTag" true "disableCreateBranch" true
"branchForm" "branch-dropdown-form"
"branchURLPrefix" (printf "%s/_cherrypick/%s/" $.RepoLink .CommitID) "branchURLSuffix" ""
"setAction" true "submitForm" true}}
</div>
<form method="get" action="{{$.RepoLink}}/_cherrypick/{{.CommitID}}/{{PathEscapeSegments $.Repository.DefaultBranch}}" id="branch-dropdown-form">
<input type="hidden" name="ref" value="{{$.Repository.DefaultBranch}}">
<input type="hidden" name="refType" value="branch">
<input type="hidden" id="cherry-pick-type" name="cherry-pick-type"><br>
<button type="submit" id="cherry-pick-submit" class="ui primary button"></button>
<div class="actions">
<button type="button" class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button type="submit" id="cherry-pick-submit" class="ui primary button"></button>
</div>
</form>
</div>
</div>
</article>
</dialog>
<dialog id="create-branch-modal">
<article>
<header>{{ctx.Locale.Tr "repo.branch.new_branch"}}</header>
@ -284,20 +287,20 @@
<button id="commit-notes-edit-button" class="ui tiny primary button tw-py-[6px] tw-px-[10px]">{{ctx.Locale.Tr "edit"}}</button>
<button class="ui tiny button red show-modal tw-py-[6px] tw-px-[10px]" data-modal="#delete-note-modal">{{ctx.Locale.Tr "remove"}}</button>
</div>
<div class="ui small modal" id="delete-note-modal">
<div class="header">
{{ctx.Locale.Tr "repo.diff.git-notes.remove-header"}}
</div>
<div class="content">
<p>{{ctx.Locale.Tr "repo.diff.git-notes.remove-body"}}</p>
<div class="text right actions">
<form action="{{.Link}}/notes/remove" method="post">
<dialog id="delete-note-modal">
<article>
<header>{{ctx.Locale.Tr "repo.diff.git-notes.remove-header"}}</header>
<div class="content">
<p>{{ctx.Locale.Tr "repo.diff.git-notes.remove-body"}}</p>
</div>
<form action="{{.Link}}/notes/remove" method="post">
<div class="text right actions">
<button type="button" class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button type="submit" class="ui red button">{{ctx.Locale.Tr "remove"}}</button>
</form>
</div>
</div>
</div>
</div>
</form>
</article>
</dialog>
{{end}}
</div>
<div id="commit-notes-display-area" class="ui bottom attached info segment git-notes">

View file

@ -26,24 +26,24 @@
>
{{svg "octicon-repo-forked"}}<span class="text not-mobile">{{ctx.Locale.Tr "repo.fork"}}</span>
</a>
<div class="ui small modal" id="fork-repo-modal">
<div class="header">
{{ctx.Locale.Tr "repo.already_forked" .Name}}
</div>
<div class="content tw-text-left">
<div class="ui list">
{{range $.UserAndOrgForks}}
<div class="ui item tw-py-2">
<a href="{{.Link}}">{{svg "octicon-repo-forked" 16 "tw-mr-2"}}{{.FullName}}</a>
</div>
<dialog id="fork-repo-modal">
<article>
<header>{{ctx.Locale.Tr "repo.already_forked" .Name}}</header>
<div class="content tw-text-left">
<div class="ui list">
{{range $.UserAndOrgForks}}
<div class="ui item tw-py-2">
<a href="{{.Link}}">{{svg "octicon-repo-forked" 16 "tw-mr-2"}}{{.FullName}}</a>
</div>
{{end}}
</div>
{{if $.CanSignedUserFork}}
<div class="divider"></div>
<a href="{{$.RepoLink}}/fork">{{ctx.Locale.Tr "repo.fork_to_different_account"}}</a>
{{end}}
</div>
{{if $.CanSignedUserFork}}
<div class="divider"></div>
<a href="{{$.RepoLink}}/fork">{{ctx.Locale.Tr "repo.fork_to_different_account"}}</a>
{{end}}
</div>
</div>
</article>
</dialog>
<a class="ui basic label" href="{{.Link}}/forks"
aria-label="{{ctx.Locale.TrPluralString .NumForks "fork.n_forks" (printf "%d" .NumForks)}}"
>

View file

@ -21,32 +21,32 @@
{{ctx.Locale.Tr "repo.issues.lock"}}
{{end}}
</button>
<div class="ui tiny modal" id="lock">
<div class="header">
{{if .Issue.IsLocked}}
{{ctx.Locale.Tr "repo.issues.unlock.title"}}
{{else}}
{{ctx.Locale.Tr "repo.issues.lock.title"}}
{{end}}
</div>
<div class="content">
<div class="ui warning message">
<dialog class="tw-overflow-visible" id="lock">
<article>
<header>
{{if .Issue.IsLocked}}
{{ctx.Locale.Tr "repo.issues.unlock.notice_1"}}<br>
{{ctx.Locale.Tr "repo.issues.unlock.notice_2"}}<br>
{{ctx.Locale.Tr "repo.issues.unlock.title"}}
{{else}}
{{ctx.Locale.Tr "repo.issues.lock.notice_1"}}<br>
{{ctx.Locale.Tr "repo.issues.lock.notice_2"}}<br>
{{ctx.Locale.Tr "repo.issues.lock.notice_3"}}<br>
{{ctx.Locale.Tr "repo.issues.lock.title"}}
{{end}}
</header>
<div class="content">
<div class="ui warning message">
{{if .Issue.IsLocked}}
{{ctx.Locale.Tr "repo.issues.unlock.notice_1"}}<br>
{{ctx.Locale.Tr "repo.issues.unlock.notice_2"}}<br>
{{else}}
{{ctx.Locale.Tr "repo.issues.lock.notice_1"}}<br>
{{ctx.Locale.Tr "repo.issues.lock.notice_2"}}<br>
{{ctx.Locale.Tr "repo.issues.lock.notice_3"}}<br>
{{end}}
</div>
</div>
<form class="ui form form-fetch-action" action="{{.Issue.Link}}{{if .Issue.IsLocked}}/unlock{{else}}/lock{{end}}"
method="post">
<form class="ui form form-fetch-action" action="{{.Issue.Link}}{{if .Issue.IsLocked}}/unlock{{else}}/lock{{end}}" method="post">
<div class="content">
{{if not .Issue.IsLocked}}
<div class="field">
<strong> {{ctx.Locale.Tr "repo.issues.lock.reason"}} </strong>
<strong>{{ctx.Locale.Tr "repo.issues.lock.reason"}}</strong>
</div>
<div class="field">
@ -70,10 +70,10 @@
</div>
</div>
{{end}}
<div class="text right actions">
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button class="ui red button">
</div>
<div class="actions">
<button type="button" class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button type="submit" class="ui red button">
{{if .Issue.IsLocked}}
{{ctx.Locale.Tr "repo.issues.unlock_confirm"}}
{{else}}
@ -82,30 +82,32 @@
</button>
</div>
</form>
</div>
</div>
</article>
</dialog>
<button class="tw-mt-1 fluid ui show-modal button" data-modal="#sidebar-delete-issue">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "repo.issues.delete"}}
</button>
<div class="ui g-modal-confirm modal" id="sidebar-delete-issue">
<div class="header">
{{if .Issue.IsPull}}
{{ctx.Locale.Tr "repo.pulls.delete.title"}}
{{else}}
{{ctx.Locale.Tr "repo.issues.delete.title"}}
{{end}}
</div>
<div class="content">
<p>
<dialog id="sidebar-delete-issue">
<article>
<header>
{{if .Issue.IsPull}}
{{ctx.Locale.Tr "repo.pulls.delete.text"}}
{{ctx.Locale.Tr "repo.pulls.delete.title"}}
{{else}}
{{ctx.Locale.Tr "repo.issues.delete.text"}}
{{ctx.Locale.Tr "repo.issues.delete.title"}}
{{end}}
</p>
</div>
<form action="{{.Issue.Link}}/delete" method="post">
{{template "base/modal_actions_confirm" .}}
</form>
</div>
</header>
<div class="content">
<p>
{{if .Issue.IsPull}}
{{ctx.Locale.Tr "repo.pulls.delete.text"}}
{{else}}
{{ctx.Locale.Tr "repo.issues.delete.text"}}
{{end}}
</p>
</div>
<form action="{{.Issue.Link}}/delete" method="post">
{{template "base/modal_actions_confirm" .}}
</form>
</article>
</dialog>

View file

@ -15,27 +15,29 @@
<a href="#" class="ui muted icon tw-flex tw-items-center show-modal" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.dismiss_review"}}" data-modal="#dismiss-review-modal-{{.Review.ID}}">
{{svg "octicon-x" 20}}
</a>
<div class="ui small modal" id="dismiss-review-modal-{{.Review.ID}}">
<div class="header">
{{ctx.Locale.Tr "repo.issues.dismiss_review"}}
</div>
<div class="content">
<div class="ui warning message">
{{ctx.Locale.Tr "repo.issues.dismiss_review_warning"}}
<dialog id="dismiss-review-modal-{{.Review.ID}}">
<article>
<header>{{ctx.Locale.Tr "repo.issues.dismiss_review"}}</header>
<div class="content">
<div class="ui warning message">
{{ctx.Locale.Tr "repo.issues.dismiss_review_warning"}}
</div>
</div>
<form class="ui form dismiss-review-form" id="dismiss-review-{{.Review.ID}}" action="{{$.RepoLink}}/issues/dismiss_review" method="post">
<input type="hidden" name="review_id" value="{{.Review.ID}}">
<div class="field">
<label for="message">{{ctx.Locale.Tr "action.review_dismissed_reason"}}</label>
<input id="message" name="message">
<div class="content">
<input type="hidden" name="review_id" value="{{.Review.ID}}">
<div class="field">
<label for="message">{{ctx.Locale.Tr "action.review_dismissed_reason"}}</label>
<input id="message" name="message">
</div>
</div>
<div class="text right actions">
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button class="ui red button" type="submit">{{ctx.Locale.Tr "ok"}}</button>
<div class="actions">
<button type="button" class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button type="submit" class="ui red button">{{ctx.Locale.Tr "ok"}}</button>
</div>
</form>
</div>
</div>
</article>
</dialog>
{{end}}
{{if .Review.Stale}}
<span data-tooltip-content="{{ctx.Locale.Tr "repo.issues.is_stale"}}">

View file

@ -51,49 +51,50 @@
</div>
</div>
<div class="ui small modal" id="delete-repo-modal">
<div class="header">
{{ctx.Locale.Tr "repo.settings.delete"}}
</div>
<div class="content">
<div class="ui warning message">
{{ctx.Locale.Tr "repo.settings.delete_notices_1"}}<br>
{{ctx.Locale.Tr "repo.settings.delete_notices_2" .Repository.FullName}}
{{if .Repository.NumForks}}<br>
{{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}
{{end}}
<dialog id="delete-repo-modal">
<article>
<header>{{ctx.Locale.Tr "repo.settings.delete"}}</header>
<div class="content">
<div class="ui warning message">
{{ctx.Locale.Tr "repo.settings.delete_notices_1"}}<br>
{{ctx.Locale.Tr "repo.settings.delete_notices_2" .Repository.FullName}}
{{if .Repository.NumForks}}<br>
{{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}
{{end}}
</div>
</div>
<form class="ui form" action="{{.Link}}/settings" method="post">
<input type="hidden" name="action" value="delete">
<div class="field">
<label>
{{ctx.Locale.Tr "repo.settings.enter_repo_name"}}
<span class="text red">{{.Repository.FullName}}</span>
</label>
<div class="content">
<input type="hidden" name="action" value="delete">
<div class="field">
<label>
{{ctx.Locale.Tr "repo.settings.enter_repo_name"}}
<span class="text red">{{.Repository.FullName}}</span>
</label>
</div>
<div class="required field">
<label for="repo_name_to_delete">{{ctx.Locale.Tr "repo.settings.confirmation_string"}}</label>
<input id="repo_name_to_delete" name="repo_name" required>
</div>
</div>
<div class="required field">
<label for="repo_name_to_delete">{{ctx.Locale.Tr "repo.settings.confirmation_string"}}</label>
<input id="repo_name_to_delete" name="repo_name" required>
</div>
<div class="text right actions">
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button class="ui red button">{{ctx.Locale.Tr "repo.settings.confirm_delete"}}</button>
<div class="actions">
<button type="button" class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button type="submit" class="ui red button">{{ctx.Locale.Tr "repo.settings.confirm_delete"}}</button>
</div>
</form>
</div>
</div>
</article>
</dialog>
<div class="ui g-modal-confirm modal" id="cancel-repo-modal">
<div class="header">
{{ctx.Locale.Tr "migrate.cancel.title"}}
</div>
<form action="{{.Link}}/settings/migrate/cancel" method="post">
<div class="content">
{{ctx.Locale.Tr "migrate.cancel.confirmation"}}
</div>
{{template "base/modal_actions_confirm" .}}
</form>
</div>
<dialog id="cancel-repo-modal">
<article>
<header>{{ctx.Locale.Tr "migrate.cancel.title"}}</header>
<form action="{{.Link}}/settings/migrate/cancel" method="post">
<div class="content">
{{ctx.Locale.Tr "migrate.cancel.confirmation"}}
</div>
{{template "base/modal_actions_confirm" .}}
</form>
</article>
</dialog>
{{template "base/footer" .}}

View file

@ -34,19 +34,17 @@
</table>
{{template "base/paginate" .}}
{{range .LFSFiles}}
<div class="ui g-modal-confirm modal" id="delete-{{.Oid}}">
<div class="header">
{{ctx.Locale.Tr "repo.settings.lfs_delete" .Oid}}
</div>
<div class="content">
<p>
{{ctx.Locale.Tr "repo.settings.lfs_delete_warning"}}
</p>
<dialog id="delete-{{.Oid}}">
<article>
<header>{{ctx.Locale.Tr "repo.settings.lfs_delete" .Oid}}</header>
<div class="content">
<p>{{ctx.Locale.Tr "repo.settings.lfs_delete_warning"}}</p>
</div>
<form class="ui form" action="{{$.Link}}/delete/{{.Oid}}" method="post">
{{template "base/modal_actions_confirm"}}
</form>
</div>
</div>
</article>
</dialog>
{{end}}
</div>
{{template "repo/settings/layout_footer" .}}

View file

@ -559,46 +559,80 @@
{{if .Permission.IsOwner}}
{{if .Repository.IsMirror}}
<div class="ui small modal" id="convert-mirror-repo-modal">
<div class="header">
{{ctx.Locale.Tr "repo.settings.convert"}}
</div>
<div class="content">
<div class="ui warning message">
{{ctx.Locale.Tr "repo.settings.convert_notices_1"}}
<dialog id="convert-mirror-repo-modal">
<article>
<header>{{ctx.Locale.Tr "repo.settings.convert"}}</header>
<div class="content">
<div class="ui warning message">
{{ctx.Locale.Tr "repo.settings.convert_notices_1"}}
</div>
</div>
<form class="ui form" action="{{.Link}}" method="post">
<input type="hidden" name="action" value="convert">
<div class="field">
<label>
{{ctx.Locale.Tr "repo.settings.enter_repo_name"}}
<span class="text red">{{.Repository.FullName}}</span>
</label>
<div class="content">
<input type="hidden" name="action" value="convert">
<div class="field">
<label>
{{ctx.Locale.Tr "repo.settings.enter_repo_name"}}
<span class="text red">{{.Repository.FullName}}</span>
</label>
</div>
<div class="required field">
<label>{{ctx.Locale.Tr "repo.settings.confirmation_string"}}</label>
<input name="repo_name" required maxlength="100">
</div>
</div>
<div class="required field">
<label>{{ctx.Locale.Tr "repo.settings.confirmation_string"}}</label>
<input name="repo_name" required maxlength="100">
</div>
<div class="text right actions">
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button class="ui red button">{{ctx.Locale.Tr "repo.settings.convert_confirm"}}</button>
<div class="actions">
<button type="button" class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button type="submit" class="ui red button">{{ctx.Locale.Tr "repo.settings.convert_confirm"}}</button>
</div>
</form>
</div>
</div>
</article>
</dialog>
{{end}}
{{if and .Repository.IsFork .Repository.Owner.CanCreateRepo}}
<div class="ui small modal" id="convert-fork-repo-modal">
<div class="header">
{{ctx.Locale.Tr "repo.settings.convert_fork"}}
</div>
<div class="content">
<div class="ui warning message">
{{ctx.Locale.Tr "repo.settings.convert_fork_notices_1"}}
<dialog id="convert-fork-repo-modal">
<article>
<header>{{ctx.Locale.Tr "repo.settings.convert_fork"}}</header>
<div class="content">
<div class="ui warning message">
{{ctx.Locale.Tr "repo.settings.convert_fork_notices_1"}}
</div>
</div>
<form class="ui form" action="{{.Link}}" method="post">
<input type="hidden" name="action" value="convert_fork">
<div class="content">
<input type="hidden" name="action" value="convert_fork">
<div class="field">
<label>
{{ctx.Locale.Tr "repo.settings.enter_repo_name"}}
<span class="text red">{{.Repository.FullName}}</span>
</label>
</div>
<div class="required field">
<label>{{ctx.Locale.Tr "repo.settings.confirmation_string"}}</label>
<input name="repo_name" required>
</div>
</div>
<div class="actions">
<button type="button" class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button type="submit" class="ui red button">{{ctx.Locale.Tr "repo.settings.convert_fork_confirm"}}</button>
</div>
</form>
</article>
</dialog>
{{end}}
<dialog id="transfer-repo-modal">
<article>
<header>{{ctx.Locale.Tr "repo.settings.transfer.modal.title"}}</header>
<div class="content">
<div class="ui warning message">
{{ctx.Locale.Tr "repo.settings.transfer_notices_1"}} <br>
{{ctx.Locale.Tr "repo.settings.transfer_notices_2"}} <br>
{{ctx.Locale.Tr "repo.settings.transfer_notices_3"}}
</div>
</div>
<form class="ui form" action="{{.Link}}" method="post">
<div class="content">
<input type="hidden" name="action" value="transfer">
<div class="field">
<label>
{{ctx.Locale.Tr "repo.settings.enter_repo_name"}}
@ -609,127 +643,66 @@
<label>{{ctx.Locale.Tr "repo.settings.confirmation_string"}}</label>
<input name="repo_name" required>
</div>
<div class="text right actions">
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button class="ui red button">{{ctx.Locale.Tr "repo.settings.convert_fork_confirm"}}</button>
<div class="required field">
<label for="new_owner_name">{{ctx.Locale.Tr "repo.settings.transfer_owner"}}</label>
<input id="new_owner_name" name="new_owner_name" required>
</div>
</form>
</div>
</div>
{{end}}
<div class="ui small modal" id="transfer-repo-modal">
<div class="header">
{{ctx.Locale.Tr "repo.settings.transfer.modal.title"}}
</div>
<div class="content">
<div class="ui warning message">
{{ctx.Locale.Tr "repo.settings.transfer_notices_1"}} <br>
{{ctx.Locale.Tr "repo.settings.transfer_notices_2"}} <br>
{{ctx.Locale.Tr "repo.settings.transfer_notices_3"}}
</div>
<form class="ui form" action="{{.Link}}" method="post">
<input type="hidden" name="action" value="transfer">
<div class="field">
<label>
{{ctx.Locale.Tr "repo.settings.enter_repo_name"}}
<span class="text red">{{.Repository.FullName}}</span>
</label>
</div>
<div class="required field">
<label>{{ctx.Locale.Tr "repo.settings.confirmation_string"}}</label>
<input name="repo_name" required>
</div>
<div class="required field">
<label for="new_owner_name">{{ctx.Locale.Tr "repo.settings.transfer_owner"}}</label>
<input id="new_owner_name" name="new_owner_name" required>
</div>
<div class="text right actions">
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button class="ui red button">{{ctx.Locale.Tr "repo.settings.transfer_perform"}}</button>
<div class="actions">
<button type="button" class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button type="submit" class="ui red button">{{ctx.Locale.Tr "repo.settings.transfer_perform"}}</button>
</div>
</form>
</div>
</div>
</article>
</dialog>
<div class="ui small modal" id="delete-repo-modal">
<div class="header">
{{ctx.Locale.Tr "repo.settings.delete"}}
</div>
<div class="content">
<div class="ui warning message">
{{ctx.Locale.Tr "repo.settings.delete_notices_1"}}<br>
{{ctx.Locale.Tr "repo.settings.delete_notices_2" .Repository.FullName}}
{{if .Repository.NumForks}}<br>
{{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}
{{end}}
<dialog id="delete-repo-modal">
<article>
<header>{{ctx.Locale.Tr "repo.settings.delete"}}</header>
<div class="content">
<div class="ui warning message">
{{ctx.Locale.Tr "repo.settings.delete_notices_1"}}<br>
{{ctx.Locale.Tr "repo.settings.delete_notices_2" .Repository.FullName}}
{{if .Repository.NumForks}}<br>
{{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}
{{end}}
</div>
</div>
<form class="ui form" action="{{.Link}}" method="post">
<input type="hidden" name="action" value="delete">
<div class="field">
<label>
{{ctx.Locale.Tr "repo.settings.enter_repo_name"}}
<span class="text red">{{.Repository.FullName}}</span>
</label>
<div class="content">
<input type="hidden" name="action" value="delete">
<div class="field">
<label>
{{ctx.Locale.Tr "repo.settings.enter_repo_name"}}
<span class="text red">{{.Repository.FullName}}</span>
</label>
</div>
<div class="required field">
<label for="repo_name_to_delete">{{ctx.Locale.Tr "repo.settings.confirmation_string"}}</label>
<input id="repo_name_to_delete" name="repo_name" required>
</div>
</div>
<div class="required field">
<label for="repo_name_to_delete">{{ctx.Locale.Tr "repo.settings.confirmation_string"}}</label>
<input id="repo_name_to_delete" name="repo_name" required>
</div>
<div class="text right actions">
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button class="ui red button">{{ctx.Locale.Tr "repo.settings.confirm_delete"}}</button>
<div class="actions">
<button type="button" class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button type="submit" class="ui red button">{{ctx.Locale.Tr "repo.settings.confirm_delete"}}</button>
</div>
</form>
</div>
</div>
</article>
</dialog>
{{if .Repository.UnitEnabled $.Context $.UnitTypeWiki}}
<div class="ui small modal" id="delete-wiki-modal">
<div class="header">
{{ctx.Locale.Tr "repo.settings.wiki_delete"}}
</div>
<div class="content">
<div class="ui warning message">
{{ctx.Locale.Tr "repo.settings.delete_notices_1"}}<br>
{{ctx.Locale.Tr "repo.settings.wiki_delete_notices_1" .Repository.Name}}
</div>
<form class="ui form" action="{{.Link}}" method="post">
<input type="hidden" name="action" value="delete-wiki">
<div class="field">
<label>
{{ctx.Locale.Tr "repo.settings.enter_repo_name"}}
<span class="text red">{{.Repository.FullName}}</span>
</label>
</div>
<div class="required field">
<label>{{ctx.Locale.Tr "repo.settings.confirmation_string"}}</label>
<input name="repo_name" required>
</div>
<div class="text right actions">
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button class="ui red button">{{ctx.Locale.Tr "repo.settings.confirm_wiki_delete"}}</button>
</div>
</form>
</div>
</div>
{{if ne $.Repository.GetWikiBranchName .DefaultWikiBranchName}}
<div class="ui small modal" id="rename-wiki-branch-modal">
<div class="header">
{{ctx.Locale.Tr "repo.settings.wiki_rename_branch_main"}}
</div>
<dialog id="delete-wiki-modal">
<article>
<header>{{ctx.Locale.Tr "repo.settings.wiki_delete"}}</header>
<div class="content">
<div class="ui warning message">
<ul>
<li>{{ctx.Locale.Tr "repo.settings.wiki_rename_branch_main_notices_1"}}</li>
<li>{{ctx.Locale.Tr "repo.settings.wiki_rename_branch_main_notices_2" .Repository.Name}}</li>
</ul>
{{ctx.Locale.Tr "repo.settings.delete_notices_1"}}<br>
{{ctx.Locale.Tr "repo.settings.wiki_delete_notices_1" .Repository.Name}}
</div>
<form class="ui form" action="{{.Link}}" method="post">
<input type="hidden" name="action" value="rename-wiki-branch">
</div>
<form class="ui form" action="{{.Link}}" method="post">
<div class="content">
<input type="hidden" name="action" value="delete-wiki">
<div class="field">
<label>
{{ctx.Locale.Tr "repo.settings.enter_repo_name"}}
@ -740,40 +713,75 @@
<label>{{ctx.Locale.Tr "repo.settings.confirmation_string"}}</label>
<input name="repo_name" required>
</div>
<div class="text right actions">
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button class="ui red button">{{ctx.Locale.Tr "repo.settings.confirm_wiki_branch_rename"}}</button>
</div>
<div class="actions">
<button type="button" class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button type="submit" class="ui red button">{{ctx.Locale.Tr "repo.settings.confirm_wiki_delete"}}</button>
</div>
</form>
</article>
</dialog>
{{if ne $.Repository.GetWikiBranchName .DefaultWikiBranchName}}
<dialog id="rename-wiki-branch-modal">
<article>
<header>{{ctx.Locale.Tr "repo.settings.wiki_rename_branch_main"}}</header>
<div class="content">
<div class="ui warning message">
<ul>
<li>{{ctx.Locale.Tr "repo.settings.wiki_rename_branch_main_notices_1"}}</li>
<li>{{ctx.Locale.Tr "repo.settings.wiki_rename_branch_main_notices_2" .Repository.Name}}</li>
</ul>
</div>
</div>
<form class="ui form" action="{{.Link}}" method="post">
<div class="content">
<input type="hidden" name="action" value="rename-wiki-branch">
<div class="field">
<label>
{{ctx.Locale.Tr "repo.settings.enter_repo_name"}}
<span class="text red">{{.Repository.FullName}}</span>
</label>
</div>
<div class="required field">
<label>{{ctx.Locale.Tr "repo.settings.confirmation_string"}}</label>
<input name="repo_name" required>
</div>
</div>
<div class="actions">
<button type="button" class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button type="submit" class="ui red button">{{ctx.Locale.Tr "repo.settings.confirm_wiki_branch_rename"}}</button>
</div>
</form>
</div>
</div>
</article>
</dialog>
{{end}}
{{end}}
{{if not .Repository.IsMirror}}
<div class="ui g-modal-confirm modal" id="archive-repo-modal">
<div class="header">
{{if .Repository.IsArchived}}
{{ctx.Locale.Tr "repo.settings.unarchive.header"}}
{{else}}
{{ctx.Locale.Tr "repo.settings.archive.header"}}
{{end}}
</div>
<div class="content">
<div class="ui warning message">
<dialog id="archive-repo-modal">
<article>
<header>
{{if .Repository.IsArchived}}
{{ctx.Locale.Tr "repo.settings.unarchive.text"}}
{{ctx.Locale.Tr "repo.settings.unarchive.header"}}
{{else}}
{{ctx.Locale.Tr "repo.settings.archive.text"}}
{{ctx.Locale.Tr "repo.settings.archive.header"}}
{{end}}
</header>
<div class="content">
<div class="ui warning message">
{{if .Repository.IsArchived}}
{{ctx.Locale.Tr "repo.settings.unarchive.text"}}
{{else}}
{{ctx.Locale.Tr "repo.settings.archive.text"}}
{{end}}
</div>
</div>
<form action="{{.Link}}" method="post">
<input type="hidden" name="action" value="{{if .Repository.IsArchived}}unarchive{{else}}archive{{end}}">
<input type="hidden" name="repo_id" value="{{.Repository.ID}}">
<div class="text right actions">
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button class="ui red button">
<button type="button" class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
<button type="submit" class="ui red button">
{{if .Repository.IsArchived}}
{{ctx.Locale.Tr "repo.settings.unarchive.button"}}
{{else}}
@ -781,9 +789,9 @@
{{end}}
</button>
</div>
</form>
</div>
</div>
</form>
</article>
</dialog>
{{end}}
{{end}}

View file

@ -1,29 +1,29 @@
<div class="ui small modal" id="push-mirror-edit-modal">
<div class="header">
{{ctx.Locale.Tr "repo.settings.mirror_settings.push_mirror.edit_sync_time"}}
</div>
<div class="content">
<dialog id="push-mirror-edit-modal">
<article>
<header>{{ctx.Locale.Tr "repo.settings.mirror_settings.push_mirror.edit_sync_time"}}</header>
<form class="ui form ignore-dirty" method="post">
<input type="hidden" name="action" value="push-mirror-update">
<input type="hidden" name="push_mirror_id" id="push-mirror-edit-id">
<div class="field">
<label for="name">{{ctx.Locale.Tr "repo.settings.mirror_settings.mirrored_repository"}}</label>
<div class="ui small input">
<input id="push-mirror-edit-address" readonly>
<div class="content">
<input type="hidden" name="action" value="push-mirror-update">
<input type="hidden" name="push_mirror_id" id="push-mirror-edit-id">
<div class="field">
<label for="name">{{ctx.Locale.Tr "repo.settings.mirror_settings.mirrored_repository"}}</label>
<div class="ui small input">
<input id="push-mirror-edit-address" readonly>
</div>
</div>
</div>
<div class="inline field">
<label for="push-mirror-edit-interval">{{ctx.Locale.Tr "repo.mirror_interval" .MinimumMirrorInterval}}</label>
<div class="ui small input">
<input id="push-mirror-edit-interval" name="push_mirror_interval" autofocus>
<div class="inline field">
<label for="push-mirror-edit-interval">{{ctx.Locale.Tr "repo.mirror_interval" .MinimumMirrorInterval}}</label>
<div class="ui small input">
<input id="push-mirror-edit-interval" name="push_mirror_interval" autofocus>
</div>
</div>
</div>
<div class="field">
<label for="push-mirror-edit-branch-filter">{{ctx.Locale.Tr "repo.settings.push_mirror.branch_filter.label"}}</label>
<div class="ui small input">
<input id="push-mirror-edit-branch-filter" name="push_mirror_branch_filter" maxlength="2048">
<div class="field">
<label for="push-mirror-edit-branch-filter">{{ctx.Locale.Tr "repo.settings.push_mirror.branch_filter.label"}}</label>
<div class="ui small input">
<input id="push-mirror-edit-branch-filter" name="push_mirror_branch_filter" maxlength="2048">
</div>
<p class="help">{{ctx.Locale.Tr "repo.settings.push_mirror.branch_filter.description" "https://forgejo.org/docs/latest/user/repo-mirror/#branch-filter" "Forgejo"}}</p>
</div>
<p class="help">{{ctx.Locale.Tr "repo.settings.push_mirror.branch_filter.description" "https://forgejo.org/docs/latest/user/repo-mirror/#branch-filter" "Forgejo"}}</p>
</div>
<div class="actions">
<button class="ui small basic cancel button">
@ -36,5 +36,5 @@
</button>
</div>
</form>
</div>
</div>
</article>
</dialog>

View file

@ -64,55 +64,57 @@ Template Attributes:
{{ctx.Locale.Tr "loading"}}
</div>
<div class="ui small modal tw-w-fit" data-modal-name="new-markdown-table">
<div class="header">{{ctx.Locale.Tr "editor.table_modal.header"}}</div>
<dialog data-modal-name="new-markdown-table">
<article>
<header>{{ctx.Locale.Tr "editor.table_modal.header"}}</header>
<div class="ui form content" data-selector-name="form">
<input type="hidden" name="table-header" value="{{ctx.Locale.Tr "editor.table_modal.placeholder.header"}}" disabled>
<input type="hidden" name="table-content" value="{{ctx.Locale.Tr "editor.table_modal.placeholder.content"}}" disabled>
<table>
<tbody>
<tr>
<td><label>{{ctx.Locale.Tr "editor.table_modal.label.rows"}}</label></td>
<td><input type="number" name="table-rows" class="js-enable" min="1" value="2" disabled required></td>
</tr>
<tr>
<td><label>{{ctx.Locale.Tr "editor.table_modal.label.columns"}}</label></td>
<td><input type="number" name="table-columns" class="js-enable" min="1" value="2" disabled required></td>
</tr>
</tbody>
</table>
</div>
<div class="ui form content" data-selector-name="form">
<input type="hidden" name="table-header" value="{{ctx.Locale.Tr "editor.table_modal.placeholder.header"}}" disabled>
<input type="hidden" name="table-content" value="{{ctx.Locale.Tr "editor.table_modal.placeholder.content"}}" disabled>
<table>
<tbody>
<tr>
<td><label>{{ctx.Locale.Tr "editor.table_modal.label.rows"}}</label></td>
<td><input type="number" name="table-rows" class="js-enable" min="1" value="2" disabled required></td>
</tr>
<tr>
<td><label>{{ctx.Locale.Tr "editor.table_modal.label.columns"}}</label></td>
<td><input type="number" name="table-columns" class="js-enable" min="1" value="2" disabled required></td>
</tr>
</tbody>
</table>
</div>
<div class="text right actions">
<button class="ui cancel button" data-selector-name="cancel-button">{{ctx.Locale.Tr "cancel"}}</button>
<button class="ui primary button" data-selector-name="ok-button">{{ctx.Locale.Tr "ok"}}</button>
</div>
</div>
<div class="actions">
<button class="ui cancel button" data-selector-name="cancel-button">{{ctx.Locale.Tr "cancel"}}</button>
<button class="ui primary button" data-selector-name="ok-button">{{ctx.Locale.Tr "ok"}}</button>
</div>
</article>
</dialog>
<div class="ui small modal" data-modal-name="new-markdown-link">
<div class="header">{{ctx.Locale.Tr "editor.link_modal.header"}}</div>
<fieldset class="content">
<dialog data-modal-name="new-markdown-link">
<article>
<header>{{ctx.Locale.Tr "editor.link_modal.header"}}</header>
<div class="ui form" data-selector-name="form">
<label>
{{ctx.Locale.Tr "editor.link_modal.url"}}
<input name="link-url" class="js-enable" disabled required dir="auto" autocomplete="off">
</label>
<label>
{{ctx.Locale.Tr "editor.link_modal.description"}}
<input name="link-description" class="js-enable" disabled required dir="auto" autocomplete="off">
</label>
<div class="content">
<label>
{{ctx.Locale.Tr "editor.link_modal.url"}}
<input name="link-url" class="js-enable" disabled required dir="auto" autocomplete="off">
</label>
<label>
{{ctx.Locale.Tr "editor.link_modal.description"}}
<input name="link-description" class="js-enable" disabled required dir="auto" autocomplete="off">
</label>
<div class="help">
{{ctx.Locale.Tr "editor.link_modal.paste_reminder"}}
<div class="help">
{{ctx.Locale.Tr "editor.link_modal.paste_reminder"}}
</div>
</div>
<div class="text right actions">
<button class="ui cancel button" data-selector-name="cancel-button">{{ctx.Locale.Tr "cancel"}}</button>
<button class="ui primary button" data-selector-name="ok-button">{{ctx.Locale.Tr "ok"}}</button>
</div>
</div>
</fieldset>
</div>
</article>
</dialog>
</div>

View file

@ -57,63 +57,63 @@
</div>
{{/* Add secret dialog */}}
<div class="ui small modal" id="add-secret-modal">
<div class="header">
<span id="actions-modal-header"></span>
</div>
<form class="ui form form-fetch-action" method="post">
<fieldset class="content">
<div class="field">
{{ctx.Locale.Tr "secrets.description"}}
</div>
<div class="field">
<label class="required" for="secret-name">{{ctx.Locale.Tr "name"}}</label>
<input autofocus required
id="secret-name"
name="name"
value="{{.name}}"
pattern="^(?!FORGEJO_|GITEA_|GITHUB_)[a-zA-Z_][a-zA-Z0-9_]*$"
>
<p id="name-description" class="help">{{ctx.Locale.Tr "actions.secrets.creation.name_description"}}</p>
</div>
<div class="field">
<label class="required" for="secret-data">{{ctx.Locale.Tr "value"}}</label>
<textarea required
id="secret-data"
name="data"
></textarea>
<p id="secret-data-description" class="help">{{ctx.Locale.Tr "actions.secrets.creation.value_description"}}</p>
</div>
</fieldset>
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</form>
</div>
<dialog id="add-secret-modal">
<article>
<header><span id="actions-modal-header"></span></header>
<form class="ui form form-fetch-action" method="post">
<fieldset class="content">
<div class="field">
{{ctx.Locale.Tr "secrets.description"}}
</div>
<div class="field">
<label class="required" for="secret-name">{{ctx.Locale.Tr "name"}}</label>
<input autofocus required
id="secret-name"
name="name"
value="{{.name}}"
pattern="^(?!FORGEJO_|GITEA_|GITHUB_)[a-zA-Z_][a-zA-Z0-9_]*$"
>
<p id="name-description" class="help">{{ctx.Locale.Tr "actions.secrets.creation.name_description"}}</p>
</div>
<div class="field">
<label class="required" for="secret-data">{{ctx.Locale.Tr "value"}}</label>
<textarea required
id="secret-data"
name="data"
></textarea>
<p id="secret-data-description" class="help">{{ctx.Locale.Tr "actions.secrets.creation.value_description"}}</p>
</div>
</fieldset>
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</form>
</article>
</dialog>
{{/* Edit secret dialog */}}
<div class="ui small modal" id="edit-secret-modal">
<div class="header">
<span id="actions-modal-header"></span>
</div>
<form class="ui form form-fetch-action" method="post">
<fieldset class="content">
<div class="field">
{{ctx.Locale.Tr "secrets.description"}}
</div>
<div class="field">
<label class="required" for="dialog-secret-name">{{ctx.Locale.Tr "name"}}</label>
<input autofocus required
id="dialog-secret-name"
name="name"
value="{{.name}}"
pattern="^(?!FORGEJO_|GITEA_|GITHUB_)[a-zA-Z_][a-zA-Z0-9_]*$"
>
<p id="name-description" class="help">{{ctx.Locale.Tr "actions.secrets.mutation.name_description"}}</p>
</div>
<div class="field">
<label for="dialog-secret-data">{{ctx.Locale.Tr "value"}}</label>
<textarea id="dialog-secret-data" name="data"></textarea>
<p id="secret-data-description" class="help">{{ctx.Locale.Tr "actions.secrets.mutation.value_description"}}</p>
</div>
</fieldset>
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</form>
</div>
<dialog id="edit-secret-modal">
<article>
<header><span id="actions-modal-header"></span></header>
<form class="ui form form-fetch-action" method="post">
<fieldset class="content">
<div class="field">
{{ctx.Locale.Tr "secrets.description"}}
</div>
<div class="field">
<label class="required" for="dialog-secret-name">{{ctx.Locale.Tr "name"}}</label>
<input autofocus required
id="dialog-secret-name"
name="name"
value="{{.name}}"
pattern="^(?!FORGEJO_|GITEA_|GITHUB_)[a-zA-Z_][a-zA-Z0-9_]*$"
>
<p id="name-description" class="help">{{ctx.Locale.Tr "actions.secrets.mutation.name_description"}}</p>
</div>
<div class="field">
<label for="dialog-secret-data">{{ctx.Locale.Tr "value"}}</label>
<textarea id="dialog-secret-data" name="data"></textarea>
<p id="secret-data-description" class="help">{{ctx.Locale.Tr "actions.secrets.mutation.value_description"}}</p>
</div>
</fieldset>
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</form>
</article>
</dialog>

View file

@ -59,33 +59,34 @@
</div>
{{/** Edit variable dialog */}}
<div class="ui small modal" id="edit-variable-modal">
<div class="header"></div>
<form class="ui form form-fetch-action" method="post">
<fieldset class="content">
<div class="field">
{{ctx.Locale.Tr "actions.variables.description"}}
</div>
<div class="field">
<label for="dialog-variable-name">{{ctx.Locale.Tr "name"}}</label>
<input autofocus required
name="name"
id="dialog-variable-name"
value="{{.name}}"
pattern="^(?!CI$)(?!FORGEJO_|GITEA_|GITHUB_)[a-zA-Z_][a-zA-Z0-9_]*$"
>
<p id="name-description" class="help">{{ctx.Locale.Tr "actions.variables.mutation.name_description"}}</p>
</div>
<div class="field">
<label for="dialog-variable-data">{{ctx.Locale.Tr "value"}}</label>
<textarea required
name="data"
id="dialog-variable-data"
></textarea>
<p id="data-description" class="help">{{ctx.Locale.Tr "actions.variables.mutation.value_description"}}</p>
</div>
</fieldset>
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</form>
</div>
<dialog id="edit-variable-modal">
<article>
<header></header>
<form class="ui form form-fetch-action" method="post">
<fieldset class="content">
<div class="field">
{{ctx.Locale.Tr "actions.variables.description"}}
</div>
<div class="field">
<label for="dialog-variable-name">{{ctx.Locale.Tr "name"}}</label>
<input autofocus required
name="name"
id="dialog-variable-name"
value="{{.name}}"
pattern="^(?!CI$)(?!FORGEJO_|GITEA_|GITHUB_)[a-zA-Z_][a-zA-Z0-9_]*$"
>
<p id="name-description" class="help">{{ctx.Locale.Tr "actions.variables.mutation.name_description"}}</p>
</div>
<div class="field">
<label for="dialog-variable-data">{{ctx.Locale.Tr "value"}}</label>
<textarea required
name="data"
id="dialog-variable-data"
></textarea>
<p id="data-description" class="help">{{ctx.Locale.Tr "actions.variables.mutation.value_description"}}</p>
</div>
</fieldset>
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</form>
</article>
</dialog>

View file

@ -56,3 +56,29 @@ test('Admin email list', async ({page}) => {
await expect(page.locator('[data-uid="9"] svg')).toHaveClass(/octicon-check/);
}
});
test('Admin: delete a user', async ({page}) => {
const response = await page.goto('/admin/users/1/edit');
expect(response?.status()).toBe(200);
const modal = page.locator('#delete-user-modal');
const okButton = page.locator('#delete-user-modal .primary.button');
// Check that modal appears after clicking
await expect(modal).toBeHidden();
await expect(okButton).toBeHidden();
await page.locator('[data-modal="#delete-user-modal"]').click();
await expect(modal).toBeVisible();
await expect(okButton).toBeVisible();
// Agree with deletion
await okButton.click();
// Should have been redirected to /admin/users/1
await expect(page).toHaveURL(/\/admin\/users\/1$/);
// This test doesn't actually delete a user as it attempts to delete the doer and
// receives an error. This is enough to test that the request reaches the correct
// endpoint without causing e2e retry headache
await expect(page.locator('#flash-message')).toBeVisible();
});

View file

@ -16,8 +16,12 @@ test('copy src file path to clipboard', async ({page}) => {
expect(response?.status()).toBe(200);
await page.click('[data-clipboard-text]');
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardText).toContain('README.md');
await expect(async () => {
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardText).toContain('README.md');
}).toPass();
await expect(page.getByText('Copied')).toBeVisible();
await screenshot(page, page.getByText('Copied'), 50);
});
@ -27,8 +31,12 @@ test('copy diff file path to clipboard', async ({page}) => {
expect(response?.status()).toBe(200);
await page.click('[data-clipboard-text]');
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardText).toContain('README.md');
await expect(async () => {
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardText).toContain('README.md');
}).toPass();
await expect(page.getByText('Copied')).toBeVisible();
await screenshot(page, page.getByText('Copied'), 50);
});

View file

@ -48,8 +48,10 @@ async function assertCopy(page: Page, startWith: string) {
const copyLink = preview.locator('.octicon-copy').locator('..');
await copyLink.click();
const clipboardContent = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardContent).toContain(startWith);
await expect(async () => {
const clipboardContent = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardContent).toContain(startWith);
}).toPass();
}
test('Paste image in new comment', async ({page}) => {

View file

@ -379,6 +379,9 @@ test('Issue: Reference', async ({page}) => {
);
await page.getByRole('button', {name: 'Copy'}).click();
const reference = await page.evaluate(() => navigator.clipboard.readText());
expect(reference).toBe('user2/repo1#1');
await expect(async () => {
const reference = await page.evaluate(() => navigator.clipboard.readText());
expect(reference).toBe('user2/repo1#1');
}).toPass();
});

View file

@ -367,7 +367,7 @@ test('Markdown insert table', async ({page}) => {
const newTableButton = area.locator('button[data-md-action="new-table"]');
await newTableButton.click();
const newTableModal = page.locator('[data-modal-name="new-markdown-table"].active');
const newTableModal = page.locator('[data-modal-name="new-markdown-table"][open]');
await expect(newTableModal).toBeVisible();
await screenshot(page);
@ -417,9 +417,9 @@ test('Markdown insert link', async ({page}) => {
const newLinkButton = area.locator('button[data-md-action="new-link"]');
await newLinkButton.click();
const newLinkModal = page.locator('[data-modal-name="new-markdown-link"].active');
const newLinkModal = page.locator('[data-modal-name="new-markdown-link"][open]');
await expect(newLinkModal).toBeVisible();
await accessibilityCheck({page}, ['[data-modal-name="new-markdown-link"].active'], [], []);
await accessibilityCheck({page}, ['[data-modal-name="new-markdown-link"][open]'], [], []);
await screenshot(page);
const urlInput = newLinkModal.locator('input[name="link-url"]');
@ -455,9 +455,9 @@ test('Markdown insert link', async ({page}) => {
await textarea.press('ControlOrMeta+KeyK');
const newLinkModal = page.locator('[data-modal-name="new-markdown-link"].active');
const newLinkModal = page.locator('[data-modal-name="new-markdown-link"][open]');
await expect(newLinkModal).toBeVisible();
await accessibilityCheck({page}, ['[data-modal-name="new-markdown-link"].active'], [], []);
await accessibilityCheck({page}, ['[data-modal-name="new-markdown-link"][open]'], [], []);
await screenshot(page);
const urlInput = newLinkModal.locator('input[name="link-url"]');
@ -579,7 +579,7 @@ test('Multiple combo markdown: insert table', async ({page}) => {
const newTableButtonOne = page.locator('[for="_combo_markdown_editor_0"] button[data-md-action="new-table"]');
await newTableButtonOne.click();
const newTableModalOne = page.locator('div[data-markdown-table-modal-id="0"]');
const newTableModalOne = page.locator('dialog[data-markdown-table-modal-id="0"]');
await expect(newTableModalOne).toBeVisible();
await newTableModalOne.locator('input[name="table-rows"]').fill('3');
@ -601,7 +601,7 @@ test('Multiple combo markdown: insert table', async ({page}) => {
const newTableButtonTwo = page.locator('[for="_combo_markdown_editor_1"] button[data-md-action="new-table"]');
await newTableButtonTwo.click();
const newTableModalTwo = page.locator('div[data-markdown-table-modal-id="1"]');
const newTableModalTwo = page.locator('dialog[data-markdown-table-modal-id="1"]');
await expect(newTableModalTwo).toBeVisible();
await newTableModalTwo.locator('input[name="table-rows"]').fill('2');

View file

@ -103,27 +103,3 @@ test('Dialog modal: width', async ({page, isMobile}) => {
expect(width).toBe(800);
}
});
test('Dialog modal: short viewport', async ({page, isMobile}) => {
test.skip(isMobile);
// Small height for viewport.
await page.setViewportSize({
width: 1000,
height: 200,
});
await page.goto('/user2/repo1/settings');
// Open modal with long content
const deleteModal = page.locator('#delete-repo-modal');
await expect(deleteModal).toBeHidden();
await page.getByRole('button', {name: 'Delete this repository'}).click();
await expect(deleteModal).toBeVisible();
// Scroll to the bottom.
const scrollY = await page.evaluate(() => document.querySelector('.ui.dimmer').scrollHeight);
await page.mouse.wheel(0, scrollY);
const scrollTop = await page.evaluate(() => document.querySelector('.ui.dimmer').scrollTop);
expect(scrollTop).toBeGreaterThan(0);
});

View file

@ -148,11 +148,13 @@ test('Copy line permalink', async ({page}) => {
const response = await page.goto('/user2/repo1/src/branch/master/README.md?display=source#L1');
expect(response?.status()).toBe(200);
await page.locator('.code-line-button').click();
// eslint-disable-next-line playwright/no-force-option
await page.locator('.tippy-box .copy-line-permalink').click({force: true});
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardText).toContain('README.md?display=source#L1');
await expect(async () => {
await page.locator('.code-line-button').click();
// eslint-disable-next-line playwright/no-force-option
await page.locator('.tippy-box .copy-line-permalink').click({force: true});
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardText).toContain('README.md?display=source#L1');
}).toPass();
});
test('Line menu styles', async ({page}) => {

View file

@ -121,9 +121,12 @@ test.describe('Runners of user2', () => {
await expect(page).toHaveTitle(/^Set up runner runner-991301 .*/);
await expect(page.getByRole('heading', {name: 'Set up runner runner-991301'})).toBeVisible();
await page.getByRole('button', {name: 'Copy runner UUID'}).click();
const runnerUUID = await page.evaluate(() => navigator.clipboard.readText());
expect(runnerUUID).toMatch(uuidPattern);
let runnerUUID;
await expect(async () => {
await page.getByRole('button', {name: 'Copy runner UUID'}).click();
runnerUUID = await page.evaluate(() => navigator.clipboard.readText());
expect(runnerUUID).toMatch(uuidPattern);
}).toPass();
let runnerToken;
await expect(async () => {
@ -230,14 +233,20 @@ test.describe('Runners of user2', () => {
await expect(page).toHaveTitle(/^Set up runner runner-2 .*/);
await expect(page.getByRole('heading', {name: 'Set up runner runner-2'})).toBeVisible();
await page.getByRole('button', {name: 'Copy runner UUID'}).click();
const runnerUUID = await page.evaluate(() => navigator.clipboard.readText());
expect(runnerUUID).toEqual('3a20ad8d-d5d6-4b7b-ba55-841ac8264c17');
let runnerUUID;
await expect(async () => {
await page.getByRole('button', {name: 'Copy runner UUID'}).click();
runnerUUID = await page.evaluate(() => navigator.clipboard.readText());
expect(runnerUUID).toEqual('3a20ad8d-d5d6-4b7b-ba55-841ac8264c17');
}).toPass();
await page.getByRole('button', {name: 'Copy runner token'}).click();
const runnerToken = await page.evaluate(() => navigator.clipboard.readText());
expect(runnerToken).not.toEqual('9730f9d2c6c731f07582788d1a1fe72a6b999a17');
expect(runnerToken).toMatch(tokenPattern);
let runnerToken;
await expect(async () => {
await page.getByRole('button', {name: 'Copy runner token'}).click();
runnerToken = await page.evaluate(() => navigator.clipboard.readText());
expect(runnerToken).not.toEqual('9730f9d2c6c731f07582788d1a1fe72a6b999a17');
expect(runnerToken).toMatch(tokenPattern);
}).toPass();
await expect(page.getByRole('term')).toHaveText(['UUID', 'Token']);
await expect(page.getByRole('definition')).toContainText([runnerUUID, runnerToken]);
@ -427,13 +436,19 @@ test.describe('Global runners', () => {
await expect(page).toHaveTitle(/^Set up runner runner-473465 .*/);
await expect(page.getByRole('heading', {name: 'Set up runner runner-473465'})).toBeVisible();
await page.getByRole('button', {name: 'Copy runner UUID'}).click();
const runnerUUID = await page.evaluate(() => navigator.clipboard.readText());
expect(runnerUUID).toMatch(uuidPattern);
let runnerUUID;
await expect(async () => {
await page.getByRole('button', {name: 'Copy runner UUID'}).click();
runnerUUID = await page.evaluate(() => navigator.clipboard.readText());
expect(runnerUUID).toMatch(uuidPattern);
}).toPass();
await page.getByRole('button', {name: 'Copy runner token'}).click();
const runnerToken = await page.evaluate(() => navigator.clipboard.readText());
expect(runnerToken).toMatch(tokenPattern);
let runnerToken;
await expect(async () => {
await page.getByRole('button', {name: 'Copy runner token'}).click();
runnerToken = await page.evaluate(() => navigator.clipboard.readText());
expect(runnerToken).toMatch(tokenPattern);
}).toPass();
await expect(page.getByRole('term')).toHaveText(['UUID', 'Token']);
await expect(page.getByRole('definition')).toContainText([runnerUUID, runnerToken]);

View file

@ -63,7 +63,7 @@ func testRepoArchiveElements(t *testing.T, tr translation.Locale, doc *HTMLDoc,
// Test modal
modal := doc.Find("#archive-repo-modal")
testRepoArchiveElement(t, tr, modal, ".header", opType+".header")
testRepoArchiveElement(t, tr, modal, "header", opType+".header")
testRepoArchiveElement(t, tr, modal, ".message", opType+".text")
testRepoArchiveElement(t, tr, modal, ".button.red", opType+".button")
}

View file

@ -13,6 +13,7 @@ import {showErrorToast} from '../modules/toast.js';
import {request, POST, GET} from '../modules/fetch.js';
import '../htmx.js';
import {initTab} from '../modules/tab.ts';
import {initGlobalShowModal} from './show-modal.ts';
const {appUrl, appSubUrl, i18n} = window.config;
@ -439,53 +440,6 @@ export function initGlobalLinkActions() {
});
}
export function initGlobalShowModal() {
// A ".show-modal" button will show a modal dialog defined by its "data-modal" attribute.
// Each "data-modal-{target}" attribute will be filled to target element's value or text-content.
// * First, try to query '#target'
// * Then, try to query '.target'
// * Then, try to query 'target' as HTML tag
// If there is a ".{attr}" part like "data-modal-form.action", then the form's "action" attribute will be set.
$('.show-modal').on('click', function (e) {
e.preventDefault();
const modalSelector = this.getAttribute('data-modal');
const $modal = $(modalSelector);
if (!$modal.length) {
throw new Error('no modal for this action');
}
const modalAttrPrefix = 'data-modal-';
for (const attrib of this.attributes) {
if (!attrib.name.startsWith(modalAttrPrefix)) {
continue;
}
const attrTargetCombo = attrib.name.substring(modalAttrPrefix.length);
const [attrTargetName, attrTargetAttr] = attrTargetCombo.split('.');
// try to find target by: "#target" -> ".target" -> "target tag"
let $attrTarget = $modal.find(`#${attrTargetName}`);
if (!$attrTarget.length) $attrTarget = $modal.find(`.${attrTargetName}`);
if (!$attrTarget.length) $attrTarget = $modal.find(`${attrTargetName}`);
if (!$attrTarget.length) continue; // TODO: show errors in dev mode to remind developers that there is a bug
if (attrTargetAttr) {
$attrTarget[0][attrTargetAttr] = attrib.value;
} else if ($attrTarget[0].matches('input, textarea')) {
$attrTarget.val(attrib.value); // FIXME: add more supports like checkbox
} else {
$attrTarget.text(attrib.value); // FIXME: it should be more strict here, only handle div/span/p
}
}
$modal.modal('setting', {
onApprove: () => {
// "form-fetch-action" can handle network errors gracefully,
// so keep the modal dialog to make users can re-submit the form if anything wrong happens.
if ($modal.find('.form-fetch-action').length) return false;
},
}).modal('show');
});
}
export function initGlobalButtons() {
// There are many "cancel button" elements in modal dialogs, Fomantic UI expects they are button-like elements but never submit a form.
// However, Gitea misuses the modal dialog and put the cancel buttons inside forms, so we must prevent the form submission.

View file

@ -96,8 +96,8 @@ class ComboMarkdownEditor {
this.textareaMarkdownToolbar.querySelector('button[data-md-action="unindent"]')?.addEventListener('click', () => {
this.indentSelection(true, false);
});
this.textareaMarkdownToolbar.querySelector('button[data-md-action="new-table"]')?.setAttribute('data-modal', `div[data-markdown-table-modal-id="${this.elementIdSuffix}"]`);
this.textareaMarkdownToolbar.querySelector('button[data-md-action="new-link"]')?.setAttribute('data-modal', `div[data-markdown-link-modal-id="${this.elementIdSuffix}"]`);
this.textareaMarkdownToolbar.querySelector('button[data-md-action="new-table"]')?.setAttribute('data-modal', `dialog[data-markdown-table-modal-id="${this.elementIdSuffix}"]`);
this.textareaMarkdownToolbar.querySelector('button[data-md-action="new-link"]')?.setAttribute('data-modal', `dialog[data-markdown-link-modal-id="${this.elementIdSuffix}"]`);
// Find all data-md-ctrl-shortcut elements in the markdown toolbar.
const shortcutKeys = new Map();
@ -263,7 +263,7 @@ class ComboMarkdownEditor {
addNewTable(event) {
const elementId = event.target.getAttribute('data-element-id');
const newTableModal = document.querySelector(`div[data-markdown-table-modal-id="${elementId}"]`);
const newTableModal = document.querySelector(`dialog[data-markdown-table-modal-id="${elementId}"]`);
const form = newTableModal.querySelector('div[data-selector-name="form"]');
// Validate input fields
@ -295,8 +295,9 @@ class ComboMarkdownEditor {
}
setupTableInserter() {
const newTableModal = this.container.querySelector('div[data-modal-name="new-markdown-table"]');
const newTableModal = this.container.querySelector('dialog[data-modal-name="new-markdown-table"]');
newTableModal.setAttribute('data-markdown-table-modal-id', this.elementIdSuffix);
document.body.append(newTableModal); // Contains form elements, avoid conflict with form of comment editor.
const button = newTableModal.querySelector('button[data-selector-name="ok-button"]');
button.setAttribute('data-element-id', this.elementIdSuffix);
@ -305,7 +306,7 @@ class ComboMarkdownEditor {
addNewLink(event) {
const elementId = event.target.getAttribute('data-element-id');
const newLinkModal = document.querySelector(`div[data-markdown-link-modal-id="${elementId}"]`);
const newLinkModal = document.querySelector(`dialog[data-markdown-link-modal-id="${elementId}"]`);
const form = newLinkModal.querySelector('div[data-selector-name="form"]');
// Validate input fields
@ -330,25 +331,22 @@ class ComboMarkdownEditor {
}
setupLinkInserter() {
const newLinkModal = this.container.querySelector('div[data-modal-name="new-markdown-link"]');
const newLinkModal = this.container.querySelector('dialog[data-modal-name="new-markdown-link"]');
newLinkModal.setAttribute('data-markdown-link-modal-id', this.elementIdSuffix);
const textarea = document.getElementById(`_combo_markdown_editor_${this.elementIdSuffix}`);
document.body.append(newLinkModal); // Contains form elements, avoid conflict with form of comment editor.
$(newLinkModal).modal({
// Pre-fill the description field from the selection to create behavior similar
// to pasting an URL over selected text.
onShow: () => {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
newLinkModal.$modal = {onShow: () => {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
if (start !== end) {
const selection = textarea.value.slice(start ?? undefined, end ?? undefined);
newLinkModal.querySelector('input[name="link-description"]').value = selection;
} else {
newLinkModal.querySelector('input[name="link-description"]').value = '';
}
},
});
if (start !== end) {
const selection = textarea.value.slice(start ?? undefined, end ?? undefined);
newLinkModal.querySelector('input[name="link-description"]').value = selection;
} else {
newLinkModal.querySelector('input[name="link-description"]').value = '';
}
}};
const button = newLinkModal.querySelector('button[data-selector-name="ok-button"]');
button.setAttribute('data-element-id', this.elementIdSuffix);

View file

@ -27,7 +27,7 @@ import {attachRefIssueContextPopup} from './contextpopup.js';
import {POST} from '../modules/fetch.js';
import {MarkdownQuote} from '@github/quote-selection';
import {toAbsoluteUrl} from '../utils.js';
import {initDropzone, initGlobalShowModal, initDisabledInputs} from './common-global.js';
import {initDropzone, initDisabledInputs} from './common-global.js';
export function initRepoCommentForm() {
const $commentForm = $('.comment.form');
@ -386,8 +386,6 @@ async function onEditContent(event) {
tabEditor?.click();
}
initGlobalShowModal();
// Show write/preview tab and copy raw content as needed
showElem(editContentZone);
hideElem(renderContent);

View file

@ -0,0 +1,54 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
import {showModal} from '../modules/modal.ts';
// Initialize all elements that have the `show-modal` class. The modal ID that
// is specified in the `data-modal` attribute will be shown. The shown modal
// can be modified by adding more attributes:
// * `data-modal-$TARGET="$VALUE"`, If $TARGET contains a dot then its split
// as $TARGET and $ATTR. $TARGET will first be queried as an identifier, then as
// a classname and then as an element tag name in the modal element. If $ATTR
// exists then the target element will have attribute $ATTR set to value $VALUE,
// otherwise if the element is of type input or textarea then the value is set
// to $VALUE otherwise the textContent of that element is set to $VALUE.
export function initGlobalShowModal() {
document.addEventListener('click', (e) => {
if (!(e.target instanceof Element)) {
return;
}
const target = e.target.closest('.show-modal');
if (!target) {
return;
}
e.preventDefault();
const modal = document.querySelector<HTMLDialogElement>(target.getAttribute('data-modal'));
if (!modal) {
throw new Error('No modal found for this action');
}
const modalAttrPrefix = 'data-modal-';
for (const attrib of (target as HTMLElement).attributes) {
if (!attrib.name.startsWith(modalAttrPrefix)) {
continue;
}
const attrTargetCombo = attrib.name.substring(modalAttrPrefix.length);
const [attrTargetName, attrTargetAttr] = attrTargetCombo.split('.');
// try to find target by: "#target" -> ".target" -> "target tag"
const attrTarget = modal.querySelector(`#${attrTargetName}, .${attrTargetName}, ${attrTargetName}`);
if (attrTargetAttr) {
attrTarget.setAttribute(attrTargetAttr, attrib.value);
} else if (attrTarget instanceof HTMLInputElement || attrTarget instanceof HTMLTextAreaElement) {
attrTarget.value = attrib.value; // FIXME: add more supports like checkbox
} else {
attrTarget.textContent = attrib.value; // FIXME: it should be more strict here, only handle div/span/p
}
}
showModal(modal, undefined);
});
}

View file

@ -3,8 +3,14 @@
// showModal will show the given modal and run `onApprove` if the approve/ok/yes
// button is pressed.
export function showModal(modalID: string, onApprove: () => void) {
const modal = document.getElementById(modalID) as HTMLDialogElement;
export function showModal(modalID: string | HTMLDialogElement, onApprove: () => void) {
let modal: HTMLDialogElement;
if (typeof modalID === 'string') {
modal = document.getElementById(modalID) as HTMLDialogElement;
} else {
modal = modalID;
}
// Move the modal to `<body>`, to avoid inheriting any bad CSS or if the
// parent becomes `display: hidden`.
document.body.append(modal);
@ -15,6 +21,9 @@ export function showModal(modalID: string, onApprove: () => void) {
}, {once: true, passive: true});
modal.querySelector('.ok')?.addEventListener('click', onApprove, {passive: true});
// Call a `onShow` callback if one is registered for this element.
modal?.$modal?.onShow();
// The modal is ready to be shown.
modal.showModal();
}

View file

@ -14,3 +14,9 @@ type CodeMirrorLanguage = typeof import('@codemirror/language');
type CodeMirrorSearch = typeof import('@codemirror/search');
type CodeMirrorState = typeof import('@codemirror/state');
type CodeMirrorView = typeof import('@codemirror/view');
interface HTMLDialogElement {
$modal?: {
onShow?: () => void;
}
}