mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-18 21:41:17 +00:00
Improved: remote management
This commit is contained in:
@@ -166,7 +166,7 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
|
||||
} as ObsidianLiveSyncSettings;
|
||||
|
||||
console.log(`[Command] setup -> ${settingsPath}`);
|
||||
await core.services.setting.applyPartial(nextSettings, true);
|
||||
await core.services.setting.applyExternalSettings(nextSettings, true);
|
||||
await core.services.control.applySettings();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ function createCoreMock() {
|
||||
applySettings: vi.fn(async () => {}),
|
||||
},
|
||||
setting: {
|
||||
applyExternalSettings: vi.fn(async () => {}),
|
||||
applyPartial: vi.fn(async () => {}),
|
||||
},
|
||||
},
|
||||
@@ -176,9 +177,9 @@ describe("runCommand abnormal cases", () => {
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(core.services.setting.applyPartial).toHaveBeenCalledTimes(1);
|
||||
expect(core.services.setting.applyExternalSettings).toHaveBeenCalledTimes(1);
|
||||
expect(core.services.control.applySettings).toHaveBeenCalledTimes(1);
|
||||
const [appliedSettings, saveImmediately] = core.services.setting.applyPartial.mock.calls[0];
|
||||
const [appliedSettings, saveImmediately] = core.services.setting.applyExternalSettings.mock.calls[0];
|
||||
expect(saveImmediately).toBe(true);
|
||||
expect(appliedSettings.couchDB_URI).toBe("http://127.0.0.1:5984");
|
||||
expect(appliedSettings.couchDB_DBNAME).toBe("livesync-test-db");
|
||||
@@ -198,7 +199,7 @@ describe("runCommand abnormal cases", () => {
|
||||
})
|
||||
).rejects.toThrow();
|
||||
|
||||
expect(core.services.setting.applyPartial).not.toHaveBeenCalled();
|
||||
expect(core.services.setting.applyExternalSettings).not.toHaveBeenCalled();
|
||||
expect(core.services.control.applySettings).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
{
|
||||
"name": "self-hosted-livesync-cli",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"main": "dist/index.cjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"cli": "node dist/index.cjs",
|
||||
"buildRun": "npm run build && npm run cli --",
|
||||
"build:docker": "docker build -f Dockerfile -t livesync-cli ../../..",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json",
|
||||
"test:unit": "cd ../../.. && npx vitest run --config vitest.config.unit.ts src/apps/cli/main.unit.spec.ts src/apps/cli/commands/utils.unit.spec.ts src/apps/cli/commands/runCommand.unit.spec.ts src/apps/cli/commands/p2p.unit.spec.ts",
|
||||
"test:e2e:two-vaults": "bash test/test-e2e-two-vaults-with-docker-linux.sh",
|
||||
"test:e2e:two-vaults:common": "bash test/test-e2e-two-vaults-common.sh",
|
||||
"test:e2e:two-vaults:matrix": "bash test/test-e2e-two-vaults-matrix.sh",
|
||||
"test:e2e:push-pull": "bash test/test-push-pull-linux.sh",
|
||||
"test:e2e:setup-put-cat": "bash test/test-setup-put-cat-linux.sh",
|
||||
"test:e2e:sync-two-local": "bash test/test-sync-two-local-databases-linux.sh",
|
||||
"test:e2e:p2p": "bash test/test-p2p-three-nodes-conflict-linux.sh",
|
||||
"test:e2e:p2p-upload-download-repro": "bash test/test-p2p-upload-download-repro-linux.sh",
|
||||
"test:e2e:p2p-host": "bash test/test-p2p-host-linux.sh",
|
||||
"test:e2e:p2p-sync": "bash test/test-p2p-sync-linux.sh",
|
||||
"test:e2e:mirror": "bash test/test-mirror-linux.sh",
|
||||
"pretest:e2e:all": "npm run build",
|
||||
"test:e2e:all": " export RUN_BUILD=0 && npm run test:e2e:setup-put-cat && npm run test:e2e:push-pull && npm run test:e2e:sync-two-local && npm run test:e2e:p2p && npm run test:e2e:mirror && npm run test:e2e:two-vaults && npm run test:e2e:p2p",
|
||||
"pretest:e2e:docker:all": "npm run build:docker",
|
||||
"test:e2e:docker:push-pull": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-push-pull-linux.sh",
|
||||
"test:e2e:docker:setup-put-cat": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-setup-put-cat-linux.sh",
|
||||
"test:e2e:docker:mirror": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-mirror-linux.sh",
|
||||
"test:e2e:docker:sync-two-local": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-sync-two-local-databases-linux.sh",
|
||||
"test:e2e:docker:p2p": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-p2p-three-nodes-conflict-linux.sh",
|
||||
"test:e2e:docker:p2p-sync": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-p2p-sync-linux.sh",
|
||||
"test:e2e:docker:all": "export RUN_BUILD=0 && npm run test:e2e:docker:setup-put-cat && npm run test:e2e:docker:push-pull && npm run test:e2e:docker:sync-two-local && npm run test:e2e:docker:mirror"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {}
|
||||
}
|
||||
{
|
||||
"name": "self-hosted-livesync-cli",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"main": "dist/index.cjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"cli": "node dist/index.cjs",
|
||||
"buildRun": "npm run build && npm run cli --",
|
||||
"build:docker": "docker build -f Dockerfile -t livesync-cli ../../..",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json",
|
||||
"test:unit": "cd ../../.. && npx vitest run --config vitest.config.unit.ts src/apps/cli/main.unit.spec.ts src/apps/cli/commands/utils.unit.spec.ts src/apps/cli/commands/runCommand.unit.spec.ts src/apps/cli/commands/p2p.unit.spec.ts",
|
||||
"test:e2e:two-vaults": "bash test/test-e2e-two-vaults-with-docker-linux.sh",
|
||||
"test:e2e:two-vaults:common": "bash test/test-e2e-two-vaults-common.sh",
|
||||
"test:e2e:two-vaults:matrix": "bash test/test-e2e-two-vaults-matrix.sh",
|
||||
"test:e2e:push-pull": "bash test/test-push-pull-linux.sh",
|
||||
"test:e2e:setup-put-cat": "bash test/test-setup-put-cat-linux.sh",
|
||||
"test:e2e:sync-two-local": "bash test/test-sync-two-local-databases-linux.sh",
|
||||
"test:e2e:p2p": "bash test/test-p2p-three-nodes-conflict-linux.sh",
|
||||
"test:e2e:p2p-upload-download-repro": "bash test/test-p2p-upload-download-repro-linux.sh",
|
||||
"test:e2e:p2p-host": "bash test/test-p2p-host-linux.sh",
|
||||
"test:e2e:p2p-sync": "bash test/test-p2p-sync-linux.sh",
|
||||
"test:e2e:mirror": "bash test/test-mirror-linux.sh",
|
||||
"pretest:e2e:all": "npm run build",
|
||||
"test:e2e:all": " export RUN_BUILD=0 && npm run test:e2e:setup-put-cat && npm run test:e2e:push-pull && npm run test:e2e:sync-two-local && npm run test:e2e:p2p && npm run test:e2e:mirror && npm run test:e2e:two-vaults && npm run test:e2e:p2p",
|
||||
"pretest:e2e:docker:all": "npm run build:docker",
|
||||
"test:e2e:docker:push-pull": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-push-pull-linux.sh",
|
||||
"test:e2e:docker:setup-put-cat": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-setup-put-cat-linux.sh",
|
||||
"test:e2e:docker:mirror": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-mirror-linux.sh",
|
||||
"test:e2e:docker:sync-two-local": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-sync-two-local-databases-linux.sh",
|
||||
"test:e2e:docker:p2p": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-p2p-three-nodes-conflict-linux.sh",
|
||||
"test:e2e:docker:p2p-sync": "RUN_BUILD=0 LIVESYNC_TEST_DOCKER=1 bash test/test-p2p-sync-linux.sh",
|
||||
"test:e2e:docker:all": "export RUN_BUILD=0 && npm run test:e2e:docker:setup-put-cat && npm run test:e2e:docker:push-pull && npm run test:e2e:docker:sync-two-local && npm run test:e2e:docker:mirror"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
{
|
||||
"name": "livesync-cli-runtime",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"description": "Runtime dependencies for Self-hosted LiveSync CLI Docker image",
|
||||
"dependencies": {
|
||||
"commander": "^14.0.3",
|
||||
"werift": "^0.22.9",
|
||||
"pouchdb-adapter-http": "^9.0.0",
|
||||
"pouchdb-adapter-idb": "^9.0.0",
|
||||
"pouchdb-adapter-indexeddb": "^9.0.0",
|
||||
"pouchdb-adapter-leveldb": "^9.0.0",
|
||||
"pouchdb-adapter-memory": "^9.0.0",
|
||||
"pouchdb-core": "^9.0.0",
|
||||
"pouchdb-errors": "^9.0.0",
|
||||
"pouchdb-find": "^9.0.0",
|
||||
"pouchdb-mapreduce": "^9.0.0",
|
||||
"pouchdb-merge": "^9.0.0",
|
||||
"pouchdb-replication": "^9.0.0",
|
||||
"pouchdb-utils": "^9.0.0",
|
||||
"pouchdb-wrappers": "*",
|
||||
"transform-pouch": "^2.0.0"
|
||||
}
|
||||
}
|
||||
{
|
||||
"name": "livesync-cli-runtime",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"description": "Runtime dependencies for Self-hosted LiveSync CLI Docker image",
|
||||
"dependencies": {
|
||||
"commander": "^14.0.3",
|
||||
"werift": "^0.22.9",
|
||||
"pouchdb-adapter-http": "^9.0.0",
|
||||
"pouchdb-adapter-idb": "^9.0.0",
|
||||
"pouchdb-adapter-indexeddb": "^9.0.0",
|
||||
"pouchdb-adapter-leveldb": "^9.0.0",
|
||||
"pouchdb-adapter-memory": "^9.0.0",
|
||||
"pouchdb-core": "^9.0.0",
|
||||
"pouchdb-errors": "^9.0.0",
|
||||
"pouchdb-find": "^9.0.0",
|
||||
"pouchdb-mapreduce": "^9.0.0",
|
||||
"pouchdb-merge": "^9.0.0",
|
||||
"pouchdb-replication": "^9.0.0",
|
||||
"pouchdb-utils": "^9.0.0",
|
||||
"pouchdb-wrappers": "*",
|
||||
"transform-pouch": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,7 +276,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
|
||||
}
|
||||
}
|
||||
}
|
||||
await this.services.setting.applyPartial(remoteConfig, true);
|
||||
await this.services.setting.applyExternalSettings(remoteConfig, true);
|
||||
if (yn !== DROP) {
|
||||
await this.plugin.core.services.appLifecycle.scheduleRestart();
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ And you can also drop the local database to rebuild from the remote device.`,
|
||||
|
||||
// this.plugin.settings = remoteConfig;
|
||||
// await this.plugin.saveSettings();
|
||||
await this.core.services.setting.applyPartial(remoteConfig);
|
||||
await this.core.services.setting.applyExternalSettings(remoteConfig);
|
||||
if (yn === DROP) {
|
||||
await this.core.rebuilder.scheduleFetch();
|
||||
} else {
|
||||
|
||||
2
src/lib
2
src/lib
Submodule src/lib updated: d14de2d8fc...cdd8693498
@@ -162,8 +162,8 @@ export class ModuleObsidianSettingsAsMarkdown extends AbstractModule {
|
||||
result == APPLY_AND_REBUILD ||
|
||||
result == APPLY_AND_FETCH
|
||||
) {
|
||||
this.core.settings = settingToApply;
|
||||
await this.services.setting.saveSettingData();
|
||||
await this.services.setting.applyExternalSettings(settingToApply, true);
|
||||
this.services.setting.clearUsedPassphrase();
|
||||
if (result == APPLY_ONLY) {
|
||||
this._log("Loaded settings have been applied!", LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
|
||||
@@ -3,8 +3,10 @@ import {
|
||||
REMOTE_MINIO,
|
||||
REMOTE_P2P,
|
||||
DEFAULT_SETTINGS,
|
||||
LOG_LEVEL_NOTICE,
|
||||
type ObsidianLiveSyncSettings,
|
||||
} from "../../../lib/src/common/types.ts";
|
||||
import { Menu } from "@/deps.ts";
|
||||
import { $msg } from "../../../lib/src/common/i18n.ts";
|
||||
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
||||
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
|
||||
@@ -24,6 +26,7 @@ import { SetupManager, UserMode } from "../SetupManager.ts";
|
||||
import { OnDialogSettingsDefault, type AllSettings } from "./settingConstants.ts";
|
||||
import { activateRemoteConfiguration } from "../../../lib/src/serviceFeatures/remoteConfig.ts";
|
||||
import { ConnectionStringParser } from "../../../lib/src/common/ConnectionString.ts";
|
||||
import type { RemoteConfigurationResult } from "../../../lib/src/common/ConnectionString.ts";
|
||||
import type { RemoteConfiguration } from "../../../lib/src/common/models/setting.type.ts";
|
||||
import SetupRemote from "../SetupWizard/dialogs/SetupRemote.svelte";
|
||||
import SetupRemoteCouchDB from "../SetupWizard/dialogs/SetupRemoteCouchDB.svelte";
|
||||
@@ -67,6 +70,29 @@ function serializeRemoteConfiguration(settings: ObsidianLiveSyncSettings): strin
|
||||
return ConnectionStringParser.serialize({ type: "couchdb", settings });
|
||||
}
|
||||
|
||||
function setEmojiButton(button: any, emoji: string, tooltip: string) {
|
||||
button.setButtonText(emoji);
|
||||
button.setTooltip(tooltip, { delay: 10, placement: "top" });
|
||||
// button.buttonEl.addClass("clickable-icon");
|
||||
button.buttonEl.addClass("mod-muted");
|
||||
return button;
|
||||
}
|
||||
|
||||
function suggestRemoteConfigurationName(parsed: RemoteConfigurationResult): string {
|
||||
if (parsed.type === "couchdb") {
|
||||
try {
|
||||
const url = new URL(parsed.settings.couchDB_URI);
|
||||
return `CouchDB ${url.host}`;
|
||||
} catch {
|
||||
return "Imported CouchDB";
|
||||
}
|
||||
}
|
||||
if (parsed.type === "s3") {
|
||||
return `S3 ${parsed.settings.bucket || parsed.settings.endpoint}`;
|
||||
}
|
||||
return `P2P ${parsed.settings.P2P_roomID || "Remote"}`;
|
||||
}
|
||||
|
||||
export function paneRemoteConfig(
|
||||
this: ObsidianLiveSyncSettingTab,
|
||||
paneEl: HTMLElement,
|
||||
@@ -237,11 +263,60 @@ export function paneRemoteConfig(
|
||||
await persistRemoteConfigurations(this.editingSettings.activeConfigurationId === id);
|
||||
refreshList();
|
||||
};
|
||||
const importRemoteConfiguration = async () => {
|
||||
const importedURI = await this.services.UI.confirm.askString(
|
||||
"Import connection",
|
||||
"Paste a connection string",
|
||||
""
|
||||
);
|
||||
if (importedURI === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const trimmedURI = importedURI.trim();
|
||||
if (trimmedURI === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
let parsed: RemoteConfigurationResult;
|
||||
try {
|
||||
parsed = ConnectionStringParser.parse(trimmedURI);
|
||||
} catch (ex) {
|
||||
this.services.API.addLog(`Failed to import remote configuration: ${ex}`, LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultName = suggestRemoteConfigurationName(parsed);
|
||||
const name = await this.services.UI.confirm.askString("Remote name", "Display name", defaultName);
|
||||
if (name === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = createRemoteConfigurationId();
|
||||
const configs = cloneRemoteConfigurations(this.editingSettings.remoteConfigurations);
|
||||
configs[id] = {
|
||||
id,
|
||||
name: name.trim() || defaultName,
|
||||
uri: ConnectionStringParser.serialize(parsed),
|
||||
isEncrypted: false,
|
||||
};
|
||||
this.editingSettings.remoteConfigurations = configs;
|
||||
if (!this.editingSettings.activeConfigurationId) {
|
||||
this.editingSettings.activeConfigurationId = id;
|
||||
}
|
||||
await persistRemoteConfigurations(this.editingSettings.activeConfigurationId === id);
|
||||
refreshList();
|
||||
};
|
||||
actions.addButton((button) =>
|
||||
button.setButtonText("Add New Connection").onClick(async () => {
|
||||
setEmojiButton(button, "➕", "Add new connection").onClick(async () => {
|
||||
await addRemoteConfiguration();
|
||||
})
|
||||
);
|
||||
actions.addButton((button) =>
|
||||
setEmojiButton(button, "📥", "Import connection").onClick(async () => {
|
||||
await importRemoteConfiguration();
|
||||
})
|
||||
);
|
||||
const refreshList = () => {
|
||||
listContainer.empty();
|
||||
const configs = this.editingSettings.remoteConfigurations || {};
|
||||
@@ -256,7 +331,7 @@ export function paneRemoteConfig(
|
||||
}
|
||||
|
||||
row.addButton((btn) =>
|
||||
btn.setButtonText("Configure").onClick(async () => {
|
||||
setEmojiButton(btn, "🔧", "Configure").onClick(async () => {
|
||||
const parsed = ConnectionStringParser.parse(config.uri);
|
||||
const workSettings = createBaseRemoteSettings();
|
||||
if (parsed.type === "couchdb") {
|
||||
@@ -284,83 +359,10 @@ export function paneRemoteConfig(
|
||||
refreshList();
|
||||
})
|
||||
);
|
||||
row.addButton((btn) =>
|
||||
btn.setButtonText("Rename").onClick(async () => {
|
||||
const nextName = await this.services.UI.confirm.askString(
|
||||
"Remote name",
|
||||
"Display name",
|
||||
config.name
|
||||
);
|
||||
if (nextName === false) {
|
||||
return;
|
||||
}
|
||||
const nextConfigs = cloneRemoteConfigurations(this.editingSettings.remoteConfigurations);
|
||||
nextConfigs[config.id] = {
|
||||
...config,
|
||||
name: nextName.trim() || config.name,
|
||||
};
|
||||
this.editingSettings.remoteConfigurations = nextConfigs;
|
||||
await persistRemoteConfigurations();
|
||||
refreshList();
|
||||
})
|
||||
);
|
||||
row.addButton((btn) =>
|
||||
btn.setButtonText("Duplicate").onClick(async () => {
|
||||
const nextName = await this.services.UI.confirm.askString(
|
||||
"Duplicate remote",
|
||||
"Display name",
|
||||
`${config.name} (Copy)`
|
||||
);
|
||||
if (nextName === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextId = createRemoteConfigurationId();
|
||||
const nextConfigs = cloneRemoteConfigurations(this.editingSettings.remoteConfigurations);
|
||||
nextConfigs[nextId] = {
|
||||
...config,
|
||||
id: nextId,
|
||||
name: nextName.trim() || `${config.name} (Copy)`,
|
||||
};
|
||||
this.editingSettings.remoteConfigurations = nextConfigs;
|
||||
await persistRemoteConfigurations();
|
||||
refreshList();
|
||||
})
|
||||
);
|
||||
row.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText("Delete")
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
const confirmed = await this.services.UI.confirm.askYesNoDialog(
|
||||
`Delete remote configuration '${config.name}'?`,
|
||||
{ title: "Delete Remote Configuration", defaultOption: "No" }
|
||||
);
|
||||
if (confirmed !== "yes") {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextConfigs = cloneRemoteConfigurations(
|
||||
this.editingSettings.remoteConfigurations
|
||||
);
|
||||
delete nextConfigs[config.id];
|
||||
this.editingSettings.remoteConfigurations = nextConfigs;
|
||||
|
||||
let syncActiveRemote = false;
|
||||
if (this.editingSettings.activeConfigurationId === config.id) {
|
||||
const nextActiveId = Object.keys(nextConfigs)[0] || "";
|
||||
this.editingSettings.activeConfigurationId = nextActiveId;
|
||||
syncActiveRemote = nextActiveId !== "";
|
||||
}
|
||||
|
||||
await persistRemoteConfigurations(syncActiveRemote);
|
||||
refreshList();
|
||||
})
|
||||
);
|
||||
|
||||
row.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText("Activate")
|
||||
.setButtonText("✅")
|
||||
.setTooltip("Activate", { delay: 10, placement: "top" })
|
||||
.setDisabled(config.id === this.editingSettings.activeConfigurationId)
|
||||
.onClick(async () => {
|
||||
this.editingSettings.activeConfigurationId = config.id;
|
||||
@@ -368,6 +370,97 @@ export function paneRemoteConfig(
|
||||
refreshList();
|
||||
})
|
||||
);
|
||||
|
||||
row.addButton((btn) =>
|
||||
setEmojiButton(btn, "…", "More actions").onClick(() => {
|
||||
const menu = new Menu()
|
||||
.addItem((item) => {
|
||||
item.setTitle("🪪 Rename").onClick(async () => {
|
||||
const nextName = await this.services.UI.confirm.askString(
|
||||
"Remote name",
|
||||
"Display name",
|
||||
config.name
|
||||
);
|
||||
if (nextName === false) {
|
||||
return;
|
||||
}
|
||||
const nextConfigs = cloneRemoteConfigurations(
|
||||
this.editingSettings.remoteConfigurations
|
||||
);
|
||||
nextConfigs[config.id] = {
|
||||
...config,
|
||||
name: nextName.trim() || config.name,
|
||||
};
|
||||
this.editingSettings.remoteConfigurations = nextConfigs;
|
||||
await persistRemoteConfigurations();
|
||||
refreshList();
|
||||
});
|
||||
})
|
||||
.addItem((item) => {
|
||||
item.setTitle("📤 Export").onClick(async () => {
|
||||
await this.services.UI.promptCopyToClipboard(
|
||||
`Remote configuration: ${config.name}`,
|
||||
config.uri
|
||||
);
|
||||
});
|
||||
})
|
||||
.addItem((item) => {
|
||||
item.setTitle("🧬 Duplicate").onClick(async () => {
|
||||
const nextName = await this.services.UI.confirm.askString(
|
||||
"Duplicate remote",
|
||||
"Display name",
|
||||
`${config.name} (Copy)`
|
||||
);
|
||||
if (nextName === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextId = createRemoteConfigurationId();
|
||||
const nextConfigs = cloneRemoteConfigurations(
|
||||
this.editingSettings.remoteConfigurations
|
||||
);
|
||||
nextConfigs[nextId] = {
|
||||
...config,
|
||||
id: nextId,
|
||||
name: nextName.trim() || `${config.name} (Copy)`,
|
||||
};
|
||||
this.editingSettings.remoteConfigurations = nextConfigs;
|
||||
await persistRemoteConfigurations();
|
||||
refreshList();
|
||||
});
|
||||
})
|
||||
.addSeparator()
|
||||
.addItem((item) => {
|
||||
item.setTitle("🗑 Delete").onClick(async () => {
|
||||
const confirmed = await this.services.UI.confirm.askYesNoDialog(
|
||||
`Delete remote configuration '${config.name}'?`,
|
||||
{ title: "Delete Remote Configuration", defaultOption: "No" }
|
||||
);
|
||||
if (confirmed !== "yes") {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextConfigs = cloneRemoteConfigurations(
|
||||
this.editingSettings.remoteConfigurations
|
||||
);
|
||||
delete nextConfigs[config.id];
|
||||
this.editingSettings.remoteConfigurations = nextConfigs;
|
||||
|
||||
let syncActiveRemote = false;
|
||||
if (this.editingSettings.activeConfigurationId === config.id) {
|
||||
const nextActiveId = Object.keys(nextConfigs)[0] || "";
|
||||
this.editingSettings.activeConfigurationId = nextActiveId;
|
||||
syncActiveRemote = nextActiveId !== "";
|
||||
}
|
||||
|
||||
await persistRemoteConfigurations(syncActiveRemote);
|
||||
refreshList();
|
||||
});
|
||||
});
|
||||
const rect = btn.buttonEl.getBoundingClientRect();
|
||||
menu.showAtPosition({ x: rect.left, y: rect.bottom });
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
refreshList();
|
||||
|
||||
@@ -275,6 +275,10 @@ export class SetupManager extends AbstractModule {
|
||||
activate: boolean = true,
|
||||
extra: () => void = () => {}
|
||||
): Promise<boolean> {
|
||||
newConf = await this.services.setting.adjustSettings({
|
||||
...this.settings,
|
||||
...newConf,
|
||||
});
|
||||
let userMode = _userMode;
|
||||
if (userMode === UserMode.Unknown) {
|
||||
if (isObjectDifferent(this.settings, newConf, true) === false) {
|
||||
@@ -368,13 +372,8 @@ export class SetupManager extends AbstractModule {
|
||||
* @returns Promise that resolves to true if settings applied successfully, false otherwise
|
||||
*/
|
||||
async applySetting(newConf: ObsidianLiveSyncSettings, userMode: UserMode) {
|
||||
const newSetting = {
|
||||
...this.core.settings,
|
||||
...newConf,
|
||||
};
|
||||
this.core.settings = newSetting;
|
||||
this.services.setting.clearUsedPassphrase();
|
||||
await this.services.setting.saveSettingData();
|
||||
await this.services.setting.applyExternalSettings(newConf, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
157
src/modules/features/SetupManager.unit.spec.ts
Normal file
157
src/modules/features/SetupManager.unit.spec.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { DEFAULT_SETTINGS, REMOTE_COUCHDB, type ObsidianLiveSyncSettings } from "../../lib/src/common/types";
|
||||
import { SettingService } from "../../lib/src/services/base/SettingService";
|
||||
import { ServiceContext } from "../../lib/src/services/base/ServiceBase";
|
||||
|
||||
vi.mock("./SetupWizard/dialogs/Intro.svelte", () => ({ default: {} }));
|
||||
vi.mock("./SetupWizard/dialogs/SelectMethodNewUser.svelte", () => ({ default: {} }));
|
||||
vi.mock("./SetupWizard/dialogs/SelectMethodExisting.svelte", () => ({ default: {} }));
|
||||
vi.mock("./SetupWizard/dialogs/ScanQRCode.svelte", () => ({ default: {} }));
|
||||
vi.mock("./SetupWizard/dialogs/UseSetupURI.svelte", () => ({ default: {} }));
|
||||
vi.mock("./SetupWizard/dialogs/OutroNewUser.svelte", () => ({ default: {} }));
|
||||
vi.mock("./SetupWizard/dialogs/OutroExistingUser.svelte", () => ({ default: {} }));
|
||||
vi.mock("./SetupWizard/dialogs/OutroAskUserMode.svelte", () => ({ default: {} }));
|
||||
vi.mock("./SetupWizard/dialogs/SetupRemote.svelte", () => ({ default: {} }));
|
||||
vi.mock("./SetupWizard/dialogs/SetupRemoteCouchDB.svelte", () => ({ default: {} }));
|
||||
vi.mock("./SetupWizard/dialogs/SetupRemoteBucket.svelte", () => ({ default: {} }));
|
||||
vi.mock("./SetupWizard/dialogs/SetupRemoteP2P.svelte", () => ({ default: {} }));
|
||||
vi.mock("./SetupWizard/dialogs/SetupRemoteE2EE.svelte", () => ({ default: {} }));
|
||||
|
||||
vi.mock("../../lib/src/API/processSetting.ts", () => ({
|
||||
decodeSettingsFromQRCodeData: vi.fn(),
|
||||
}));
|
||||
|
||||
import { decodeSettingsFromQRCodeData } from "../../lib/src/API/processSetting.ts";
|
||||
import { SetupManager, UserMode } from "./SetupManager";
|
||||
|
||||
class TestSettingService extends SettingService<ServiceContext> {
|
||||
protected setItem(_key: string, _value: string): void {}
|
||||
protected getItem(_key: string): string {
|
||||
return "";
|
||||
}
|
||||
protected deleteItem(_key: string): void {}
|
||||
protected saveData(_setting: ObsidianLiveSyncSettings): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
protected loadData(): Promise<ObsidianLiveSyncSettings | undefined> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
function createLegacyRemoteSetting(): ObsidianLiveSyncSettings {
|
||||
return {
|
||||
...DEFAULT_SETTINGS,
|
||||
remoteConfigurations: {},
|
||||
activeConfigurationId: "",
|
||||
remoteType: REMOTE_COUCHDB,
|
||||
couchDB_URI: "http://localhost:5984",
|
||||
couchDB_USER: "user",
|
||||
couchDB_PASSWORD: "password",
|
||||
couchDB_DBNAME: "vault",
|
||||
};
|
||||
}
|
||||
|
||||
function createSetupManager() {
|
||||
const setting = new TestSettingService(new ServiceContext(), {
|
||||
APIService: {
|
||||
getSystemVaultName: vi.fn(() => "vault"),
|
||||
getAppID: vi.fn(() => "app"),
|
||||
confirm: {
|
||||
askString: vi.fn(() => Promise.resolve("")),
|
||||
},
|
||||
addLog: vi.fn(),
|
||||
addCommand: vi.fn(),
|
||||
registerWindow: vi.fn(),
|
||||
addRibbonIcon: vi.fn(),
|
||||
registerProtocolHandler: vi.fn(),
|
||||
} as any,
|
||||
});
|
||||
setting.settings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
remoteConfigurations: {},
|
||||
activeConfigurationId: "",
|
||||
};
|
||||
vi.spyOn(setting, "saveSettingData").mockResolvedValue();
|
||||
|
||||
const dialogManager = {
|
||||
openWithExplicitCancel: vi.fn(),
|
||||
open: vi.fn(),
|
||||
};
|
||||
const services = {
|
||||
API: {
|
||||
addLog: vi.fn(),
|
||||
addCommand: vi.fn(),
|
||||
registerWindow: vi.fn(),
|
||||
addRibbonIcon: vi.fn(),
|
||||
registerProtocolHandler: vi.fn(),
|
||||
},
|
||||
UI: {
|
||||
dialogManager,
|
||||
},
|
||||
setting,
|
||||
} as any;
|
||||
const core: any = {
|
||||
_services: services,
|
||||
rebuilder: {
|
||||
scheduleRebuild: vi.fn(() => Promise.resolve()),
|
||||
scheduleFetch: vi.fn(() => Promise.resolve()),
|
||||
},
|
||||
};
|
||||
Object.defineProperty(core, "services", {
|
||||
get() {
|
||||
return services;
|
||||
},
|
||||
});
|
||||
Object.defineProperty(core, "settings", {
|
||||
get() {
|
||||
return setting.settings;
|
||||
},
|
||||
set(value: ObsidianLiveSyncSettings) {
|
||||
setting.settings = value;
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
manager: new SetupManager(core),
|
||||
setting,
|
||||
dialogManager,
|
||||
core,
|
||||
};
|
||||
}
|
||||
|
||||
describe("SetupManager", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("onUseSetupURI should normalise imported legacy remote settings before applying", async () => {
|
||||
const { manager, setting, dialogManager } = createSetupManager();
|
||||
dialogManager.openWithExplicitCancel
|
||||
.mockResolvedValueOnce(createLegacyRemoteSetting())
|
||||
.mockResolvedValueOnce("compatible-existing-user");
|
||||
|
||||
const result = await manager.onUseSetupURI(UserMode.Unknown, "mock-config://settings");
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(setting.currentSettings().remoteConfigurations["legacy-couchdb"]?.uri).toContain(
|
||||
"sls+http://user:password@localhost:5984"
|
||||
);
|
||||
expect(setting.currentSettings().activeConfigurationId).toBe("legacy-couchdb");
|
||||
});
|
||||
|
||||
it("decodeQR should normalise imported legacy remote settings before applying", async () => {
|
||||
const { manager, setting, dialogManager } = createSetupManager();
|
||||
vi.mocked(decodeSettingsFromQRCodeData).mockReturnValue(createLegacyRemoteSetting());
|
||||
dialogManager.openWithExplicitCancel.mockResolvedValueOnce("compatible-existing-user");
|
||||
|
||||
const result = await manager.decodeQR("qr-data");
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(decodeSettingsFromQRCodeData).toHaveBeenCalledWith("qr-data");
|
||||
expect(setting.currentSettings().remoteConfigurations["legacy-couchdb"]?.uri).toContain(
|
||||
"sls+http://user:password@localhost:5984"
|
||||
);
|
||||
expect(setting.currentSettings().activeConfigurationId).toBe("legacy-couchdb");
|
||||
});
|
||||
});
|
||||
@@ -176,7 +176,7 @@ export async function adjustSettingToRemote(
|
||||
...config,
|
||||
...Object.fromEntries(differentItems),
|
||||
} satisfies ObsidianLiveSyncSettings;
|
||||
await host.services.setting.applyPartial(config, true);
|
||||
await host.services.setting.applyExternalSettings(config, true);
|
||||
log("Remote configuration applied.", LOG_LEVEL_NOTICE);
|
||||
canProceed = true;
|
||||
const updatedConfig = host.services.setting.currentSettings();
|
||||
|
||||
@@ -49,6 +49,10 @@ const createSettingServiceMock = () => {
|
||||
return {
|
||||
settings,
|
||||
currentSettings: vi.fn(() => settings),
|
||||
applyExternalSettings: vi.fn((partial: any, _feedback?: boolean) => {
|
||||
Object.assign(settings, partial);
|
||||
return Promise.resolve();
|
||||
}),
|
||||
applyPartial: vi.fn((partial: any, _feedback?: boolean) => {
|
||||
Object.assign(settings, partial);
|
||||
return Promise.resolve();
|
||||
@@ -552,7 +556,7 @@ describe("Red Flag Feature", () => {
|
||||
|
||||
await adjustSettingToRemote(host as any, createLoggerMock(), config);
|
||||
expect(host.mocks.ui.confirm.askSelectStringDialogue).toHaveBeenCalled();
|
||||
expect(host.mocks.setting.applyPartial).toHaveBeenCalled();
|
||||
expect(host.mocks.setting.applyExternalSettings).toHaveBeenCalled();
|
||||
}
|
||||
);
|
||||
const mismatchAcceptedKeys = Object.keys(TweakValuesRecommendedTemplate).filter(
|
||||
@@ -579,7 +583,7 @@ describe("Red Flag Feature", () => {
|
||||
|
||||
await adjustSettingToRemote(host as any, createLoggerMock(), config);
|
||||
|
||||
expect(host.mocks.setting.applyPartial).toHaveBeenCalled();
|
||||
expect(host.mocks.setting.applyExternalSettings).toHaveBeenCalled();
|
||||
expect(host.mocks.ui.confirm.askSelectStringDialogue).not.toHaveBeenCalled();
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user