Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/behat-sqlite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:
- 1025:1025
- 8025:8025

name: SQLite PHP 8.2 Nextcloud master
name: SQLite PHP 8.3 Nextcloud master

steps:
- name: Checkout server
Expand All @@ -74,10 +74,10 @@ jobs:
persist-credentials: false
path: apps/${{ env.APP_NAME }}

- name: Set up php 8.2
- name: Set up php 8.3
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0
with:
php-version: '8.2'
php-version: '8.3'
extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, sockets, sqlite, pdo_sqlite, xmlreader, xmlwriter, zip, zlib
coverage: none
ini-file: development
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,10 @@ jobs:
persist-credentials: false
path: apps/${{ env.APP_NAME }}

- name: Set up php 8.2
- name: Set up php 8.3
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2.37.0
with:
php-version: '8.2'
php-version: '8.3'
extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, sqlite, pdo_sqlite, xmlreader, xmlwriter, zip, zlib
coverage: none
ini-file: development
Expand Down
6 changes: 2 additions & 4 deletions lib/Migration/Version1001Date20260404010000.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,8 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt
return null;
}

$table->changeColumn('active', [
'notnull' => false,
'default' => true,
]);
$activeColumn->setNotnull(false);
$activeColumn->setDefault(true);

return $schema;
}
Expand Down
8 changes: 7 additions & 1 deletion lib/Settings/PersonalInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,24 @@
use OCP\Settings\ISettings;

class PersonalInfo implements ISettings {
private const RENAMED_CORE_SECTION = 'OCA\\Settings\\Sections\\Personal\\ProfileContact';

#[\Override]
public function getForm(): TemplateResponse {
return new TemplateResponse(Application::APP_ID, 'settings-personal-info');
}

#[\Override]
public function getSection(): string {
return 'personal-info';
return $this->coreSectionExists(self::RENAMED_CORE_SECTION) ? 'profile-contact' : 'personal-info';
}

#[\Override]
public function getPriority(): int {
return 80;
}

protected function coreSectionExists(string $className): bool {
return class_exists($className);
}
}
69 changes: 42 additions & 27 deletions playwright/e2e/profile-fields.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@ const adminPassword = process.env.NEXTCLOUD_ADMIN_PASSWORD ?? 'admin'

const optionInput = (page, index: number) => page.getByTestId(`profile-fields-admin-option-row-${index}`).locator('input')

const openPersonalSettings = async(page) => {
for (const section of ['profile-contact', 'personal-info']) {
await page.goto(`./settings/user/${section}`)
try {
await page.getByTestId('profile-fields-personal').waitFor({ state: 'visible', timeout: 10_000 })

return section
} catch {
continue
}
}

throw new Error('Could not find the personal profile fields section')
}

