feat: move more modals to native dialogs (#9636)

Follow up of forgejo/forgejo#8859

Move the following modals to native dialogs:
- Admin notice.
- Edit label.
- New label.
- Update email in admin's email list.

Each has a E2E test to screenshot the modal and test functionality.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9636
Reviewed-by: 0ko <0ko@noreply.codeberg.org>
Co-authored-by: Gusted <postmaster@gusted.xyz>
Co-committed-by: Gusted <postmaster@gusted.xyz>
This commit is contained in:
Gusted 2025-10-13 17:48:49 +02:00 committed by 0ko
parent d0a6f93f9e
commit 8eb8f49581
13 changed files with 242 additions and 149 deletions

View file

@ -75,13 +75,12 @@
{{template "base/paginate" .}}
<div class="ui g-modal-confirm modal" id="change-email-modal">
<div class="header">
{{ctx.Locale.Tr "admin.emails.change_email_header"}}
</div>
<div class="content">
<p class="center">{{ctx.Locale.Tr "admin.emails.change_email_text"}}</p>
<dialog id="change-email-modal">
<article>
<header>{{ctx.Locale.Tr "admin.emails.change_email_header"}}</header>
<div class="content">
<p class="center">{{ctx.Locale.Tr "admin.emails.change_email_text"}}</p>
</div>
<form class="ui form" id="email-action-form" action="{{AppSubUrl}}/admin/emails/activate" method="post">
{{$.CsrfTokenHtml}}
@ -99,9 +98,8 @@
{{template "base/modal_actions_confirm" .}}
</div>
</form>
</div>
</div>
</article>
</dialog>
</div>
<div class="ui g-modal-confirm delete modal" id="delete-email">

View file

@ -62,9 +62,14 @@
{{template "base/paginate" .}}
</div>
<div class="ui modal admin" id="detail-modal">
<div class="header">{{ctx.Locale.Tr "admin.notices.view_detail_header"}}</div>
<div class="content"><pre></pre></div>
</div>
<dialog id="detail-modal">
<article>
<header>{{ctx.Locale.Tr "admin.notices.view_detail_header"}}</header>
<div class="content"><pre></pre></div>
<div class="actions">
<button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
</div>
</article>
</dialog>
{{template "admin/layout_footer" .}}

View file

@ -22,7 +22,7 @@ The ".ok.button" and ".cancel.button" selectors are also used by Fomantic Modal
{{if .ModalButtonCancelText}}{{$textNegitive = .ModalButtonCancelText}}{{end}}
{{if .ModalButtonOkText}}{{$textPositive = .ModalButtonOkText}}{{end}}
<button class="ui cancel button">{{svg "octicon-x"}} {{$textNegitive}}</button>
<button type="button" class="ui cancel button">{{svg "octicon-x"}} {{$textNegitive}}</button>
<button class="ui primary ok button">{{svg "octicon-check"}} {{$textPositive}}</button>
{{end}}
</div>

View file

@ -9,64 +9,64 @@
{{template "base/modal_actions_confirm" .}}
</div>
<div class="ui small edit-label modal">
<div class="header">
{{ctx.Locale.Tr "repo.issues.label_modify"}}
</div>
<div class="content">
<form class="ui edit-label form ignore-dirty" action="{{$.Link}}/edit" method="post">
{{.CsrfTokenHtml}}
<input id="label-modal-id" name="id" type="hidden">
<div class="required field">
<label for="name">{{ctx.Locale.Tr "repo.issues.label_title"}}</label>
<div class="ui small input">
<input class="label-name-input" name="title" placeholder="{{ctx.Locale.Tr "repo.issues.new_label_placeholder"}}" autofocus required maxlength="50">
<dialog id="edit-label-modal">
<article>
<header>{{ctx.Locale.Tr "repo.issues.label_modify"}}</header>
<div class="content">
<form class="ui edit-label form ignore-dirty" action="{{$.Link}}/edit" method="post">
{{.CsrfTokenHtml}}
<input id="label-modal-id" name="id" type="hidden">
<div class="required field">
<label for="name">{{ctx.Locale.Tr "repo.issues.label_title"}}</label>
<div class="ui small input">
<input class="label-name-input" name="title" placeholder="{{ctx.Locale.Tr "repo.issues.new_label_placeholder"}}" autofocus required maxlength="50">
</div>
</div>
</div>
<div class="field label-exclusive-input-field">
<div class="ui checkbox">
<input class="label-exclusive-input" name="exclusive" type="checkbox">
<label>{{ctx.Locale.Tr "repo.issues.label_exclusive"}}</label>
<div class="field label-exclusive-input-field">
<div class="ui checkbox">
<input class="label-exclusive-input" name="exclusive" type="checkbox">
<label>{{ctx.Locale.Tr "repo.issues.label_exclusive"}}</label>
</div>
<br>
<small class="desc">{{ctx.Locale.Tr "repo.issues.label_exclusive_desc"}}</small>
<div class="desc tw-ml-1 tw-mt-2 tw-hidden label-exclusive-warning">
{{svg "octicon-alert"}} {{ctx.Locale.Tr "repo.issues.label_exclusive_warning"}}
</div>
<br>
</div>
<br>
<small class="desc">{{ctx.Locale.Tr "repo.issues.label_exclusive_desc"}}</small>
<div class="desc tw-ml-1 tw-mt-2 tw-hidden label-exclusive-warning">
{{svg "octicon-alert"}} {{ctx.Locale.Tr "repo.issues.label_exclusive_warning"}}
<div class="field label-is-archived-input-field">
<div class="ui checkbox">
<input class="label-is-archived-input" name="is_archived" type="checkbox">
<label>{{ctx.Locale.Tr "repo.issues.label_archive"}}</label>
</div>
<i class="tw-ml-1" data-tooltip-content={{ctx.Locale.Tr "repo.issues.label_archive_tooltip"}} data-tooltip-appendto="parent">
{{svg "octicon-info"}}
</i>
</div>
<br>
</div>
<div class="field label-is-archived-input-field">
<div class="ui checkbox">
<input class="label-is-archived-input" name="is_archived" type="checkbox">
<label>{{ctx.Locale.Tr "repo.issues.label_archive"}}</label>
<div class="field">
<label for="description">{{ctx.Locale.Tr "repo.issues.label_description"}}</label>
<div class="ui small fluid input">
<input class="label-desc-input" name="description" placeholder="{{ctx.Locale.Tr "repo.issues.new_label_desc_placeholder"}}" maxlength="200">
</div>
</div>
<i class="tw-ml-1" data-tooltip-content={{ctx.Locale.Tr "repo.issues.label_archive_tooltip"}}>
{{svg "octicon-info"}}
</i>
</div>
<div class="field">
<label for="description">{{ctx.Locale.Tr "repo.issues.label_description"}}</label>
<div class="ui small fluid input">
<input class="label-desc-input" name="description" placeholder="{{ctx.Locale.Tr "repo.issues.new_label_desc_placeholder"}}" maxlength="200">
<div class="field color-field">
<label for="color">{{ctx.Locale.Tr "repo.issues.label_color"}}</label>
<div class="column js-color-picker-input">
<input name="color" value="#70c24a"placeholder="#c320f6" required maxlength="7">
{{template "repo/issue/label_precolors"}}
</div>
</div>
</div>
<div class="field color-field">
<label for="color">{{ctx.Locale.Tr "repo.issues.label_color"}}</label>
<div class="column js-color-picker-input">
<input name="color" value="#70c24a"placeholder="#c320f6" required maxlength="7">
{{template "repo/issue/label_precolors"}}
</div>
</div>
</form>
</div>
<div class="actions">
<button class="ui small basic cancel button">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "cancel"}}
</button>
<button class="ui primary small approve button">
{{svg "fontawesome-save"}}
{{ctx.Locale.Tr "save"}}
</button>
</div>
</div>
</form>
</div>
<div class="actions">
<button class="ui small basic cancel button">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "cancel"}}
</button>
<button class="ui primary small approve button">
{{svg "fontawesome-save"}}
{{ctx.Locale.Tr "save"}}
</button>
</div>
</article>
</dialog>

