mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-11-01 05:48:18 +00:00
Merge pull request #1297 from vrtmrz/fix/shared-dialogue-alignment
Refine confirmation and setup dialogue layouts
This commit is contained in:
7 files changed
+230
-14
No files matched your search
Generated
+16
-5
@@ -24,7 +24,7 @@
|
|||||||
"@smithy/util-retry": "^4.4.5",
|
"@smithy/util-retry": "^4.4.5",
|
||||||
"@vrtmrz/browser-ui-kit": "0.1.0",
|
"@vrtmrz/browser-ui-kit": "0.1.0",
|
||||||
"@vrtmrz/livesync-commonlib": "0.2.2",
|
"@vrtmrz/livesync-commonlib": "0.2.2",
|
||||||
"@vrtmrz/obsidian-plugin-kit": "0.1.4",
|
"@vrtmrz/obsidian-plugin-kit": "0.1.6",
|
||||||
"@vrtmrz/ui-interactions": "0.1.2",
|
"@vrtmrz/ui-interactions": "0.1.2",
|
||||||
"@vrtmrz/value-store": "0.1.0",
|
"@vrtmrz/value-store": "0.1.0",
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
@@ -4170,12 +4170,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vrtmrz/obsidian-plugin-kit": {
|
"node_modules/@vrtmrz/obsidian-plugin-kit": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/@vrtmrz/obsidian-plugin-kit/-/obsidian-plugin-kit-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vrtmrz/obsidian-plugin-kit/-/obsidian-plugin-kit-0.1.6.tgz",
|
||||||
"integrity": "sha512-MxZgd7UOr8DXk0e+JsAXJmYj/8bfIoEliI4ruvp1Cu4U+mmVI/p63nZ5we3dDW+EaCgRAgF+ztuVDkcMUk1DwA==",
|
"integrity": "sha512-88HhpN3G53qIQxvsTpe7VE6/unW3YtcZjmqko3Q58yRl/Y+iJDruT1HuEQYEgtkWAB6F2T93z2fhqhvRBXDDWQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vrtmrz/ui-interactions": "0.1.2"
|
"@vrtmrz/secret-store": "0.1.0",
|
||||||
|
"@vrtmrz/ui-interactions": "0.1.2",
|
||||||
|
"@vrtmrz/value-store": "0.1.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"obsidian": ">=1.8.7"
|
"obsidian": ">=1.8.7"
|
||||||
@@ -4195,6 +4197,15 @@
|
|||||||
"playwright": ">=1.50.0"
|
"playwright": ">=1.50.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vrtmrz/secret-store": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vrtmrz/secret-store/-/secret-store-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-9qkJQdt5IRxhPIIQr55bRMaKZjDpF9fLpTtIOsR0Ab90Wl52hj3wsY28EUtgCCRZ1qbXHAgDuplbg0NNMjAolQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vrtmrz/value-store": "0.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vrtmrz/ui-interactions": {
|
"node_modules/@vrtmrz/ui-interactions": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vrtmrz/ui-interactions/-/ui-interactions-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@vrtmrz/ui-interactions/-/ui-interactions-0.1.2.tgz",
|
||||||
|
|||||||
+1
-1
@@ -205,7 +205,7 @@
|
|||||||
"@smithy/util-retry": "^4.4.5",
|
"@smithy/util-retry": "^4.4.5",
|
||||||
"@vrtmrz/browser-ui-kit": "0.1.0",
|
"@vrtmrz/browser-ui-kit": "0.1.0",
|
||||||
"@vrtmrz/livesync-commonlib": "0.2.2",
|
"@vrtmrz/livesync-commonlib": "0.2.2",
|
||||||
"@vrtmrz/obsidian-plugin-kit": "0.1.4",
|
"@vrtmrz/obsidian-plugin-kit": "0.1.6",
|
||||||
"@vrtmrz/ui-interactions": "0.1.2",
|
"@vrtmrz/ui-interactions": "0.1.2",
|
||||||
"@vrtmrz/value-store": "0.1.0",
|
"@vrtmrz/value-store": "0.1.0",
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
|
|||||||
@@ -314,14 +314,16 @@
|
|||||||
{#if processing}
|
{#if processing}
|
||||||
{translateMessage("Checking connection... Please wait.")}
|
{translateMessage("Checking connection... Please wait.")}
|
||||||
{:else}
|
{:else}
|
||||||
|
{#if setupMode === "settings"}
|
||||||
|
<InfoNote warning>
|
||||||
|
{translateMessage(
|
||||||
|
"Saving without a successful connection test keeps this profile, but automatic synchronisation may fail until the connection is corrected."
|
||||||
|
)}
|
||||||
|
</InfoNote>
|
||||||
|
{/if}
|
||||||
<UserDecisions>
|
<UserDecisions>
|
||||||
<Decision title={primaryActionTitle} important disabled={!canProceed} commit={() => checkAndCommit()} />
|
<Decision title={primaryActionTitle} important disabled={!canProceed} commit={() => checkAndCommit()} />
|
||||||
{#if setupMode === "settings"}
|
{#if setupMode === "settings"}
|
||||||
<InfoNote warning>
|
|
||||||
{translateMessage(
|
|
||||||
"Saving without a successful connection test keeps this profile, but automatic synchronisation may fail until the connection is corrected."
|
|
||||||
)}
|
|
||||||
</InfoNote>
|
|
||||||
<Decision
|
<Decision
|
||||||
title={translateMessage("Save without connecting")}
|
title={translateMessage("Save without connecting")}
|
||||||
disabled={!canProceed}
|
disabled={!canProceed}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding-bottom: var(--keyboard-height, 0px);
|
padding-bottom: 0;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
-webkit-user-select: text;
|
-webkit-user-select: text;
|
||||||
}
|
}
|
||||||
@@ -76,6 +76,10 @@
|
|||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dialog-host :global(.button-group > button) {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.dialog-host :global(.button-group) {
|
.dialog-host :global(.button-group) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ Set `showVerboseLog: true` only in isolated plug-in data when a focused investig
|
|||||||
|
|
||||||
Collect evidence before cleanup, and keep process, Vault, profile, and remote-fixture cleanup in `finally`. After `app.emulateMobile(true)`, use the active CDP renderer for fixture operations because Obsidian may remove desktop-only CLI commands. Visually inspect screenshots before copying selected images into user documentation; a passing locator assertion does not establish that a dialogue is readable or unobstructed.
|
Collect evidence before cleanup, and keep process, Vault, profile, and remote-fixture cleanup in `finally`. After `app.emulateMobile(true)`, use the active CDP renderer for fixture operations because Obsidian may remove desktop-only CLI commands. Visually inspect screenshots before copying selected images into user documentation; a passing locator assertion does not establish that a dialogue is readable or unobstructed.
|
||||||
|
|
||||||
|
The dialogue scenario simulates the host keyboard-height variable while the mobile CouchDB setup dialogue is open. It verifies that the space after the final action does not grow with the keyboard height and restores the original variable and scroll position afterwards. It also checks that the warning about saving without a connection test scrolls with the form and leaves room for at least two input rows above the fixed actions. This layout fixture does not open a physical software keyboard.
|
||||||
|
|
||||||
## Local Setup
|
## Local Setup
|
||||||
|
|
||||||
Set `OBSIDIAN_BINARY` when Obsidian is not installed in a standard location. Set `OBSIDIAN_CLI` as well when its companion executable is outside the built-in discovery paths.
|
Set `OBSIDIAN_BINARY` when Obsidian is not installed in a standard location. Set `OBSIDIAN_CLI` as well when its companion executable is outside the built-in discovery paths.
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import type { Page } from "playwright";
|
import type { Locator, Page } from "playwright";
|
||||||
import { decodeSettingsFromSetupURI } from "@vrtmrz/livesync-commonlib/setup-uri";
|
import { decodeSettingsFromSetupURI } from "@vrtmrz/livesync-commonlib/setup-uri";
|
||||||
import { decryptString } from "@vrtmrz/livesync-commonlib/compat/encryption/stringEncryption";
|
import { decryptString } from "@vrtmrz/livesync-commonlib/compat/encryption/stringEncryption";
|
||||||
import { SETTING_KEY_P2P_DEVICE_NAME } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
import { SETTING_KEY_P2P_DEVICE_NAME } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||||
import { $msg } from "../../../src/common/translation.ts";
|
import { $msg } from "../../../src/common/translation.ts";
|
||||||
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
||||||
import { createE2eCouchDbPluginData, waitForLiveSyncCoreReady } from "../runner/liveSyncWorkflow.ts";
|
import { createE2eCouchDbPluginData, waitForLiveSyncCoreReady } from "../runner/liveSyncWorkflow.ts";
|
||||||
import { assertMobileDialogueLayout, assertMobileNoticeLayout, setObsidianMobileTestMode } from "../runner/mobileUi.ts";
|
import {
|
||||||
|
assertMobileDialogueLayout,
|
||||||
|
assertMobileNoticeLayout,
|
||||||
|
iPhoneSafeArea,
|
||||||
|
mobileViewport,
|
||||||
|
setObsidianMobileTestMode,
|
||||||
|
} from "../runner/mobileUi.ts";
|
||||||
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
|
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
|
||||||
import {
|
import {
|
||||||
captureObsidianDialogue,
|
captureObsidianDialogue,
|
||||||
@@ -400,6 +406,185 @@ async function verifyRemoteSizeNoticeAndDialogue(): Promise<{
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function assertDialogueActionAlignment(modal: Locator, mode: DialogueMode, name: string): Promise<void> {
|
||||||
|
const groups = await modal.locator(".dialog-host .button-group").evaluateAll((elements) => {
|
||||||
|
return elements
|
||||||
|
.filter((element) => element.getClientRects().length > 0)
|
||||||
|
.map((element) => {
|
||||||
|
const group = element.getBoundingClientRect();
|
||||||
|
const buttons = Array.from(element.children)
|
||||||
|
.filter((child) => child instanceof HTMLButtonElement && child.getClientRects().length > 0)
|
||||||
|
.map((child) => {
|
||||||
|
const rect = child.getBoundingClientRect();
|
||||||
|
return { left: rect.left, right: rect.right, width: rect.width };
|
||||||
|
});
|
||||||
|
return { group: { left: group.left, right: group.right, width: group.width }, buttons };
|
||||||
|
})
|
||||||
|
.filter((group) => group.buttons.length > 0);
|
||||||
|
});
|
||||||
|
if (groups.length === 0) {
|
||||||
|
throw new Error(`Expected visible action buttons in ${name} (${mode}).`);
|
||||||
|
}
|
||||||
|
console.log(JSON.stringify({ event: "dialogue-action-alignment", name, mode, groups }));
|
||||||
|
for (const { group, buttons } of groups) {
|
||||||
|
for (const button of buttons) {
|
||||||
|
if (Math.abs(button.left - group.left) > 1 || Math.abs(button.right - group.right) > 1) {
|
||||||
|
throw new Error(
|
||||||
|
`Expected ${name} (${mode}) action buttons to align with both edges of their content column: ${JSON.stringify({ group, button })}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertNoExtraKeyboardSpace(page: Page, modal: Locator): Promise<void> {
|
||||||
|
const original = await page.evaluate(() => ({
|
||||||
|
value: document.body.style.getPropertyValue("--keyboard-height"),
|
||||||
|
priority: document.body.style.getPropertyPriority("--keyboard-height"),
|
||||||
|
}));
|
||||||
|
const originalScroll = await modal.locator(".modal-content").evaluate((element) => element.scrollTop);
|
||||||
|
const keyboardHeight = await page.evaluate(() => Math.round(innerHeight * 0.4));
|
||||||
|
let baseline: number | undefined;
|
||||||
|
try {
|
||||||
|
for (const height of [0, keyboardHeight, 0]) {
|
||||||
|
await page.evaluate((value) => document.body.style.setProperty("--keyboard-height", `${value}px`), height);
|
||||||
|
await modal.locator(".modal-content").evaluate((element) => {
|
||||||
|
element.scrollTop = element.scrollHeight;
|
||||||
|
});
|
||||||
|
const gap = await modal.locator(".dialog-host").evaluate((host) => {
|
||||||
|
const buttons = Array.from(host.querySelectorAll("button")).filter(
|
||||||
|
(button) => button.getClientRects().length > 0
|
||||||
|
);
|
||||||
|
const last = buttons[buttons.length - 1];
|
||||||
|
if (last === undefined) throw new Error("The dialogue has no action button.");
|
||||||
|
return host.getBoundingClientRect().bottom - last.getBoundingClientRect().bottom;
|
||||||
|
});
|
||||||
|
console.log(JSON.stringify({ event: "dialogue-keyboard-spacing", height, gap, baseline }));
|
||||||
|
baseline ??= gap;
|
||||||
|
if (Math.abs(gap - baseline) > 1) {
|
||||||
|
throw new Error(
|
||||||
|
`Expected the keyboard not to add blank content after the final action: ${JSON.stringify({ height, gap, baseline })}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await page.evaluate(({ value, priority }) => {
|
||||||
|
if (value === "") document.body.style.removeProperty("--keyboard-height");
|
||||||
|
else document.body.style.setProperty("--keyboard-height", value, priority);
|
||||||
|
}, original);
|
||||||
|
await modal.locator(".modal-content").evaluate((element, top) => {
|
||||||
|
element.scrollTop = top;
|
||||||
|
}, originalScroll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertCouchDBKeyboardFormSpace(page: Page, modal: Locator): Promise<void> {
|
||||||
|
const originalViewport = await page.evaluate(() => ({ width: innerWidth, height: innerHeight }));
|
||||||
|
const properties = ["--keyboard-height", "--safe-area-inset-top", "--safe-area-inset-bottom"];
|
||||||
|
const original = await page.evaluate(
|
||||||
|
(names) =>
|
||||||
|
names.map((name) => ({
|
||||||
|
name,
|
||||||
|
value: document.body.style.getPropertyValue(name),
|
||||||
|
priority: document.body.style.getPropertyPriority(name),
|
||||||
|
})),
|
||||||
|
properties
|
||||||
|
);
|
||||||
|
const content = modal.locator(".modal-content");
|
||||||
|
const originalScroll = await content.evaluate((element) => element.scrollTop);
|
||||||
|
const keyboardHeight = Math.round(mobileViewport.height * 0.42);
|
||||||
|
try {
|
||||||
|
await page.setViewportSize(mobileViewport);
|
||||||
|
await page.evaluate(
|
||||||
|
({ keyboardHeight, top }) => {
|
||||||
|
document.body.style.setProperty("--keyboard-height", `${keyboardHeight}px`);
|
||||||
|
document.body.style.setProperty("--safe-area-inset-bottom", `${keyboardHeight}px`);
|
||||||
|
document.body.style.setProperty("--safe-area-inset-top", `${top}px`);
|
||||||
|
},
|
||||||
|
{ keyboardHeight, top: iPhoneSafeArea.top }
|
||||||
|
);
|
||||||
|
await modal.locator('input[name="couchdb-username"]').focus();
|
||||||
|
await page.waitForTimeout(350);
|
||||||
|
await page.screenshot({
|
||||||
|
path: join(
|
||||||
|
process.env.E2E_OBSIDIAN_DIAGNOSTICS_DIR ?? "/tmp/obsidian-livesync-e2e",
|
||||||
|
"setup-couchdb-keyboard-form.png"
|
||||||
|
),
|
||||||
|
animations: "disabled",
|
||||||
|
style: ".notice-container { visibility: hidden !important; }",
|
||||||
|
});
|
||||||
|
const warning = modal.locator(".sls-info-note").filter({
|
||||||
|
hasText:
|
||||||
|
"Saving without a successful connection test keeps this profile, but automatic synchronisation may fail until the connection is corrected.",
|
||||||
|
});
|
||||||
|
if ((await warning.count()) !== 1) throw new Error("Expected the untested-save warning to remain available.");
|
||||||
|
const pinned = await warning.evaluate((element) => {
|
||||||
|
for (let parent = element.parentElement; parent !== null; parent = parent.parentElement) {
|
||||||
|
if (parent.classList.contains("modal-content")) break;
|
||||||
|
const position = getComputedStyle(parent).position;
|
||||||
|
if (position === "sticky" || position === "fixed") return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
if (pinned) throw new Error("The untested-save warning still occupies the fixed action area.");
|
||||||
|
const bounds = await modal.evaluate((element) => {
|
||||||
|
const host = element.querySelector(".dialog-host");
|
||||||
|
const content = element.querySelector(".modal-content");
|
||||||
|
const actionGroups = host?.querySelectorAll(":scope > .button-group");
|
||||||
|
const footerElement = actionGroups?.item(actionGroups.length - 1);
|
||||||
|
const rowElement = host?.querySelector(".row");
|
||||||
|
if (!host || !content || !footerElement || !rowElement)
|
||||||
|
throw new Error("CouchDB form layout is unavailable.");
|
||||||
|
const footer = footerElement.getBoundingClientRect();
|
||||||
|
const row = rowElement.getBoundingClientRect();
|
||||||
|
const actions = Array.from(footerElement.querySelectorAll("button"), (button) =>
|
||||||
|
button.getBoundingClientRect()
|
||||||
|
);
|
||||||
|
const footerTop = Math.min(footer.top, ...actions.map((action) => action.top));
|
||||||
|
const footerBottom = Math.max(footer.bottom, ...actions.map((action) => action.bottom));
|
||||||
|
const modalElement = element.querySelector(".modal");
|
||||||
|
if (!modalElement) throw new Error("The CouchDB modal is unavailable.");
|
||||||
|
const modalRect = modalElement.getBoundingClientRect();
|
||||||
|
const contentRect = content.getBoundingClientRect();
|
||||||
|
const headerBottom =
|
||||||
|
element.querySelector(".modal-header")?.getBoundingClientRect().bottom ?? modalRect.top;
|
||||||
|
const formTop = Math.max(contentRect.top, modalRect.top, headerBottom);
|
||||||
|
return {
|
||||||
|
width: innerWidth,
|
||||||
|
height: innerHeight,
|
||||||
|
formTop,
|
||||||
|
footerTop,
|
||||||
|
formHeight: Math.min(footerTop, contentRect.bottom, modalRect.bottom) - formTop,
|
||||||
|
rowHeight: row.height + (parseFloat(getComputedStyle(host).rowGap) || 0),
|
||||||
|
footerBottom,
|
||||||
|
actionBounds: actions.map((action) => ({ top: action.top, bottom: action.bottom })),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
console.log(JSON.stringify({ event: "couchdb-keyboard-form-space", keyboardHeight, bounds }));
|
||||||
|
if (
|
||||||
|
bounds.width !== mobileViewport.width ||
|
||||||
|
bounds.height !== mobileViewport.height ||
|
||||||
|
bounds.formHeight < bounds.rowHeight * 2 ||
|
||||||
|
bounds.footerBottom > bounds.height - keyboardHeight + 1
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`Expected room for at least two input rows above keyboard actions: ${JSON.stringify(bounds)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await page.evaluate((entries) => {
|
||||||
|
for (const { name, value, priority } of entries) {
|
||||||
|
if (value === "") document.body.style.removeProperty(name);
|
||||||
|
else document.body.style.setProperty(name, value, priority);
|
||||||
|
}
|
||||||
|
}, original);
|
||||||
|
await page.setViewportSize(originalViewport);
|
||||||
|
await content.evaluate((element, top) => {
|
||||||
|
element.scrollTop = top;
|
||||||
|
}, originalScroll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function verifyRemoteSelectionDialogue(mode: DialogueMode): Promise<string> {
|
async function verifyRemoteSelectionDialogue(mode: DialogueMode): Promise<string> {
|
||||||
await openRemoteSelectionDialogue();
|
await openRemoteSelectionDialogue();
|
||||||
const screenshotPath = await captureObsidianDialogue(
|
const screenshotPath = await captureObsidianDialogue(
|
||||||
@@ -446,6 +631,7 @@ async function verifyRemoteSelectionDialogue(mode: DialogueMode): Promise<string
|
|||||||
await modal
|
await modal
|
||||||
.getByRole("button", { name: "No, please take me back" })
|
.getByRole("button", { name: "No, please take me back" })
|
||||||
.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||||
|
await assertDialogueActionAlignment(modal, mode, "remote selection dialogue");
|
||||||
if (mode === "mobile") {
|
if (mode === "mobile") {
|
||||||
await assertMobileDialogueLayout(page, modal, "remote selection dialogue");
|
await assertMobileDialogueLayout(page, modal, "remote selection dialogue");
|
||||||
}
|
}
|
||||||
@@ -526,8 +712,11 @@ async function verifyCouchDBSettingsDialogue(mode: DialogueMode): Promise<string
|
|||||||
throw new Error(`Expected vertical CouchDB actions, received ${flexDirection}.`);
|
throw new Error(`Expected vertical CouchDB actions, received ${flexDirection}.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await assertDialogueActionAlignment(modal, mode, "CouchDB settings dialogue");
|
||||||
if (mode === "mobile") {
|
if (mode === "mobile") {
|
||||||
await assertMobileDialogueLayout(page, modal, "CouchDB settings dialogue");
|
await assertMobileDialogueLayout(page, modal, "CouchDB settings dialogue");
|
||||||
|
await assertNoExtraKeyboardSpace(page, modal);
|
||||||
|
await assertCouchDBKeyboardFormSpace(page, modal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
- Buttons in confirmation and setup dialogues now align with the content above them. Confirmation dialogue buttons also have space for keyboard focus outlines.
|
||||||
|
|
||||||
|
- Mobile confirmation dialogues now retain Obsidian's standard content spacing. Text and password prompts stay above the software keyboard.
|
||||||
|
|
||||||
|
- Setup dialogues no longer add a large blank area below their buttons when the software keyboard is open.
|
||||||
|
|
||||||
|
- CouchDB connection fields have more room above the software keyboard. The warning about saving without a connection test now scrolls with the form.
|
||||||
|
|
||||||
- A new Join or scheduled Fetch asks for data-handling choices again, even after a previous Fetch changed the local database suffix.
|
- A new Join or scheduled Fetch asks for data-handling choices again, even after a previous Fetch changed the local database suffix.
|
||||||
- Fast Setup retains interrupted choices in an editable Markdown code block in the fetch flag. On restart, we confirm the current remote and saved choices, choose again, or cancel; a remaining flag never reapplies choices silently.
|
- Fast Setup retains interrupted choices in an editable Markdown code block in the fetch flag. On restart, we confirm the current remote and saved choices, choose again, or cancel; a remaining flag never reapplies choices silently.
|
||||||
- If the saved selection cannot be read, we can choose new options for that attempt without repairing or rewriting the flag. Completing or cancelling the operation removes the whole flag.
|
- If the saved selection cannot be read, we can choose new options for that attempt without repairing or rewriting the flag. Completing or cancelling the operation removes the whole flag.
|
||||||
|
|||||||
Reference in new issue
Block a user