diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 5a89662cb2..e8c8860bbe 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -748,13 +748,14 @@ blocked_users = Blocked users public_profile = Public profile biography_placeholder = Tell others a little bit about yourself! (Markdown is supported) location_placeholder = Share your approximate location with others -profile_desc = Control how your profile is shown to other users. Your primary email address will be used for notifications, password recovery and web-based Git operations. +profile_desc = About you password_username_disabled = Non-local users are not allowed to change their username. Please contact your site administrator for more details. full_name = Full name website = Website location = Location pronouns = Pronouns pronouns_custom = Custom +pronouns_custom_label = Custom pronouns pronouns_unspecified = Unspecified update_theme = Change theme update_profile = Update profile @@ -849,7 +850,7 @@ add_email_success = The new email address has been added. email_preference_set_success = Email preference has been set successfully. add_openid_success = The new OpenID address has been added. keep_email_private = Hide email address -keep_email_private_popup = This will hide your email address from your profile. It will no longer be the default for commits made via the web interface, like file uploads and edits, and will not be used for merge commits. Instead a special address %s can be used to associate commits with your account. Note that changing this option will not affect existing commits. +keep_email_private_popup = Your email address will not be shown on your profile and will not be the default for commits made via the web interface, like file uploads, edits, and merge commits. Instead, a special address %s can be used to link commits to your account. This option will not affect existing commits. openid_desc = OpenID lets you delegate authentication to an external provider. manage_ssh_keys = Manage SSH keys diff --git a/templates/user/settings/account.tmpl b/templates/user/settings/account.tmpl index a97136f407..55431392c7 100644 --- a/templates/user/settings/account.tmpl +++ b/templates/user/settings/account.tmpl @@ -40,9 +40,9 @@
+
{{ctx.Locale.Tr "settings.email_desc"}}
{{if $.EnableNotifyMail}}
-
{{ctx.Locale.Tr "settings.email_desc"}}
{{$.CsrfTokenHtml}} diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl index a007380387..5ecaada4e2 100644 --- a/templates/user/settings/profile.tmpl +++ b/templates/user/settings/profile.tmpl @@ -4,126 +4,121 @@ {{ctx.Locale.Tr "settings.public_profile"}}
-

{{ctx.Locale.Tr "settings.profile_desc"}}

{{.CsrfTokenHtml}} -
-
-
- - -
-
- - - -
- {{if not .SignedUser.KeepEmailPrivate}} -
- -

{{.SignedUser.Email}}