View file

@ -1,48 +1,48 @@
<div class="ui small new-label modal">
<div class="header">
{{ctx.Locale.Tr "repo.issues.new_label"}}
</div>
<div class="content">
<form class="ui new-label form ignore-dirty" action="{{$.Link}}/new" method="post">
{{.CsrfTokenHtml}}
<div class="required field">
<label for="name">{{ctx.Locale.Tr "repo.issues.label_title"}}</label>
<div class="ui small input">
<input class="label-name-input" name="title" placeholder="{{ctx.Locale.Tr "repo.issues.new_label_placeholder"}}" autofocus required maxlength="50">
<dialog id="new-label-modal">
<article>
<header>{{ctx.Locale.Tr "repo.issues.new_label"}}</header>
<div class="content">
<form class="ui new-label form ignore-dirty" action="{{$.Link}}/new" method="post">
{{.CsrfTokenHtml}}
<div class="required field">
<label for="name">{{ctx.Locale.Tr "repo.issues.label_title"}}</label>
<div class="ui small input">
<input class="label-name-input" name="title" placeholder="{{ctx.Locale.Tr "repo.issues.new_label_placeholder"}}" autofocus required maxlength="50">
</div>
</div>
</div>
<div class="field label-exclusive-input-field">
<div class="ui checkbox">
<input class="label-exclusive-input" name="exclusive" type="checkbox">
<label>{{ctx.Locale.Tr "repo.issues.label_exclusive"}}</label>
<div class="field label-exclusive-input-field">
<div class="ui checkbox">
<input class="label-exclusive-input" name="exclusive" type="checkbox">
<label>{{ctx.Locale.Tr "repo.issues.label_exclusive"}}</label>
</div>
<br>
<small class="desc">{{ctx.Locale.Tr "repo.issues.label_exclusive_desc"}}</small>
</div>
<br>
<small class="desc">{{ctx.Locale.Tr "repo.issues.label_exclusive_desc"}}</small>
</div>
<div class="field">
<label for="description">{{ctx.Locale.Tr "repo.issues.label_description"}}</label>
<div class="ui small fluid input">
<input class="label-desc-input" name="description" placeholder="{{ctx.Locale.Tr "repo.issues.new_label_desc_placeholder"}}" maxlength="200">
<div class="field">
<label for="description">{{ctx.Locale.Tr "repo.issues.label_description"}}</label>
<div class="ui small fluid input">
<input class="label-desc-input" name="description" placeholder="{{ctx.Locale.Tr "repo.issues.new_label_desc_placeholder"}}" maxlength="200">
</div>
</div>
</div>
<div class="field color-field">
<label for="color">{{ctx.Locale.Tr "repo.issues.label_color"}}</label>
<div class="js-color-picker-input column">
<input name="color" value="#70c24a" placeholder="#c320f6" required maxlength="7">
{{template "repo/issue/label_precolors"}}
<div class="field color-field">
<label for="color">{{ctx.Locale.Tr "repo.issues.label_color"}}</label>
<div class="js-color-picker-input column">
<input name="color" value="#70c24a" placeholder="#c320f6" required maxlength="7">
{{template "repo/issue/label_precolors"}}
</div>
</div>
</div>
</form>
</div>
</form>
</div>
<div class="actions">
<button class="ui cancel button">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "cancel"}}
</button>
<button class="ui primary ok button">
{{svg "octicon-check"}}
{{ctx.Locale.Tr "repo.issues.create_label"}}
</button>
</div>
</div>
<div class="actions">
<button class="ui cancel button">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "cancel"}}
</button>
<button class="ui primary ok button">
{{svg "octicon-check"}}
{{ctx.Locale.Tr "repo.issues.create_label"}}
</button>
</div>
</article>
</dialog>

