mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-09-22 02:27:07 +00:00
Route pending settings through initialisation choices
This commit is contained in:
+16
-2
@@ -121,6 +121,10 @@ function isGroup(item: SettingDefinitionItem): item is SettingDefinitionGroup {
|
||||
return "type" in item && item.type === "group";
|
||||
}
|
||||
|
||||
function isAction(item: SettingDefinitionItem): item is Extract<SettingDefinitionItem, { action: unknown }> {
|
||||
return "action" in item && typeof item.action === "function";
|
||||
}
|
||||
|
||||
function itemLabel(item: SettingDefinitionItem): string {
|
||||
if (isPage(item)) return item.name;
|
||||
if (isGroup(item)) return item.heading ?? "";
|
||||
@@ -179,7 +183,7 @@ beforeEach(() => {
|
||||
describe("ObsidianLiveSyncSettingTab native page lifecycle", () => {
|
||||
it("keeps Quick Setup first while synchronisation is inactive and separates synchronisation pages from it", () => {
|
||||
const tab = createSettingsTab();
|
||||
const definitions = tab.getSettingDefinitions();
|
||||
const definitions = tab.getSettingDefinitions().filter(isGroup);
|
||||
|
||||
expect(definitions.slice(0, 3).map(itemLabel)).toEqual([
|
||||
"🧙♂️ Quick Setup",
|
||||
@@ -191,7 +195,7 @@ describe("ObsidianLiveSyncSettingTab native page lifecycle", () => {
|
||||
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();
|
||||
const definitions = tab.getSettingDefinitions().filter(isGroup);
|
||||
|
||||
expect(definitions.slice(0, 3).map(itemLabel)).toEqual([
|
||||
"🔄 Synchronisation",
|
||||
@@ -200,6 +204,16 @@ describe("ObsidianLiveSyncSettingTab native page lifecycle", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps the pending initialisation action visible on the root settings page", () => {
|
||||
const tab = createSettingsTab();
|
||||
tab.editingSettings.handleFilenameCaseSensitive = !tab.initialSettings!.handleFilenameCaseSensitive;
|
||||
|
||||
const action = tab.getSettingDefinitions().find(isAction);
|
||||
|
||||
expect(action?.name).toBe("Apply");
|
||||
expect(typeof action?.visible === "function" ? action.visible() : action?.visible).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps Remote Configuration and Sync Settings as native pages inside the Synchronisation group", () => {
|
||||
const tab = createSettingsTab();
|
||||
const definitions = tab.getSettingDefinitions();
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
type ObsidianLiveSyncSettings,
|
||||
type RemoteDBSettings,
|
||||
LOG_LEVEL_NOTICE,
|
||||
FlagFilesHumanReadable,
|
||||
REMOTE_COUCHDB,
|
||||
REMOTE_MINIO,
|
||||
type ConfigLevel,
|
||||
@@ -79,6 +78,7 @@ import type {
|
||||
} from "obsidian";
|
||||
import { createExtraMenuSettingSpecGroup, createGeneralSettingSpecGroups } from "./GeneralSettingSpecs.ts";
|
||||
import { SetupManager } from "@/modules/features/SetupManager.ts";
|
||||
import { isP2PMainRemote } from "@/common/remoteConfiguration.ts";
|
||||
|
||||
// For creating a document
|
||||
// const toc = new Set<string>();
|
||||
@@ -907,10 +907,11 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
]);
|
||||
const laterGroups = [setupOtherDevices, maintenance, extraFeatures, advancedSettings, helpAndInformation];
|
||||
|
||||
const pendingInitialisation = this.createRebuildRequiredAction();
|
||||
if (this.isAnySyncEnabled()) {
|
||||
return [synchronisation, generalSettings, quickSetup, ...laterGroups];
|
||||
return [pendingInitialisation, synchronisation, generalSettings, quickSetup, ...laterGroups];
|
||||
}
|
||||
return [quickSetup, synchronisation, generalSettings, ...laterGroups];
|
||||
return [pendingInitialisation, quickSetup, synchronisation, generalSettings, ...laterGroups];
|
||||
}
|
||||
|
||||
private beginRenderScope(refresh: () => void): Component {
|
||||
@@ -1031,50 +1032,53 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
Logger(`Passphrase is not valid, please fix it.`, LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
const OPTION_FETCH = $msg("obsidianLiveSyncSettingTab.optionFetchFromRemote");
|
||||
const OPTION_REBUILD_BOTH = $msg("obsidianLiveSyncSettingTab.optionRebuildBoth");
|
||||
const OPTION_ONLY_SETTING = $msg("obsidianLiveSyncSettingTab.optionSaveOnlySettings");
|
||||
const OPTION_CANCEL = $msg("obsidianLiveSyncSettingTab.optionCancel");
|
||||
const title = $msg("obsidianLiveSyncSettingTab.titleRebuildRequired");
|
||||
const note = $msg("obsidianLiveSyncSettingTab.msgRebuildRequired", {
|
||||
OPTION_REBUILD_BOTH,
|
||||
OPTION_FETCH,
|
||||
OPTION_ONLY_SETTING,
|
||||
const keepEditing = $msg("Ui.SetupWizard.ApplySettingsInitialisation.KeepEditing");
|
||||
const setupManager = this.core.getModule(SetupManager);
|
||||
const result = await setupManager.applySettingsWithInitialisationChoice({
|
||||
isP2P: isP2PMainRemote(this.editingSettings),
|
||||
validateChoice: async (mode) => {
|
||||
if (mode !== "fetch" || (await this.checkWorkingPassphrase())) {
|
||||
return true;
|
||||
}
|
||||
const continueFetch = $msg("Ui.SetupWizard.ApplySettingsInitialisation.ContinueFetch");
|
||||
return (
|
||||
(await this.core.confirm.confirmWithMessage(
|
||||
$msg("Ui.SetupWizard.ApplySettingsInitialisation.RemoteVerificationTitle"),
|
||||
$msg("Ui.SetupWizard.ApplySettingsInitialisation.RemoteVerificationGuidance"),
|
||||
[continueFetch, keepEditing],
|
||||
keepEditing
|
||||
)) === continueFetch
|
||||
);
|
||||
},
|
||||
applySettings: async () => {
|
||||
if (!this.editingSettings.encrypt) {
|
||||
this.editingSettings.passphrase = "";
|
||||
}
|
||||
await this.saveAllDirtySettings();
|
||||
},
|
||||
});
|
||||
const buttons = [
|
||||
OPTION_FETCH,
|
||||
OPTION_REBUILD_BOTH, // OPTION_REBUILD_REMOTE,
|
||||
OPTION_ONLY_SETTING,
|
||||
OPTION_CANCEL,
|
||||
];
|
||||
const result = await this.core.confirm.confirmWithMessage(title, note, buttons, OPTION_CANCEL);
|
||||
if (result == OPTION_CANCEL) return;
|
||||
if (result == OPTION_FETCH) {
|
||||
if (!(await this.checkWorkingPassphrase())) {
|
||||
if (
|
||||
(await this.core.confirm.askYesNoDialog($msg("obsidianLiveSyncSettingTab.msgAreYouSureProceed"), {
|
||||
defaultOption: "No",
|
||||
})) != "yes"
|
||||
)
|
||||
return;
|
||||
if (result.result === "scheduled") {
|
||||
this.closeSetting();
|
||||
return;
|
||||
}
|
||||
if (result.result === "failed") {
|
||||
return;
|
||||
}
|
||||
|
||||
const applyWithoutInitialisation = $msg(
|
||||
"Ui.SetupWizard.ApplySettingsInitialisation.ApplyWithoutInitialisation"
|
||||
);
|
||||
const fallback = await this.core.confirm.confirmWithMessage(
|
||||
$msg("Ui.SetupWizard.ApplySettingsInitialisation.BypassTitle"),
|
||||
$msg("Ui.SetupWizard.ApplySettingsInitialisation.BypassGuidance"),
|
||||
[applyWithoutInitialisation, keepEditing],
|
||||
keepEditing
|
||||
);
|
||||
if (fallback === applyWithoutInitialisation) {
|
||||
if (!this.editingSettings.encrypt) {
|
||||
this.editingSettings.passphrase = "";
|
||||
}
|
||||
}
|
||||
if (!this.editingSettings.encrypt) {
|
||||
this.editingSettings.passphrase = "";
|
||||
}
|
||||
await this.saveAllDirtySettings();
|
||||
await Promise.resolve(this.applyAllSettings());
|
||||
if (result == OPTION_FETCH) {
|
||||
await this.core.storageAccess.writeFileAuto(FlagFilesHumanReadable.FETCH_ALL, "");
|
||||
this.services.appLifecycle.scheduleRestart();
|
||||
this.closeSetting();
|
||||
// await rebuildDB("localOnly");
|
||||
} else if (result == OPTION_REBUILD_BOTH) {
|
||||
await this.core.storageAccess.writeFileAuto(FlagFilesHumanReadable.REBUILD_ALL, "");
|
||||
this.services.appLifecycle.scheduleRestart();
|
||||
this.closeSetting();
|
||||
} else if (result == OPTION_ONLY_SETTING) {
|
||||
await this.services.setting.saveSettingData();
|
||||
await this.saveAllDirtySettings();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { DEFAULT_SETTINGS, REMOTE_COUCHDB } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
|
||||
const negotiationMocks = vi.hoisted(() => ({
|
||||
checkSyncInfo: vi.fn(async () => true),
|
||||
}));
|
||||
const settingsInitialisationMocks = vi.hoisted(() => ({
|
||||
applySettingsWithInitialisationChoice: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/deps.ts", () => ({
|
||||
App: class {},
|
||||
@@ -63,6 +66,10 @@ vi.mock("./PaneMaintenance.ts", () => ({ paneMaintenance: vi.fn() }));
|
||||
import { LiveSyncCouchDBReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator";
|
||||
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
||||
|
||||
beforeEach(() => {
|
||||
settingsInitialisationMocks.applySettingsWithInitialisationChoice.mockReset();
|
||||
});
|
||||
|
||||
describe("ObsidianLiveSyncSettingTab passphrase verification", () => {
|
||||
it("closes the finite remote connection after checking synchronisation information", async () => {
|
||||
const remoteDatabase = {
|
||||
@@ -95,6 +102,116 @@ describe("ObsidianLiveSyncSettingTab passphrase verification", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ObsidianLiveSyncSettingTab pending-setting initialisation", () => {
|
||||
function createSettingsTab() {
|
||||
const saveSettingData = vi.fn(async () => undefined);
|
||||
const confirmWithMessage = vi.fn();
|
||||
const plugin = {
|
||||
app: {},
|
||||
core: {
|
||||
settings: {
|
||||
...DEFAULT_SETTINGS,
|
||||
handleFilenameCaseSensitive: false,
|
||||
},
|
||||
getModule: vi.fn(() => settingsInitialisationMocks),
|
||||
confirm: {
|
||||
confirmWithMessage,
|
||||
},
|
||||
services: {
|
||||
setting: {
|
||||
saveSettingData,
|
||||
getDeviceAndVaultName: vi.fn(() => ""),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const tab = new ObsidianLiveSyncSettingTab({} as never, plugin as never);
|
||||
Object.assign(tab, {
|
||||
_editingSettings: {
|
||||
...DEFAULT_SETTINGS,
|
||||
handleFilenameCaseSensitive: true,
|
||||
},
|
||||
initialSettings: {
|
||||
...DEFAULT_SETTINGS,
|
||||
handleFilenameCaseSensitive: false,
|
||||
},
|
||||
});
|
||||
vi.spyOn(tab, "isPassphraseValid").mockResolvedValue(true);
|
||||
vi.spyOn(tab, "checkWorkingPassphrase").mockResolvedValue(true);
|
||||
const closeSetting = vi.spyOn(tab, "closeSetting").mockImplementation(() => undefined);
|
||||
return { tab, saveSettingData, confirmWithMessage, closeSetting };
|
||||
}
|
||||
|
||||
it("keeps pending settings in the editing buffer when initialisation and the fallback are cancelled", async () => {
|
||||
const { tab, saveSettingData, confirmWithMessage, closeSetting } = createSettingsTab();
|
||||
settingsInitialisationMocks.applySettingsWithInitialisationChoice.mockResolvedValueOnce({
|
||||
result: "cancelled",
|
||||
});
|
||||
confirmWithMessage.mockResolvedValueOnce("Keep Editing");
|
||||
|
||||
await tab.confirmRebuild();
|
||||
|
||||
expect(settingsInitialisationMocks.applySettingsWithInitialisationChoice).toHaveBeenCalledOnce();
|
||||
expect(confirmWithMessage).toHaveBeenCalledWith(
|
||||
"Apply Settings without Initialisation?",
|
||||
expect.any(String),
|
||||
["Apply without Initialisation", "Keep Editing"],
|
||||
"Keep Editing"
|
||||
);
|
||||
expect(saveSettingData).not.toHaveBeenCalled();
|
||||
expect(tab.editingSettings.handleFilenameCaseSensitive).toBe(true);
|
||||
expect(tab.core.settings.handleFilenameCaseSensitive).toBe(false);
|
||||
expect(closeSetting).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("applies pending settings only after a separately confirmed initialisation bypass", async () => {
|
||||
const { tab, saveSettingData, confirmWithMessage, closeSetting } = createSettingsTab();
|
||||
settingsInitialisationMocks.applySettingsWithInitialisationChoice.mockResolvedValueOnce({
|
||||
result: "cancelled",
|
||||
});
|
||||
confirmWithMessage.mockResolvedValueOnce("Apply without Initialisation");
|
||||
|
||||
await tab.confirmRebuild();
|
||||
|
||||
expect(settingsInitialisationMocks.applySettingsWithInitialisationChoice).toHaveBeenCalledOnce();
|
||||
expect(saveSettingData).toHaveBeenCalledOnce();
|
||||
expect(tab.core.settings.handleFilenameCaseSensitive).toBe(true);
|
||||
expect(closeSetting).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("closes settings only after initialisation has been scheduled", async () => {
|
||||
const { tab, saveSettingData, confirmWithMessage, closeSetting } = createSettingsTab();
|
||||
settingsInitialisationMocks.applySettingsWithInitialisationChoice.mockImplementationOnce(
|
||||
async ({ applySettings }: { applySettings: () => Promise<void> }) => {
|
||||
await applySettings();
|
||||
return { result: "scheduled", mode: "rebuild" };
|
||||
}
|
||||
);
|
||||
|
||||
await tab.confirmRebuild();
|
||||
|
||||
expect(saveSettingData).toHaveBeenCalledOnce();
|
||||
expect(confirmWithMessage).not.toHaveBeenCalled();
|
||||
expect(closeSetting).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("does not offer the settings-only fallback after an initialisation failure", async () => {
|
||||
const { tab, saveSettingData, confirmWithMessage, closeSetting } = createSettingsTab();
|
||||
settingsInitialisationMocks.applySettingsWithInitialisationChoice.mockResolvedValueOnce({
|
||||
result: "failed",
|
||||
mode: "fetch",
|
||||
});
|
||||
|
||||
await tab.confirmRebuild();
|
||||
|
||||
expect(saveSettingData).not.toHaveBeenCalled();
|
||||
expect(confirmWithMessage).not.toHaveBeenCalled();
|
||||
expect(tab.editingSettings.handleFilenameCaseSensitive).toBe(true);
|
||||
expect(tab.core.settings.handleFilenameCaseSensitive).toBe(false);
|
||||
expect(closeSetting).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ObsidianLiveSyncSettingTab declarative settings boundary", () => {
|
||||
function createSettingsTab() {
|
||||
const saveSettingData = vi.fn(async () => undefined);
|
||||
|
||||
Reference in New Issue
Block a user