Limit commands to applicable contexts

This commit is contained in:
vorotamoroz
2026-07-24 16:13:49 +00:00
parent 6afeb0b409
commit 0e5475b7e3
27 changed files with 727 additions and 75 deletions
+5 -1
View File
@@ -65,7 +65,11 @@ export function useSetupQRCodeFeature(host: NecessaryServices<"API" | "UI" | "se
host.services.API.addCommand({
id: "livesync-setting-qr",
name: "Show settings as a QR code",
callback: () => fireAndForget(encodeSetupSettingsAsQR(host)),
checkCallback: (checking) => {
if (!host.services.setting.currentSettings().isConfigured) return false;
if (!checking) fireAndForget(encodeSetupSettingsAsQR(host));
return true;
},
});
host.services.context.events.onEvent(EVENT_REQUEST_SHOW_SETUP_QR, () =>
fireAndForget(() => encodeSetupSettingsAsQR(host))
@@ -114,4 +114,44 @@ describe("setupObsidian/qrCode", () => {
);
expect(onEventSpy).toHaveBeenCalledWith(EVENT_REQUEST_SHOW_SETUP_QR, expect.any(Function));
});
it("keeps the QR command out of the palette until setup is complete", async () => {
const addHandler = vi.fn();
const commands: Array<{
id: string;
checkCallback?: (checking: boolean) => boolean | void;
}> = [];
const settings = { isConfigured: false };
const host = {
services: {
context: createServiceContext(),
API: {
addCommand: vi.fn((command) => commands.push(command)),
},
appLifecycle: {
onLoaded: {
addHandler,
},
},
setting: {
currentSettings: vi.fn(() => settings),
},
UI: {
confirm: {
confirmWithMessage: vi.fn(),
},
},
},
} as any;
useSetupQRCodeFeature(host);
const loadedHandler = addHandler.mock.calls[0][0] as () => Promise<boolean>;
await loadedHandler();
const command = commands.find((candidate) => candidate.id === "livesync-setting-qr")!;
expect(command.checkCallback?.(true)).toBe(false);
settings.isConfigured = true;
expect(command.checkCallback?.(true)).toBe(true);
});
});
@@ -47,11 +47,6 @@ export function useSetupManagerHandlersFeature(
setupManager: SetupManager
) {
host.services.appLifecycle.onLoaded.addHandler(() => {
host.services.API.addCommand({
id: "livesync-open-onboarding",
name: "Open onboarding wizard",
callback: () => fireAndForget(() => openOnboarding(setupManager)),
});
host.services.API.addCommand({
id: "livesync-opensetupuri",
name: "Use the copied setup URI (Formerly Open setup URI)",
@@ -109,7 +109,7 @@ describe("setupObsidian/setupManagerHandlers", () => {
expect(preventDefault).toHaveBeenCalledOnce();
});
it("useSetupManagerHandlersFeature should register onLoaded handler that wires command and events", async () => {
it("keeps onboarding out of the command palette while wiring the setup URI command and events", async () => {
const addHandler = vi.fn();
const addCommand = vi.fn();
const events = { onEvent: vi.fn() };
@@ -147,10 +147,9 @@ describe("setupObsidian/setupManagerHandlers", () => {
const loadedHandler = addHandler.mock.calls[0][0] as () => Promise<boolean>;
await loadedHandler();
expect(addCommand).toHaveBeenCalledWith(
expect(addCommand).not.toHaveBeenCalledWith(
expect.objectContaining({
id: "livesync-open-onboarding",
name: "Open onboarding wizard",
})
);
expect(addCommand).toHaveBeenCalledWith(
+17 -3
View File
@@ -50,19 +50,33 @@ export function useSetupURIFeature(host: NecessaryServices<"API" | "UI" | "setti
host.services.API.addCommand({
id: "livesync-copysetupuri",
name: "Copy settings as a new setup URI",
callback: () => fireAndForget(copySetupURI(host, log)),
checkCallback: (checking) => {
if (!host.services.setting.currentSettings().isConfigured) return false;
if (!checking) fireAndForget(copySetupURI(host, log));
return true;
},
});
host.services.API.addCommand({
id: "livesync-copysetupuri-short",
name: "Copy settings as a new setup URI (With customization sync)",
callback: () => fireAndForget(copySetupURI(host, log, false)),
checkCallback: (checking) => {
const settings = host.services.setting.currentSettings();
if (!settings.isConfigured || !settings.usePluginSync) return false;
if (!checking) fireAndForget(copySetupURI(host, log, false));
return true;
},
});
host.services.API.addCommand({
id: "livesync-copysetupurifull",
name: "Copy settings as a new setup URI (Full)",
callback: () => fireAndForget(copySetupURIFull(host, log)),
checkCallback: (checking) => {
const settings = host.services.setting.currentSettings();
if (!settings.isConfigured || !settings.useAdvancedMode) return false;
if (!checking) fireAndForget(copySetupURIFull(host, log));
return true;
},
});
host.services.context.events.onEvent(EVENT_REQUEST_COPY_SETUP_URI, () =>
@@ -156,4 +156,57 @@ describe("setupObsidian/setupUri", () => {
expect(addCommand).toHaveBeenCalledWith(expect.objectContaining({ id: "livesync-copysetupurifull" }));
expect(onEventSpy).toHaveBeenCalledWith(EVENT_REQUEST_COPY_SETUP_URI, expect.any(Function));
});
it("shows Setup URI variants only when their configuration level is relevant", async () => {
const addHandler = vi.fn();
const commands: Array<{
id: string;
checkCallback?: (checking: boolean) => boolean | void;
}> = [];
const settings = {
isConfigured: false,
usePluginSync: false,
useAdvancedMode: false,
};
const host = {
services: {
context: createServiceContext(),
API: {
addCommand: vi.fn((command) => commands.push(command)),
addLog: vi.fn(),
},
appLifecycle: {
onLoaded: {
addHandler,
},
},
setting: {
currentSettings: vi.fn(() => settings),
},
UI: {
confirm: {
askString: vi.fn(() => "pass"),
},
promptCopyToClipboard: vi.fn(() => true),
},
},
} as any;
useSetupURIFeature(host);
const loadedHandler = addHandler.mock.calls[0][0] as () => Promise<boolean>;
await loadedHandler();
const command = (id: string) => commands.find((candidate) => candidate.id === id)!;
expect(command("livesync-copysetupuri").checkCallback?.(true)).toBe(false);
settings.isConfigured = true;
expect(command("livesync-copysetupuri").checkCallback?.(true)).toBe(true);
expect(command("livesync-copysetupuri-short").checkCallback?.(true)).toBe(false);
expect(command("livesync-copysetupurifull").checkCallback?.(true)).toBe(false);
settings.usePluginSync = true;
settings.useAdvancedMode = true;
expect(command("livesync-copysetupuri-short").checkCallback?.(true)).toBe(true);
expect(command("livesync-copysetupurifull").checkCallback?.(true)).toBe(true);
});
});
+35 -14
View File
@@ -67,7 +67,12 @@ export function useP2PReplicatorUI(
showWindow: (type: string) => Promise<void>;
showWindowOnRight?: (type: string) => Promise<void>;
registerWindow: (type: string, factory: (leaf: WorkspaceLeaf) => unknown) => void;
addCommand: (command: { id: string; name: string; callback: () => void }) => unknown;
addCommand: (command: {
id: string;
name: string;
callback?: () => void;
checkCallback?: (checking: boolean) => boolean | void;
}) => unknown;
addRibbonIcon: (
icon: string,
title: string,
@@ -146,8 +151,12 @@ export function useP2PReplicatorUI(
api.addCommand({
id: "open-p2p-server-status",
name: "P2P Sync : Open P2P Status",
callback: () => {
void openStatusPane();
checkCallback: (checking) => {
if (!hasP2PConfiguration(host.services.setting.currentSettings())) return false;
if (!checking) {
void openStatusPane();
}
return true;
},
});
host.services.API.addCommand({
@@ -155,11 +164,15 @@ export function useP2PReplicatorUI(
name: "Replicate P2P to default peer",
checkCallback: (isChecking: boolean) => {
const settings = host.services.setting.currentSettings();
if (isChecking) {
if (settings.remoteType == REMOTE_P2P) return false;
return replicator.replicator?.server?.isServing ?? false;
const isAvailable =
hasP2PConfiguration(settings) &&
settings.remoteType !== REMOTE_P2P &&
(replicator.replicator?.server?.isServing ?? false);
if (!isAvailable) return false;
if (!isChecking) {
runOpenReplication();
}
runOpenReplication();
return true;
},
});
host.services.API.addCommand({
@@ -167,11 +180,15 @@ export function useP2PReplicatorUI(
name: "Replicate now by P2P",
checkCallback: (isChecking: boolean) => {
const settings = host.services.setting.currentSettings();
if (isChecking) {
if (settings.remoteType == REMOTE_P2P) return false;
return replicator.replicator?.server?.isServing ?? false;
const isAvailable =
hasP2PConfiguration(settings) &&
settings.remoteType !== REMOTE_P2P &&
(replicator.replicator?.server?.isServing ?? false);
if (!isAvailable) return false;
if (!isChecking) {
runOpenReplication();
}
runOpenReplication();
return true;
},
});
@@ -179,10 +196,14 @@ export function useP2PReplicatorUI(
id: "p2p-sync-targets",
name: "P2P: Sync with targets",
checkCallback: (isChecking: boolean) => {
if (isChecking) {
return replicator.replicator?.server?.isServing ?? false;
const isAvailable =
hasP2PConfiguration(host.services.setting.currentSettings()) &&
(replicator.replicator?.server?.isServing ?? false);
if (!isAvailable) return false;
if (!isChecking) {
void replicator.replicator?.replicateFromCommand(true);
}
void replicator.replicator?.replicateFromCommand(true);
return true;
},
});
@@ -85,7 +85,12 @@ describe("useP2PReplicatorUI commands", () => {
onSettingLoaded: { addHandler: vi.fn() },
onLayoutReady: { addHandler: vi.fn() },
},
setting: { currentSettings: vi.fn(() => ({ remoteType: "COUCHDB" })) },
setting: {
currentSettings: vi.fn(() => ({
remoteType: "COUCHDB",
P2P_Enabled: true,
})),
},
replicator: { runFiniteReplicationActivity },
},
} as any;
@@ -143,7 +148,11 @@ describe("useP2PReplicatorUI commands", () => {
});
it("retains only the current P2P status command and routes existing open requests to it", async () => {
const commands: Array<{ id: string; callback?: () => void }> = [];
const commands: Array<{
id: string;
callback?: () => void;
checkCallback?: (checking: boolean) => boolean | void;
}> = [];
let initialise: (() => Promise<unknown>) | undefined;
const showWindow = vi.fn(async () => undefined);
const showWindowOnRight = vi.fn(async () => undefined);
@@ -183,12 +192,90 @@ describe("useP2PReplicatorUI commands", () => {
expect(commands.map((command) => command.id)).not.toContain("open-p2p-replicator");
expect(commands.map((command) => command.id)).toContain("open-p2p-server-status");
expect(commands.find((command) => command.id === "open-p2p-server-status")?.checkCallback?.(true)).toBe(false);
eventHub.emitEvent(EVENT_REQUEST_OPEN_P2P);
await vi.waitFor(() => expect(showWindowOnRight).toHaveBeenCalledWith("p2p-status"));
expect(showWindow).not.toHaveBeenCalledWith("p2p");
});
it("shows P2P commands only when a P2P configuration exists and their runtime prerequisites are met", async () => {
const commands: Array<{
id: string;
checkCallback?: (checking: boolean) => boolean | void;
}> = [];
let initialise: (() => Promise<unknown>) | undefined;
let settings: Record<string, unknown> = {
remoteType: "COUCHDB",
remoteConfigurations: {},
};
const host = {
services: {
context: createServiceContext(),
API: {
showWindow: vi.fn(async () => undefined),
showWindowOnRight: vi.fn(async () => undefined),
registerWindow: vi.fn(),
addCommand: vi.fn((command) => commands.push(command)),
addRibbonIcon: vi.fn(),
},
appLifecycle: {
onInitialise: {
addHandler: vi.fn((handler) => {
initialise = handler;
}),
},
onSettingLoaded: { addHandler: vi.fn() },
onLayoutReady: { addHandler: vi.fn() },
},
setting: {
currentSettings: vi.fn(() => settings),
onSettingSaved: { addHandler: vi.fn() },
},
replicator: { runFiniteReplicationActivity: vi.fn() },
},
} as any;
const p2p = {
replicator: {
server: { isServing: true },
openReplication: vi.fn(),
replicateFromCommand: vi.fn(),
},
} as any;
useP2PReplicatorUI(host, {} as any, p2p);
await initialise?.();
for (const commandId of [
"open-p2p-server-status",
"replicate-now-by-p2p-default-peer",
"replicate-now-by-p2p",
"p2p-sync-targets",
]) {
expect(commands.find(({ id }) => id === commandId)?.checkCallback?.(true)).toBe(false);
}
settings = {
...settings,
remoteConfigurations: {
peer: {
id: "peer",
name: "Peer",
uri: "sls+p2p://room?passphrase=secret",
isEncrypted: false,
},
},
};
for (const commandId of [
"open-p2p-server-status",
"replicate-now-by-p2p-default-peer",
"replicate-now-by-p2p",
"p2p-sync-targets",
]) {
expect(commands.find(({ id }) => id === commandId)?.checkCallback?.(true)).toBe(true);
}
});
it("does not open the P2P status pane automatically when the workspace becomes ready", async () => {
let layoutReady: (() => Promise<unknown>) | undefined;
const showWindow = vi.fn(async () => undefined);