const chooseFieldType = async(page, label: 'Text' | 'Number' | 'Select') => {
await page.getByTestId('profile-fields-admin-type-select').click()
await page.getByRole('option', { name: label, exact: true }).click()
Expand All @@ -30,8 +45,8 @@ const collectEmbeddedLayoutMetrics = async(page, fieldKey: string) => {
const customInput = page.getByTestId(`profile-fields-personal-input-${fieldKey}`)
const embeddedShell = page.locator('#profile-fields-personal-info-shell')

await aboutInput.scrollIntoViewIfNeeded()
await customInput.scrollIntoViewIfNeeded()
await aboutInput.scrollIntoViewIfNeeded({ timeout: 10_000 })
await customInput.scrollIntoViewIfNeeded({ timeout: 10_000 })

const aboutBox = await aboutInput.boundingBox()
const customFieldBox = await customField.boundingBox()
Expand Down Expand Up @@ -70,6 +85,7 @@ const expectEmbeddedLayoutAtWidth = async(

expect(Math.abs(aboutBox.width - customInputBox.width)).toBeLessThanOrEqual(6)
expect(customFieldBox.y).toBeGreaterThanOrEqual(aboutBottom - 1)
expect(embeddedShellBox.width).toBeGreaterThanOrEqual(aboutBox.width - 1)
if (expectExpandedShell) {
expect(embeddedShellBox.width).toBeGreaterThan(aboutBox.width + 100)
}
Expand Down Expand Up @@ -187,33 +203,30 @@ test('admin can reorder field definitions by dragging the list handles', async (

const firstHandle = page.getByTestId(`profile-fields-admin-definition-handle-${firstFieldKey}`)
const secondHandle = page.getByTestId(`profile-fields-admin-definition-handle-${secondFieldKey}`)
const verticalOrder = async() => {
const firstBox = await firstHandle.boundingBox()
const secondBox = await secondHandle.boundingBox()
expect(firstBox).not.toBeNull()
expect(secondBox).not.toBeNull()
return {
firstY: firstBox!.y,
secondY: secondBox!.y,
}
}
const draggedOrder = async() => (await page
.locator('.profile-fields-admin__list .profile-fields-admin__list-item-subname')
.allTextContents())
.filter((listedKey) => listedKey === firstFieldKey || listedKey === secondFieldKey)

await expect.poll(draggedOrder).toEqual([firstFieldKey, secondFieldKey])

let order = await verticalOrder()
expect(order.firstY).toBeLessThan(order.secondY)
await secondHandle.hover()
await page.mouse.down()

await secondHandle.dragTo(firstHandle)
const firstHandleBox = await firstHandle.boundingBox()
expect(firstHandleBox).not.toBeNull()
const dropX = firstHandleBox!.x + firstHandleBox!.width / 2
const dropY = firstHandleBox!.y + firstHandleBox!.height / 2

await expect.poll(verticalOrder).toEqual(expect.objectContaining({
firstY: expect.any(Number),
secondY: expect.any(Number),
}))
order = await verticalOrder()
expect(order.secondY).toBeLessThan(order.firstY)
await page.mouse.move(dropX, dropY, { steps: 10 })
await page.mouse.move(dropX, dropY)
await page.mouse.up()

await expect.poll(draggedOrder).toEqual([secondFieldKey, firstFieldKey])

await page.reload()
await expect(page.getByTestId('profile-fields-admin')).toBeVisible()
order = await verticalOrder()
expect(order.secondY).toBeLessThan(order.firstY)
await expect.poll(draggedOrder).toEqual([secondFieldKey, firstFieldKey])
} finally {
await deleteDefinitionByFieldKey(page.request, firstFieldKey)
await deleteDefinitionByFieldKey(page.request, secondFieldKey)
Expand Down Expand Up @@ -479,7 +492,7 @@ test('embedded personal settings autosave a user-visible field', async ({ page }
})

try {
await page.goto('./settings/user/personal-info')
const section = await openPersonalSettings(page)
const fieldCard = page.getByTestId(`profile-fields-personal-field-${fieldKey}`)
const fieldInput = page.getByTestId(`profile-fields-personal-input-${fieldKey}`)
const visibilityPanel = page.getByTestId('profile-fields-personal-visibility-panel')
Expand All @@ -503,10 +516,12 @@ test('embedded personal settings autosave a user-visible field', async ({ page }
await expect(page.getByTestId(`profile-fields-personal-visibility-${fieldKey}`)).toBeVisible()
await expect(page.getByTestId(`profile-fields-personal-visibility-${fieldKey}`)).toContainText('Hide')

const spansTheGrid = section === 'personal-info'

for (const viewport of [
{ width: 1400, expectExpandedShell: true },
{ width: 1200, expectExpandedShell: true },
{ width: 900, expectExpandedShell: true },
{ width: 1400, expectExpandedShell: spansTheGrid },
{ width: 1200, expectExpandedShell: spansTheGrid },
{ width: 900, expectExpandedShell: spansTheGrid },
{ width: 640, expectExpandedShell: false },
]) {
await expectEmbeddedLayoutAtWidth(page, fieldKey, viewport)
Expand Down
1 change: 1 addition & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
errorLevel="5"
findUnusedBaselineEntry="true"
findUnusedCode="false"
findUnusedIssueHandlerSuppression="false"
resolveFromConfigFile="true"
phpVersion="8.1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
Expand Down
117 changes: 1 addition & 116 deletions src/settings-personal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,125 +5,10 @@ import 'vite/modulepreload-polyfill'

import { translate as t } from '@nextcloud/l10n'
import { createApp } from 'vue'
import { prepareEmbeddedPersonalInfoShell } from './utils/embeddedShellPlacement'
import PersonalSettings from './views/PersonalSettings.vue'

const PERSONAL_INFO_SELECTOR = '#profile-fields-personal-info-settings'
const EMBEDDED_VISIBILITY_ANCHOR_ID = 'profile-fields-personal-visibility-anchor'

let embeddedShellPlacementFrame = 0
let embeddedShellResizeDebounceTimer = 0
let embeddedVisibilityAnchorObserver: MutationObserver | null = null
let observedProfileVisibilityRoot: HTMLElement | null = null

const personalSettingsUsesMultipleColumns = (personalSettings: HTMLElement) => {
const gridTemplateColumns = window.getComputedStyle(personalSettings).gridTemplateColumns
if (gridTemplateColumns === '') {
return personalSettings.getBoundingClientRect().width >= 900
}

return gridTemplateColumns
.split(' ')
.map((value) => value.trim())
.filter(Boolean)
.length > 1
}

const ensureEmbeddedVisibilityAnchor = () => {
const profileVisibilityRoot = document.querySelector<HTMLElement>('#profile-visibility')
if (profileVisibilityRoot === null) {
return
}

if (observedProfileVisibilityRoot !== profileVisibilityRoot) {
embeddedVisibilityAnchorObserver?.disconnect()
embeddedVisibilityAnchorObserver = new MutationObserver(() => {
ensureEmbeddedVisibilityAnchor()
})
embeddedVisibilityAnchorObserver.observe(profileVisibilityRoot, { childList: true })
observedProfileVisibilityRoot = profileVisibilityRoot
}

let anchor = profileVisibilityRoot.querySelector<HTMLElement>(`#${EMBEDDED_VISIBILITY_ANCHOR_ID}`)
if (anchor !== null) {
return
}

anchor = document.createElement('div')
anchor.id = EMBEDDED_VISIBILITY_ANCHOR_ID
anchor.className = 'profile-fields-personal-info-visibility-anchor'
const insertionTarget = profileVisibilityRoot.querySelector('.visibility-dropdowns')
if (insertionTarget !== null) {
insertionTarget.appendChild(anchor)
} else {
profileVisibilityRoot.appendChild(anchor)
}
window.dispatchEvent(new CustomEvent('profile-fields:embedded-visibility-anchor-ready'))
}

const scheduleEmbeddedPersonalInfoShellPlacement = () => {
if (embeddedShellPlacementFrame !== 0) {
window.cancelAnimationFrame(embeddedShellPlacementFrame)
}

embeddedShellPlacementFrame = window.requestAnimationFrame(() => {
embeddedShellPlacementFrame = 0
syncEmbeddedPersonalInfoShellPlacement()
})
}

const scheduleEmbeddedPersonalInfoShellPlacementAfterResize = () => {
if (embeddedShellResizeDebounceTimer !== 0) {
window.clearTimeout(embeddedShellResizeDebounceTimer)
}

embeddedShellResizeDebounceTimer = window.setTimeout(() => {
embeddedShellResizeDebounceTimer = 0
scheduleEmbeddedPersonalInfoShellPlacement()
}, 140)
}

const syncEmbeddedPersonalInfoShellPlacement = () => {
const shell = document.querySelector<HTMLElement>('#profile-fields-personal-info-shell')
const personalSettings = document.querySelector<HTMLElement>('#personal-settings')
const profileVisibilitySection = document.querySelector<HTMLElement>('#profile-visibility')?.closest('.personal-settings-section') as HTMLElement | null
if (shell === null || personalSettings === null) {
return
}

ensureEmbeddedVisibilityAnchor()

const useGridPlacement = personalSettingsUsesMultipleColumns(personalSettings)
personalSettings.classList.toggle('profile-fields-personal-info-grid', useGridPlacement)
personalSettings.classList.toggle('profile-fields-personal-info-stacked', !useGridPlacement)
shell.classList.toggle('personal-settings-setting-box', useGridPlacement)
shell.classList.toggle('personal-settings-section', !useGridPlacement)
shell.classList.toggle('profile-fields-personal-info-box--stacked', !useGridPlacement)

if (useGridPlacement) {
const insertionTarget = personalSettings.querySelector('#personal-settings-group-container')
?? personalSettings.querySelector('.msg')

if (insertionTarget !== null) {
personalSettings.insertBefore(shell, insertionTarget)
} else {
personalSettings.appendChild(shell)
}
} else if (profileVisibilitySection !== null && profileVisibilitySection.parentNode !== null) {
profileVisibilitySection.parentNode.insertBefore(shell, profileVisibilitySection)
} else if (personalSettings.parentNode !== null) {
personalSettings.parentNode.insertBefore(shell, personalSettings.nextSibling)
}

shell.style.marginTop = ''

shell.dataset.profileFieldsEmbeddedReady = 'true'
}

const prepareEmbeddedPersonalInfoShell = () => {
scheduleEmbeddedPersonalInfoShellPlacement()
window.addEventListener('resize', scheduleEmbeddedPersonalInfoShellPlacementAfterResize, { passive: true })
window.addEventListener('load', scheduleEmbeddedPersonalInfoShellPlacement, { once: true, passive: true })
}

const ensureEmbeddedProfileAnchor = () => {
const profileSection = document.querySelector('#vue-profile-section section')
Expand Down
Loading
Loading