View file

@ -0,0 +1,58 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
// @watch start
// web_src/js/features/admin/**
// templates/admin/**
// @watch end
import {expect} from '@playwright/test';
import {test} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test.use({user: 'user1'});
test('Admin notices modal', async ({page}) => {
const response = await page.goto('/admin/notices');
expect(response?.status()).toBe(200);
await page.getByText('description1').click();
await expect(page.locator('#detail-modal .content')).toHaveText('description1');
await screenshot(page, page.locator('#detail-modal'));
await page.getByText('Cancel').click();
await expect(page.locator('#change-email-modal')).toBeHidden();
await page.getByText('description2').click();
await expect(page.locator('#detail-modal .content')).toHaveText('description2');
await screenshot(page, page.locator('#detail-modal'));
await page.getByText('Cancel').click();
await expect(page.locator('#change-email-modal')).toBeHidden();
await page.getByText('description3').click();
await expect(page.locator('#detail-modal .content')).toHaveText('description3');
await screenshot(page, page.locator('#detail-modal'));
await page.getByText('Cancel').click();
await expect(page.locator('#change-email-modal')).toBeHidden();
});
test('Admin email list', async ({page}) => {
const response = await page.goto('/admin/emails');
expect(response?.status()).toBe(200);
await page.locator('[data-uid="21"]').click();
await expect(page.locator('#change-email-modal .content')).toHaveText('Are you sure you want to update this email address?');
await screenshot(page, page.locator('#change-email-modal .content'));
await page.locator('#email-action-form').getByText('No').click();
await expect(page.locator('#change-email-modal')).toBeHidden();
const activated = await page.locator('[data-uid="9"] .svg').evaluate((node) => node.classList.contains('octicon-check'));
await page.locator('[data-uid="9"]').click();
await page.getByRole('button', {name: 'Yes'}).click();
// Retry-proof
if (activated) {
await expect(page.locator('[data-uid="9"] .svg')).toHaveClass(/octicon-x/);
} else {
await expect(page.locator('[data-uid="9"] svg')).toHaveClass(/octicon-check/);
}
});

