diff --git a/.github/workflows/behat-sqlite.yml b/.github/workflows/behat-sqlite.yml index fe76835b..eb41f9e7 100644 --- a/.github/workflows/behat-sqlite.yml +++ b/.github/workflows/behat-sqlite.yml @@ -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 @@ -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 diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 4912adf0..5f4d1812 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -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 diff --git a/lib/Migration/Version1001Date20260404010000.php b/lib/Migration/Version1001Date20260404010000.php index fb219a19..6d8637c1 100644 --- a/lib/Migration/Version1001Date20260404010000.php +++ b/lib/Migration/Version1001Date20260404010000.php @@ -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; } diff --git a/lib/Settings/PersonalInfo.php b/lib/Settings/PersonalInfo.php index ce093fc0..d11a6e14 100644 --- a/lib/Settings/PersonalInfo.php +++ b/lib/Settings/PersonalInfo.php @@ -14,6 +14,8 @@ 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'); @@ -21,11 +23,15 @@ public function getForm(): TemplateResponse { #[\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); + } } diff --git a/playwright/e2e/profile-fields.spec.ts b/playwright/e2e/profile-fields.spec.ts index a6018f0a..bf6abb9a 100644 --- a/playwright/e2e/profile-fields.spec.ts +++ b/playwright/e2e/profile-fields.spec.ts @@ -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() @@ -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() @@ -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) } @@ -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) @@ -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') @@ -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) diff --git a/psalm.xml b/psalm.xml index 22e35d0e..fae68114 100644 --- a/psalm.xml +++ b/psalm.xml @@ -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" diff --git a/src/settings-personal.ts b/src/settings-personal.ts index 1f9e5139..c8f81e99 100644 --- a/src/settings-personal.ts +++ b/src/settings-personal.ts @@ -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('#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(`#${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('#profile-fields-personal-info-shell') - const personalSettings = document.querySelector('#personal-settings') - const profileVisibilitySection = document.querySelector('#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') diff --git a/src/tests/utils/embeddedShellPlacement.spec.ts b/src/tests/utils/embeddedShellPlacement.spec.ts new file mode 100644 index 00000000..1af90e89 --- /dev/null +++ b/src/tests/utils/embeddedShellPlacement.spec.ts @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2026 LibreCode coop and LibreCode contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const flushPlacement = async() => { + await new Promise((resolve) => setTimeout(resolve, 0)) + await new Promise((resolve) => setTimeout(resolve, 0)) +} + +const shell = () => document.querySelector('#profile-fields-personal-info-shell') + +const renderShell = (layout: string) => { + document.body.innerHTML = ` + ${layout} +
+
+
+ ` +} + +const prepare = async() => { + const { prepareEmbeddedPersonalInfoShell } = await import('../../utils/embeddedShellPlacement') + + prepareEmbeddedPersonalInfoShell() + await flushPlacement() +} + +describe('embeddedShellPlacement', () => { + beforeEach(() => { + vi.resetModules() + vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => setTimeout(() => callback(0), 0) as unknown as number) + vi.stubGlobal('cancelAnimationFrame', (handle: number) => clearTimeout(handle)) + }) + + afterEach(() => { + vi.unstubAllGlobals() + document.body.innerHTML = '' + }) + + it('joins the single column section introduced in Nextcloud 35', async() => { + renderShell('
') + + await prepare() + + const section = document.querySelector('.settings-column .settings-section') + expect(shell()?.parentElement).toBe(section) + expect(shell()).toBe(section?.lastElementChild) + expect(shell()?.classList.contains('profile-fields-personal-info-box--column')).toBe(true) + expect(shell()?.classList.contains('personal-settings-setting-box')).toBe(false) + }) + + it('waits for the core settings column to be rendered', async() => { + renderShell('
') + + await prepare() + + expect(shell()?.parentElement).toBe(document.body) + + const mountPoint = document.querySelector('#profile-contact-settings') as HTMLElement + mountPoint.outerHTML = '
' + await flushPlacement() + + expect(shell()?.parentElement).toBe(document.querySelector('.settings-section')) + }) + + it('stays last in the settings column when the core renders a late property section', async() => { + renderShell('
') + + await prepare() + + const section = document.querySelector('.settings-section') as HTMLElement + expect(shell()).toBe(section.lastElementChild) + + section.appendChild(document.createElement('div')) + await flushPlacement() + + expect(shell()).toBe(section.lastElementChild) + }) + + it('joins the grid of the legacy personal info section', async() => { + vi.stubGlobal('getComputedStyle', () => ({ gridTemplateColumns: '1fr 1fr' })) + renderShell('
') + + await prepare() + + expect(shell()?.parentElement).toBe(document.querySelector('#personal-settings')) + expect(shell()?.nextElementSibling).toBe(document.querySelector('#personal-settings-group-container')) + expect(shell()?.classList.contains('personal-settings-setting-box')).toBe(true) + expect(shell()?.classList.contains('profile-fields-personal-info-box--column')).toBe(false) + }) + + it('stacks above the visibility section of the legacy narrow layout', async() => { + renderShell(` +
+
+ `) + + await prepare() + + const visibilitySection = document.querySelector('#profile-visibility')?.closest('.personal-settings-section') + expect(shell()?.nextElementSibling).toBe(visibilitySection) + expect(shell()?.classList.contains('profile-fields-personal-info-box--stacked')).toBe(true) + expect(visibilitySection?.querySelector('#profile-fields-personal-visibility-anchor')).not.toBeNull() + }) +}) diff --git a/src/utils/embeddedShellPlacement.ts b/src/utils/embeddedShellPlacement.ts new file mode 100644 index 00000000..01d69fc5 --- /dev/null +++ b/src/utils/embeddedShellPlacement.ts @@ -0,0 +1,163 @@ +// SPDX-FileCopyrightText: 2026 LibreCode coop and LibreCode contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +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 +let coreLayoutObserver: MutationObserver | 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('#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(`#${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 observeCoreLayout = () => { + if (coreLayoutObserver !== null) { + return + } + + coreLayoutObserver = new MutationObserver(() => { + scheduleEmbeddedPersonalInfoShellPlacement() + }) + coreLayoutObserver.observe(document.body ?? document.documentElement, { childList: true, subtree: true }) +} + +const stopObservingCoreLayout = () => { + coreLayoutObserver?.disconnect() + coreLayoutObserver = null +} + +const syncEmbeddedPersonalInfoShellPlacementInColumn = (shell: HTMLElement, settingsColumn: HTMLElement) => { + shell.classList.remove('personal-settings-setting-box', 'personal-settings-section', 'profile-fields-personal-info-box--stacked') + shell.classList.add('profile-fields-personal-info-box--column') + + const insertionTarget = settingsColumn.querySelector('.settings-section') ?? settingsColumn + if (shell.parentElement !== insertionTarget || shell.nextElementSibling !== null) { + insertionTarget.appendChild(shell) + } + + shell.style.marginTop = '' + + shell.dataset.profileFieldsEmbeddedReady = 'true' +} + +const syncEmbeddedPersonalInfoShellPlacement = () => { + const shell = document.querySelector('#profile-fields-personal-info-shell') + if (shell === null) { + return + } + + const settingsColumn = document.querySelector('.settings-column') + if (settingsColumn !== null) { + syncEmbeddedPersonalInfoShellPlacementInColumn(shell, settingsColumn) + + return + } + + const personalSettings = document.querySelector('#personal-settings') + const profileVisibilitySection = document.querySelector('#profile-visibility')?.closest('.personal-settings-section') ?? null + if (personalSettings === null) { + return + } + + stopObservingCoreLayout() + 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' +} + +export const prepareEmbeddedPersonalInfoShell = () => { + observeCoreLayout() + scheduleEmbeddedPersonalInfoShellPlacement() + window.addEventListener('resize', scheduleEmbeddedPersonalInfoShellPlacementAfterResize, { passive: true }) + window.addEventListener('load', scheduleEmbeddedPersonalInfoShellPlacement, { once: true, passive: true }) +} diff --git a/src/views/PersonalSettings.vue b/src/views/PersonalSettings.vue index 64e8891f..163a5d4e 100644 --- a/src/views/PersonalSettings.vue +++ b/src/views/PersonalSettings.vue @@ -811,6 +811,34 @@ onBeforeUnmount(() => { max-inline-size: none; } +.profile-fields-personal-info-box--column { + display: block; + width: 100%; + max-width: 100%; + min-width: 0; + padding: 0; + box-sizing: border-box; +} + +.profile-fields-personal-info-box--column #profile-fields-personal-info-settings > .profile-fields-personal { + display: block; +} + +.profile-fields-personal-info-box--column #profile-fields-personal-info-settings > .profile-fields-personal .profile-fields-personal__grid--embedded { + display: grid; + grid-template-columns: 1fr; + gap: 0; +} + +.profile-fields-personal-info-box--column #profile-fields-personal-info-settings > .profile-fields-personal .profile-fields-personal__card--embedded { + padding: 6px 0; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; + max-inline-size: none; +} +