mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-08-27 05:47:07 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
011a8405b5 | ||
|
|
f5f7aab11f | ||
|
|
9854319c96 | ||
|
|
4ab0689c7f | ||
|
|
edeac6f7e2 | ||
|
|
783fbb8f23 | ||
|
|
8644af6128 | ||
|
|
4ff5b4dfe8 | ||
|
|
0b1f5ca719 | ||
|
|
9de4f2952d | ||
|
|
330f2acd42 | ||
|
|
1a84b08de5 | ||
|
|
ce7988fe7c | ||
|
|
537e0e42c3 | ||
|
|
a5e1acb960 | ||
|
|
b8191e548d |
@@ -245,6 +245,12 @@ export class ModuleExample extends AbstractObsidianModule {
|
|||||||
|
|
||||||
- Settings are defined by Commonlib (`ObsidianLiveSyncSettings`)
|
- Settings are defined by Commonlib (`ObsidianLiveSyncSettings`)
|
||||||
- Configuration metadata is supplied by the Commonlib settings exports
|
- Configuration metadata is supplied by the Commonlib settings exports
|
||||||
|
- Obsidian may request declarative definitions immediately from
|
||||||
|
`Plugin.addSettingTab()`. Register a settings tab which reads persisted values
|
||||||
|
from the sequential `onSettingLoaded` lifecycle, seed its editing snapshot
|
||||||
|
before registration, and keep definition construction independent of local
|
||||||
|
database and replicator readiness. See
|
||||||
|
[the declarative settings adapter ADR](docs/adr/2026_08_declarative_settings_adapter.md).
|
||||||
- Use `this.services.setting.saveSettingData()` instead of using plugin methods directly
|
- Use `this.services.setting.saveSettingData()` instead of using plugin methods directly
|
||||||
|
|
||||||
### Database Operations
|
### Database Operations
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
date: 2026-08-25
|
date: 2026-08-25
|
||||||
commonlib-version: "0.1.19"
|
commonlib-version: "0.1.19"
|
||||||
self-hosted-livesync-version: "1.0.18"
|
self-hosted-livesync-version: "1.0.20"
|
||||||
status: accepted
|
status: accepted
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -15,7 +15,8 @@ limited to one-key, immediately persisted controls. Complex pages retain their
|
|||||||
existing renderers instead of being forced through a general abstraction.
|
existing renderers instead of being forced through a general abstraction.
|
||||||
Settings pending application which require database initialisation now delegate
|
Settings pending application which require database initialisation now delegate
|
||||||
their decision, scheduling, and restart boundary to `SetupManager` and
|
their decision, scheduling, and restart boundary to `SetupManager` and
|
||||||
`Rebuilder`.
|
`Rebuilder`. Setting-tab registration and definition construction also follow
|
||||||
|
the persisted-settings lifecycle rather than transient runtime readiness.
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
@@ -25,6 +26,15 @@ native rendering, validation, navigation, and global settings search. When the
|
|||||||
method returns a non-empty array, Obsidian does not call the existing
|
method returns a non-empty array, Obsidian does not call the existing
|
||||||
`display()` implementation.
|
`display()` implementation.
|
||||||
|
|
||||||
|
Obsidian may call `getSettingDefinitions()` as soon as a tab is passed to
|
||||||
|
`Plugin.addSettingTab()`. Registering the tab during initialisation therefore
|
||||||
|
allowed definition construction to observe constructor defaults before
|
||||||
|
persisted settings had loaded. The former landing-page predicate also inspected
|
||||||
|
the active replicator, although the local database and replicator are created
|
||||||
|
only after the settings-loaded lifecycle. On start-up this ordering could emit
|
||||||
|
a spurious missing-replicator warning and produce a landing-page order from
|
||||||
|
transient state.
|
||||||
|
|
||||||
Self-hosted LiveSync still supports Obsidian versions before 1.13 through its
|
Self-hosted LiveSync still supports Obsidian versions before 1.13 through its
|
||||||
`minAppVersion` of 1.7.2. It must therefore retain an imperative `display()`
|
`minAppVersion` of 1.7.2. It must therefore retain an imperative `display()`
|
||||||
fallback unless the minimum supported Obsidian version is raised separately.
|
fallback unless the minimum supported Obsidian version is raised separately.
|
||||||
@@ -168,13 +178,13 @@ for narrow mobile displays while preventing the unheaded page entries from
|
|||||||
appearing to continue the preceding Quick Setup group. The root order reflects
|
appearing to continue the preceding Quick Setup group. The root order reflects
|
||||||
the current task:
|
the current task:
|
||||||
|
|
||||||
| Current state | First root sections |
|
| Configuration state | First root sections |
|
||||||
| --------------------------- | ----------------------------------------------------------------------------------- |
|
| ------------------- | --------------------------------------------------------------------------------------------------------- |
|
||||||
| Synchronisation is inactive | Quick Setup, Synchronisation (Remote Configuration and Sync Settings), then General |
|
| Unconfigured | Quick Setup, Synchronisation (Remote Configuration and Sync Settings), then General |
|
||||||
| Synchronisation is active | Synchronisation (Remote Configuration and Sync Settings), General, then Quick Setup |
|
| Configured | Synchronisation (Remote Configuration and Sync Settings), General, Set up other devices, then Quick Setup |
|
||||||
|
|
||||||
Set up other devices follows the Quick Setup and General groups when the
|
Set up other devices is hidden until the plug-in is configured. The remaining
|
||||||
plug-in is configured. The remaining destinations are grouped explicitly:
|
destinations are grouped explicitly:
|
||||||
|
|
||||||
| Group | Pages |
|
| Group | Pages |
|
||||||
| ------------------------ | ---------------------------------------- |
|
| ------------------------ | ---------------------------------------- |
|
||||||
@@ -189,10 +199,11 @@ requests a catalogue refresh after persistence. External setting reloads use
|
|||||||
the same boundary. Constructing the definitions still performs no persistence,
|
the same boundary. Constructing the definitions still performs no persistence,
|
||||||
service, file, database, or network operation.
|
service, file, database, or network operation.
|
||||||
|
|
||||||
The imperative renderer retains its existing default-page selection: Quick Setup for
|
The imperative renderer uses the same stable distinction for its default-page
|
||||||
an inactive configuration and General for an active configuration. The landing
|
selection: Quick Setup for an unconfigured installation and General for a
|
||||||
composition is therefore a native 1.13 improvement rather than a behaviour
|
configured installation. The landing composition is therefore a native 1.13
|
||||||
change for earlier supported Obsidian versions.
|
improvement rather than a separate interpretation of synchronisation state on
|
||||||
|
earlier supported Obsidian versions.
|
||||||
|
|
||||||
The custom `SettingPage` adapter class will be constructed lazily from the
|
The custom `SettingPage` adapter class will be constructed lazily from the
|
||||||
1.13-or-later path. `SettingPage` may remain a normal runtime import because the
|
1.13-or-later path. `SettingPage` may remain a normal runtime import because the
|
||||||
@@ -355,6 +366,23 @@ and side-effect free. Obsidian calls the method during search indexing and
|
|||||||
again on updates; it must perform no file, database, network, or settings
|
again on updates; it must perform no file, database, network, or settings
|
||||||
write.
|
write.
|
||||||
|
|
||||||
|
### Register the setting tab after persisted settings load
|
||||||
|
|
||||||
|
The settings module registers its `PluginSettingTab` from the sequential
|
||||||
|
`onSettingLoaded` lifecycle, not from `onInitialise`. Immediately before
|
||||||
|
registration, it seeds the tab's editing and initial snapshots through
|
||||||
|
`reloadAllSettings(true)`. Skipping the update request is intentional because
|
||||||
|
the tab is not yet owned by Obsidian; `addSettingTab()` may request definitions
|
||||||
|
immediately after this seeding step.
|
||||||
|
|
||||||
|
This lifecycle still precedes local database opening and replicator activation.
|
||||||
|
Definition construction must therefore depend only on the seeded setting
|
||||||
|
snapshot, static catalogue data, and translations. In particular, root-page
|
||||||
|
ordering is based on the persisted `isConfigured` value. It must not inspect
|
||||||
|
automatic synchronisation triggers, the active replicator, replication status,
|
||||||
|
database readiness, files, or the network. Runtime operations remain explicit
|
||||||
|
actions which run after the user selects them.
|
||||||
|
|
||||||
### Give imperative pages an explicit lifetime and refresh boundary
|
### Give imperative pages an explicit lifetime and refresh boundary
|
||||||
|
|
||||||
The present `display()` renders every pane together, so arrays of
|
The present `display()` renders every pane together, so arrays of
|
||||||
@@ -549,7 +577,12 @@ Stage C1 and the landing-page focused unit tests verify:
|
|||||||
identifiers and names;
|
identifiers and names;
|
||||||
- Appearance, Logging, Extra menus, and Advanced are native-items child pages,
|
- Appearance, Logging, Extra menus, and Advanced are native-items child pages,
|
||||||
and ten child pages retain custom factories;
|
and ten child pages retain custom factories;
|
||||||
- inactive and active configurations use their specified landing-page order;
|
- configured and unconfigured installations use their specified landing-page
|
||||||
|
order regardless of transient replication status;
|
||||||
|
- definition construction does not request the active replicator before the
|
||||||
|
database is ready;
|
||||||
|
- the settings tab is registered only after persisted settings load, and its
|
||||||
|
editing snapshot is seeded before registration without requesting a render;
|
||||||
- Remote Configuration and Sync Settings remain native navigable pages inside
|
- Remote Configuration and Sync Settings remain native navigable pages inside
|
||||||
the separate Synchronisation group;
|
the separate Synchronisation group;
|
||||||
- maintenance, extra features, advanced settings, and help have explicit page
|
- maintenance, extra features, advanced settings, and help have explicit page
|
||||||
@@ -626,17 +659,27 @@ persistence of the same Advanced value. The shared E2E navigator owns both the
|
|||||||
separate settings renderer used by Obsidian 1.13 and the legacy
|
separate settings renderer used by Obsidian 1.13 and the legacy
|
||||||
`.sls-setting-menu-btn` interface.
|
`.sls-setting-menu-btn` interface.
|
||||||
|
|
||||||
The Stage C2 landing composition was then exercised on Obsidian 1.13.4. With
|
Before the start-up lifecycle correction, the Stage C2 landing composition was
|
||||||
synchronisation inactive, the real interface rendered Quick Setup, a separate
|
exercised on Obsidian 1.13.4 with a configured installation whose automatic
|
||||||
Synchronisation group containing Remote Configuration and Sync Settings, and a
|
synchronisation triggers were disabled. Under the former predicate, the real
|
||||||
General Settings group containing Appearance, Logging, and Extra menus in the
|
interface rendered Quick Setup, a separate Synchronisation group containing
|
||||||
specified order. It opened all 14 nested settings pages, found the Advanced
|
Remote Configuration and Sync Settings, and a General Settings group containing
|
||||||
control through global settings search, and restored its saved value after
|
Appearance, Logging, and Extra menus in that order. It opened all 14 nested
|
||||||
reopening settings. In mobile test mode, Remote Configuration remained inside
|
settings pages, found the Advanced control through global settings search, and
|
||||||
the initial viewport below the two Quick Setup actions and the Synchronisation
|
restored its saved value after reopening settings. In mobile test mode, Remote
|
||||||
heading. The complete scenario also passed with the same bundle on Obsidian
|
Configuration remained inside the initial viewport below the two Quick Setup
|
||||||
1.12.7, confirming that the imperative fallback retained its navigation and
|
actions and the Synchronisation heading. The complete scenario also passed with
|
||||||
save behaviour.
|
the same bundle on Obsidian 1.12.7, confirming that the imperative fallback
|
||||||
|
retained its navigation and save behaviour.
|
||||||
|
|
||||||
|
The start-up lifecycle correction was subsequently exercised with the same
|
||||||
|
official Obsidian 1.13.4 build. The settings scenario captured and verified the
|
||||||
|
exact configured and unconfigured root-group orders, including Set up other
|
||||||
|
devices before Quick Setup for a configured installation. The same bundle
|
||||||
|
opened General Settings by default through the imperative fallback on Obsidian
|
||||||
|
1.12.7. Focused unit tests own the earlier lifecycle boundary: persisted
|
||||||
|
settings are copied before registration, and definition construction does not
|
||||||
|
request an active replicator.
|
||||||
|
|
||||||
## Expansion Checkpoints
|
## Expansion Checkpoints
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,22 @@
|
|||||||
|
|
||||||
This document contains earlier published releases from the 1.0 line of the [current Self-hosted LiveSync release history](../../updates.md). Beta and release-candidate builds published before 1.0.0 are recorded in the [1.0 preview history](1.0-previews.md). Earlier release lines continue in the [0.25 history](0.25.md) and the [legacy history](legacy.md).
|
This document contains earlier published releases from the 1.0 line of the [current Self-hosted LiveSync release history](../../updates.md). Beta and release-candidate builds published before 1.0.0 are recorded in the [1.0 preview history](1.0-previews.md). Earlier release lines continue in the [0.25 history](0.25.md) and the [legacy history](legacy.md).
|
||||||
|
|
||||||
|
## 1.0.15
|
||||||
|
|
||||||
|
15th August, 2026
|
||||||
|
|
||||||
|
### Synchronisation and storage
|
||||||
|
|
||||||
|
#### Improved
|
||||||
|
|
||||||
|
- Start-up offline scanning is now faster, especially for larger Vaults using path obfuscation (Commonlib 0.1.15).
|
||||||
|
|
||||||
|
### Interface and translation
|
||||||
|
|
||||||
|
#### Improved
|
||||||
|
|
||||||
|
- The Traditional Chinese translation catalogue has been completed and polished for broader coverage and more natural, consistent terminology (PR #1106). Thank you to @nimula for the contribution!
|
||||||
|
|
||||||
## 1.0.14
|
## 1.0.14
|
||||||
|
|
||||||
14th August, 2026
|
14th August, 2026
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ NOTE: This document not completed. I'll improve this doc in a while. but your co
|
|||||||
|
|
||||||
There are many settings in Self-hosted LiveSync. This document describes each setting in detail (not how-to). Configuration and settings are divided into several categories and indicated by icons. The icon is as follows:
|
There are many settings in Self-hosted LiveSync. This document describes each setting in detail (not how-to). Configuration and settings are divided into several categories and indicated by icons. The icon is as follows:
|
||||||
|
|
||||||
On Obsidian 1.13 or later, the root settings page is organised by task. When synchronisation is inactive, **Quick Setup** appears first. Once any synchronisation mode is active, **Synchronisation** and **General Settings** move ahead of **Quick Setup**. **Set up other devices** appears after this plug-in has been configured. Earlier supported Obsidian versions retain a pane-based interface with the same controls.
|
On Obsidian 1.13 or later, the root settings page is organised by task. On an unconfigured installation, **Quick Setup** appears first, followed by **Synchronisation** and **General Settings**. Once this plug-in has been configured, **Synchronisation** and **General Settings** appear first, followed by **Set up other devices** and **Quick Setup**. Earlier supported Obsidian versions retain a pane-based interface with the same controls; they open **Quick Setup** when unconfigured and **General Settings** when configured.
|
||||||
|
|
||||||
| Icon | Root group | Contents or availability |
|
| Icon | Root group | Contents or availability |
|
||||||
| :--: | ------------------------ | ------------------------------------------------------------- |
|
| :--: | ------------------------ | ------------------------------------------------------------- |
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-livesync",
|
"id": "obsidian-livesync",
|
||||||
"name": "Self-hosted LiveSync",
|
"name": "Self-hosted LiveSync",
|
||||||
"version": "1.0.19",
|
"version": "1.0.21",
|
||||||
"minAppVersion": "1.7.2",
|
"minAppVersion": "1.7.2",
|
||||||
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||||
"author": "vorotamoroz",
|
"author": "vorotamoroz",
|
||||||
|
|||||||
Generated
+5
-5
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "1.0.19",
|
"version": "1.0.21",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "1.0.19",
|
"version": "1.0.21",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"src/apps/cli",
|
"src/apps/cli",
|
||||||
@@ -15813,7 +15813,7 @@
|
|||||||
},
|
},
|
||||||
"src/apps/cli": {
|
"src/apps/cli": {
|
||||||
"name": "self-hosted-livesync-cli",
|
"name": "self-hosted-livesync-cli",
|
||||||
"version": "1.0.19-cli",
|
"version": "1.0.21-cli",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^4.0.0",
|
"chokidar": "^4.0.0",
|
||||||
"minimatch": "^10.2.5",
|
"minimatch": "^10.2.5",
|
||||||
@@ -15838,7 +15838,7 @@
|
|||||||
},
|
},
|
||||||
"src/apps/webapp": {
|
"src/apps/webapp": {
|
||||||
"name": "livesync-webapp",
|
"name": "livesync-webapp",
|
||||||
"version": "1.0.19-webapp",
|
"version": "1.0.21-webapp",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"octagonal-wheels": "^0.1.53"
|
"octagonal-wheels": "^0.1.53"
|
||||||
},
|
},
|
||||||
@@ -15850,7 +15850,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/apps/webpeer": {
|
"src/apps/webpeer": {
|
||||||
"version": "1.0.19-webpeer",
|
"version": "1.0.21-webpeer",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"octagonal-wheels": "^0.1.53"
|
"octagonal-wheels": "^0.1.53"
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "1.0.19",
|
"version": "1.0.21",
|
||||||
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "self-hosted-livesync-cli",
|
"name": "self-hosted-livesync-cli",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.19-cli",
|
"version": "1.0.21-cli",
|
||||||
"main": "dist/index.cjs",
|
"main": "dist/index.cjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "livesync-webapp",
|
"name": "livesync-webapp",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.19-webapp",
|
"version": "1.0.21-webapp",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Browser-based Self-hosted LiveSync using FileSystem API",
|
"description": "Browser-based Self-hosted LiveSync using FileSystem API",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "webpeer",
|
"name": "webpeer",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.19-webpeer",
|
"version": "1.0.21-webpeer",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import { openObsidianSettings } from "@/common/obsidianSettings.ts";
|
|||||||
export class ModuleObsidianSettingDialogue extends AbstractObsidianModule {
|
export class ModuleObsidianSettingDialogue extends AbstractObsidianModule {
|
||||||
settingTab!: ObsidianLiveSyncSettingTab;
|
settingTab!: ObsidianLiveSyncSettingTab;
|
||||||
|
|
||||||
_everyOnloadStart(): Promise<boolean> {
|
_everyOnloadAfterLoadSettings(): Promise<boolean> {
|
||||||
this.settingTab = new ObsidianLiveSyncSettingTab(this.app, this.plugin);
|
this.settingTab = new ObsidianLiveSyncSettingTab(this.app, this.plugin);
|
||||||
|
this.settingTab.reloadAllSettings(true);
|
||||||
this.plugin.addSettingTab(this.settingTab);
|
this.plugin.addSettingTab(this.settingTab);
|
||||||
eventHub.onEvent(EVENT_REQUEST_OPEN_SETTINGS, () => this.openSetting());
|
eventHub.onEvent(EVENT_REQUEST_OPEN_SETTINGS, () => this.openSetting());
|
||||||
|
|
||||||
@@ -24,6 +25,6 @@ export class ModuleObsidianSettingDialogue extends AbstractObsidianModule {
|
|||||||
return `${"appId" in this.app ? this.app.appId : ""}`;
|
return `${"appId" in this.app ? this.app.appId : ""}`;
|
||||||
}
|
}
|
||||||
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
override onBindFunction(core: LiveSyncCore, services: typeof core.services): void {
|
||||||
services.appLifecycle.onInitialise.addHandler(this._everyOnloadStart.bind(this));
|
services.appLifecycle.onSettingLoaded.addHandler(this._everyOnloadAfterLoadSettings.bind(this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const settingTabState = vi.hoisted(() => ({
|
||||||
|
callOrder: [] as string[],
|
||||||
|
reloadAllSettings: vi.fn<(skipUpdate?: boolean) => void>(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const eventHubState = vi.hoisted(() => ({
|
||||||
|
onEvent: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./SettingDialogue/ObsidianLiveSyncSettingTab.ts", () => ({
|
||||||
|
ObsidianLiveSyncSettingTab: class ObsidianLiveSyncSettingTab {
|
||||||
|
reloadAllSettings(skipUpdate?: boolean) {
|
||||||
|
settingTabState.callOrder.push(`reload:${String(skipUpdate)}`);
|
||||||
|
settingTabState.reloadAllSettings(skipUpdate);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/common/events.ts", () => ({
|
||||||
|
EVENT_REQUEST_OPEN_SETTINGS: "request-open-settings",
|
||||||
|
eventHub: eventHubState,
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { ModuleObsidianSettingDialogue } from "./ModuleObsidianSettingTab.ts";
|
||||||
|
|
||||||
|
function createModuleHarness() {
|
||||||
|
let initialisationHandler: (() => Promise<boolean>) | undefined;
|
||||||
|
let settingsLoadedHandler: (() => Promise<boolean>) | undefined;
|
||||||
|
const plugin = {
|
||||||
|
app: {},
|
||||||
|
addSettingTab: vi.fn(() => settingTabState.callOrder.push("add-setting-tab")),
|
||||||
|
};
|
||||||
|
const services = {
|
||||||
|
appLifecycle: {
|
||||||
|
onInitialise: {
|
||||||
|
addHandler: vi.fn((handler: () => Promise<boolean>) => {
|
||||||
|
initialisationHandler = handler;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
onSettingLoaded: {
|
||||||
|
addHandler: vi.fn((handler: () => Promise<boolean>) => {
|
||||||
|
settingsLoadedHandler = handler;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const module = Object.assign(Object.create(ModuleObsidianSettingDialogue.prototype), {
|
||||||
|
plugin,
|
||||||
|
core: { services },
|
||||||
|
}) as ModuleObsidianSettingDialogue;
|
||||||
|
|
||||||
|
module.onBindFunction(module.core as never, services as never);
|
||||||
|
|
||||||
|
return {
|
||||||
|
initialisationHandler: () => initialisationHandler,
|
||||||
|
module,
|
||||||
|
plugin,
|
||||||
|
services,
|
||||||
|
settingsLoadedHandler: () => settingsLoadedHandler,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("ModuleObsidianSettingDialogue startup lifecycle", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
settingTabState.callOrder.length = 0;
|
||||||
|
settingTabState.reloadAllSettings.mockClear();
|
||||||
|
eventHubState.onEvent.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("registers the setting tab after persisted settings have loaded", () => {
|
||||||
|
const { initialisationHandler, services, settingsLoadedHandler } = createModuleHarness();
|
||||||
|
|
||||||
|
expect(services.appLifecycle.onInitialise.addHandler).not.toHaveBeenCalled();
|
||||||
|
expect(services.appLifecycle.onSettingLoaded.addHandler).toHaveBeenCalledOnce();
|
||||||
|
expect(initialisationHandler()).toBeUndefined();
|
||||||
|
expect(settingsLoadedHandler()).toBeTypeOf("function");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("seeds the setting editor without requesting a render before registration", async () => {
|
||||||
|
const { initialisationHandler, settingsLoadedHandler } = createModuleHarness();
|
||||||
|
const handler = settingsLoadedHandler() ?? initialisationHandler();
|
||||||
|
|
||||||
|
expect(handler).toBeTypeOf("function");
|
||||||
|
await handler!();
|
||||||
|
|
||||||
|
expect(settingTabState.reloadAllSettings).toHaveBeenCalledWith(true);
|
||||||
|
expect(settingTabState.callOrder).toEqual(["reload:true", "add-setting-tab"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -24,7 +24,8 @@ import {
|
|||||||
type AllBooleanItemKey,
|
type AllBooleanItemKey,
|
||||||
} from "./settingConstants.ts";
|
} from "./settingConstants.ts";
|
||||||
import { $msg } from "@/common/translation";
|
import { $msg } from "@/common/translation";
|
||||||
import { setButtonDestructiveState, wrapMemo, type AutoWireOption, type OnUpdateResult } from "./SettingPane.ts";
|
import { wrapMemo, type AutoWireOption, type OnUpdateResult } from "./SettingPane.ts";
|
||||||
|
import { setButtonDestructiveState } from "./settingComponentStyles.ts";
|
||||||
|
|
||||||
export class LiveSyncSetting extends Setting {
|
export class LiveSyncSetting extends Setting {
|
||||||
autoWiredComponent?: TextComponent | ToggleComponent | DropdownComponent | ButtonComponent | TextAreaComponent;
|
autoWiredComponent?: TextComponent | ToggleComponent | DropdownComponent | ButtonComponent | TextAreaComponent;
|
||||||
|
|||||||
+49
-15
@@ -145,22 +145,36 @@ function findPage(tab: ObsidianLiveSyncSettingTab, name: string): SettingDefinit
|
|||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSettingsTab(): ObsidianLiveSyncSettingTab {
|
type SettingsTabOptions = {
|
||||||
const plugin = {
|
activeReplicatorGetter?: () => { syncStatus: "CONNECTED" | "PAUSED" } | undefined;
|
||||||
app: {},
|
replicationStatus?: "CLOSED" | "CONNECTED" | "PAUSED";
|
||||||
core: {
|
};
|
||||||
settings: { ...DEFAULT_SETTINGS, useAdvancedMode: true },
|
|
||||||
confirm: {
|
function createSettingsTab(options: SettingsTabOptions = {}): ObsidianLiveSyncSettingTab {
|
||||||
askInPopup: vi.fn(),
|
const core = {
|
||||||
|
settings: { ...DEFAULT_SETTINGS, useAdvancedMode: true },
|
||||||
|
confirm: {
|
||||||
|
askInPopup: vi.fn(),
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
setting: {
|
||||||
|
getDeviceAndVaultName: vi.fn(() => ""),
|
||||||
|
saveSettingData: vi.fn(async () => undefined),
|
||||||
},
|
},
|
||||||
services: {
|
replicator: {
|
||||||
setting: {
|
replicationStatics: {
|
||||||
getDeviceAndVaultName: vi.fn(() => ""),
|
value: { syncStatus: options.replicationStatus ?? "CLOSED" },
|
||||||
saveSettingData: vi.fn(async () => undefined),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
Object.defineProperty(core, "replicator", {
|
||||||
|
get: options.activeReplicatorGetter ?? (() => undefined),
|
||||||
|
});
|
||||||
|
const plugin = {
|
||||||
|
app: {},
|
||||||
|
core,
|
||||||
|
};
|
||||||
const tab = new ObsidianLiveSyncSettingTab({} as never, plugin as never);
|
const tab = new ObsidianLiveSyncSettingTab({} as never, plugin as never);
|
||||||
Object.assign(tab, {
|
Object.assign(tab, {
|
||||||
_editingSettings: { ...DEFAULT_SETTINGS, useAdvancedMode: true },
|
_editingSettings: { ...DEFAULT_SETTINGS, useAdvancedMode: true },
|
||||||
@@ -181,8 +195,27 @@ beforeEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("ObsidianLiveSyncSettingTab native page lifecycle", () => {
|
describe("ObsidianLiveSyncSettingTab native page lifecycle", () => {
|
||||||
it("keeps Quick Setup first while synchronisation is inactive and separates synchronisation pages from it", () => {
|
it("builds definitions before database readiness without requesting the active replicator", () => {
|
||||||
|
const activeReplicatorGetter = vi.fn(() => {
|
||||||
|
throw new Error("The active replicator is not ready");
|
||||||
|
});
|
||||||
|
const tab = createSettingsTab({ activeReplicatorGetter });
|
||||||
|
|
||||||
|
expect(() => tab.getSettingDefinitions()).not.toThrow();
|
||||||
|
expect(activeReplicatorGetter).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps Quick Setup first while LiveSync is not configured, regardless of transient replication status", () => {
|
||||||
|
const tab = createSettingsTab({ replicationStatus: "CONNECTED" });
|
||||||
|
tab.editingSettings.isConfigured = false;
|
||||||
|
const definitions = tab.getSettingDefinitions().filter(isGroup);
|
||||||
|
|
||||||
|
expect(definitions[0]?.heading).toBe("🧙♂️ Quick Setup");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps Quick Setup first while LiveSync is not configured and separates synchronisation pages from it", () => {
|
||||||
const tab = createSettingsTab();
|
const tab = createSettingsTab();
|
||||||
|
tab.editingSettings.isConfigured = false;
|
||||||
const definitions = tab.getSettingDefinitions().filter(isGroup);
|
const definitions = tab.getSettingDefinitions().filter(isGroup);
|
||||||
|
|
||||||
expect(definitions.slice(0, 3).map(itemLabel)).toEqual([
|
expect(definitions.slice(0, 3).map(itemLabel)).toEqual([
|
||||||
@@ -192,14 +225,15 @@ describe("ObsidianLiveSyncSettingTab native page lifecycle", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps the synchronisation group first and orders General Settings before Quick Setup while synchronisation is active", () => {
|
it("keeps the synchronisation group first for a configured device with automatic triggers disabled", () => {
|
||||||
const tab = createSettingsTab();
|
const tab = createSettingsTab();
|
||||||
tab.editingSettings.liveSync = true;
|
tab.editingSettings.isConfigured = true;
|
||||||
const definitions = tab.getSettingDefinitions().filter(isGroup);
|
const definitions = tab.getSettingDefinitions().filter(isGroup);
|
||||||
|
|
||||||
expect(definitions.slice(0, 3).map(itemLabel)).toEqual([
|
expect(definitions.slice(0, 4).map(itemLabel)).toEqual([
|
||||||
"🔄 Synchronisation",
|
"🔄 Synchronisation",
|
||||||
"⚙️ General Settings",
|
"⚙️ General Settings",
|
||||||
|
"📲 Set up other devices",
|
||||||
"🧙♂️ Quick Setup",
|
"🧙♂️ Quick Setup",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ import {
|
|||||||
eventHub,
|
eventHub,
|
||||||
} from "@/common/events.ts";
|
} from "@/common/events.ts";
|
||||||
import {
|
import {
|
||||||
enableOnly,
|
|
||||||
// findAttrFromParent,
|
// findAttrFromParent,
|
||||||
// getLevelStr,
|
// getLevelStr,
|
||||||
setLevelClass,
|
setLevelClass,
|
||||||
@@ -587,19 +586,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
"encrypt",
|
"encrypt",
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
isAnySyncEnabled() {
|
isLiveSyncConfigured() {
|
||||||
if (this.isConfiguredAs("isConfigured", false)) return false;
|
return this.isConfiguredAs("isConfigured", true);
|
||||||
if (this.isConfiguredAs("liveSync", true)) return true;
|
|
||||||
if (this.isConfiguredAs("periodicReplication", true)) return true;
|
|
||||||
if (this.isConfiguredAs("syncOnFileOpen", true)) return true;
|
|
||||||
if (this.isConfiguredAs("syncOnSave", true)) return true;
|
|
||||||
if (this.isConfiguredAs("syncOnEditorSave", true)) return true;
|
|
||||||
if (this.isConfiguredAs("syncOnStart", true)) return true;
|
|
||||||
if (this.isConfiguredAs("syncAfterMerge", true)) return true;
|
|
||||||
if (this.isConfiguredAs("syncOnFileOpen", true)) return true;
|
|
||||||
if (this.core?.replicator?.syncStatus == "CONNECTED") return true;
|
|
||||||
if (this.core?.replicator?.syncStatus == "PAUSED") return true;
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private supportsDeclarativeSettings(): boolean {
|
private supportsDeclarativeSettings(): boolean {
|
||||||
@@ -905,13 +893,20 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
getPage("help"),
|
getPage("help"),
|
||||||
getPage("change-log"),
|
getPage("change-log"),
|
||||||
]);
|
]);
|
||||||
const laterGroups = [setupOtherDevices, maintenance, extraFeatures, advancedSettings, helpAndInformation];
|
const laterGroups = [maintenance, extraFeatures, advancedSettings, helpAndInformation];
|
||||||
|
|
||||||
const pendingInitialisation = this.createRebuildRequiredAction();
|
const pendingInitialisation = this.createRebuildRequiredAction();
|
||||||
if (this.isAnySyncEnabled()) {
|
if (this.isLiveSyncConfigured()) {
|
||||||
return [pendingInitialisation, synchronisation, generalSettings, quickSetup, ...laterGroups];
|
return [
|
||||||
|
pendingInitialisation,
|
||||||
|
synchronisation,
|
||||||
|
generalSettings,
|
||||||
|
setupOtherDevices,
|
||||||
|
quickSetup,
|
||||||
|
...laterGroups,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
return [pendingInitialisation, quickSetup, synchronisation, generalSettings, ...laterGroups];
|
return [pendingInitialisation, quickSetup, synchronisation, generalSettings, setupOtherDevices, ...laterGroups];
|
||||||
}
|
}
|
||||||
|
|
||||||
private beginRenderScope(refresh: () => void): Component {
|
private beginRenderScope(refresh: () => void): Component {
|
||||||
@@ -936,8 +931,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
this.controlledElementFunc.length = 0;
|
this.controlledElementFunc.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
enableOnlySyncDisabled = enableOnly(() => !this.isAnySyncEnabled());
|
|
||||||
|
|
||||||
onlyOnP2POrCouchDB = () =>
|
onlyOnP2POrCouchDB = () =>
|
||||||
({
|
({
|
||||||
visibility:
|
visibility:
|
||||||
@@ -1184,7 +1177,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
|
|
||||||
void yieldNextAnimationFrame().then(() => {
|
void yieldNextAnimationFrame().then(() => {
|
||||||
if (this.selectedScreen == "") {
|
if (this.selectedScreen == "") {
|
||||||
if (this.isAnySyncEnabled()) {
|
if (this.isLiveSyncConfigured()) {
|
||||||
changeDisplay("20");
|
changeDisplay("20");
|
||||||
} else {
|
} else {
|
||||||
changeDisplay("110");
|
changeDisplay("110");
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ import {
|
|||||||
import { HiddenFileSync } from "@/features/HiddenFileSync/CmdHiddenFileSync.ts";
|
import { HiddenFileSync } from "@/features/HiddenFileSync/CmdHiddenFileSync.ts";
|
||||||
import { EVENT_REQUEST_SHOW_HISTORY } from "@/common/obsidianEvents.ts";
|
import { EVENT_REQUEST_SHOW_HISTORY } from "@/common/obsidianEvents.ts";
|
||||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||||
import { setButtonDestructiveState, type PageFunctions } from "./SettingPane.ts";
|
import type { PageFunctions } from "./SettingPane.ts";
|
||||||
|
import { setButtonDestructiveState } from "./settingComponentStyles.ts";
|
||||||
import { isNotFoundError } from "@vrtmrz/livesync-commonlib/compat/common/utils.doc";
|
import { isNotFoundError } from "@vrtmrz/livesync-commonlib/compat/common/utils.doc";
|
||||||
import {
|
import {
|
||||||
chooseAndCopyFileDatabaseInfo,
|
chooseAndCopyFileDatabaseInfo,
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import { fireAndForget } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
|||||||
import { LiveSyncCouchDBReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator";
|
import { LiveSyncCouchDBReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator";
|
||||||
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
||||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
||||||
import { setButtonDestructiveState, visibleOnly, type PageFunctions } from "./SettingPane";
|
import { visibleOnly, type PageFunctions } from "./SettingPane";
|
||||||
|
import { setButtonDestructiveState } from "./settingComponentStyles.ts";
|
||||||
export function paneMaintenance(
|
export function paneMaintenance(
|
||||||
this: ObsidianLiveSyncSettingTab,
|
this: ObsidianLiveSyncSettingTab,
|
||||||
paneEl: HTMLElement,
|
paneEl: HTMLElement,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
|||||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||||
import type { PageFunctions } from "./SettingPane.ts";
|
import type { PageFunctions } from "./SettingPane.ts";
|
||||||
import { visibleOnly } from "./SettingPane.ts";
|
import { visibleOnly } from "./SettingPane.ts";
|
||||||
|
import { setButtonAdditionalActionState, setSettingAdditionalActionsState } from "./settingComponentStyles.ts";
|
||||||
import { PouchDB } from "@vrtmrz/livesync-commonlib/compat/pouchdb/pouchdb-browser";
|
import { PouchDB } from "@vrtmrz/livesync-commonlib/compat/pouchdb/pouchdb-browser";
|
||||||
import { ExtraSuffixIndexedDB } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
import { ExtraSuffixIndexedDB } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||||
import { migrateDatabases } from "./settingUtils.ts";
|
import { migrateDatabases } from "./settingUtils.ts";
|
||||||
@@ -177,7 +178,7 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen
|
|||||||
new Setting(paneEl).autoWireToggle("disableCheckingConfigMismatch");
|
new Setting(paneEl).autoWireToggle("disableCheckingConfigMismatch");
|
||||||
});
|
});
|
||||||
void addPanel(paneEl, "Remediation").then((paneEl) => {
|
void addPanel(paneEl, "Remediation").then((paneEl) => {
|
||||||
const setting = new Setting(paneEl);
|
const setting = setSettingAdditionalActionsState(new Setting(paneEl));
|
||||||
const dateEl = setting.controlEl.createSpan();
|
const dateEl = setting.controlEl.createSpan();
|
||||||
setting
|
setting
|
||||||
.addText((text) => {
|
.addText((text) => {
|
||||||
@@ -215,6 +216,9 @@ export function panePatches(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElemen
|
|||||||
})
|
})
|
||||||
.setAuto("maxMTimeForReflectEvents")
|
.setAuto("maxMTimeForReflectEvents")
|
||||||
.addApplyButton(["maxMTimeForReflectEvents"]);
|
.addApplyButton(["maxMTimeForReflectEvents"]);
|
||||||
|
if (setting.applyButtonComponent) {
|
||||||
|
setButtonAdditionalActionState(setting.applyButtonComponent);
|
||||||
|
}
|
||||||
|
|
||||||
this.addOnSaved("maxMTimeForReflectEvents", async (key) => {
|
this.addOnSaved("maxMTimeForReflectEvents", async (key) => {
|
||||||
const buttons = ["Restart Now", "Later"] as const;
|
const buttons = ["Restart Now", "Later"] as const;
|
||||||
|
|||||||
@@ -12,17 +12,33 @@ const remediationHarness = vi.hoisted(() => {
|
|||||||
onChange: vi.fn(),
|
onChange: vi.fn(),
|
||||||
setValue: vi.fn(),
|
setValue: vi.fn(),
|
||||||
};
|
};
|
||||||
|
const setButtonClassState = vi.fn();
|
||||||
|
const setSettingClassState = vi.fn();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createSpan,
|
createSpan,
|
||||||
dateElement,
|
dateElement,
|
||||||
inputEl,
|
inputEl,
|
||||||
|
setButtonClassState,
|
||||||
|
setSettingClassState,
|
||||||
textComponent,
|
textComponent,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock("./LiveSyncSetting.ts", () => ({
|
vi.mock("./LiveSyncSetting.ts", () => ({
|
||||||
LiveSyncSetting: class LiveSyncSetting {
|
LiveSyncSetting: class LiveSyncSetting {
|
||||||
|
applyButtonComponent = {
|
||||||
|
buttonEl: {
|
||||||
|
classList: {
|
||||||
|
toggle: remediationHarness.setButtonClassState,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
settingEl = {
|
||||||
|
classList: {
|
||||||
|
toggle: remediationHarness.setSettingClassState,
|
||||||
|
},
|
||||||
|
};
|
||||||
controlEl = {
|
controlEl = {
|
||||||
createSpan: remediationHarness.createSpan,
|
createSpan: remediationHarness.createSpan,
|
||||||
};
|
};
|
||||||
@@ -93,5 +109,10 @@ describe("panePatches remediation setting", () => {
|
|||||||
expect(createSpan).not.toHaveBeenCalled();
|
expect(createSpan).not.toHaveBeenCalled();
|
||||||
expect(remediationHarness.createSpan).toHaveBeenCalledOnce();
|
expect(remediationHarness.createSpan).toHaveBeenCalledOnce();
|
||||||
expect(remediationHarness.dateElement.textContent).toBe("No limit configured");
|
expect(remediationHarness.dateElement.textContent).toBe("No limit configured");
|
||||||
|
expect(remediationHarness.setSettingClassState).toHaveBeenCalledWith(
|
||||||
|
"sls-setting-with-additional-actions",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(remediationHarness.setButtonClassState).toHaveBeenCalledWith("sls-setting-additional-action", true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,12 @@ import { Menu, type ButtonComponent } from "@/deps.ts";
|
|||||||
import { $msg } from "@/common/translation";
|
import { $msg } from "@/common/translation";
|
||||||
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
||||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||||
import { setButtonDestructiveState, type PageFunctions } from "./SettingPane.ts";
|
import {
|
||||||
|
setButtonAdditionalActionState,
|
||||||
|
setButtonDestructiveState,
|
||||||
|
setSettingAdditionalActionsState,
|
||||||
|
} from "./settingComponentStyles.ts";
|
||||||
|
import type { PageFunctions } from "./SettingPane.ts";
|
||||||
// import { visibleOnly } from "./SettingPane.ts";
|
// import { visibleOnly } from "./SettingPane.ts";
|
||||||
import InfoPanel from "./InfoPanel.svelte";
|
import InfoPanel from "./InfoPanel.svelte";
|
||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
@@ -105,7 +110,7 @@ export function paneRemoteConfig(
|
|||||||
void addPanel(paneEl, "E2EE Configuration", () => {}).then((paneEl) => {
|
void addPanel(paneEl, "E2EE Configuration", () => {}).then((paneEl) => {
|
||||||
const infoPanel = new SveltePanel(InfoPanel, paneEl, E2EESummaryWritable);
|
const infoPanel = new SveltePanel(InfoPanel, paneEl, E2EESummaryWritable);
|
||||||
this.lifetimeComponent.register(() => infoPanel.destroy());
|
this.lifetimeComponent.register(() => infoPanel.destroy());
|
||||||
const setupButton = new Setting(paneEl).setName("Configure E2EE");
|
const setupButton = setSettingAdditionalActionsState(new Setting(paneEl).setName("Configure E2EE"));
|
||||||
setupButton
|
setupButton
|
||||||
.addButton((button) =>
|
.addButton((button) =>
|
||||||
setButtonDestructiveState(button)
|
setButtonDestructiveState(button)
|
||||||
@@ -118,7 +123,7 @@ export function paneRemoteConfig(
|
|||||||
.setButtonText("Configure")
|
.setButtonText("Configure")
|
||||||
)
|
)
|
||||||
.addButton((button) =>
|
.addButton((button) =>
|
||||||
setButtonDestructiveState(button)
|
setButtonDestructiveState(setButtonAdditionalActionState(button))
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
const setupManager = this.core.getModule(SetupManager);
|
const setupManager = this.core.getModule(SetupManager);
|
||||||
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
|
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
const runtime = vi.hoisted(() => ({
|
const runtime = vi.hoisted(() => ({
|
||||||
|
buttonClasses: [] as string[],
|
||||||
panels: [] as Array<{ destroy: ReturnType<typeof vi.fn> }>,
|
panels: [] as Array<{ destroy: ReturnType<typeof vi.fn> }>,
|
||||||
|
settingClasses: [] as string[],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@vrtmrz/livesync-commonlib/compat/common/types", () => ({
|
vi.mock("@vrtmrz/livesync-commonlib/compat/common/types", () => ({
|
||||||
@@ -21,6 +23,13 @@ vi.mock("@/common/translation", () => ({
|
|||||||
vi.mock("./LiveSyncSetting.ts", () => ({
|
vi.mock("./LiveSyncSetting.ts", () => ({
|
||||||
LiveSyncSetting: class {
|
LiveSyncSetting: class {
|
||||||
nameEl = { addClass: vi.fn(), appendText: vi.fn() };
|
nameEl = { addClass: vi.fn(), appendText: vi.fn() };
|
||||||
|
settingEl = {
|
||||||
|
classList: {
|
||||||
|
toggle: (value: string, enabled: boolean) => {
|
||||||
|
if (enabled) runtime.settingClasses.push(value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
setName() {
|
setName() {
|
||||||
return this;
|
return this;
|
||||||
@@ -30,7 +39,26 @@ vi.mock("./LiveSyncSetting.ts", () => ({
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
addButton() {
|
addButton(callback: (button: unknown) => void) {
|
||||||
|
const button = {
|
||||||
|
buttonEl: {
|
||||||
|
classList: {
|
||||||
|
toggle: (value: string, enabled: boolean) => {
|
||||||
|
if (enabled) runtime.buttonClasses.push(value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setDestructive() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
onClick() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
setButtonText() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
callback(button);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +113,9 @@ function createPanelElement(): HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
runtime.buttonClasses.length = 0;
|
||||||
runtime.panels.length = 0;
|
runtime.panels.length = 0;
|
||||||
|
runtime.settingClasses.length = 0;
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -96,7 +126,13 @@ describe("paneRemoteConfig", () => {
|
|||||||
register: vi.fn((callback: () => unknown) => callbacks.push(callback)),
|
register: vi.fn((callback: () => unknown) => callbacks.push(callback)),
|
||||||
unload: vi.fn(() => callbacks.splice(0).forEach((callback) => callback())),
|
unload: vi.fn(() => callbacks.splice(0).forEach((callback) => callback())),
|
||||||
};
|
};
|
||||||
const addPanel = vi.fn((_parent: HTMLElement, _heading: string) => Promise.resolve(createPanelElement()));
|
const addPanel = vi.fn((_parent: HTMLElement, heading: string) => ({
|
||||||
|
then(callback: (paneEl: HTMLElement) => void) {
|
||||||
|
if (heading === "E2EE Configuration") {
|
||||||
|
callback(createPanelElement());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
const host = {
|
const host = {
|
||||||
editingSettings: { remoteConfigurations: {} },
|
editingSettings: { remoteConfigurations: {} },
|
||||||
core: { settings: { remoteConfigurations: {} } },
|
core: { settings: { remoteConfigurations: {} } },
|
||||||
@@ -105,6 +141,8 @@ describe("paneRemoteConfig", () => {
|
|||||||
|
|
||||||
paneRemoteConfig.call(host as never, {} as HTMLElement, { addPanel } as never);
|
paneRemoteConfig.call(host as never, {} as HTMLElement, { addPanel } as never);
|
||||||
await vi.waitFor(() => expect(runtime.panels).toHaveLength(1));
|
await vi.waitFor(() => expect(runtime.panels).toHaveLength(1));
|
||||||
|
expect(runtime.settingClasses).toContain("sls-setting-with-additional-actions");
|
||||||
|
expect(runtime.buttonClasses).toEqual(["sls-setting-additional-action"]);
|
||||||
|
|
||||||
lifetimeComponent.unload();
|
lifetimeComponent.unload();
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
type ConfigLevel,
|
type ConfigLevel,
|
||||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||||
import type { AllSettingItemKey, AllSettings } from "./settingConstants";
|
import type { AllSettingItemKey, AllSettings } from "./settingConstants";
|
||||||
import type { ButtonComponent } from "@/deps.ts";
|
|
||||||
|
|
||||||
export const combineOnUpdate = (func1: OnUpdateFunc, func2: OnUpdateFunc): OnUpdateFunc => {
|
export const combineOnUpdate = (func1: OnUpdateFunc, func2: OnUpdateFunc): OnUpdateFunc => {
|
||||||
return () => ({
|
return () => ({
|
||||||
@@ -39,25 +38,6 @@ export function setStyle(el: HTMLElement, styleHead: string, condition: () => bo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies destructive-action styling without requiring Obsidian 1.13 at
|
|
||||||
* runtime. Older supported versions used the `mod-warning` class for the same
|
|
||||||
* presentation.
|
|
||||||
*/
|
|
||||||
export function setButtonDestructiveState(button: ButtonComponent, isDestructive = true): ButtonComponent {
|
|
||||||
const compatibleButton = button as unknown as {
|
|
||||||
setDestructive?: () => ButtonComponent;
|
|
||||||
removeDestructive?: () => ButtonComponent;
|
|
||||||
};
|
|
||||||
const updateNativeStyle = isDestructive ? compatibleButton.setDestructive : compatibleButton.removeDestructive;
|
|
||||||
if (typeof updateNativeStyle === "function") {
|
|
||||||
updateNativeStyle.call(button);
|
|
||||||
} else {
|
|
||||||
button.buttonEl.classList.toggle("mod-warning", isDestructive);
|
|
||||||
}
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function visibleOnly(cond: () => boolean): OnUpdateFunc {
|
export function visibleOnly(cond: () => boolean): OnUpdateFunc {
|
||||||
return () => ({
|
return () => ({
|
||||||
visibility: cond(),
|
visibility: cond(),
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
import type { ButtonComponent } from "@/deps.ts";
|
|
||||||
import { describe, expect, it, vi } from "vitest";
|
|
||||||
import { setButtonDestructiveState } from "./SettingPane.ts";
|
|
||||||
|
|
||||||
type CompatibleButton = ButtonComponent & {
|
|
||||||
setDestructive?: () => ButtonComponent;
|
|
||||||
removeDestructive?: () => ButtonComponent;
|
|
||||||
};
|
|
||||||
|
|
||||||
function createButton(overrides: Partial<CompatibleButton> = {}): CompatibleButton {
|
|
||||||
return {
|
|
||||||
buttonEl: {
|
|
||||||
classList: {
|
|
||||||
toggle: vi.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...overrides,
|
|
||||||
} as unknown as CompatibleButton;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("setButtonDestructiveState", () => {
|
|
||||||
it("uses the native destructive-button API when it is available", () => {
|
|
||||||
const setDestructive = vi.fn();
|
|
||||||
const removeDestructive = vi.fn();
|
|
||||||
const button = createButton({ setDestructive, removeDestructive });
|
|
||||||
|
|
||||||
expect(setButtonDestructiveState(button, true)).toBe(button);
|
|
||||||
expect(setButtonDestructiveState(button, false)).toBe(button);
|
|
||||||
|
|
||||||
expect(setDestructive).toHaveBeenCalledOnce();
|
|
||||||
expect(removeDestructive).toHaveBeenCalledOnce();
|
|
||||||
expect(button.buttonEl.classList.toggle).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses the legacy warning class when the native API is unavailable", () => {
|
|
||||||
const button = createButton();
|
|
||||||
|
|
||||||
setButtonDestructiveState(button, true);
|
|
||||||
setButtonDestructiveState(button, false);
|
|
||||||
|
|
||||||
expect(button.buttonEl.classList.toggle).toHaveBeenNthCalledWith(1, "mod-warning", true);
|
|
||||||
expect(button.buttonEl.classList.toggle).toHaveBeenNthCalledWith(2, "mod-warning", false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import type { ButtonComponent, Setting } from "@/deps.ts";
|
||||||
|
|
||||||
|
const SETTING_WITH_ADDITIONAL_ACTIONS_CLASS = "sls-setting-with-additional-actions";
|
||||||
|
const ADDITIONAL_ACTION_CLASS = "sls-setting-additional-action";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies destructive-action styling without requiring Obsidian 1.13 at
|
||||||
|
* runtime. Older supported versions used the `mod-warning` class for the same
|
||||||
|
* presentation.
|
||||||
|
*/
|
||||||
|
export function setButtonDestructiveState<T extends ButtonComponent>(button: T, isDestructive = true): T {
|
||||||
|
const compatibleButton = button as unknown as {
|
||||||
|
setDestructive?: () => ButtonComponent;
|
||||||
|
removeDestructive?: () => ButtonComponent;
|
||||||
|
};
|
||||||
|
const updateNativeStyle = isDestructive ? compatibleButton.setDestructive : compatibleButton.removeDestructive;
|
||||||
|
if (typeof updateNativeStyle === "function") {
|
||||||
|
updateNativeStyle.call(button);
|
||||||
|
} else {
|
||||||
|
button.buttonEl.classList.toggle("mod-warning", isDestructive);
|
||||||
|
}
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets whether a setting row contains actions which may move onto a later line. */
|
||||||
|
export function setSettingAdditionalActionsState<T extends Setting>(setting: T, hasAdditionalActions = true): T {
|
||||||
|
setting.settingEl.classList.toggle(SETTING_WITH_ADDITIONAL_ACTIONS_CLASS, hasAdditionalActions);
|
||||||
|
return setting;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets whether a button is an additional action which may move onto a later line. */
|
||||||
|
export function setButtonAdditionalActionState<T extends ButtonComponent>(button: T, isAdditionalAction = true): T {
|
||||||
|
button.buttonEl.classList.toggle(ADDITIONAL_ACTION_CLASS, isAdditionalAction);
|
||||||
|
return button;
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import type { ButtonComponent, Setting } from "@/deps.ts";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import {
|
||||||
|
setButtonAdditionalActionState,
|
||||||
|
setButtonDestructiveState,
|
||||||
|
setSettingAdditionalActionsState,
|
||||||
|
} from "./settingComponentStyles.ts";
|
||||||
|
|
||||||
|
type CompatibleButton = ButtonComponent & {
|
||||||
|
setDestructive?: () => ButtonComponent;
|
||||||
|
removeDestructive?: () => ButtonComponent;
|
||||||
|
};
|
||||||
|
|
||||||
|
function createButton(overrides: Partial<CompatibleButton> = {}): CompatibleButton {
|
||||||
|
return {
|
||||||
|
buttonEl: {
|
||||||
|
classList: {
|
||||||
|
toggle: vi.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...overrides,
|
||||||
|
} as unknown as CompatibleButton;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSetting(): Setting {
|
||||||
|
return {
|
||||||
|
settingEl: {
|
||||||
|
classList: {
|
||||||
|
toggle: vi.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as Setting;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("setSettingAdditionalActionsState", () => {
|
||||||
|
it("sets whether the supplied setting row contains additional actions", () => {
|
||||||
|
const setting = createSetting();
|
||||||
|
|
||||||
|
expect(setSettingAdditionalActionsState(setting, true)).toBe(setting);
|
||||||
|
expect(setSettingAdditionalActionsState(setting, false)).toBe(setting);
|
||||||
|
expect(setting.settingEl.classList.toggle).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
"sls-setting-with-additional-actions",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(setting.settingEl.classList.toggle).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
"sls-setting-with-additional-actions",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setButtonAdditionalActionState", () => {
|
||||||
|
it("sets whether the supplied button is an additional action", () => {
|
||||||
|
const button = createButton();
|
||||||
|
|
||||||
|
expect(setButtonAdditionalActionState(button, true)).toBe(button);
|
||||||
|
expect(setButtonAdditionalActionState(button, false)).toBe(button);
|
||||||
|
expect(button.buttonEl.classList.toggle).toHaveBeenNthCalledWith(1, "sls-setting-additional-action", true);
|
||||||
|
expect(button.buttonEl.classList.toggle).toHaveBeenNthCalledWith(2, "sls-setting-additional-action", false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setButtonDestructiveState", () => {
|
||||||
|
it("uses the native destructive-button API when it is available", () => {
|
||||||
|
const setDestructive = vi.fn();
|
||||||
|
const removeDestructive = vi.fn();
|
||||||
|
const button = createButton({ setDestructive, removeDestructive });
|
||||||
|
|
||||||
|
expect(setButtonDestructiveState(button, true)).toBe(button);
|
||||||
|
expect(setButtonDestructiveState(button, false)).toBe(button);
|
||||||
|
|
||||||
|
expect(setDestructive).toHaveBeenCalledOnce();
|
||||||
|
expect(removeDestructive).toHaveBeenCalledOnce();
|
||||||
|
expect(button.buttonEl.classList.toggle).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the legacy warning class when the native API is unavailable", () => {
|
||||||
|
const button = createButton();
|
||||||
|
|
||||||
|
setButtonDestructiveState(button, true);
|
||||||
|
setButtonDestructiveState(button, false);
|
||||||
|
|
||||||
|
expect(button.buttonEl.classList.toggle).toHaveBeenNthCalledWith(1, "mod-warning", true);
|
||||||
|
expect(button.buttonEl.classList.toggle).toHaveBeenNthCalledWith(2, "mod-warning", false);
|
||||||
|
});
|
||||||
|
});
|
||||||
+19
-2
@@ -536,14 +536,31 @@ div.workspace-leaf-content[data-type="bases"] .livesync-status {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sls-setting-panel-title {
|
.sls-setting-panel-title {
|
||||||
position: sticky;
|
|
||||||
font-size: medium;
|
font-size: medium;
|
||||||
top: 2.5em;
|
|
||||||
background-color: var(--background-secondary-alt);
|
background-color: var(--background-secondary-alt);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 0.5em 1em;
|
padding: 0.5em 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.is-mobile .sls-setting button {
|
||||||
|
max-width: 100%;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.is-mobile .sls-setting-with-additional-actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.is-mobile .sls-setting-with-additional-actions .setting-item-control {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1 1 100%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.is-mobile .sls-setting .sls-setting-additional-action {
|
||||||
|
flex: 1 1 12rem;
|
||||||
|
}
|
||||||
|
|
||||||
.active-pane .sls-setting-panel-title {
|
.active-pane .sls-setting-panel-title {
|
||||||
border: 1px solid var(--interactive-accent);
|
border: 1px solid var(--interactive-accent);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { mkdir } from "node:fs/promises";
|
import { mkdir } from "node:fs/promises";
|
||||||
import { VER } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
import { VER } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||||
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
||||||
import { waitForLiveSyncCoreReady } from "../runner/liveSyncWorkflow.ts";
|
import { createE2eObsidianDeviceLocalState, waitForLiveSyncCoreReady } from "../runner/liveSyncWorkflow.ts";
|
||||||
import { assertMobileDialogueLayout, setObsidianMobileTestMode } from "../runner/mobileUi.ts";
|
import {
|
||||||
|
assertMobileDialogueLayout,
|
||||||
|
setObsidianMobileTestMode,
|
||||||
|
setObsidianMobileTestModeBeforePluginStart,
|
||||||
|
} from "../runner/mobileUi.ts";
|
||||||
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
|
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
|
||||||
import {
|
import {
|
||||||
allowPendingObsidianTestVaultOpenAction,
|
allowPendingObsidianTestVaultOpenAction,
|
||||||
@@ -14,7 +18,7 @@ import {
|
|||||||
withObsidianPage,
|
withObsidianPage,
|
||||||
} from "../runner/ui.ts";
|
} from "../runner/ui.ts";
|
||||||
import { createTemporaryVault } from "../runner/vault.ts";
|
import { createTemporaryVault } from "../runner/vault.ts";
|
||||||
import type { Locator } from "playwright";
|
import type { Locator, Page } from "playwright";
|
||||||
|
|
||||||
const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_SETTINGS_TIMEOUT_MS ?? 10000);
|
const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_SETTINGS_TIMEOUT_MS ?? 10000);
|
||||||
const settingsOnly = process.env.E2E_OBSIDIAN_SETTINGS_ONLY === "true";
|
const settingsOnly = process.env.E2E_OBSIDIAN_SETTINGS_ONLY === "true";
|
||||||
@@ -34,6 +38,11 @@ type LiveSyncTestPlugin = {
|
|||||||
applySettings: () => Promise<void>;
|
applySettings: () => Promise<void>;
|
||||||
isP2P: boolean;
|
isP2P: boolean;
|
||||||
}) => Promise<unknown>;
|
}) => Promise<unknown>;
|
||||||
|
settingTab?: {
|
||||||
|
editingSettings: { isConfigured: boolean };
|
||||||
|
initialSettings?: { isConfigured: boolean };
|
||||||
|
requestCatalogueRefresh(): void;
|
||||||
|
};
|
||||||
}[];
|
}[];
|
||||||
settings: {
|
settings: {
|
||||||
handleFilenameCaseSensitive: boolean;
|
handleFilenameCaseSensitive: boolean;
|
||||||
@@ -84,17 +93,14 @@ const settingsPageNames = [
|
|||||||
"Change Log",
|
"Change Log",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
async function assertDeclarativeLandingOrder(root: Locator): Promise<void> {
|
async function assertDeclarativeLandingOrder(root: Locator, configured: boolean): Promise<void> {
|
||||||
|
const synchronisation = ["Synchronisation", "Remote Configuration", "Sync Settings"];
|
||||||
|
const generalSettings = ["General Settings", "Appearance", "Logging", "Extra menus"];
|
||||||
|
const setup = configured
|
||||||
|
? [...synchronisation, ...generalSettings, "📲 Set up other devices", "Quick Setup"]
|
||||||
|
: ["Quick Setup", ...synchronisation, ...generalSettings, "📲 Set up other devices"];
|
||||||
const labels = [
|
const labels = [
|
||||||
"Quick Setup",
|
...setup,
|
||||||
"Synchronisation",
|
|
||||||
"Remote Configuration",
|
|
||||||
"Sync Settings",
|
|
||||||
"General Settings",
|
|
||||||
"Appearance",
|
|
||||||
"Logging",
|
|
||||||
"Extra menus",
|
|
||||||
"📲 Set up other devices",
|
|
||||||
"Maintenance and recovery",
|
"Maintenance and recovery",
|
||||||
"Maintenance",
|
"Maintenance",
|
||||||
"Hatch",
|
"Hatch",
|
||||||
@@ -131,45 +137,221 @@ async function assertDeclarativeLandingOrder(root: Locator): Promise<void> {
|
|||||||
}, labels);
|
}, labels);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function scrollDeclarativeLandingToTop(root: Locator): Promise<void> {
|
async function scrollDeclarativeLandingToTop(root: Locator, configured: boolean): Promise<void> {
|
||||||
const quickSetupHeading = root.locator(".setting-item-heading").filter({ hasText: "Quick Setup" }).first();
|
const firstHeading = root
|
||||||
await quickSetupHeading.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
.locator(".setting-item-heading")
|
||||||
await quickSetupHeading.scrollIntoViewIfNeeded();
|
.filter({ hasText: configured ? "Synchronisation" : "Quick Setup" })
|
||||||
|
.first();
|
||||||
|
await firstHeading.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||||
|
await firstHeading.scrollIntoViewIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function captureDeclarativeMobileLanding(): Promise<string | undefined> {
|
async function setConfiguredStateForLandingInspection(page: Page, configured: boolean): Promise<void> {
|
||||||
|
await page.evaluate((nextConfigured) => {
|
||||||
|
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
|
||||||
|
if (plugin === undefined) throw new Error("Self-hosted LiveSync is unavailable");
|
||||||
|
const settingDialogue = plugin.core.modules.find(
|
||||||
|
(module) => module.constructor.name === "ModuleObsidianSettingDialogue"
|
||||||
|
);
|
||||||
|
if (settingDialogue?.settingTab === undefined) {
|
||||||
|
throw new Error("The Self-hosted LiveSync setting tab is unavailable");
|
||||||
|
}
|
||||||
|
settingDialogue.settingTab.editingSettings.isConfigured = nextConfigured;
|
||||||
|
if (settingDialogue.settingTab.initialSettings !== undefined) {
|
||||||
|
settingDialogue.settingTab.initialSettings.isConfigured = nextConfigured;
|
||||||
|
}
|
||||||
|
settingDialogue.settingTab.requestCatalogueRefresh();
|
||||||
|
}, configured);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function captureDeclarativeMobileSettings(): Promise<
|
||||||
|
| {
|
||||||
|
landingPage: string;
|
||||||
|
maintenance: string;
|
||||||
|
patches: string;
|
||||||
|
remoteConfiguration: string;
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
> {
|
||||||
const port = obsidianRemoteDebuggingPort();
|
const port = obsidianRemoteDebuggingPort();
|
||||||
await setObsidianMobileTestMode(port, true, uiTimeoutMs);
|
return await withObsidianPage(port, async (page) => {
|
||||||
try {
|
const settingsNavigator = await openLiveSyncSettings(page, uiTimeoutMs);
|
||||||
return await withObsidianPage(port, async (page) => {
|
if (settingsNavigator.renderer !== "declarative") {
|
||||||
const settingsNavigator = await openLiveSyncSettings(page, uiTimeoutMs);
|
|
||||||
if (settingsNavigator.renderer !== "declarative") {
|
|
||||||
await settingsNavigator.close();
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
await settingsNavigator.returnToCatalogue();
|
|
||||||
await scrollDeclarativeLandingToTop(settingsNavigator.dialogue);
|
|
||||||
await assertDeclarativeLandingOrder(settingsNavigator.dialogue);
|
|
||||||
const remoteConfiguration = settingsNavigator.dialogue
|
|
||||||
.locator(".setting-item-name")
|
|
||||||
.filter({ hasText: "Remote Configuration" })
|
|
||||||
.first();
|
|
||||||
await remoteConfiguration.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
|
||||||
const path = `${diagnosticsDirectory}/settings-declarative-landing-mobile.png`;
|
|
||||||
await settingsNavigator.dialogue.screenshot({ ...settingsScreenshotOptions, path });
|
|
||||||
const remotePosition = await remoteConfiguration.evaluate((element) => {
|
|
||||||
const bounds = element.getBoundingClientRect();
|
|
||||||
return { top: bounds.top, bottom: bounds.bottom, viewportHeight: window.innerHeight };
|
|
||||||
});
|
|
||||||
if (remotePosition.top < 0 || remotePosition.bottom > remotePosition.viewportHeight) {
|
|
||||||
throw new Error("Remote Configuration was not visible at the top of the mobile settings landing page.");
|
|
||||||
}
|
|
||||||
await settingsNavigator.close();
|
await settingsNavigator.close();
|
||||||
return path;
|
return undefined;
|
||||||
|
}
|
||||||
|
await settingsNavigator.returnToCatalogue();
|
||||||
|
await scrollDeclarativeLandingToTop(settingsNavigator.dialogue, true);
|
||||||
|
await assertDeclarativeLandingOrder(settingsNavigator.dialogue, true);
|
||||||
|
const remoteConfiguration = settingsNavigator.dialogue
|
||||||
|
.locator(".setting-item-name")
|
||||||
|
.filter({ hasText: "Remote Configuration" })
|
||||||
|
.first();
|
||||||
|
await remoteConfiguration.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||||
|
const path = `${diagnosticsDirectory}/settings-declarative-landing-mobile.png`;
|
||||||
|
await settingsNavigator.dialogue.screenshot({ ...settingsScreenshotOptions, path });
|
||||||
|
const remotePosition = await remoteConfiguration.evaluate((element) => {
|
||||||
|
const bounds = element.getBoundingClientRect();
|
||||||
|
return { top: bounds.top, bottom: bounds.bottom, viewportHeight: window.innerHeight };
|
||||||
});
|
});
|
||||||
} finally {
|
if (remotePosition.top < 0 || remotePosition.bottom > remotePosition.viewportHeight) {
|
||||||
await setObsidianMobileTestMode(port, false, uiTimeoutMs);
|
throw new Error("Remote Configuration was not visible at the top of the mobile settings landing page.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const remotePage = await settingsNavigator.openPage("Remote Configuration");
|
||||||
|
const e2eeHeading = remotePage
|
||||||
|
.locator("h4.sls-setting-panel-title")
|
||||||
|
.filter({ hasText: "E2EE Configuration" })
|
||||||
|
.first();
|
||||||
|
const e2eeActions = remotePage.locator(".setting-item").filter({
|
||||||
|
has: settingsNavigator.page.getByText("Configure E2EE", { exact: true }),
|
||||||
|
});
|
||||||
|
await e2eeHeading.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||||
|
await e2eeActions.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||||
|
|
||||||
|
const layoutFailures: string[] = [];
|
||||||
|
const actionLayout = await e2eeActions.evaluate((setting) => {
|
||||||
|
const control = setting.querySelector<HTMLElement>(".setting-item-control");
|
||||||
|
if (control === null) throw new Error("The E2EE action row did not contain a control group.");
|
||||||
|
const settingBounds = setting.getBoundingClientRect();
|
||||||
|
const buttonBounds = Array.from(control.querySelectorAll("button")).map((button) =>
|
||||||
|
button.getBoundingClientRect()
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
controlClientWidth: control.clientWidth,
|
||||||
|
controlScrollWidth: control.scrollWidth,
|
||||||
|
rightmostButton: Math.max(...buttonBounds.map((bounds) => bounds.right)),
|
||||||
|
settingRight: settingBounds.right,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
actionLayout.controlScrollWidth > actionLayout.controlClientWidth + 1 ||
|
||||||
|
actionLayout.rightmostButton > actionLayout.settingRight + 1
|
||||||
|
) {
|
||||||
|
layoutFailures.push(`the E2EE actions overflowed their setting row (${JSON.stringify(actionLayout)})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await remotePage.evaluate((content) => {
|
||||||
|
content.scrollTop = content.scrollHeight - content.clientHeight;
|
||||||
|
content.dispatchEvent(new Event("scroll", { bubbles: true }));
|
||||||
|
});
|
||||||
|
await settingsNavigator.page.waitForTimeout(50);
|
||||||
|
const panelLayout = await e2eeHeading.evaluate((heading) => {
|
||||||
|
const infoPanel = heading.parentElement?.querySelector<HTMLElement>(".info-panel");
|
||||||
|
if (infoPanel === null || infoPanel === undefined) {
|
||||||
|
throw new Error("The E2EE section did not contain its information panel.");
|
||||||
|
}
|
||||||
|
const headingBounds = heading.getBoundingClientRect();
|
||||||
|
const infoBounds = infoPanel.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
headingBottom: headingBounds.bottom,
|
||||||
|
headingPosition: getComputedStyle(heading).position,
|
||||||
|
headingTop: headingBounds.top,
|
||||||
|
infoBottom: infoBounds.bottom,
|
||||||
|
infoTop: infoBounds.top,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
panelLayout.headingBottom > panelLayout.infoTop + 1 &&
|
||||||
|
panelLayout.headingTop < panelLayout.infoBottom - 1
|
||||||
|
) {
|
||||||
|
layoutFailures.push(`the E2EE section heading overlapped its contents (${JSON.stringify(panelLayout)})`);
|
||||||
|
}
|
||||||
|
const remotePath = `${diagnosticsDirectory}/settings-declarative-remote-mobile.png`;
|
||||||
|
await settingsNavigator.dialogue.screenshot({ ...settingsScreenshotOptions, path: remotePath });
|
||||||
|
if (layoutFailures.length > 0) {
|
||||||
|
throw new Error(`The mobile Remote Configuration layout was invalid: ${layoutFailures.join("; ")}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const maintenancePage = await settingsNavigator.openPage("Maintenance");
|
||||||
|
const markResolvedButton = maintenancePage
|
||||||
|
.locator(".op-warn button")
|
||||||
|
.filter({ hasText: "I've made a backup, mark this device 'resolved'" })
|
||||||
|
.first();
|
||||||
|
await markResolvedButton.evaluate((button) => {
|
||||||
|
const warning = button.closest<HTMLElement>(".op-warn");
|
||||||
|
if (warning === null) throw new Error("The Maintenance recovery action had no warning container.");
|
||||||
|
warning.removeClass("sls-setting-hidden");
|
||||||
|
});
|
||||||
|
await markResolvedButton.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||||
|
await markResolvedButton.scrollIntoViewIfNeeded();
|
||||||
|
const maintenanceLayout = await markResolvedButton.evaluate((button) => {
|
||||||
|
const content = button.closest<HTMLElement>(".vertical-tab-content");
|
||||||
|
if (content === null) throw new Error("The Maintenance button was outside the settings content.");
|
||||||
|
const buttonBounds = button.getBoundingClientRect();
|
||||||
|
const contentBounds = content.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
buttonLeft: buttonBounds.left,
|
||||||
|
buttonRight: buttonBounds.right,
|
||||||
|
contentLeft: contentBounds.left,
|
||||||
|
contentRight: contentBounds.right,
|
||||||
|
rootClientWidth: document.documentElement.clientWidth,
|
||||||
|
rootScrollWidth: document.documentElement.scrollWidth,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const maintenancePath = `${diagnosticsDirectory}/settings-declarative-maintenance-mobile.png`;
|
||||||
|
await settingsNavigator.dialogue.screenshot({ ...settingsScreenshotOptions, path: maintenancePath });
|
||||||
|
if (
|
||||||
|
maintenanceLayout.buttonLeft < maintenanceLayout.contentLeft - 1 ||
|
||||||
|
maintenanceLayout.buttonRight > maintenanceLayout.contentRight + 1 ||
|
||||||
|
maintenanceLayout.rootScrollWidth > maintenanceLayout.rootClientWidth + 1
|
||||||
|
) {
|
||||||
|
layoutFailures.push(
|
||||||
|
`the Maintenance recovery action overflowed the settings pane (${JSON.stringify(maintenanceLayout)})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const patchesPage = await settingsNavigator.openPage("Patches");
|
||||||
|
const remediationSetting = patchesPage.locator(".setting-item").filter({
|
||||||
|
has: settingsNavigator.page.locator('input[type="datetime-local"]'),
|
||||||
|
});
|
||||||
|
await remediationSetting.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||||
|
await remediationSetting.scrollIntoViewIfNeeded();
|
||||||
|
const patchesLayout = await remediationSetting.evaluate((setting) => {
|
||||||
|
const content = setting.closest<HTMLElement>(".vertical-tab-content");
|
||||||
|
const control = setting.querySelector<HTMLElement>(".setting-item-control");
|
||||||
|
if (content === null || control === null) {
|
||||||
|
throw new Error("The Patches remediation row was incomplete.");
|
||||||
|
}
|
||||||
|
const applyButton = control.querySelector<HTMLElement>("button");
|
||||||
|
if (applyButton === null) throw new Error("The Patches remediation row did not contain Apply.");
|
||||||
|
const settingBounds = setting.getBoundingClientRect();
|
||||||
|
const contentBounds = content.getBoundingClientRect();
|
||||||
|
const buttonBounds = applyButton.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
buttonRight: buttonBounds.right,
|
||||||
|
contentRight: contentBounds.right,
|
||||||
|
controlClientWidth: control.clientWidth,
|
||||||
|
controlScrollWidth: control.scrollWidth,
|
||||||
|
rootClientWidth: document.documentElement.clientWidth,
|
||||||
|
rootScrollWidth: document.documentElement.scrollWidth,
|
||||||
|
settingRight: settingBounds.right,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const patchesPath = `${diagnosticsDirectory}/settings-declarative-patches-mobile.png`;
|
||||||
|
await settingsNavigator.dialogue.screenshot({ ...settingsScreenshotOptions, path: patchesPath });
|
||||||
|
if (
|
||||||
|
patchesLayout.buttonRight > patchesLayout.settingRight + 1 ||
|
||||||
|
patchesLayout.buttonRight > patchesLayout.contentRight + 1 ||
|
||||||
|
patchesLayout.controlScrollWidth > patchesLayout.controlClientWidth + 1 ||
|
||||||
|
patchesLayout.rootScrollWidth > patchesLayout.rootClientWidth + 1
|
||||||
|
) {
|
||||||
|
layoutFailures.push(
|
||||||
|
`the Patches remediation actions overflowed their setting row (${JSON.stringify(patchesLayout)})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layoutFailures.length > 0) {
|
||||||
|
throw new Error(`The mobile settings layout was invalid: ${layoutFailures.join("; ")}.`);
|
||||||
|
}
|
||||||
|
await settingsNavigator.close();
|
||||||
|
return {
|
||||||
|
landingPage: path,
|
||||||
|
maintenance: maintenancePath,
|
||||||
|
patches: patchesPath,
|
||||||
|
remoteConfiguration: remotePath,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openSettingsInitialisationDialogueForInspection(isP2P: boolean): Promise<void> {
|
async function openSettingsInitialisationDialogueForInspection(isP2P: boolean): Promise<void> {
|
||||||
@@ -437,8 +619,8 @@ async function verifyConfigDoctorFollowsCompatibilityReview(): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function verifyEffectiveSettings(): Promise<void> {
|
async function verifyEffectiveSettings(): Promise<"declarative" | "imperative"> {
|
||||||
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
|
return await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
|
||||||
const sleepPreferences = await page.evaluate(() => {
|
const sleepPreferences = await page.evaluate(() => {
|
||||||
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
|
const plugin = (globalThis as ObsidianTestGlobal).app?.plugins?.plugins["obsidian-livesync"];
|
||||||
if (plugin === undefined) throw new Error("Self-hosted LiveSync is unavailable");
|
if (plugin === undefined) throw new Error("Self-hosted LiveSync is unavailable");
|
||||||
@@ -462,6 +644,12 @@ async function verifyEffectiveSettings(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let settingsNavigator = await openLiveSyncSettings(page, uiTimeoutMs);
|
let settingsNavigator = await openLiveSyncSettings(page, uiTimeoutMs);
|
||||||
|
if (settingsNavigator.renderer === "imperative") {
|
||||||
|
await settingsNavigator.dialogue.screenshot({
|
||||||
|
...settingsScreenshotOptions,
|
||||||
|
path: `${diagnosticsDirectory}/settings-imperative-landing.png`,
|
||||||
|
});
|
||||||
|
}
|
||||||
for (const hiddenPage of ["Selector", "Customisation sync", "Advanced", "Power users", "Patches"]) {
|
for (const hiddenPage of ["Selector", "Customisation sync", "Advanced", "Power users", "Patches"]) {
|
||||||
if (await settingsNavigator.isPageListed(hiddenPage)) {
|
if (await settingsNavigator.isPageListed(hiddenPage)) {
|
||||||
throw new Error(`${hiddenPage} was visible before its feature level was enabled.`);
|
throw new Error(`${hiddenPage} was visible before its feature level was enabled.`);
|
||||||
@@ -568,12 +756,22 @@ async function verifyEffectiveSettings(): Promise<void> {
|
|||||||
|
|
||||||
if (settingsNavigator.renderer === "declarative") {
|
if (settingsNavigator.renderer === "declarative") {
|
||||||
await settingsNavigator.returnToCatalogue();
|
await settingsNavigator.returnToCatalogue();
|
||||||
await scrollDeclarativeLandingToTop(settingsNavigator.dialogue);
|
await scrollDeclarativeLandingToTop(settingsNavigator.dialogue, true);
|
||||||
await settingsNavigator.dialogue.screenshot({
|
await settingsNavigator.dialogue.screenshot({
|
||||||
...settingsScreenshotOptions,
|
...settingsScreenshotOptions,
|
||||||
path: `${diagnosticsDirectory}/settings-declarative-landing.png`,
|
path: `${diagnosticsDirectory}/settings-declarative-landing.png`,
|
||||||
});
|
});
|
||||||
await assertDeclarativeLandingOrder(settingsNavigator.dialogue);
|
await assertDeclarativeLandingOrder(settingsNavigator.dialogue, true);
|
||||||
|
await setConfiguredStateForLandingInspection(page, false);
|
||||||
|
await scrollDeclarativeLandingToTop(settingsNavigator.dialogue, false);
|
||||||
|
await assertDeclarativeLandingOrder(settingsNavigator.dialogue, false);
|
||||||
|
await settingsNavigator.dialogue.screenshot({
|
||||||
|
...settingsScreenshotOptions,
|
||||||
|
path: `${diagnosticsDirectory}/settings-declarative-landing-unconfigured.png`,
|
||||||
|
});
|
||||||
|
await setConfiguredStateForLandingInspection(page, true);
|
||||||
|
await scrollDeclarativeLandingToTop(settingsNavigator.dialogue, true);
|
||||||
|
await assertDeclarativeLandingOrder(settingsNavigator.dialogue, true);
|
||||||
const rerunOnboarding = settingsNavigator.dialogue
|
const rerunOnboarding = settingsNavigator.dialogue
|
||||||
.locator(".setting-item-name")
|
.locator(".setting-item-name")
|
||||||
.filter({ hasText: "Rerun Onboarding Wizard" })
|
.filter({ hasText: "Rerun Onboarding Wizard" })
|
||||||
@@ -647,7 +845,9 @@ async function verifyEffectiveSettings(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderer = settingsNavigator.renderer;
|
||||||
await settingsNavigator.close();
|
await settingsNavigator.close();
|
||||||
|
return renderer;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -690,7 +890,11 @@ async function verifyPendingSettingsInitialisationFlow(): Promise<{ choice: stri
|
|||||||
has: settingsNavigator.page.getByText("Changes need to be applied!", { exact: true }),
|
has: settingsNavigator.page.getByText("Changes need to be applied!", { exact: true }),
|
||||||
});
|
});
|
||||||
await applySetting.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
await applySetting.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||||
await applySetting.getByRole("button", { name: "Apply", exact: true }).click({ timeout: uiTimeoutMs });
|
if (settingsNavigator.renderer === "declarative") {
|
||||||
|
await applySetting.click({ timeout: uiTimeoutMs });
|
||||||
|
} else {
|
||||||
|
await applySetting.getByRole("button", { name: "Apply", exact: true }).click({ timeout: uiTimeoutMs });
|
||||||
|
}
|
||||||
|
|
||||||
const choiceDialogue = await waitForVisibleObsidianDialogue(
|
const choiceDialogue = await waitForVisibleObsidianDialogue(
|
||||||
settingsNavigator.page,
|
settingsNavigator.page,
|
||||||
@@ -750,6 +954,72 @@ async function verifyPendingSettingsInitialisationFlow(): Promise<{ choice: stri
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createSettingsPluginData(settingsOnlyRun: boolean): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
doctorProcessedVersion: settingsOnlyRun ? "1.0.0" : "0.25.27",
|
||||||
|
isConfigured: true,
|
||||||
|
liveSync: false,
|
||||||
|
versionUpFlash: settingsOnlyRun ? "" : compatibilityReviewMessage,
|
||||||
|
notifyThresholdOfRemoteStorageSize: 0,
|
||||||
|
syncOnStart: false,
|
||||||
|
syncOnSave: false,
|
||||||
|
syncOnEditorSave: false,
|
||||||
|
syncOnFileOpen: false,
|
||||||
|
syncAfterMerge: false,
|
||||||
|
periodicReplication: false,
|
||||||
|
handleFilenameCaseSensitive: false,
|
||||||
|
useAdvancedMode: false,
|
||||||
|
usePowerUserMode: false,
|
||||||
|
useEdgeCaseMode: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function captureDeclarativeMobileSettingsInFreshSession(
|
||||||
|
binary: string,
|
||||||
|
cliBinary: string
|
||||||
|
): Promise<
|
||||||
|
| {
|
||||||
|
landingPage: string;
|
||||||
|
maintenance: string;
|
||||||
|
patches: string;
|
||||||
|
remoteConfiguration: string;
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
> {
|
||||||
|
// Enter mobile mode before LiveSync first loads so Obsidian fires the
|
||||||
|
// mobile settings-registration lifecycle used by a real mobile start-up.
|
||||||
|
const vault = await createTemporaryVault();
|
||||||
|
let session: ObsidianLiveSyncSession | undefined;
|
||||||
|
try {
|
||||||
|
session = await startObsidianLiveSyncSession({
|
||||||
|
binary,
|
||||||
|
cliBinary,
|
||||||
|
vault,
|
||||||
|
startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000),
|
||||||
|
pluginData: {
|
||||||
|
...createSettingsPluginData(true),
|
||||||
|
useAdvancedMode: true,
|
||||||
|
useEdgeCaseMode: true,
|
||||||
|
usePowerUserMode: true,
|
||||||
|
},
|
||||||
|
localStorageEntries: createE2eObsidianDeviceLocalState(vault.name),
|
||||||
|
lifecycle: {
|
||||||
|
beforePluginStart: async ({ remoteDebuggingPort }) => {
|
||||||
|
await setObsidianMobileTestModeBeforePluginStart(remoteDebuggingPort, true, uiTimeoutMs);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await waitForLiveSyncCoreReady(cliBinary, session.cliEnv);
|
||||||
|
await resumePendingCompatibilityReviewForSettings();
|
||||||
|
return await captureDeclarativeMobileSettings();
|
||||||
|
} finally {
|
||||||
|
if (session) {
|
||||||
|
await session.app.stop();
|
||||||
|
}
|
||||||
|
await vault.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
const binary = requireObsidianBinary();
|
const binary = requireObsidianBinary();
|
||||||
const cli = discoverObsidianCli();
|
const cli = discoverObsidianCli();
|
||||||
@@ -759,29 +1029,14 @@ async function main(): Promise<void> {
|
|||||||
const vault = await createTemporaryVault();
|
const vault = await createTemporaryVault();
|
||||||
await mkdir(diagnosticsDirectory, { recursive: true });
|
await mkdir(diagnosticsDirectory, { recursive: true });
|
||||||
let session: ObsidianLiveSyncSession | undefined;
|
let session: ObsidianLiveSyncSession | undefined;
|
||||||
|
let settingsRenderer: "declarative" | "imperative" | undefined;
|
||||||
try {
|
try {
|
||||||
session = await startObsidianLiveSyncSession({
|
session = await startObsidianLiveSyncSession({
|
||||||
binary,
|
binary,
|
||||||
cliBinary: cli.binary,
|
cliBinary: cli.binary,
|
||||||
vault,
|
vault,
|
||||||
startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000),
|
startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000),
|
||||||
pluginData: {
|
pluginData: createSettingsPluginData(settingsOnly),
|
||||||
doctorProcessedVersion: settingsOnly ? "1.0.0" : "0.25.27",
|
|
||||||
isConfigured: true,
|
|
||||||
liveSync: false,
|
|
||||||
versionUpFlash: settingsOnly ? "" : compatibilityReviewMessage,
|
|
||||||
notifyThresholdOfRemoteStorageSize: 0,
|
|
||||||
syncOnStart: false,
|
|
||||||
syncOnSave: false,
|
|
||||||
syncOnEditorSave: false,
|
|
||||||
syncOnFileOpen: false,
|
|
||||||
syncAfterMerge: false,
|
|
||||||
periodicReplication: false,
|
|
||||||
handleFilenameCaseSensitive: false,
|
|
||||||
useAdvancedMode: false,
|
|
||||||
usePowerUserMode: false,
|
|
||||||
useEdgeCaseMode: false,
|
|
||||||
},
|
|
||||||
lifecycle: settingsOnly
|
lifecycle: settingsOnly
|
||||||
? {
|
? {
|
||||||
afterLaunch: async ({ remoteDebuggingPort }) => {
|
afterLaunch: async ({ remoteDebuggingPort }) => {
|
||||||
@@ -798,11 +1053,9 @@ async function main(): Promise<void> {
|
|||||||
await verifyCompatibilityReview();
|
await verifyCompatibilityReview();
|
||||||
await verifyConfigDoctorFollowsCompatibilityReview();
|
await verifyConfigDoctorFollowsCompatibilityReview();
|
||||||
}
|
}
|
||||||
await verifyEffectiveSettings();
|
settingsRenderer = await verifyEffectiveSettings();
|
||||||
const initialisation = await verifyPendingSettingsInitialisationFlow();
|
const initialisation = await verifyPendingSettingsInitialisationFlow();
|
||||||
const p2pInitialisation = await captureP2PSettingsInitialisationDialogue();
|
const p2pInitialisation = await captureP2PSettingsInitialisationDialogue();
|
||||||
const mobileLanding = await captureDeclarativeMobileLanding();
|
|
||||||
if (mobileLanding) console.log(`Declarative mobile settings landing page: ${mobileLanding}`);
|
|
||||||
console.log(
|
console.log(
|
||||||
`Pending-settings initialisation screenshots: ${initialisation.choice}, ${initialisation.fallback}, ${p2pInitialisation}`
|
`Pending-settings initialisation screenshots: ${initialisation.choice}, ${initialisation.fallback}, ${p2pInitialisation}`
|
||||||
);
|
);
|
||||||
@@ -813,6 +1066,17 @@ async function main(): Promise<void> {
|
|||||||
}
|
}
|
||||||
await vault.dispose();
|
await vault.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mobileSettings =
|
||||||
|
settingsRenderer === "declarative"
|
||||||
|
? await captureDeclarativeMobileSettingsInFreshSession(binary, cli.binary)
|
||||||
|
: undefined;
|
||||||
|
if (mobileSettings) {
|
||||||
|
console.log(`Declarative mobile settings landing page: ${mobileSettings.landingPage}`);
|
||||||
|
console.log(`Declarative mobile Remote Configuration page: ${mobileSettings.remoteConfiguration}`);
|
||||||
|
console.log(`Declarative mobile Maintenance page: ${mobileSettings.maintenance}`);
|
||||||
|
console.log(`Declarative mobile Patches page: ${mobileSettings.patches}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((error: unknown) => {
|
main().catch((error: unknown) => {
|
||||||
|
|||||||
+17
-18
@@ -12,17 +12,32 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
## 1.0.19
|
## 1.0.21
|
||||||
|
|
||||||
|
26th August, 2026
|
||||||
|
|
||||||
|
It is becoming more 'ordinary' with each release, but please let me know if anything has become less convenient.
|
||||||
|
|
||||||
|
### Interface and translation
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
|
||||||
|
- Remote Configuration section headings no longer overlap their contents when scrolling on mobile. Action buttons in Remote Configuration, Maintenance, and Patches now remain inside the settings pane on narrow screens.
|
||||||
|
|
||||||
|
## 1.0.20
|
||||||
|
|
||||||
|
~~1.0.19~~ was cancelled because prerelease validation exposed an incorrect warning at start-up.
|
||||||
|
|
||||||
25th August, 2026
|
25th August, 2026
|
||||||
|
|
||||||
I had grown quite fond of the settings screen, but it seems that a simpler, healthier life is called for.
|
I know this is the second time I have said it, but I had grown quite fond of the settings screen. It seems, however, that a simpler, healthier life is called for.
|
||||||
|
|
||||||
### Interface and translation
|
### Interface and translation
|
||||||
|
|
||||||
#### Fixed
|
#### Fixed
|
||||||
|
|
||||||
- Compatibility pause warnings now direct you to the dedicated compatibility review instead of the Change Log.
|
- Compatibility pause warnings now direct you to the dedicated compatibility review instead of the Change Log.
|
||||||
|
- The Obsidian 1.13 settings page now waits for saved settings before choosing its initial layout. This prevents a spurious missing-replicator warning at start-up, keeps configured devices on the Synchronisation-first layout even when automatic synchronisation triggers are disabled, and keeps Quick Setup first on unconfigured devices.
|
||||||
|
|
||||||
#### Improved
|
#### Improved
|
||||||
|
|
||||||
@@ -78,19 +93,3 @@ I had grown quite fond of the settings screen, but it seems that a simpler, heal
|
|||||||
|
|
||||||
- One-shot CouchDB synchronisation now releases stalled web-compatible connection checks before replication starts, so a later synchronisation can make a fresh attempt (Commonlib 0.1.16).
|
- One-shot CouchDB synchronisation now releases stalled web-compatible connection checks before replication starts, so a later synchronisation can make a fresh attempt (Commonlib 0.1.16).
|
||||||
- The 60-second safeguard applies only to pre-replication checks. It does not limit ordinary synchronisation, and the **Use Internal API** path is unchanged.
|
- The 60-second safeguard applies only to pre-replication checks. It does not limit ordinary synchronisation, and the **Use Internal API** path is unchanged.
|
||||||
|
|
||||||
## 1.0.15
|
|
||||||
|
|
||||||
15th August, 2026
|
|
||||||
|
|
||||||
### Synchronisation and storage
|
|
||||||
|
|
||||||
#### Improved
|
|
||||||
|
|
||||||
- Start-up offline scanning is now faster, especially for larger Vaults using path obfuscation (Commonlib 0.1.15).
|
|
||||||
|
|
||||||
### Interface and translation
|
|
||||||
|
|
||||||
#### Improved
|
|
||||||
|
|
||||||
- The Traditional Chinese translation catalogue has been completed and polished for broader coverage and more natural, consistent terminology (PR #1106). Thank you to @nimula for the contribution!
|
|
||||||
|
|||||||
+3
-1
@@ -31,5 +31,7 @@
|
|||||||
"1.0.16": "1.7.2",
|
"1.0.16": "1.7.2",
|
||||||
"1.0.17": "1.7.2",
|
"1.0.17": "1.7.2",
|
||||||
"1.0.18": "1.7.2",
|
"1.0.18": "1.7.2",
|
||||||
"1.0.19": "1.7.2"
|
"1.0.19": "1.7.2",
|
||||||
|
"1.0.20": "1.7.2",
|
||||||
|
"1.0.21": "1.7.2"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user