View file

@ -0,0 +1,43 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
// @watch start
// templates/repo/issues/labels/**
// web_src/js/features/comp/LabelEdit.js
// @watch end
import {expect} from '@playwright/test';
import {test, dynamic_id} from './utils_e2e.ts';
import {screenshot} from './shared/screenshots.ts';
test.use({user: 'user2'});
test('New label', async ({page}) => {
const response = await page.goto('/user2/repo1/labels');
expect(response?.status()).toBe(200);
await page.getByRole('button', {name: 'New label'}).click();
await expect(page.locator('#new-label-modal')).toBeVisible();
await screenshot(page, page.locator('#new-label-modal'));
const labelName = dynamic_id();
await page.keyboard.type(labelName);
await page.getByRole('button', {name: 'Create label'}).click();
await page.locator('.label-title').filter({hasText: labelName}).isVisible();
});
test('Edit label', async ({page}) => {
const response = await page.goto('/user2/repo1/labels');
expect(response?.status()).toBe(200);
await page.getByText('Edit').first().click();
await expect(page.locator('#edit-label-modal')).toBeVisible();
await screenshot(page, page.locator('#edit-label-modal'));
const labelName = dynamic_id();
await page.keyboard.type(labelName);
await page.getByRole('button', {name: 'Save'}).click();
await page.locator('.label-title').filter({hasText: labelName}).isVisible();
});

View file