-
- {{end}} -
- - -
-
- - -
-
- - -
-
- + -
- -
- -
- - + + -
-
- + + + + + + + +
+ {{ctx.Locale.Tr "settings.privacy"}} + + + +
- {{ctx.Locale.Tr "settings.keep_email_private_popup" .SignedUser.GetPlaceholderEmail}} -
+ {{ctx.Locale.Tr "settings.keep_email_private"}} + {{ctx.Locale.Tr "settings.keep_email_private_popup" .SignedUser.GetPlaceholderEmail}} + -
-
- +
- {{ctx.Locale.Tr "settings.keep_activity_private.description" (printf "/%s?tab=activity" .SignedUser.Name)}} -
+ {{ctx.Locale.Tr "settings.keep_activity_private"}} + {{ctx.Locale.Tr "settings.keep_activity_private.description" (printf "/%s?tab=activity" .SignedUser.Name)}} + + -
- -
- -
+
diff --git a/tests/e2e/user-settings.test.e2e.ts b/tests/e2e/user-settings.test.e2e.ts new file mode 100644 index 0000000000..b36a17b5b2 --- /dev/null +++ b/tests/e2e/user-settings.test.e2e.ts @@ -0,0 +1,65 @@ +// @watch start +// templates/user/settings/**.tmpl +// web_src/css/{form,user}.css +// @watch end + +import {expect} from '@playwright/test'; +import {test, save_visual, login_user, login} from './utils_e2e.ts'; +import {validate_form} from './shared/forms.ts'; + +test.beforeAll(async ({browser}, workerInfo) => { + await login_user(browser, workerInfo, 'user2'); +}); + +test('User: Profile settings', async ({browser}, workerInfo) => { + const page = await login({browser}, workerInfo); + await page.goto('/user/settings'); + + await page.getByLabel('Full name').fill('SecondUser'); + await page.locator('#pronouns-dropdown').click(); + await page.getByRole('option', {name: 'she/her'}).click(); + await page.getByPlaceholder('Tell others a little bit').fill('I am a playwright test running for several seconds.'); + await page.getByPlaceholder('Tell others a little bit').press('Tab'); + await page.getByLabel('Website').fill('https://forgejo.org'); + await page.getByPlaceholder('Share your approximate').fill('on a computer chip'); + await page.getByLabel('User visibility').click(); + await page.getByLabel('Visible only to signed-in').click(); + await page.getByLabel('Hide email address Your email').uncheck(); + await page.getByLabel('Hide activity from profile').check(); + + await validate_form({page}, 'fieldset'); + await save_visual(page); + await page.getByRole('button', {name: 'Update profile'}).click(); + await expect(page.getByText('Your profile has been updated.')).toBeVisible(); + await page.getByRole('link', {name: 'public activity'}).click(); + await expect(page.getByText('Your activity is only visible')).toBeVisible(); + await save_visual(page); + + await page.goto('/user2'); + await expect(page.getByText('SecondUser')).toBeVisible(); + await expect(page.getByText('on a computer chip')).toBeVisible(); + await expect(page.locator('li').filter({hasText: 'user2@example.com'})).toBeVisible(); + await expect(page.locator('li').filter({hasText: 'https://forgejo.org'})).toBeVisible(); + await expect(page.getByText('I am a playwright test')).toBeVisible(); + await save_visual(page); + + await page.goto('/user/settings'); + await page.locator('#pronouns-dropdown').click(); + await page.getByRole('option', {name: 'Custom'}).click(); + await page.getByLabel('Custom pronouns').fill('rob/ot'); + await page.getByLabel('User visibility').click(); + await page.getByLabel('Visible to everyone').click(); + await page.getByLabel('Hide email address Your email').check(); + await page.getByLabel('Hide activity from profile').uncheck(); + await expect(page.getByText('Your profile has been updated.')).toBeHidden(); + await validate_form({page}, 'fieldset'); + await save_visual(page); + await page.getByRole('button', {name: 'Update profile'}).click(); + await expect(page.getByText('Your profile has been updated.')).toBeVisible(); + + await page.goto('/user2'); + await expect(page.getByText('SecondUser')).toBeVisible(); + await expect(page.locator('li').filter({hasText: 'user2@example.com'})).toBeHidden(); + await page.goto('/user2?tab=activity'); + await expect(page.getByText('Your activity is visible to everyone')).toBeVisible(); +}); diff --git a/tests/e2e/utils_e2e.ts b/tests/e2e/utils_e2e.ts index 31fc999fb0..7e25441ea3 100644 --- a/tests/e2e/utils_e2e.ts +++ b/tests/e2e/utils_e2e.ts @@ -105,6 +105,7 @@ export async function save_visual(page: Page) { page.locator('#repo_migrating'), // update order of recently created repos is not fully deterministic page.locator('.flex-item-main').filter({hasText: 'relative time in repo'}), + page.locator('#activity-feed'), // dynamic IDs in fixed-size inputs page.locator('input[value*="dyn-id-"]'), ], diff --git a/tests/integration/auth_ldap_test.go b/tests/integration/auth_ldap_test.go index 9bcb532d75..62a7d18904 100644 --- a/tests/integration/auth_ldap_test.go +++ b/tests/integration/auth_ldap_test.go @@ -185,7 +185,6 @@ func TestLDAPUserSignin(t *testing.T) { assert.Equal(t, u.UserName, htmlDoc.GetInputValueByName("name")) assert.Equal(t, u.FullName, htmlDoc.GetInputValueByName("full_name")) - assert.Equal(t, u.Email, htmlDoc.Find("#signed-user-email").Text()) } func TestLDAPAuthChange(t *testing.T) { diff --git a/tests/integration/setting_test.go b/tests/integration/setting_test.go index 4677770fed..5635faad38 100644 --- a/tests/integration/setting_test.go +++ b/tests/integration/setting_test.go @@ -157,23 +157,3 @@ func TestSettingSecurityAuthSource(t *testing.T) { assert.Contains(t, resp.Body.String(), `gitlab-active`) assert.Contains(t, resp.Body.String(), `gitlab-inactive`) } - -func TestSettingShowUserEmailSettings(t *testing.T) { - defer tests.PrepareTestEnv(t)() - - // user1: keep_email_private = false, user2: keep_email_private = true - - // user1 can see own visible email - session := loginUser(t, "user1") - req := NewRequest(t, "GET", "/user/settings") - resp := session.MakeRequest(t, req, http.StatusOK) - htmlDoc := NewHTMLParser(t, resp.Body) - assert.Contains(t, htmlDoc.doc.Find("#signed-user-email").Text(), "user1@example.com") - - // user2 cannot see own hidden email - session = loginUser(t, "user2") - req = NewRequest(t, "GET", "/user/settings") - resp = session.MakeRequest(t, req, http.StatusOK) - htmlDoc = NewHTMLParser(t, resp.Body) - assert.NotContains(t, htmlDoc.doc.Find("#signed-user-email").Text(), "user2@example.com") -} diff --git a/tests/integration/user_profile_activity_test.go b/tests/integration/user_profile_activity_test.go index 77d188903e..f1269e5e93 100644 --- a/tests/integration/user_profile_activity_test.go +++ b/tests/integration/user_profile_activity_test.go @@ -42,25 +42,16 @@ func TestUserProfileActivity(t *testing.T) { // Verify the hint for all types of users: admin, self, guest testUser2ActivityVisibility(t, userAdmin, "This activity is visible to everyone, but as an administrator you can also see interactions in private spaces.", true) - hintLink := testUser2ActivityVisibility(t, userRegular, "Your activity is visible to everyone, except for interactions in private spaces. Configure.", true) + testUser2ActivityVisibility(t, userRegular, "Your activity is visible to everyone, except for interactions in private spaces. Configure.", true) testUser2ActivityVisibility(t, userGuest, "", true) - // When viewing own profile, the user is offered to configure activity visibility. Verify that the link is correct and works, also check that it links back to the activity tab. - linkCorrect := assert.EqualValues(t, "/user/settings#keep-activity-private", hintLink) - if linkCorrect { - page := NewHTMLParser(t, userRegular.MakeRequest(t, NewRequest(t, "GET", hintLink), http.StatusOK).Body) - activityLink, exists := page.Find(".field:has(.checkbox#keep-activity-private) .help a").Attr("href") - assert.True(t, exists) - assert.EqualValues(t, "/user2?tab=activity", activityLink) - } - // = Private profile, but public activity = // Set profile visibility of user2 to private testChangeUserProfileVisibility(t, userRegular, structs.VisibleTypePrivate) // When profile activity is configured as public, but the profile is private, tell the user about this and link to visibility settings. - hintLink = testUser2ActivityVisibility(t, userRegular, "Your activity is only visible to you and the instance administrators because your profile is private. Configure.", true) + hintLink := testUser2ActivityVisibility(t, userRegular, "Your activity is only visible to you and the instance administrators because your profile is private. Configure.", true) assert.EqualValues(t, "/user/settings#visibility-setting", hintLink) // When the profile is private, tell the admin about this. diff --git a/web_src/css/form.css b/web_src/css/form.css index bf50114344..916d76fd0c 100644 --- a/web_src/css/form.css +++ b/web_src/css/form.css @@ -1,5 +1,5 @@ fieldset { - margin: 0.5em 0 1em; + margin: 0.2em 0 0.3em; padding: 0; } @@ -8,6 +8,12 @@ fieldset legend { margin-bottom: 0.75em; } +fieldset + fieldset > legend { + width: 100%; + padding-top: 1em; + border-top: 1px solid var(--color-secondary); +} + fieldset label { display: block; margin-bottom: 0.6em; diff --git a/web_src/css/user.css b/web_src/css/user.css index 16d431e2a7..bceb16fcf9 100644 --- a/web_src/css/user.css +++ b/web_src/css/user.css @@ -143,7 +143,3 @@ .notifications-item:hover .notifications-updated { display: none; } - -#pronouns-dropdown, #pronouns-custom { - width: 140px; -} diff --git a/web_src/js/features/user-settings.js b/web_src/js/features/user-settings.js index 717ef945e1..85815aa6ff 100644 --- a/web_src/js/features/user-settings.js +++ b/web_src/js/features/user-settings.js @@ -1,7 +1,8 @@ import {hideElem, showElem} from '../utils/dom.js'; function onPronounsDropdownUpdate() { - const pronounsCustom = document.getElementById('pronouns-custom'); + const pronounsCustom = document.getElementById('label-pronouns-custom'); + const pronounsCustomInput = pronounsCustom.querySelector('input'); const pronounsDropdown = document.getElementById('pronouns-dropdown'); const pronounsInput = pronounsDropdown.querySelector('input'); // must be kept in sync with `routers/web/user/setting/profile.go` @@ -15,49 +16,35 @@ function onPronounsDropdownUpdate() { ); if (isCustom) { if (pronounsInput.value === '!') { - pronounsCustom.value = ''; + pronounsCustomInput.value = ''; } else { - pronounsCustom.value = pronounsInput.value; + pronounsCustomInput.value = pronounsInput.value; } - pronounsCustom.style.display = ''; + showElem(pronounsCustom); } else { - pronounsCustom.style.display = 'none'; + hideElem(pronounsCustom); } } function onPronounsCustomUpdate() { - const pronounsCustom = document.getElementById('pronouns-custom'); + const pronounsCustomInput = document.querySelector('#label-pronouns-custom input'); const pronounsInput = document.querySelector('#pronouns-dropdown input'); - pronounsInput.value = pronounsCustom.value; + pronounsInput.value = pronounsCustomInput.value; } export function initUserSettings() { if (!document.querySelectorAll('.user.settings.profile').length) return; - const usernameInput = document.getElementById('username'); - if (!usernameInput) return; - usernameInput.addEventListener('input', function () { - const prompt = document.getElementById('name-change-prompt'); - const promptRedirect = document.getElementById('name-change-redirect-prompt'); - if (this.value.toLowerCase() !== this.getAttribute('data-name').toLowerCase()) { - showElem(prompt); - showElem(promptRedirect); - } else { - hideElem(prompt); - hideElem(promptRedirect); - } - }); - - const pronounsDropdown = document.getElementById('pronouns-dropdown'); - const pronounsCustom = document.getElementById('pronouns-custom'); + const pronounsDropdown = document.getElementById('label-pronouns'); + const pronounsCustomInput = document.querySelector('#label-pronouns-custom input'); const pronounsInput = pronounsDropdown.querySelector('input'); // If JS is disabled, the page will show the custom input, as the dropdown requires JS to work. // JS progressively enhances the input by adding a dropdown, but it works regardless. - pronounsCustom.removeAttribute('name'); + pronounsCustomInput.removeAttribute('name'); pronounsInput.setAttribute('name', 'pronouns'); - pronounsDropdown.style.display = ''; + showElem(pronounsDropdown); onPronounsDropdownUpdate(); pronounsInput.addEventListener('change', onPronounsDropdownUpdate); - pronounsCustom.addEventListener('input', onPronounsCustomUpdate); + pronounsCustomInput.addEventListener('input', onPronounsCustomUpdate); }