mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-08-27 22:07:07 +00:00
Reorganise settings around common tasks
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { $msg, $t } from "@/common/translation";
|
||||
import { SUPPORTED_I18N_LANGS } from "@/common/rosetta";
|
||||
import { NetworkWarningStyles } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const";
|
||||
import type { SettingSpecGroup } from "./SettingSpec.ts";
|
||||
|
||||
export type GeneralSettingSpecContext = {
|
||||
showEditorStatusDetails: () => boolean;
|
||||
showVerboseLog: () => boolean;
|
||||
};
|
||||
|
||||
/** Build the shared Appearance and Logging controls. */
|
||||
export function createGeneralSettingSpecGroups({
|
||||
showEditorStatusDetails,
|
||||
showVerboseLog,
|
||||
}: GeneralSettingSpecContext): readonly SettingSpecGroup[] {
|
||||
return [
|
||||
{
|
||||
heading: $msg("obsidianLiveSyncSettingTab.titleAppearance"),
|
||||
items: [
|
||||
{
|
||||
key: "displayLanguage",
|
||||
control: {
|
||||
type: "dropdown",
|
||||
options: () =>
|
||||
Object.fromEntries(
|
||||
SUPPORTED_I18N_LANGS.map((language) => [language, $t(`lang-${language}`)])
|
||||
),
|
||||
},
|
||||
},
|
||||
{ key: "showStatusOnEditor", control: { type: "toggle" } },
|
||||
{
|
||||
key: "showOnlyIconsOnEditor",
|
||||
control: { type: "toggle" },
|
||||
visible: showEditorStatusDetails,
|
||||
},
|
||||
{ key: "showStatusOnStatusbar", control: { type: "toggle" } },
|
||||
{ key: "hideFileWarningNotice", control: { type: "toggle" } },
|
||||
{
|
||||
key: "networkWarningStyle",
|
||||
control: {
|
||||
type: "dropdown",
|
||||
options: () => ({
|
||||
[NetworkWarningStyles.BANNER]: "Show full banner",
|
||||
[NetworkWarningStyles.ICON]: "Show icon only",
|
||||
[NetworkWarningStyles.HIDDEN]: "Hide completely",
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: $msg("obsidianLiveSyncSettingTab.titleLogging"),
|
||||
items: [
|
||||
{ key: "lessInformationInLog", control: { type: "toggle" } },
|
||||
{
|
||||
key: "showVerboseLog",
|
||||
control: { type: "toggle" },
|
||||
visible: showVerboseLog,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** Build the feature-level controls shown in General Settings under Extra menus. */
|
||||
export function createExtraMenuSettingSpecGroup(): SettingSpecGroup {
|
||||
return {
|
||||
heading: $msg("obsidianLiveSyncSettingTab.titleExtraMenus"),
|
||||
items: [
|
||||
{ key: "useAdvancedMode", control: { type: "toggle" } },
|
||||
{ key: "usePowerUserMode", control: { type: "toggle" } },
|
||||
{ key: "useEdgeCaseMode", control: { type: "toggle" } },
|
||||
],
|
||||
};
|
||||
}
|
||||
+229
-51
@@ -1,6 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { DEFAULT_SETTINGS } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { SettingDefinitionItem, SettingDefinitionPage } from "obsidian";
|
||||
import type { SettingDefinitionGroup, SettingDefinitionItem, SettingDefinitionPage } from "obsidian";
|
||||
import type { PageFunctions } from "./SettingPane.ts";
|
||||
|
||||
const runtime = vi.hoisted(() => ({
|
||||
@@ -9,7 +9,7 @@ const runtime = vi.hoisted(() => ({
|
||||
unload: ReturnType<typeof vi.fn>;
|
||||
callbacks: Array<() => unknown>;
|
||||
}>,
|
||||
paneGeneral: vi.fn(),
|
||||
paneChangeLog: vi.fn(),
|
||||
pageCleanup: vi.fn(),
|
||||
savedEffect: vi.fn(),
|
||||
superHide: vi.fn(),
|
||||
@@ -73,9 +73,14 @@ vi.mock("@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions", () => ({
|
||||
},
|
||||
}));
|
||||
vi.mock("@/common/events.ts", () => ({
|
||||
EVENT_ON_UNRESOLVED_ERROR: "on-unresolved-error",
|
||||
EVENT_REQUEST_COPY_SETUP_URI: "request-copy-setup-uri",
|
||||
EVENT_REQUEST_OPEN_SETUP_URI: "request-open-setup-uri",
|
||||
EVENT_REQUEST_RELOAD_SETTING_TAB: "request-reload-setting-tab",
|
||||
eventHub: { onEvent: vi.fn() },
|
||||
EVENT_REQUEST_SHOW_SETUP_QR: "request-show-setup-qr",
|
||||
eventHub: { emitEvent: vi.fn(), onEvent: vi.fn() },
|
||||
}));
|
||||
vi.mock("@/modules/features/SetupManager.ts", () => ({ SetupManager: class {} }));
|
||||
vi.mock("@vrtmrz/livesync-commonlib/compat/pouchdb/negotiation", () => ({ checkSyncInfo: vi.fn() }));
|
||||
vi.mock("@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator", () => ({
|
||||
LiveSyncCouchDBReplicator: class {},
|
||||
@@ -91,9 +96,10 @@ vi.mock("./SettingPane.ts", () => ({
|
||||
setStyle: vi.fn(),
|
||||
visibleOnly: vi.fn((condition: () => boolean) => () => ({ visibility: condition() })),
|
||||
}));
|
||||
vi.mock("./PaneChangeLog.ts", () => ({ paneChangeLog: vi.fn() }));
|
||||
vi.mock("./PaneSetup.ts", () => ({ paneSetup: vi.fn() }));
|
||||
vi.mock("./PaneGeneral.ts", () => ({ paneGeneral: runtime.paneGeneral }));
|
||||
vi.mock("./PaneChangeLog.ts", () => ({ paneChangeLog: runtime.paneChangeLog }));
|
||||
vi.mock("./PaneQuickSetup.ts", () => ({ paneQuickSetup: vi.fn() }));
|
||||
vi.mock("./PaneHelp.ts", () => ({ paneHelp: vi.fn() }));
|
||||
vi.mock("./PaneGeneral.ts", () => ({ paneGeneral: vi.fn() }));
|
||||
vi.mock("./PaneRemoteConfig.ts", () => ({ paneRemoteConfig: vi.fn() }));
|
||||
vi.mock("./PaneSelector.ts", () => ({ paneSelector: vi.fn() }));
|
||||
vi.mock("./PaneSyncSettings.ts", () => ({ paneSyncSettings: vi.fn() }));
|
||||
@@ -111,6 +117,30 @@ function isPage(item: SettingDefinitionItem): item is SettingDefinitionPage {
|
||||
return "type" in item && item.type === "page";
|
||||
}
|
||||
|
||||
function isGroup(item: SettingDefinitionItem): item is SettingDefinitionGroup {
|
||||
return "type" in item && item.type === "group";
|
||||
}
|
||||
|
||||
function itemLabel(item: SettingDefinitionItem): string {
|
||||
if (isPage(item)) return item.name;
|
||||
if (isGroup(item)) return item.heading ?? "";
|
||||
return item.name;
|
||||
}
|
||||
|
||||
function collectPages(items: readonly SettingDefinitionItem[]): SettingDefinitionPage[] {
|
||||
return items.flatMap((item) => {
|
||||
if (isPage(item)) return [item, ...collectPages(item.items ?? [])];
|
||||
if (isGroup(item)) return collectPages(item.items ?? []);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
function findPage(tab: ObsidianLiveSyncSettingTab, name: string): SettingDefinitionPage {
|
||||
const page = collectPages(tab.getSettingDefinitions()).find((candidate) => candidate.name.endsWith(` ${name}`));
|
||||
if (!page) throw new Error(`${name} custom page is unavailable`);
|
||||
return page;
|
||||
}
|
||||
|
||||
function createSettingsTab(): ObsidianLiveSyncSettingTab {
|
||||
const plugin = {
|
||||
app: {},
|
||||
@@ -137,8 +167,8 @@ function createSettingsTab(): ObsidianLiveSyncSettingTab {
|
||||
|
||||
beforeEach(() => {
|
||||
runtime.components.length = 0;
|
||||
runtime.paneGeneral.mockClear();
|
||||
runtime.paneGeneral.mockImplementation(function (this: ObsidianLiveSyncSettingTab) {
|
||||
runtime.paneChangeLog.mockClear();
|
||||
runtime.paneChangeLog.mockImplementation(function (this: ObsidianLiveSyncSettingTab) {
|
||||
this.lifetimeComponent.register(runtime.pageCleanup);
|
||||
});
|
||||
runtime.pageCleanup.mockClear();
|
||||
@@ -147,36 +177,175 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
describe("ObsidianLiveSyncSettingTab native page lifecycle", () => {
|
||||
it("returns all catalogue pages and keeps Advanced as native items", () => {
|
||||
it("keeps Quick Setup first while synchronisation is inactive and separates synchronisation pages from it", () => {
|
||||
const tab = createSettingsTab();
|
||||
const pages = tab.getSettingDefinitions().filter(isPage);
|
||||
const definitions = tab.getSettingDefinitions();
|
||||
|
||||
expect(pages).toHaveLength(12);
|
||||
expect(pages.map(({ name }) => name)).toEqual(
|
||||
createSettingsPageCatalogue().map((entry) => `${entry.icon} ${entry.name()}`)
|
||||
expect(definitions.slice(0, 3).map(itemLabel)).toEqual([
|
||||
"🧙♂️ Quick Setup",
|
||||
"🔄 Synchronisation",
|
||||
"⚙️ General Settings",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps the synchronisation group first and orders General Settings before Quick Setup while synchronisation is active", () => {
|
||||
const tab = createSettingsTab();
|
||||
tab.editingSettings.liveSync = true;
|
||||
const definitions = tab.getSettingDefinitions();
|
||||
|
||||
expect(definitions.slice(0, 3).map(itemLabel)).toEqual([
|
||||
"🔄 Synchronisation",
|
||||
"⚙️ General Settings",
|
||||
"🧙♂️ Quick Setup",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps Remote Configuration and Sync Settings as native pages inside the Synchronisation group", () => {
|
||||
const tab = createSettingsTab();
|
||||
const definitions = tab.getSettingDefinitions();
|
||||
const synchronisation = definitions.find(
|
||||
(item): item is SettingDefinitionGroup => isGroup(item) && item.heading === "🔄 Synchronisation"
|
||||
);
|
||||
|
||||
expect(synchronisation?.items?.filter(isPage).map(({ name }) => name)).toEqual([
|
||||
"🛰️ Remote Configuration",
|
||||
"🔄 Sync Settings",
|
||||
]);
|
||||
expect(
|
||||
definitions
|
||||
.filter(isPage)
|
||||
.map(({ name }) => name)
|
||||
.filter((name) => name.endsWith(" Remote Configuration") || name.endsWith(" Sync Settings"))
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("groups secondary pages by purpose instead of exposing a flat Detailed settings list", () => {
|
||||
const tab = createSettingsTab();
|
||||
const definitions = tab.getSettingDefinitions();
|
||||
const groups = definitions.filter(isGroup);
|
||||
|
||||
expect(groups.map(({ heading }) => heading)).toEqual([
|
||||
"🧙♂️ Quick Setup",
|
||||
"🔄 Synchronisation",
|
||||
"⚙️ General Settings",
|
||||
"📲 Set up other devices",
|
||||
"🛠️ Maintenance and recovery",
|
||||
"🧩 Extra features",
|
||||
"🔧 Advanced settings",
|
||||
"ℹ️ Help and information",
|
||||
]);
|
||||
expect(
|
||||
groups
|
||||
.find(({ heading }) => heading === "🛠️ Maintenance and recovery")
|
||||
?.items?.filter(isPage)
|
||||
.map(({ name }) => name)
|
||||
).toEqual(["🎛️ Maintenance", "🧰 Hatch"]);
|
||||
expect(
|
||||
groups
|
||||
.find(({ heading }) => heading === "🧩 Extra features")
|
||||
?.items?.filter(isPage)
|
||||
.map(({ name }) => name)
|
||||
).toEqual(["🚦 Selector", "🔌 Customisation sync"]);
|
||||
expect(
|
||||
groups
|
||||
.find(({ heading }) => heading === "🔧 Advanced settings")
|
||||
?.items?.filter(isPage)
|
||||
.map(({ name }) => name)
|
||||
).toEqual(["🔧 Advanced", "💪 Power users", "🩹 Patches"]);
|
||||
expect(
|
||||
groups
|
||||
.find(({ heading }) => heading === "ℹ️ Help and information")
|
||||
?.items?.filter(isPage)
|
||||
.map(({ name }) => name)
|
||||
).toEqual(["❓ Help and troubleshooting", "💬 Change Log"]);
|
||||
expect(
|
||||
groups.find(({ heading }) => heading === "📲 Set up other devices")?.items?.map(({ name }) => name)
|
||||
).toEqual(["Copy the current settings to a Setup URI", "Show QR code"]);
|
||||
});
|
||||
|
||||
it("keeps Appearance, Logging, and Extra menus inside General Settings", () => {
|
||||
const tab = createSettingsTab();
|
||||
const definitions = tab.getSettingDefinitions();
|
||||
const general = definitions.find(
|
||||
(item): item is SettingDefinitionGroup => isGroup(item) && item.heading === "⚙️ General Settings"
|
||||
);
|
||||
const generalPages = general?.items?.filter(isPage);
|
||||
const appearance = generalPages?.find(({ name }) => name === "🎨 Appearance");
|
||||
const logging = generalPages?.find(({ name }) => name === "📝 Logging");
|
||||
const extraMenus = general?.items?.find(
|
||||
(item): item is SettingDefinitionPage => isPage(item) && item.name === "🎚️ Extra menus"
|
||||
);
|
||||
|
||||
expect(generalPages?.map(({ name }) => name)).toEqual(["🎨 Appearance", "📝 Logging", "🎚️ Extra menus"]);
|
||||
expect(
|
||||
appearance?.items?.flatMap((item) => ("control" in item && item.control ? [item.control.key] : []))
|
||||
).toEqual([
|
||||
"displayLanguage",
|
||||
"showStatusOnEditor",
|
||||
"showOnlyIconsOnEditor",
|
||||
"showStatusOnStatusbar",
|
||||
"hideFileWarningNotice",
|
||||
"networkWarningStyle",
|
||||
]);
|
||||
expect(
|
||||
logging?.items?.flatMap((item) => ("control" in item && item.control ? [item.control.key] : []))
|
||||
).toEqual(["lessInformationInLog", "showVerboseLog"]);
|
||||
expect(
|
||||
extraMenus?.items?.flatMap((item) => ("control" in item && item.control ? [item.control.key] : []))
|
||||
).toEqual(["useAdvancedMode", "usePowerUserMode", "useEdgeCaseMode"]);
|
||||
});
|
||||
|
||||
it("omits the old Setup child page and keeps standard General and Advanced pages native", () => {
|
||||
const tab = createSettingsTab();
|
||||
const pages = collectPages(tab.getSettingDefinitions());
|
||||
|
||||
expect(pages).toHaveLength(14);
|
||||
expect(pages.map(({ name }) => name)).toEqual(
|
||||
expect.arrayContaining(
|
||||
createSettingsPageCatalogue()
|
||||
.filter(({ id }) => id !== "general" && id !== "quick-setup")
|
||||
.map((entry) => `${entry.icon} ${entry.name()}`)
|
||||
)
|
||||
);
|
||||
expect(pages.some(({ name }) => name.endsWith(" General Settings"))).toBe(false);
|
||||
expect(pages.some(({ name }) => name.endsWith(" Setup"))).toBe(false);
|
||||
const advanced = pages.find(({ name }) => name.endsWith(" Advanced"));
|
||||
expect(advanced?.items?.filter((item) => "type" in item && item.type === "group")).toHaveLength(4);
|
||||
expect(advanced?.items?.filter((item) => "action" in item && typeof item.action === "function")).toHaveLength(
|
||||
1
|
||||
);
|
||||
expect(advanced?.page).toBeUndefined();
|
||||
expect(pages.filter(({ page }) => page !== undefined)).toHaveLength(11);
|
||||
expect(pages.filter(({ page }) => page !== undefined)).toHaveLength(10);
|
||||
});
|
||||
|
||||
it("keeps simple setup actions on the landing page without a second Setup destination", () => {
|
||||
const tab = createSettingsTab();
|
||||
const definitions = tab.getSettingDefinitions();
|
||||
const quickSetup = definitions.find(
|
||||
(item): item is SettingDefinitionGroup => isGroup(item) && item.heading === "🧙♂️ Quick Setup"
|
||||
);
|
||||
|
||||
expect(quickSetup?.items?.map(({ name }) => name)).toEqual([
|
||||
"Connect with Setup URI",
|
||||
"Rerun Onboarding Wizard",
|
||||
"Enable LiveSync",
|
||||
]);
|
||||
expect(collectPages(definitions).some(({ name }) => name.endsWith(" Setup"))).toBe(false);
|
||||
});
|
||||
|
||||
it("constructs custom page state only when opened and disposes each rendered scope", () => {
|
||||
const tab = createSettingsTab();
|
||||
const general = tab.getSettingDefinitions().filter(isPage)[2];
|
||||
if (!general?.page) {
|
||||
throw new Error("General custom page is unavailable");
|
||||
const changeLog = findPage(tab, "Change Log");
|
||||
if (!changeLog.page) {
|
||||
throw new Error("Change Log custom page is unavailable");
|
||||
}
|
||||
|
||||
expect(runtime.components).toHaveLength(0);
|
||||
const page = general.page();
|
||||
const page = changeLog.page();
|
||||
expect(runtime.components).toHaveLength(0);
|
||||
|
||||
page.display();
|
||||
expect(runtime.paneGeneral).toHaveBeenCalledOnce();
|
||||
expect(runtime.paneChangeLog).toHaveBeenCalledOnce();
|
||||
expect(runtime.components).toHaveLength(1);
|
||||
expect(runtime.components[0].load).toHaveBeenCalledOnce();
|
||||
|
||||
@@ -192,7 +361,7 @@ describe("ObsidianLiveSyncSettingTab native page lifecycle", () => {
|
||||
});
|
||||
|
||||
it("does not run delayed pane work after its page scope has been disposed", async () => {
|
||||
runtime.paneGeneral.mockImplementation(function (
|
||||
runtime.paneChangeLog.mockImplementation(function (
|
||||
this: ObsidianLiveSyncSettingTab,
|
||||
_paneEl: HTMLElement,
|
||||
{ addPanel }: Pick<PageFunctions, "addPanel">
|
||||
@@ -202,12 +371,12 @@ describe("ObsidianLiveSyncSettingTab native page lifecycle", () => {
|
||||
});
|
||||
});
|
||||
const tab = createSettingsTab();
|
||||
const general = tab.getSettingDefinitions().filter(isPage)[2];
|
||||
if (!general?.page) {
|
||||
throw new Error("General custom page is unavailable");
|
||||
const changeLog = findPage(tab, "Change Log");
|
||||
if (!changeLog.page) {
|
||||
throw new Error("Change Log custom page is unavailable");
|
||||
}
|
||||
|
||||
const page = general.page();
|
||||
const page = changeLog.page();
|
||||
page.display();
|
||||
page.hide();
|
||||
await Promise.resolve();
|
||||
@@ -216,7 +385,7 @@ describe("ObsidianLiveSyncSettingTab native page lifecycle", () => {
|
||||
});
|
||||
|
||||
it("runs a delayed pane callback inside its active scope before a queued hide", async () => {
|
||||
runtime.paneGeneral.mockImplementation(function (
|
||||
runtime.paneChangeLog.mockImplementation(function (
|
||||
this: ObsidianLiveSyncSettingTab,
|
||||
_paneEl: HTMLElement,
|
||||
{ addPanel }: Pick<PageFunctions, "addPanel">
|
||||
@@ -226,12 +395,12 @@ describe("ObsidianLiveSyncSettingTab native page lifecycle", () => {
|
||||
});
|
||||
});
|
||||
const tab = createSettingsTab();
|
||||
const general = tab.getSettingDefinitions().filter(isPage)[2];
|
||||
if (!general?.page) {
|
||||
throw new Error("General custom page is unavailable");
|
||||
const changeLog = findPage(tab, "Change Log");
|
||||
if (!changeLog.page) {
|
||||
throw new Error("Change Log custom page is unavailable");
|
||||
}
|
||||
|
||||
const page = general.page();
|
||||
const page = changeLog.page();
|
||||
page.display();
|
||||
queueMicrotask(() => page.hide());
|
||||
await Promise.resolve();
|
||||
@@ -242,11 +411,11 @@ describe("ObsidianLiveSyncSettingTab native page lifecycle", () => {
|
||||
|
||||
it("rebuilds the catalogue when an externally loaded setting changes page visibility", () => {
|
||||
const tab = createSettingsTab();
|
||||
const general = tab.getSettingDefinitions().filter(isPage)[2];
|
||||
if (!general?.page) {
|
||||
throw new Error("General custom page is unavailable");
|
||||
const changeLog = findPage(tab, "Change Log");
|
||||
if (!changeLog.page) {
|
||||
throw new Error("Change Log custom page is unavailable");
|
||||
}
|
||||
general.page().display();
|
||||
changeLog.page().display();
|
||||
tab.core.settings.usePowerUserMode = !tab.editingSettings.usePowerUserMode;
|
||||
|
||||
tab.requestReload();
|
||||
@@ -254,13 +423,22 @@ describe("ObsidianLiveSyncSettingTab native page lifecycle", () => {
|
||||
expect(tab.update).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("rebuilds the catalogue after an Extra menus feature level is saved", async () => {
|
||||
const tab = createSettingsTab();
|
||||
tab.editingSettings.usePowerUserMode = true;
|
||||
|
||||
await tab.saveSettings(["usePowerUserMode"]);
|
||||
|
||||
expect(tab.update).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("rebuilds translated catalogue names when the display language changes externally", () => {
|
||||
const tab = createSettingsTab();
|
||||
const general = tab.getSettingDefinitions().filter(isPage)[2];
|
||||
if (!general?.page) {
|
||||
throw new Error("General custom page is unavailable");
|
||||
const changeLog = findPage(tab, "Change Log");
|
||||
if (!changeLog.page) {
|
||||
throw new Error("Change Log custom page is unavailable");
|
||||
}
|
||||
general.page().display();
|
||||
changeLog.page().display();
|
||||
tab.core.settings.displayLanguage = "ja";
|
||||
|
||||
tab.requestReload();
|
||||
@@ -270,11 +448,11 @@ describe("ObsidianLiveSyncSettingTab native page lifecycle", () => {
|
||||
|
||||
it("rebuilds the catalogue after accepting an external page-visibility setting over a dirty value", () => {
|
||||
const tab = createSettingsTab();
|
||||
const general = tab.getSettingDefinitions().filter(isPage)[2];
|
||||
if (!general?.page) {
|
||||
throw new Error("General custom page is unavailable");
|
||||
const changeLog = findPage(tab, "Change Log");
|
||||
if (!changeLog.page) {
|
||||
throw new Error("Change Log custom page is unavailable");
|
||||
}
|
||||
general.page().display();
|
||||
changeLog.page().display();
|
||||
tab.initialSettings!.usePowerUserMode = false;
|
||||
tab.editingSettings.usePowerUserMode = true;
|
||||
tab.core.settings.usePowerUserMode = true;
|
||||
@@ -298,30 +476,30 @@ describe("ObsidianLiveSyncSettingTab native page lifecycle", () => {
|
||||
|
||||
it("does not reopen a custom page when a catalogue update has already hidden it", () => {
|
||||
const tab = createSettingsTab();
|
||||
const general = tab.getSettingDefinitions().filter(isPage)[2];
|
||||
if (!general?.page) {
|
||||
throw new Error("General custom page is unavailable");
|
||||
const changeLog = findPage(tab, "Change Log");
|
||||
if (!changeLog.page) {
|
||||
throw new Error("Change Log custom page is unavailable");
|
||||
}
|
||||
const page = general.page();
|
||||
const page = changeLog.page();
|
||||
page.display();
|
||||
vi.mocked(tab.update).mockImplementation(() => page.hide());
|
||||
|
||||
tab.requestCatalogueRefresh();
|
||||
|
||||
expect(runtime.paneGeneral).toHaveBeenCalledOnce();
|
||||
expect(runtime.paneChangeLog).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("keeps saved-setting effects owned by the tab after a custom page closes", async () => {
|
||||
runtime.paneGeneral.mockImplementation(function (this: ObsidianLiveSyncSettingTab) {
|
||||
runtime.paneChangeLog.mockImplementation(function (this: ObsidianLiveSyncSettingTab) {
|
||||
this.addOnSaved("displayLanguage", runtime.savedEffect);
|
||||
});
|
||||
const tab = createSettingsTab();
|
||||
const general = tab.getSettingDefinitions().filter(isPage)[2];
|
||||
if (!general?.page) {
|
||||
throw new Error("General custom page is unavailable");
|
||||
const changeLog = findPage(tab, "Change Log");
|
||||
if (!changeLog.page) {
|
||||
throw new Error("Change Log custom page is unavailable");
|
||||
}
|
||||
|
||||
const page = general.page();
|
||||
const page = changeLog.page();
|
||||
page.display();
|
||||
page.hide();
|
||||
tab.editingSettings.displayLanguage = "ja";
|
||||
|
||||
@@ -32,7 +32,14 @@ import {
|
||||
import { $msg } from "@/common/translation";
|
||||
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
||||
import { fireAndForget, yieldNextAnimationFrame } from "octagonal-wheels/promises";
|
||||
import { EVENT_REQUEST_RELOAD_SETTING_TAB, eventHub } from "@/common/events.ts";
|
||||
import {
|
||||
EVENT_ON_UNRESOLVED_ERROR,
|
||||
EVENT_REQUEST_COPY_SETUP_URI,
|
||||
EVENT_REQUEST_OPEN_SETUP_URI,
|
||||
EVENT_REQUEST_RELOAD_SETTING_TAB,
|
||||
EVENT_REQUEST_SHOW_SETUP_QR,
|
||||
eventHub,
|
||||
} from "@/common/events.ts";
|
||||
import {
|
||||
enableOnly,
|
||||
// findAttrFromParent,
|
||||
@@ -54,12 +61,21 @@ import { MinioStorageAdapter } from "@vrtmrz/livesync-commonlib/compat/replicati
|
||||
import { closeObsidianSettings } from "@/common/obsidianSettings.ts";
|
||||
import {
|
||||
createAdvancedSettingDefinitionGroups,
|
||||
createExtraMenuSettingDefinitions,
|
||||
createGeneralSettingDefinitionGroups,
|
||||
createSettingsPageCatalogue,
|
||||
type SettingsPageEntry,
|
||||
} from "./SettingsPageCatalogue.ts";
|
||||
import { createAdvancedSettingSpecGroups } from "./AdvancedSettingSpecs.ts";
|
||||
import { isValidSettingSpecValue, type SettingSpec } from "./SettingSpec.ts";
|
||||
import type { SettingDefinitionAction, SettingDefinitionItem, SettingDefinitionPage } from "obsidian";
|
||||
import type {
|
||||
SettingDefinitionAction,
|
||||
SettingDefinitionGroup,
|
||||
SettingDefinitionItem,
|
||||
SettingDefinitionPage,
|
||||
} from "obsidian";
|
||||
import { createExtraMenuSettingSpecGroup, createGeneralSettingSpecGroups } from "./GeneralSettingSpecs.ts";
|
||||
import { SetupManager } from "@/modules/features/SetupManager.ts";
|
||||
|
||||
// For creating a document
|
||||
// const toc = new Set<string>();
|
||||
@@ -312,6 +328,12 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
eventHub.onEvent(EVENT_REQUEST_RELOAD_SETTING_TAB, () => {
|
||||
this.requestReload();
|
||||
});
|
||||
this.addOnSaved("displayLanguage", () => this.requestCatalogueRefresh());
|
||||
this.addOnSaved("showStatusOnEditor", () => eventHub.emitEvent(EVENT_ON_UNRESOLVED_ERROR));
|
||||
this.addOnSaved("networkWarningStyle", () => eventHub.emitEvent(EVENT_ON_UNRESOLVED_ERROR));
|
||||
this.addOnSaved("useAdvancedMode", () => this.requestCatalogueRefresh());
|
||||
this.addOnSaved("usePowerUserMode", () => this.requestCatalogueRefresh());
|
||||
this.addOnSaved("useEdgeCaseMode", () => this.requestCatalogueRefresh());
|
||||
}
|
||||
|
||||
async testConnection(settingOverride: Partial<ObsidianLiveSyncSettings> = {}): Promise<void> {
|
||||
@@ -339,6 +361,29 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
closeObsidianSettings(this.plugin.app);
|
||||
}
|
||||
|
||||
requestOpenSetupURI(): void {
|
||||
this.closeSetting();
|
||||
eventHub.emitEvent(EVENT_REQUEST_OPEN_SETUP_URI);
|
||||
}
|
||||
|
||||
async rerunOnboardingWizard(): Promise<void> {
|
||||
await this.core.getModule(SetupManager).startOnBoarding();
|
||||
}
|
||||
|
||||
async enableLiveSyncFromSettings(): Promise<void> {
|
||||
this.editingSettings.isConfigured = true;
|
||||
await this.saveAllDirtySettings();
|
||||
this.services.appLifecycle.askRestart();
|
||||
}
|
||||
|
||||
requestCopySetupURI(): void {
|
||||
eventHub.emitEvent(EVENT_REQUEST_COPY_SETUP_URI);
|
||||
}
|
||||
|
||||
requestShowSetupQRCode(): void {
|
||||
eventHub.emitEvent(EVENT_REQUEST_SHOW_SETUP_QR);
|
||||
}
|
||||
|
||||
handleElement(element: HTMLElement, func: OnUpdateFunc) {
|
||||
const updateFunc = ((element, func) => {
|
||||
const prev = {} as OnUpdateResult;
|
||||
@@ -409,7 +454,15 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
key === "displayLanguage" ||
|
||||
key === "useAdvancedMode" ||
|
||||
key === "usePowerUserMode" ||
|
||||
key === "useEdgeCaseMode"
|
||||
key === "useEdgeCaseMode" ||
|
||||
key === "isConfigured" ||
|
||||
key === "liveSync" ||
|
||||
key === "periodicReplication" ||
|
||||
key === "syncOnSave" ||
|
||||
key === "syncOnEditorSave" ||
|
||||
key === "syncOnStart" ||
|
||||
key === "syncOnFileOpen" ||
|
||||
key === "syncAfterMerge"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -564,9 +617,16 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
}
|
||||
|
||||
private getDeclarativeSettingSpec(key: string): SettingSpec {
|
||||
const spec = createAdvancedSettingSpecGroups({
|
||||
isCouchDB: () => this.isConfiguredAs("remoteType", REMOTE_COUCHDB),
|
||||
})
|
||||
const spec = [
|
||||
...createGeneralSettingSpecGroups({
|
||||
showEditorStatusDetails: () => this.isConfiguredAs("showStatusOnEditor", true),
|
||||
showVerboseLog: () => this.isConfiguredAs("lessInformationInLog", false),
|
||||
}),
|
||||
createExtraMenuSettingSpecGroup(),
|
||||
...createAdvancedSettingSpecGroups({
|
||||
isCouchDB: () => this.isConfiguredAs("remoteType", REMOTE_COUCHDB),
|
||||
}),
|
||||
]
|
||||
.flatMap((group) => group.items)
|
||||
.find((candidate) => candidate.key === key);
|
||||
if (!spec) {
|
||||
@@ -697,28 +757,165 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
throw new Error("Custom settings pages require Obsidian 1.13.0 or later");
|
||||
}
|
||||
|
||||
private createDeclarativePage(entry: SettingsPageEntry): SettingDefinitionPage {
|
||||
const page: SettingDefinitionPage = {
|
||||
type: "page",
|
||||
name: `${entry.icon} ${entry.name()}`,
|
||||
visible: () => this.isPageVisible(entry.level),
|
||||
};
|
||||
if (entry.content === "native") {
|
||||
page.items = [
|
||||
this.createRebuildRequiredAction(),
|
||||
...createAdvancedSettingDefinitionGroups({
|
||||
isCouchDB: () => this.isConfiguredAs("remoteType", REMOTE_COUCHDB),
|
||||
}),
|
||||
];
|
||||
} else {
|
||||
page.page = () => this.createCustomSettingPage(entry);
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
private createGeneralSettingsGroup(): SettingDefinitionGroup {
|
||||
const groups = createGeneralSettingDefinitionGroups({
|
||||
showEditorStatusDetails: () => this.isConfiguredAs("showStatusOnEditor", true),
|
||||
showVerboseLog: () => this.isConfiguredAs("lessInformationInLog", false),
|
||||
});
|
||||
const [appearance, logging] = groups;
|
||||
if (!appearance || !logging) {
|
||||
throw new Error("General settings must define Appearance and Logging groups");
|
||||
}
|
||||
return this.createPageGroup(`⚙️ ${$msg("obsidianLiveSyncSettingTab.panelGeneralSettings")}`, [
|
||||
{
|
||||
type: "page",
|
||||
name: `🎨 ${appearance.heading}`,
|
||||
items: appearance.items,
|
||||
},
|
||||
{
|
||||
type: "page",
|
||||
name: `📝 ${logging.heading}`,
|
||||
items: logging.items,
|
||||
},
|
||||
this.createExtraMenusPage(),
|
||||
]);
|
||||
}
|
||||
|
||||
private createExtraMenusPage(): SettingDefinitionPage {
|
||||
return {
|
||||
type: "page",
|
||||
name: `🎚️ ${$msg("obsidianLiveSyncSettingTab.titleExtraMenus")}`,
|
||||
items: createExtraMenuSettingDefinitions(),
|
||||
};
|
||||
}
|
||||
|
||||
private createQuickSetupGroup(): SettingDefinitionGroup {
|
||||
return {
|
||||
type: "group",
|
||||
heading: `🧙♂️ ${$msg("obsidianLiveSyncSettingTab.titleQuickSetup")}`,
|
||||
items: [
|
||||
{
|
||||
name: $msg("obsidianLiveSyncSettingTab.nameConnectSetupURI"),
|
||||
desc: $msg("obsidianLiveSyncSettingTab.descConnectSetupURI"),
|
||||
action: () => this.requestOpenSetupURI(),
|
||||
},
|
||||
{
|
||||
name: $msg("Rerun Onboarding Wizard"),
|
||||
desc: $msg("Rerun the onboarding wizard to set up Self-hosted LiveSync again."),
|
||||
action: () => fireAndForget(async () => await this.rerunOnboardingWizard()),
|
||||
},
|
||||
{
|
||||
name: $msg("obsidianLiveSyncSettingTab.nameEnableLiveSync"),
|
||||
desc: $msg("obsidianLiveSyncSettingTab.descEnableLiveSync"),
|
||||
visible: () => !this.isConfiguredAs("isConfigured", true),
|
||||
action: () => fireAndForget(async () => await this.enableLiveSyncFromSettings()),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private createSynchronisationGroup(pages: SettingDefinitionPage[]): SettingDefinitionGroup {
|
||||
return this.createPageGroup(`🔄 ${$msg("obsidianLiveSyncSettingTab.titleSynchronisation")}`, pages);
|
||||
}
|
||||
|
||||
private createPageGroup(
|
||||
heading: string,
|
||||
pages: SettingDefinitionPage[],
|
||||
visible?: () => boolean
|
||||
): SettingDefinitionGroup {
|
||||
return {
|
||||
type: "group",
|
||||
heading,
|
||||
items: pages,
|
||||
...(visible ? { visible } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
private createSetupOtherDevicesGroup(): SettingDefinitionGroup {
|
||||
return {
|
||||
type: "group",
|
||||
heading: `📲 ${$msg("obsidianLiveSyncSettingTab.titleSetupOtherDevices")}`,
|
||||
visible: () => this.isConfiguredAs("isConfigured", true),
|
||||
items: [
|
||||
{
|
||||
name: $msg("obsidianLiveSyncSettingTab.nameCopySetupURI"),
|
||||
desc: $msg("obsidianLiveSyncSettingTab.descCopySetupURI"),
|
||||
action: () => this.requestCopySetupURI(),
|
||||
},
|
||||
{
|
||||
name: $msg("Setup.ShowQRCode"),
|
||||
desc: $msg("Setup.ShowQRCode.Desc"),
|
||||
action: () => this.requestShowSetupQRCode(),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
override getSettingDefinitions(): SettingDefinitionItem[] {
|
||||
if (!this.supportsDeclarativeSettings()) {
|
||||
return [];
|
||||
}
|
||||
return createSettingsPageCatalogue().map((entry): SettingDefinitionPage => {
|
||||
const page: SettingDefinitionPage = {
|
||||
type: "page",
|
||||
name: `${entry.icon} ${entry.name()}`,
|
||||
visible: () => this.isPageVisible(entry.level),
|
||||
};
|
||||
if (entry.content === "native") {
|
||||
page.items = [
|
||||
this.createRebuildRequiredAction(),
|
||||
...createAdvancedSettingDefinitionGroups({
|
||||
isCouchDB: () => this.isConfiguredAs("remoteType", REMOTE_COUCHDB),
|
||||
}),
|
||||
];
|
||||
} else {
|
||||
page.page = () => this.createCustomSettingPage(entry);
|
||||
const catalogue = createSettingsPageCatalogue();
|
||||
const getPage = (id: string): SettingDefinitionPage => {
|
||||
const entry = catalogue.find((candidate) => candidate.id === id);
|
||||
if (!entry) {
|
||||
throw new Error(`Unknown settings page: ${id}`);
|
||||
}
|
||||
return page;
|
||||
});
|
||||
return this.createDeclarativePage(entry);
|
||||
};
|
||||
const synchronisation = this.createSynchronisationGroup([
|
||||
getPage("remote-configuration"),
|
||||
getPage("synchronisation"),
|
||||
]);
|
||||
const generalSettings = this.createGeneralSettingsGroup();
|
||||
const quickSetup = this.createQuickSetupGroup();
|
||||
const setupOtherDevices = this.createSetupOtherDevicesGroup();
|
||||
const maintenance = this.createPageGroup(
|
||||
`🛠️ ${$msg("obsidianLiveSyncSettingTab.titleMaintenanceAndRecovery")}`,
|
||||
[getPage("maintenance"), getPage("hatch")]
|
||||
);
|
||||
const extraFeatures = this.createPageGroup(
|
||||
`🧩 ${$msg("obsidianLiveSyncSettingTab.titleExtraFeaturesGroup")}`,
|
||||
[getPage("selector"), getPage("customisation-sync")],
|
||||
() => this.isPageVisible(LEVEL_ADVANCED)
|
||||
);
|
||||
const advancedSettings = this.createPageGroup(
|
||||
`🔧 ${$msg("obsidianLiveSyncSettingTab.titleAdvancedSettings")}`,
|
||||
[getPage("advanced"), getPage("power-users"), getPage("patches")],
|
||||
() =>
|
||||
this.isPageVisible(LEVEL_ADVANCED) ||
|
||||
this.isPageVisible(LEVEL_POWER_USER) ||
|
||||
this.isPageVisible(LEVEL_EDGE_CASE)
|
||||
);
|
||||
const helpAndInformation = this.createPageGroup(
|
||||
`ℹ️ ${$msg("obsidianLiveSyncSettingTab.titleHelpAndInformation")}`,
|
||||
[getPage("help"), getPage("change-log")]
|
||||
);
|
||||
const laterGroups = [setupOtherDevices, maintenance, extraFeatures, advancedSettings, helpAndInformation];
|
||||
|
||||
if (this.isAnySyncEnabled()) {
|
||||
return [synchronisation, generalSettings, quickSetup, ...laterGroups];
|
||||
}
|
||||
return [quickSetup, synchronisation, generalSettings, ...laterGroups];
|
||||
}
|
||||
|
||||
private beginRenderScope(refresh: () => void): Component {
|
||||
|
||||
@@ -27,9 +27,14 @@ vi.mock("@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions", () => ({
|
||||
},
|
||||
}));
|
||||
vi.mock("@/common/events.ts", () => ({
|
||||
EVENT_ON_UNRESOLVED_ERROR: "on-unresolved-error",
|
||||
EVENT_REQUEST_COPY_SETUP_URI: "request-copy-setup-uri",
|
||||
EVENT_REQUEST_OPEN_SETUP_URI: "request-open-setup-uri",
|
||||
EVENT_REQUEST_RELOAD_SETTING_TAB: "request-reload-setting-tab",
|
||||
eventHub: { onEvent: vi.fn() },
|
||||
EVENT_REQUEST_SHOW_SETUP_QR: "request-show-setup-qr",
|
||||
eventHub: { emitEvent: vi.fn(), onEvent: vi.fn() },
|
||||
}));
|
||||
vi.mock("@/modules/features/SetupManager.ts", () => ({ SetupManager: class {} }));
|
||||
vi.mock("@vrtmrz/livesync-commonlib/compat/pouchdb/negotiation", () => negotiationMocks);
|
||||
vi.mock("@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator", () => ({
|
||||
LiveSyncCouchDBReplicator: class {},
|
||||
@@ -42,7 +47,8 @@ vi.mock("./SettingPane.ts", () => ({
|
||||
visibleOnly: vi.fn(() => vi.fn()),
|
||||
}));
|
||||
vi.mock("./PaneChangeLog.ts", () => ({ paneChangeLog: vi.fn() }));
|
||||
vi.mock("./PaneSetup.ts", () => ({ paneSetup: vi.fn() }));
|
||||
vi.mock("./PaneQuickSetup.ts", () => ({ paneQuickSetup: vi.fn() }));
|
||||
vi.mock("./PaneHelp.ts", () => ({ paneHelp: vi.fn() }));
|
||||
vi.mock("./PaneGeneral.ts", () => ({ paneGeneral: vi.fn() }));
|
||||
vi.mock("./PaneRemoteConfig.ts", () => ({ paneRemoteConfig: vi.fn() }));
|
||||
vi.mock("./PaneSelector.ts", () => ({ paneSelector: vi.fn() }));
|
||||
|
||||
@@ -1,46 +1,22 @@
|
||||
import { $msg, $t } from "@/common/translation";
|
||||
import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "@/common/rosetta";
|
||||
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||
import type { PageFunctions } from "./SettingPane.ts";
|
||||
import { visibleOnly } from "./SettingPane.ts";
|
||||
import { EVENT_ON_UNRESOLVED_ERROR, eventHub } from "@/common/events.ts";
|
||||
import { NetworkWarningStyles } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.const";
|
||||
export function paneGeneral(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void {
|
||||
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleAppearance")).then((paneEl) => {
|
||||
const languages = Object.fromEntries([
|
||||
// ["", $msg("obsidianLiveSyncSettingTab.defaultLanguage")],
|
||||
...SUPPORTED_I18N_LANGS.map((e) => [e, $t(`lang-${e}`)]),
|
||||
]) as Record<I18N_LANGS, string>;
|
||||
new Setting(paneEl).autoWireDropDown("displayLanguage", {
|
||||
options: languages,
|
||||
});
|
||||
this.addOnSaved("displayLanguage", () => this.requestCatalogueRefresh());
|
||||
new Setting(paneEl).autoWireToggle("showStatusOnEditor");
|
||||
this.addOnSaved("showStatusOnEditor", () => {
|
||||
eventHub.emitEvent(EVENT_ON_UNRESOLVED_ERROR);
|
||||
});
|
||||
new Setting(paneEl).autoWireToggle("showOnlyIconsOnEditor", {
|
||||
onUpdate: visibleOnly(() => this.isConfiguredAs("showStatusOnEditor", true)),
|
||||
});
|
||||
new Setting(paneEl).autoWireToggle("showStatusOnStatusbar");
|
||||
new Setting(paneEl).autoWireToggle("hideFileWarningNotice");
|
||||
new Setting(paneEl).autoWireDropDown("networkWarningStyle", {
|
||||
options: {
|
||||
[NetworkWarningStyles.BANNER]: "Show full banner",
|
||||
[NetworkWarningStyles.ICON]: "Show icon only",
|
||||
[NetworkWarningStyles.HIDDEN]: "Hide completely",
|
||||
},
|
||||
});
|
||||
this.addOnSaved("networkWarningStyle", () => {
|
||||
eventHub.emitEvent(EVENT_ON_UNRESOLVED_ERROR);
|
||||
});
|
||||
});
|
||||
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleLogging")).then((paneEl) => {
|
||||
new Setting(paneEl).autoWireToggle("lessInformationInLog");
|
||||
import { createExtraMenuSettingSpecGroup, createGeneralSettingSpecGroups } from "./GeneralSettingSpecs.ts";
|
||||
import { renderLegacySettingSpec } from "./SettingSpec.ts";
|
||||
|
||||
new Setting(paneEl).autoWireToggle("showVerboseLog", {
|
||||
onUpdate: visibleOnly(() => this.isConfiguredAs("lessInformationInLog", false)),
|
||||
export function paneGeneral(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void {
|
||||
const groups = [
|
||||
...createGeneralSettingSpecGroups({
|
||||
showEditorStatusDetails: () => this.isConfiguredAs("showStatusOnEditor", true),
|
||||
showVerboseLog: () => this.isConfiguredAs("lessInformationInLog", false),
|
||||
}),
|
||||
createExtraMenuSettingSpecGroup(),
|
||||
];
|
||||
for (const group of groups) {
|
||||
void addPanel(paneEl, group.heading).then((panelEl) => {
|
||||
for (const spec of group.items) {
|
||||
renderLegacySettingSpec(new Setting(panelEl), spec);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { MarkdownRenderer, request } from "@/deps.ts";
|
||||
import { $msg } from "@/common/translation";
|
||||
import { LiveSyncError } from "@vrtmrz/livesync-commonlib/compat/common/LSError";
|
||||
import { fireAndForget } from "octagonal-wheels/promises";
|
||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||
import type { PageFunctions } from "./SettingPane.ts";
|
||||
|
||||
/** Render the online help and troubleshooting browser. */
|
||||
export function paneHelp(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void {
|
||||
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleOnlineTips")).then((panelEl) => {
|
||||
const lifetimeComponent = this.lifetimeComponent;
|
||||
let pageDisposed = false;
|
||||
lifetimeComponent.register(() => {
|
||||
pageDisposed = true;
|
||||
});
|
||||
const repo = "vrtmrz/obsidian-livesync";
|
||||
const topPath = $msg("obsidianLiveSyncSettingTab.linkTroubleshooting");
|
||||
const rawRepoURI = `https://raw.githubusercontent.com/${repo}/main`;
|
||||
this.createEl(panelEl, "div", "", (el) => {
|
||||
el.createEl("a", { text: $msg("obsidianLiveSyncSettingTab.linkOpenInBrowser") }, (anchor) => {
|
||||
anchor.href = `https://github.com/${repo}/blob/main${topPath}`;
|
||||
anchor.target = "_blank";
|
||||
anchor.rel = "noopener";
|
||||
});
|
||||
});
|
||||
const troubleShootEl = this.createEl(panelEl, "div", {
|
||||
text: "",
|
||||
cls: "sls-troubleshoot-preview",
|
||||
});
|
||||
const loadMarkdownPage = async (pathAll: string, basePathParam: string = "") => {
|
||||
troubleShootEl.setCssStyles({ minHeight: troubleShootEl.clientHeight + "px" });
|
||||
troubleShootEl.empty();
|
||||
const fullPath = pathAll.startsWith("/") ? pathAll : `${basePathParam}/${pathAll}`;
|
||||
|
||||
const directoryArr = fullPath.split("/");
|
||||
const filename = directoryArr.pop();
|
||||
const basePath = directoryArr.join("/");
|
||||
|
||||
let remoteTroubleShootMDSrc = "";
|
||||
try {
|
||||
remoteTroubleShootMDSrc = await request(`${rawRepoURI}${basePath}/${filename}`);
|
||||
} catch (ex) {
|
||||
const err = LiveSyncError.fromError(ex);
|
||||
remoteTroubleShootMDSrc = `${$msg("obsidianLiveSyncSettingTab.logErrorOccurred")}\n${err.toString()}`;
|
||||
}
|
||||
if (pageDisposed) return;
|
||||
const remoteTroubleShootMD = remoteTroubleShootMDSrc.replace(
|
||||
/\((.*?(.png)|(.jpg))\)/g,
|
||||
`(${rawRepoURI}${basePath}/$1)`
|
||||
);
|
||||
await MarkdownRenderer.render(
|
||||
this.plugin.app,
|
||||
`<a class='sls-troubleshoot-anchor'></a> [${$msg("obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting")}](${topPath}) [${$msg("obsidianLiveSyncSettingTab.linkPageTop")}](${filename})\n\n${remoteTroubleShootMD}`,
|
||||
troubleShootEl,
|
||||
`${rawRepoURI}`,
|
||||
lifetimeComponent
|
||||
);
|
||||
if (pageDisposed) return;
|
||||
troubleShootEl.querySelector<HTMLAnchorElement>(".sls-troubleshoot-anchor")?.parentElement?.setCssStyles({
|
||||
position: "sticky",
|
||||
top: "-1em",
|
||||
backgroundColor: "var(--modal-background)",
|
||||
});
|
||||
troubleShootEl.querySelectorAll<HTMLAnchorElement>("a.internal-link").forEach((anchorEl) => {
|
||||
anchorEl.addEventListener("click", (evt) => {
|
||||
fireAndForget(async () => {
|
||||
const uri = anchorEl.getAttr("data-href");
|
||||
if (!uri) return;
|
||||
if (uri.startsWith("#")) {
|
||||
evt.preventDefault();
|
||||
const elements = Array.from(
|
||||
troubleShootEl.querySelectorAll<HTMLHeadingElement>("[data-heading]")
|
||||
);
|
||||
const target = elements.find(
|
||||
(element) =>
|
||||
element.getAttr("data-heading")?.toLowerCase().split(" ").join("-") ===
|
||||
uri.substring(1).toLowerCase()
|
||||
);
|
||||
if (target) {
|
||||
target.setCssStyles({ scrollMargin: "3em" });
|
||||
target.scrollIntoView({
|
||||
behavior: "instant",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
evt.preventDefault();
|
||||
await loadMarkdownPage(uri, basePath);
|
||||
troubleShootEl.setCssStyles({ scrollMargin: "1em" });
|
||||
troubleShootEl.scrollIntoView({
|
||||
behavior: "instant",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
troubleShootEl.setCssStyles({ minHeight: "" });
|
||||
};
|
||||
void loadMarkdownPage(topPath);
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
import { EVENT_REQUEST_PERFORM_GC_V3, eventHub } from "@/common/events.ts";
|
||||
import { $msg } from "@/common/translation";
|
||||
import {
|
||||
createCoreSettingsAfterFullReset,
|
||||
createEditingSettingsAfterFullReset,
|
||||
} from "@/serviceFeatures/setupObsidian/settingsReset.ts";
|
||||
import { LOG_LEVEL_NOTICE, Logger } from "@vrtmrz/livesync-commonlib/compat/common/logger";
|
||||
import { FlagFilesHumanReadable, FlagFilesOriginal } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { fireAndForget } from "@vrtmrz/livesync-commonlib/compat/common/utils";
|
||||
@@ -369,6 +374,30 @@ export function paneMaintenance(
|
||||
});
|
||||
|
||||
void addPanel(paneEl, "Reset").then((paneEl) => {
|
||||
new Setting(paneEl)
|
||||
.setName($msg("obsidianLiveSyncSettingTab.nameDiscardSettings"))
|
||||
.addButton((button) => {
|
||||
setButtonDestructiveState(button)
|
||||
.setButtonText($msg("obsidianLiveSyncSettingTab.btnDiscard"))
|
||||
.onClick(async () => {
|
||||
if (
|
||||
(await this.core.confirm.askYesNoDialog(
|
||||
$msg("obsidianLiveSyncSettingTab.msgDiscardConfirmation"),
|
||||
{ defaultOption: "No" }
|
||||
)) !== "yes"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.editingSettings = createEditingSettingsAfterFullReset(this.editingSettings);
|
||||
await this.saveAllDirtySettings();
|
||||
this.core.settings = createCoreSettingsAfterFullReset();
|
||||
await this.services.setting.saveSettingData();
|
||||
await this.services.database.resetDatabase();
|
||||
this.services.appLifecycle.askRestart();
|
||||
});
|
||||
})
|
||||
.addOnUpdate(visibleOnly(() => this.isConfiguredAs("isConfigured", true)));
|
||||
|
||||
new Setting(paneEl)
|
||||
.setName("Delete local database to reset or uninstall Self-hosted LiveSync")
|
||||
.addButton((button) =>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { $msg } from "@/common/translation";
|
||||
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||
import { visibleOnly, type PageFunctions } from "./SettingPane.ts";
|
||||
|
||||
/** Render setup actions in the pane-based settings interface used before Obsidian 1.13. */
|
||||
export function paneQuickSetup(
|
||||
this: ObsidianLiveSyncSettingTab,
|
||||
paneEl: HTMLElement,
|
||||
{ addPanel }: PageFunctions
|
||||
): void {
|
||||
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleQuickSetup")).then((panelEl) => {
|
||||
new Setting(panelEl)
|
||||
.setName($msg("obsidianLiveSyncSettingTab.nameConnectSetupURI"))
|
||||
.setDesc($msg("obsidianLiveSyncSettingTab.descConnectSetupURI"))
|
||||
.addButton((button) => {
|
||||
button.setButtonText($msg("obsidianLiveSyncSettingTab.btnUse")).onClick(() => {
|
||||
this.requestOpenSetupURI();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(panelEl)
|
||||
.setName($msg("Rerun Onboarding Wizard"))
|
||||
.setDesc($msg("Rerun the onboarding wizard to set up Self-hosted LiveSync again."))
|
||||
.addButton((button) => {
|
||||
button.setButtonText($msg("Rerun Wizard")).onClick(async () => {
|
||||
await this.rerunOnboardingWizard();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(panelEl)
|
||||
.setName($msg("obsidianLiveSyncSettingTab.nameEnableLiveSync"))
|
||||
.setDesc($msg("obsidianLiveSyncSettingTab.descEnableLiveSync"))
|
||||
.addOnUpdate(visibleOnly(() => !this.isConfiguredAs("isConfigured", true)))
|
||||
.addButton((button) => {
|
||||
button.setButtonText($msg("obsidianLiveSyncSettingTab.btnEnable")).onClick(async () => {
|
||||
await this.enableLiveSyncFromSettings();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
void addPanel(
|
||||
paneEl,
|
||||
`📲 ${$msg("obsidianLiveSyncSettingTab.titleSetupOtherDevices")}`,
|
||||
undefined,
|
||||
visibleOnly(() => this.isConfiguredAs("isConfigured", true))
|
||||
).then((panelEl) => {
|
||||
new Setting(panelEl)
|
||||
.setName($msg("obsidianLiveSyncSettingTab.nameCopySetupURI"))
|
||||
.setDesc($msg("obsidianLiveSyncSettingTab.descCopySetupURI"))
|
||||
.addButton((button) => {
|
||||
button.setButtonText($msg("obsidianLiveSyncSettingTab.btnCopy")).onClick(() => {
|
||||
this.requestCopySetupURI();
|
||||
});
|
||||
});
|
||||
new Setting(panelEl)
|
||||
.setName($msg("Setup.ShowQRCode"))
|
||||
.setDesc($msg("Setup.ShowQRCode.Desc"))
|
||||
.addButton((button) => {
|
||||
button.setButtonText($msg("Setup.ShowQRCode")).onClick(() => {
|
||||
this.requestShowSetupQRCode();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
import { MarkdownRenderer } from "@/deps.ts";
|
||||
import { $msg } from "@/common/translation";
|
||||
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
||||
import { fireAndForget } from "octagonal-wheels/promises";
|
||||
import {
|
||||
EVENT_REQUEST_COPY_SETUP_URI,
|
||||
EVENT_REQUEST_OPEN_SETUP_URI,
|
||||
EVENT_REQUEST_SHOW_SETUP_QR,
|
||||
eventHub,
|
||||
} from "@/common/events.ts";
|
||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||
import { setButtonDestructiveState, visibleOnly, type PageFunctions } from "./SettingPane.ts";
|
||||
import { request } from "@/deps.ts";
|
||||
import { SetupManager } from "@/modules/features/SetupManager.ts";
|
||||
import { LiveSyncError } from "@vrtmrz/livesync-commonlib/compat/common/LSError";
|
||||
import {
|
||||
createCoreSettingsAfterFullReset,
|
||||
createEditingSettingsAfterFullReset,
|
||||
} from "@/serviceFeatures/setupObsidian/settingsReset.ts";
|
||||
export function paneSetup(
|
||||
this: ObsidianLiveSyncSettingTab,
|
||||
paneEl: HTMLElement,
|
||||
{ addPanel, addPane }: PageFunctions
|
||||
): void {
|
||||
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleQuickSetup")).then((paneEl) => {
|
||||
new Setting(paneEl)
|
||||
.setName($msg("obsidianLiveSyncSettingTab.nameConnectSetupURI"))
|
||||
.setDesc($msg("obsidianLiveSyncSettingTab.descConnectSetupURI"))
|
||||
.addButton((text) => {
|
||||
text.setButtonText($msg("obsidianLiveSyncSettingTab.btnUse")).onClick(() => {
|
||||
this.closeSetting();
|
||||
eventHub.emitEvent(EVENT_REQUEST_OPEN_SETUP_URI);
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(paneEl)
|
||||
.setName($msg("Rerun Onboarding Wizard"))
|
||||
.setDesc($msg("Rerun the onboarding wizard to set up Self-hosted LiveSync again."))
|
||||
.addButton((text) => {
|
||||
text.setButtonText($msg("Rerun Wizard")).onClick(async () => {
|
||||
const setupManager = this.core.getModule(SetupManager);
|
||||
await setupManager.startOnBoarding();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(paneEl)
|
||||
.setName($msg("obsidianLiveSyncSettingTab.nameEnableLiveSync"))
|
||||
.setDesc($msg("obsidianLiveSyncSettingTab.descEnableLiveSync"))
|
||||
.addOnUpdate(visibleOnly(() => !this.isConfiguredAs("isConfigured", true)))
|
||||
.addButton((text) => {
|
||||
text.setButtonText($msg("obsidianLiveSyncSettingTab.btnEnable")).onClick(async () => {
|
||||
this.editingSettings.isConfigured = true;
|
||||
await this.saveAllDirtySettings();
|
||||
this.services.appLifecycle.askRestart();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
void addPanel(
|
||||
paneEl,
|
||||
$msg("obsidianLiveSyncSettingTab.titleSetupOtherDevices"),
|
||||
undefined,
|
||||
visibleOnly(() => this.isConfiguredAs("isConfigured", true))
|
||||
).then((paneEl) => {
|
||||
new Setting(paneEl)
|
||||
.setName($msg("obsidianLiveSyncSettingTab.nameCopySetupURI"))
|
||||
.setDesc($msg("obsidianLiveSyncSettingTab.descCopySetupURI"))
|
||||
.addButton((text) => {
|
||||
text.setButtonText($msg("obsidianLiveSyncSettingTab.btnCopy")).onClick(() => {
|
||||
// await this.plugin.addOnSetup.command_copySetupURI();
|
||||
eventHub.emitEvent(EVENT_REQUEST_COPY_SETUP_URI);
|
||||
});
|
||||
});
|
||||
new Setting(paneEl)
|
||||
.setName($msg("Setup.ShowQRCode"))
|
||||
.setDesc($msg("Setup.ShowQRCode.Desc"))
|
||||
.addButton((text) => {
|
||||
text.setButtonText($msg("Setup.ShowQRCode")).onClick(() => {
|
||||
eventHub.emitEvent(EVENT_REQUEST_SHOW_SETUP_QR);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleReset")).then((paneEl) => {
|
||||
new Setting(paneEl)
|
||||
.setName($msg("obsidianLiveSyncSettingTab.nameDiscardSettings"))
|
||||
.addButton((text) => {
|
||||
setButtonDestructiveState(text)
|
||||
.setButtonText($msg("obsidianLiveSyncSettingTab.btnDiscard"))
|
||||
.onClick(async () => {
|
||||
if (
|
||||
(await this.core.confirm.askYesNoDialog(
|
||||
$msg("obsidianLiveSyncSettingTab.msgDiscardConfirmation"),
|
||||
{ defaultOption: "No" }
|
||||
)) == "yes"
|
||||
) {
|
||||
this.editingSettings = createEditingSettingsAfterFullReset(this.editingSettings);
|
||||
await this.saveAllDirtySettings();
|
||||
this.core.settings = createCoreSettingsAfterFullReset();
|
||||
await this.services.setting.saveSettingData();
|
||||
await this.services.database.resetDatabase();
|
||||
// await this.plugin.initializeDatabase();
|
||||
this.services.appLifecycle.askRestart();
|
||||
}
|
||||
});
|
||||
})
|
||||
.addOnUpdate(visibleOnly(() => this.isConfiguredAs("isConfigured", true)));
|
||||
});
|
||||
|
||||
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleExtraFeatures")).then((paneEl) => {
|
||||
new Setting(paneEl).autoWireToggle("useAdvancedMode");
|
||||
|
||||
new Setting(paneEl).autoWireToggle("usePowerUserMode");
|
||||
new Setting(paneEl).autoWireToggle("useEdgeCaseMode");
|
||||
|
||||
this.addOnSaved("useAdvancedMode", () => this.requestCatalogueRefresh());
|
||||
this.addOnSaved("usePowerUserMode", () => this.requestCatalogueRefresh());
|
||||
this.addOnSaved("useEdgeCaseMode", () => this.requestCatalogueRefresh());
|
||||
});
|
||||
|
||||
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleOnlineTips")).then((paneEl) => {
|
||||
const lifetimeComponent = this.lifetimeComponent;
|
||||
let pageDisposed = false;
|
||||
lifetimeComponent.register(() => {
|
||||
pageDisposed = true;
|
||||
});
|
||||
// this.createEl(paneEl, "h3", { text: $msg("obsidianLiveSyncSettingTab.titleOnlineTips") });
|
||||
const repo = "vrtmrz/obsidian-livesync";
|
||||
const topPath = $msg("obsidianLiveSyncSettingTab.linkTroubleshooting");
|
||||
const rawRepoURI = `https://raw.githubusercontent.com/${repo}/main`;
|
||||
this.createEl(paneEl, "div", "", (el) => {
|
||||
el.createEl("a", { text: $msg("obsidianLiveSyncSettingTab.linkOpenInBrowser") }, (anchor) => {
|
||||
anchor.href = `https://github.com/${repo}/blob/main${topPath}`;
|
||||
anchor.target = "_blank";
|
||||
anchor.rel = "noopener";
|
||||
});
|
||||
});
|
||||
const troubleShootEl = this.createEl(paneEl, "div", {
|
||||
text: "",
|
||||
cls: "sls-troubleshoot-preview",
|
||||
});
|
||||
const loadMarkdownPage = async (pathAll: string, basePathParam: string = "") => {
|
||||
troubleShootEl.setCssStyles({ minHeight: troubleShootEl.clientHeight + "px" });
|
||||
troubleShootEl.empty();
|
||||
const fullPath = pathAll.startsWith("/") ? pathAll : `${basePathParam}/${pathAll}`;
|
||||
|
||||
const directoryArr = fullPath.split("/");
|
||||
const filename = directoryArr.pop();
|
||||
const directly = directoryArr.join("/");
|
||||
const basePath = directly;
|
||||
|
||||
let remoteTroubleShootMDSrc = "";
|
||||
try {
|
||||
remoteTroubleShootMDSrc = await request(`${rawRepoURI}${basePath}/${filename}`);
|
||||
} catch (ex) {
|
||||
const err = LiveSyncError.fromError(ex);
|
||||
remoteTroubleShootMDSrc = `${$msg("obsidianLiveSyncSettingTab.logErrorOccurred")}\n${err.toString()}`;
|
||||
}
|
||||
if (pageDisposed) return;
|
||||
const remoteTroubleShootMD = remoteTroubleShootMDSrc.replace(
|
||||
/\((.*?(.png)|(.jpg))\)/g,
|
||||
`(${rawRepoURI}${basePath}/$1)`
|
||||
);
|
||||
// Render markdown
|
||||
await MarkdownRenderer.render(
|
||||
this.plugin.app,
|
||||
`<a class='sls-troubleshoot-anchor'></a> [${$msg("obsidianLiveSyncSettingTab.linkTipsAndTroubleshooting")}](${topPath}) [${$msg("obsidianLiveSyncSettingTab.linkPageTop")}](${filename})\n\n${remoteTroubleShootMD}`,
|
||||
troubleShootEl,
|
||||
`${rawRepoURI}`,
|
||||
lifetimeComponent
|
||||
);
|
||||
if (pageDisposed) return;
|
||||
// Menu
|
||||
troubleShootEl.querySelector<HTMLAnchorElement>(".sls-troubleshoot-anchor")?.parentElement?.setCssStyles({
|
||||
position: "sticky",
|
||||
top: "-1em",
|
||||
backgroundColor: "var(--modal-background)",
|
||||
});
|
||||
// Trap internal links.
|
||||
troubleShootEl.querySelectorAll<HTMLAnchorElement>("a.internal-link").forEach((anchorEl) => {
|
||||
anchorEl.addEventListener("click", (evt) => {
|
||||
fireAndForget(async () => {
|
||||
const uri = anchorEl.getAttr("data-href");
|
||||
if (!uri) return;
|
||||
if (uri.startsWith("#")) {
|
||||
evt.preventDefault();
|
||||
const elements = Array.from(
|
||||
troubleShootEl.querySelectorAll<HTMLHeadingElement>("[data-heading]")
|
||||
);
|
||||
const p = elements.find(
|
||||
(e) =>
|
||||
e.getAttr("data-heading")?.toLowerCase().split(" ").join("-") ==
|
||||
uri.substring(1).toLowerCase()
|
||||
);
|
||||
if (p) {
|
||||
p.setCssStyles({ scrollMargin: "3em" });
|
||||
p.scrollIntoView({
|
||||
behavior: "instant",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
evt.preventDefault();
|
||||
await loadMarkdownPage(uri, basePath);
|
||||
troubleShootEl.setCssStyles({ scrollMargin: "1em" });
|
||||
troubleShootEl.scrollIntoView({
|
||||
behavior: "instant",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
troubleShootEl.setCssStyles({ minHeight: "" });
|
||||
};
|
||||
void loadMarkdownPage(topPath);
|
||||
});
|
||||
}
|
||||
@@ -95,6 +95,7 @@ export function paneSyncSettings(
|
||||
|
||||
await this.saveAllDirtySettings();
|
||||
await this.services.control.applySettings();
|
||||
this.requestCatalogueRefresh();
|
||||
});
|
||||
});
|
||||
void addPanel(paneEl, $msg("obsidianLiveSyncSettingTab.titleSynchronizationMethod")).then((paneEl) => {
|
||||
@@ -129,6 +130,7 @@ export function paneSyncSettings(
|
||||
await this.saveSettings(["liveSync", "periodicReplication"]);
|
||||
|
||||
await this.services.control.applySettings();
|
||||
this.requestCatalogueRefresh();
|
||||
});
|
||||
|
||||
new Setting(paneEl).autoWireNumeric("periodicReplicationInterval", {
|
||||
@@ -144,6 +146,15 @@ export function paneSyncSettings(
|
||||
new Setting(paneEl).autoWireToggle("syncOnFileOpen", { onUpdate: onlyOnNonLiveSync });
|
||||
new Setting(paneEl).autoWireToggle("syncOnStart", { onUpdate: onlyOnNonLiveSync });
|
||||
new Setting(paneEl).autoWireToggle("syncAfterMerge", { onUpdate: onlyOnNonLiveSync });
|
||||
for (const key of [
|
||||
"syncOnSave",
|
||||
"syncOnEditorSave",
|
||||
"syncOnFileOpen",
|
||||
"syncOnStart",
|
||||
"syncAfterMerge",
|
||||
] as const) {
|
||||
this.addOnSaved(key, () => this.requestCatalogueRefresh());
|
||||
}
|
||||
// Desktop app only, and only for the sync modes that keep a background replication channel
|
||||
// (LiveSync and Periodic). Ignored on mobile, where suspending preserves battery. The
|
||||
// visibility predicate mirrors the runtime guard in ModuleObsidianEvents.
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type SettingSpec,
|
||||
} from "./SettingSpec.ts";
|
||||
import { createAdvancedSettingSpecGroups } from "./AdvancedSettingSpecs.ts";
|
||||
import { createExtraMenuSettingSpecGroup, createGeneralSettingSpecGroups } from "./GeneralSettingSpecs.ts";
|
||||
|
||||
const rangeMessages = {
|
||||
valueShouldBeInRange: ({ min, max }: { min?: number; max?: number }) => `${min ?? "~"}..${max ?? "~"}`,
|
||||
@@ -74,6 +75,52 @@ describe("Advanced setting specifications", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("General setting specifications", () => {
|
||||
it("shares every General and Logging control between the imperative and declarative renderers", () => {
|
||||
const groups = createGeneralSettingSpecGroups({
|
||||
showEditorStatusDetails: () => true,
|
||||
showVerboseLog: () => true,
|
||||
});
|
||||
|
||||
expect(groups.map(({ heading }) => heading)).toEqual(["Appearance", "Logging"]);
|
||||
expect(groups.flatMap(({ items }) => items.map(({ key }) => key))).toEqual([
|
||||
"displayLanguage",
|
||||
"showStatusOnEditor",
|
||||
"showOnlyIconsOnEditor",
|
||||
"showStatusOnStatusbar",
|
||||
"hideFileWarningNotice",
|
||||
"networkWarningStyle",
|
||||
"lessInformationInLog",
|
||||
"showVerboseLog",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps the three feature-level controls together under Extra menus", () => {
|
||||
const group = createExtraMenuSettingSpecGroup();
|
||||
|
||||
expect(group.heading).toBe("Extra menus");
|
||||
expect(group.items.map(({ key }) => key)).toEqual(["useAdvancedMode", "usePowerUserMode", "useEdgeCaseMode"]);
|
||||
});
|
||||
|
||||
it("retains the two conditional visibility rules", () => {
|
||||
let editorDetails = false;
|
||||
let verboseLog = false;
|
||||
const specs = createGeneralSettingSpecGroups({
|
||||
showEditorStatusDetails: () => editorDetails,
|
||||
showVerboseLog: () => verboseLog,
|
||||
}).flatMap(({ items }) => items);
|
||||
const editorIcons = specs.find(({ key }) => key === "showOnlyIconsOnEditor");
|
||||
const verbose = specs.find(({ key }) => key === "showVerboseLog");
|
||||
|
||||
expect(editorIcons?.visible?.()).toBe(false);
|
||||
expect(verbose?.visible?.()).toBe(false);
|
||||
editorDetails = true;
|
||||
verboseLog = true;
|
||||
expect(editorIcons?.visible?.()).toBe(true);
|
||||
expect(verbose?.visible?.()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SettingSpec conversion", () => {
|
||||
it("maps metadata and a toggle to an Obsidian definition without importing the runtime API", () => {
|
||||
const visible = vi.fn(() => true);
|
||||
|
||||
@@ -11,17 +11,23 @@ import { toObsidianSettingDefinition, type PersistedSettingKey, type SettingSpec
|
||||
import { getConfig } from "./settingConstants.ts";
|
||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||
import type { PageFunctions } from "./SettingPane.ts";
|
||||
import {
|
||||
createExtraMenuSettingSpecGroup,
|
||||
createGeneralSettingSpecGroups,
|
||||
type GeneralSettingSpecContext,
|
||||
} from "./GeneralSettingSpecs.ts";
|
||||
import { paneAdvanced } from "./PaneAdvanced.ts";
|
||||
import { paneChangeLog } from "./PaneChangeLog.ts";
|
||||
import { paneCustomisationSync } from "./PaneCustomisationSync.ts";
|
||||
import { paneGeneral } from "./PaneGeneral.ts";
|
||||
import { paneHatch } from "./PaneHatch.ts";
|
||||
import { paneMaintenance } from "./PaneMaintenance.ts";
|
||||
import { paneHelp } from "./PaneHelp.ts";
|
||||
import { panePatches } from "./PanePatches.ts";
|
||||
import { panePowerUsers } from "./PanePowerUsers.ts";
|
||||
import { paneRemoteConfig } from "./PaneRemoteConfig.ts";
|
||||
import { paneSelector } from "./PaneSelector.ts";
|
||||
import { paneSetup } from "./PaneSetup.ts";
|
||||
import { paneQuickSetup } from "./PaneQuickSetup.ts";
|
||||
import { paneSyncSettings } from "./PaneSyncSettings.ts";
|
||||
|
||||
/** The existing pane renderer used by the imperative settings tab and custom pages. */
|
||||
@@ -62,13 +68,13 @@ export function createSettingsPageCatalogue(): SettingsPageEntry[] {
|
||||
legacy: paneChangeLog,
|
||||
},
|
||||
{
|
||||
id: "setup",
|
||||
name: () => $msg("obsidianLiveSyncSettingTab.panelSetup"),
|
||||
id: "quick-setup",
|
||||
name: () => $msg("obsidianLiveSyncSettingTab.titleQuickSetup"),
|
||||
icon: "🧙♂️",
|
||||
order: 110,
|
||||
level: undefined,
|
||||
content: "custom",
|
||||
legacy: paneSetup,
|
||||
legacy: paneQuickSetup,
|
||||
},
|
||||
{
|
||||
id: "general",
|
||||
@@ -160,6 +166,15 @@ export function createSettingsPageCatalogue(): SettingsPageEntry[] {
|
||||
content: "custom",
|
||||
legacy: paneMaintenance,
|
||||
},
|
||||
{
|
||||
id: "help",
|
||||
name: () => $msg("obsidianLiveSyncSettingTab.titleHelpAndTroubleshooting"),
|
||||
icon: "❓",
|
||||
order: 90,
|
||||
level: undefined,
|
||||
content: "custom",
|
||||
legacy: paneHelp,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -169,7 +184,7 @@ const numberRangeMessage = ({ min, max }: { min?: number; max?: number }): strin
|
||||
max: max === undefined ? "~" : `${max}`,
|
||||
});
|
||||
|
||||
function toAdvancedSettingDefinition(spec: SettingSpec): ReturnType<typeof toObsidianSettingDefinition> {
|
||||
function toSettingDefinition(spec: SettingSpec): ReturnType<typeof toObsidianSettingDefinition> {
|
||||
const metadata = getConfig(spec.key);
|
||||
if (!metadata) {
|
||||
throw new Error(`Missing translated setting metadata for ${spec.key}`);
|
||||
@@ -186,6 +201,22 @@ export function createAdvancedSettingDefinitionGroups(
|
||||
return createAdvancedSettingSpecGroups(context).map((group) => ({
|
||||
type: "group",
|
||||
heading: group.heading,
|
||||
items: group.items.map(toAdvancedSettingDefinition),
|
||||
items: group.items.map(toSettingDefinition),
|
||||
}));
|
||||
}
|
||||
|
||||
/** Convert the shared General specifications to native Obsidian groups. */
|
||||
export function createGeneralSettingDefinitionGroups(
|
||||
context: GeneralSettingSpecContext
|
||||
): SettingDefinitionGroup<PersistedSettingKey>[] {
|
||||
return createGeneralSettingSpecGroups(context).map((group) => ({
|
||||
type: "group",
|
||||
heading: group.heading,
|
||||
items: group.items.map(toSettingDefinition),
|
||||
}));
|
||||
}
|
||||
|
||||
/** Convert the Extra menus feature-level controls to native Obsidian settings. */
|
||||
export function createExtraMenuSettingDefinitions(): ReturnType<typeof toObsidianSettingDefinition>[] {
|
||||
return createExtraMenuSettingSpecGroup().items.map(toSettingDefinition);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ vi.mock("@/common/translation", () => ({
|
||||
translateLiveSyncMessage: (key: string) => key,
|
||||
}));
|
||||
vi.mock("./PaneChangeLog.ts", () => ({ paneChangeLog: vi.fn() }));
|
||||
vi.mock("./PaneSetup.ts", () => ({ paneSetup: vi.fn() }));
|
||||
vi.mock("./PaneQuickSetup.ts", () => ({ paneQuickSetup: vi.fn() }));
|
||||
vi.mock("./PaneHelp.ts", () => ({ paneHelp: vi.fn() }));
|
||||
vi.mock("./PaneGeneral.ts", () => ({ paneGeneral: vi.fn() }));
|
||||
vi.mock("./PaneRemoteConfig.ts", () => ({ paneRemoteConfig: vi.fn() }));
|
||||
vi.mock("./PaneSelector.ts", () => ({ paneSelector: vi.fn() }));
|
||||
@@ -25,7 +26,7 @@ describe("settings page catalogue", () => {
|
||||
|
||||
expect(catalogue.map(({ id }) => id)).toEqual([
|
||||
"change-log",
|
||||
"setup",
|
||||
"quick-setup",
|
||||
"general",
|
||||
"remote-configuration",
|
||||
"synchronisation",
|
||||
@@ -36,11 +37,18 @@ describe("settings page catalogue", () => {
|
||||
"power-users",
|
||||
"patches",
|
||||
"maintenance",
|
||||
"help",
|
||||
]);
|
||||
expect(new Set(catalogue.map(({ id }) => id)).size).toBe(catalogue.length);
|
||||
expect(new Set(catalogue.map(({ name }) => name())).size).toBe(catalogue.length);
|
||||
expect(catalogue.filter(({ content }) => content === "native").map(({ id }) => id)).toEqual(["advanced"]);
|
||||
expect(catalogue.filter(({ content }) => content === "custom")).toHaveLength(11);
|
||||
expect(catalogue.filter(({ content }) => content === "custom")).toHaveLength(12);
|
||||
expect(catalogue.find(({ id }) => id === "quick-setup")?.name()).toBe(
|
||||
"obsidianLiveSyncSettingTab.titleQuickSetup"
|
||||
);
|
||||
expect(catalogue.find(({ id }) => id === "help")?.name()).toBe(
|
||||
"obsidianLiveSyncSettingTab.titleHelpAndTroubleshooting"
|
||||
);
|
||||
});
|
||||
|
||||
it("registers each Advanced control key exactly once", () => {
|
||||
|
||||
Reference in New Issue
Block a user