@ -29,8 +29,8 @@
min-width: 100px;
}
.admin code,
.admin pre {
:is(.admin, #detail-modal) code,
:is(.admin, #detail-modal) pre {
white-space: pre-wrap;
word-wrap: break-word;
}

View file

@ -2133,17 +2133,6 @@ details.repo-search-result summary::marker {
padding: 1em;
}
.edit-label.modal .form .column,
.new-label.modal .form .column {
padding-right: 0;
}
.edit-label.modal .form .buttons,
.new-label.modal .form .buttons {
margin-left: auto;
padding-top: 15px;
}
.stats-table {
display: table;
width: 100%;

View file

@ -2,6 +2,7 @@ import $ from 'jquery';
import {checkAppUrl} from '../common-global.js';
import {hideElem, showElem, toggleElem} from '../../utils/dom.js';
import {POST} from '../../modules/fetch.js';
import {showModal} from '../../modules/modal.ts';
const {appSubUrl} = window.config;
@ -216,7 +217,7 @@ export function initAdminCommon() {
$('.view-detail').on('click', function () {
const description = this.closest('tr').querySelector('.notice-description').textContent;
detailModal.querySelector('.content pre').textContent = description;
$(detailModal).modal('show');
showModal('detail-modal', undefined);
return false;
});

View file

@ -1,4 +1,5 @@
import $ from 'jquery';
import {showModal} from '../../modules/modal.ts';
export function initAdminEmails() {
function linkEmailAction(e) {
@ -7,7 +8,7 @@ export function initAdminEmails() {
$('#form-email').val($this.data('email'));
$('#form-primary').val($this.data('primary'));
$('#form-activate').val($this.data('activate'));
$('#change-email-modal').modal('show');
showModal('change-email-modal', undefined);
e.preventDefault();
}
$('.link-email-action').on('click', linkEmailAction);

View file

@ -1,4 +1,5 @@
import $ from 'jquery';
import {showModal} from '../../modules/modal.ts';
function isExclusiveScopeName(name) {
return /.*[^/]\/[^/].*/.test(name);
@ -27,16 +28,14 @@ export function initCompLabelEdit(selector) {
// Create label
$('.new-label.button').on('click', () => {
updateExclusiveLabelEdit('.new-label');
$('.new-label.modal').modal({
onApprove() {
const form = document.querySelector('.new-label.form');
if (!form.checkValidity()) {
form.reportValidity();
return false;
}
document.querySelector('.new-label.form').requestSubmit();
},
}).modal('show');
showModal('new-label-modal', () => {
const form = document.querySelector('.new-label.form');
if (!form.checkValidity()) {
form.reportValidity();
return false;
}
document.querySelector('.new-label.form').requestSubmit();
});
return false;
});
@ -64,16 +63,14 @@ export function initCompLabelEdit(selector) {
colorInput.value = this.getAttribute('data-color');
colorInput.dispatchEvent(new Event('input', {bubbles: true}));
$('.edit-label.modal').modal({
onApprove() {
const form = document.querySelector('.edit-label.form');
if (!form.checkValidity()) {
form.reportValidity();
return false;
}
document.querySelector('.edit-label.form').requestSubmit();
},
}).modal('show');
showModal('edit-label-modal', () => {
const form = document.querySelector('.edit-label.form');
if (!form.checkValidity()) {
form.reportValidity();
return false;
}
document.querySelector('.edit-label.form').requestSubmit();
});
return false;
});

View file

@ -81,6 +81,7 @@ function attachTooltip(target, content = null) {
hideOnClick,
placement: target.getAttribute('data-tooltip-placement') || 'top-start',
followCursor: target.getAttribute('data-tooltip-follow-cursor') || false,
...(target.getAttribute('data-tooltip-appendto') === 'parent' ? {appendTo: 'parent'} : {}),
...(target.getAttribute('data-tooltip-interactive') === 'true' ? {interactive: true, aria: {content: 'describedby', expanded: false}} : {}),
};