Skip to content

Commit a4c130c

Browse files
committed
chore: linting and e2e tests
1 parent d38e80f commit a4c130c

34 files changed

Lines changed: 1826 additions & 383 deletions

eslint.config.mts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export default defineConfig(
2828
globals: {
2929
...globals.browser,
3030
Buffer: "readonly",
31+
__DEV__: "readonly",
3132
},
3233
parserOptions: {
3334
projectService: {
@@ -85,15 +86,24 @@ export default defineConfig(
8586
"Dataview",
8687
"Excalidraw",
8788
"Fantasy Statblocks",
89+
"github.com",
8890
"GitHub",
8991
"GitHub Pages",
9092
"GitHub Pro",
9193
"GitLab",
9294
"Gitea",
95+
"https:",
9396
"IndexedDB",
9497
"Obsidian",
98+
"Publication Center",
9599
"Quartz",
100+
"Quartz Hub",
96101
"Quartz Syncer",
102+
"Settings → Core plugins",
103+
"Web Viewer",
104+
"quartz.git",
105+
"v4",
106+
"v5",
97107
],
98108
},
99109
],

src/cli/handlers/markHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export function createMarkHandler(_plugin: QuartzSyncer): CliHandler {
5151
normalizeFuzzy(file.basename).includes(normalizedQuery),
5252
);
5353
} else if (isGlob) {
54-
const unsupported = /[?{}\[\]!]/.test(pathArg);
54+
const unsupported = /[?{}[\]!]/.test(pathArg);
5555
if (unsupported) {
5656
return {
5757
success: false,

src/main.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { StatusBar, type StatusBarState } from "src/views/StatusBar";
3333
import { OperabilityFacadeImpl } from "src/operability/OperabilityFacade";
3434
import { EventBuffer } from "src/operability/EventBuffer";
3535
import { PublicationCenterManager } from "src/operability/PublicationCenterManager";
36+
import { QuartzHubManager } from "src/operability/QuartzHubManager";
3637

3738
/**
3839
* QuartzSyncer plugin settings.
@@ -181,6 +182,7 @@ export default class QuartzSyncer extends Plugin {
181182
private operabilityFacade: OperabilityFacadeImpl | null = null;
182183
private eventSink: EventBuffer | null = null;
183184
private publicationCenterManager: PublicationCenterManager | null = null;
185+
private quartzHubManager: QuartzHubManager | null = null;
184186
processRunner: ProcessRunner | null = null;
185187
binaryDetector: BinaryDetector | null = null;
186188
gitRunner: GitRunner | null = null;
@@ -220,7 +222,7 @@ export default class QuartzSyncer extends Plugin {
220222

221223
this.addCommands();
222224
this.cliHandlers = registerCliHandlers(this);
223-
this.addRibbonIcon("leaf", "Quartz Syncer publication center", () => {
225+
this.addRibbonIcon("leaf", "Quartz Syncer Publication Center", () => {
224226
this.publicationCenterManager?.open() ??
225227
new PublicationCenter(this.app, this).open();
226228
});
@@ -295,6 +297,10 @@ export default class QuartzSyncer extends Plugin {
295297
this.quartzRunner = new QuartzRunner(this.processRunner);
296298
}
297299

300+
if (Platform.isDesktopApp) {
301+
this.quartzHubManager = new QuartzHubManager(this.app, this);
302+
}
303+
298304
if (Platform.isDesktopApp && this.settings.autoPublishInterval > 0) {
299305
this.backgroundEngine.startAutoPublish(
300306
this.settings.autoPublishInterval,
@@ -320,6 +326,7 @@ export default class QuartzSyncer extends Plugin {
320326
this.operabilityFacade = null;
321327
this.eventSink = null;
322328
this.publicationCenterManager = null;
329+
this.quartzHubManager = null;
323330
this.publisher?.stopPeriodicFetch();
324331
this.backgroundEngine?.stopAutoPublish();
325332
this.backgroundEngine?.stop();
@@ -627,6 +634,10 @@ export default class QuartzSyncer extends Plugin {
627634
return this.publicationCenterManager;
628635
}
629636

637+
getQuartzHubManager(): QuartzHubManager | null {
638+
return this.quartzHubManager;
639+
}
640+
630641
getEngineStatus(): {
631642
running: boolean;
632643
pending: number;
@@ -642,7 +653,7 @@ export default class QuartzSyncer extends Plugin {
642653
private addCommands(): void {
643654
this.addCommand({
644655
id: "open-publish-modal",
645-
name: "Open publication center",
656+
name: "Open Publication Center",
646657
callback: () => {
647658
this.publicationCenterManager?.open() ??
648659
new PublicationCenter(this.app, this).open();
@@ -657,6 +668,13 @@ export default class QuartzSyncer extends Plugin {
657668
new OnboardingWizard(this.app, this).open();
658669
},
659670
});
671+
this.addCommand({
672+
id: "open-hub",
673+
name: "Open Quartz Hub",
674+
callback: () => {
675+
this.quartzHubManager?.open();
676+
},
677+
});
660678
}
661679

662680
this.addCommand({

src/operability/ActionRegistry.ts

Lines changed: 119 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { LocalFileSource } from "src/quartz/LocalFileSource";
1414
import { QuartzVersionDetector } from "src/quartz/QuartzVersionDetector";
1515
import { getValueByPath, setValueByPath } from "src/cli/handlers/cliUtils";
1616
import type { PublicationCenterManager } from "src/operability/PublicationCenterManager";
17+
import type { QuartzHubManager } from "src/operability/QuartzHubManager";
1718

1819
type PublishStatusSummary = {
1920
unpublished: number;
@@ -42,6 +43,7 @@ export class ActionRegistry {
4243
private getPublicationService: () => PublicationService | null,
4344
private getOnboardingService: () => OnboardingService | null,
4445
private getPublicationCenterManager: () => PublicationCenterManager | null,
46+
private getQuartzHubManager: () => QuartzHubManager | null,
4547
) {}
4648

4749
getPublishStatusSummary(): PublishStatusSummary | null {
@@ -103,6 +105,22 @@ export class ActionRegistry {
103105
return this.selectAllPublicationPaths();
104106
case "pub.deselectAll":
105107
return this.deselectAllPublicationPaths();
108+
case "hub.open":
109+
return this.openQuartzHub();
110+
case "hub.close":
111+
return this.closeQuartzHub();
112+
case "hub.setup.link":
113+
return this.withLock(() =>
114+
this.linkLocalRepo(action.params.path),
115+
);
116+
case "hub.setup.clone":
117+
return this.withLock(() =>
118+
this.cloneAndLinkRepo(
119+
action.params.url,
120+
action.params.dest,
121+
action.params.confirm,
122+
),
123+
);
106124
case "onboarding.start":
107125
case "onboarding.setToken":
108126
case "onboarding.createRepo":
@@ -248,7 +266,7 @@ export class ActionRegistry {
248266
private openPublicationCenter(): ActionResult {
249267
const manager = this.getPublicationCenterManager();
250268
if (!manager) {
251-
return { success: false, error: "Publication center unavailable" };
269+
return { success: false, error: "Publication Center unavailable" };
252270
}
253271
manager.open();
254272
return { success: true };
@@ -257,16 +275,111 @@ export class ActionRegistry {
257275
private closePublicationCenter(): ActionResult {
258276
const manager = this.getPublicationCenterManager();
259277
if (!manager) {
260-
return { success: false, error: "Publication center unavailable" };
278+
return { success: false, error: "Publication Center unavailable" };
279+
}
280+
manager.close();
281+
return { success: true };
282+
}
283+
284+
private openQuartzHub(): ActionResult {
285+
const manager = this.getQuartzHubManager();
286+
if (!manager) {
287+
return { success: false, error: "Quartz Hub unavailable" };
288+
}
289+
manager.open();
290+
return { success: true };
291+
}
292+
293+
private closeQuartzHub(): ActionResult {
294+
const manager = this.getQuartzHubManager();
295+
if (!manager) {
296+
return { success: false, error: "Quartz Hub unavailable" };
261297
}
262298
manager.close();
263299
return { success: true };
264300
}
265301

302+
private async linkLocalRepo(path: string): Promise<ActionResult> {
303+
if (!path) {
304+
return { success: false, error: "Path is required" };
305+
}
306+
307+
const trimmed = path.trim();
308+
const { QuartzHubService } =
309+
await import("src/services/QuartzHubService");
310+
const service = new QuartzHubService(this.plugin);
311+
const validation = service.validateRepoPath(trimmed);
312+
313+
if (!validation.ok) {
314+
return { success: false, error: validation.message };
315+
}
316+
317+
this.plugin.settings.quartzRepoPath = trimmed;
318+
this.plugin.settings.enableSystemCommands = true;
319+
await this.plugin.saveSettings();
320+
321+
return { success: true, data: { path: trimmed } };
322+
}
323+
324+
private async cloneAndLinkRepo(
325+
url: string,
326+
dest: string,
327+
confirm: true | undefined,
328+
): Promise<ActionResult> {
329+
if (!confirm) {
330+
return { success: false, error: "Confirmation required" };
331+
}
332+
333+
if (!url || !dest) {
334+
return {
335+
success: false,
336+
error: "URL and destination path are required",
337+
};
338+
}
339+
340+
const gitRunner = this.plugin.gitRunner;
341+
const npmRunner = this.plugin.npmRunner;
342+
343+
if (!gitRunner) {
344+
return { success: false, error: "Git runner unavailable" };
345+
}
346+
347+
if (!npmRunner) {
348+
return { success: false, error: "Npm runner unavailable" };
349+
}
350+
351+
const parentDir = dest.substring(0, dest.lastIndexOf("/")) || "/";
352+
const dirName = dest.substring(dest.lastIndexOf("/") + 1);
353+
354+
const cloneResult = await gitRunner.clone(url, dirName, {
355+
cwd: parentDir,
356+
});
357+
if (!cloneResult.ok) {
358+
return {
359+
success: false,
360+
error: `Clone failed: ${cloneResult.error}`,
361+
};
362+
}
363+
364+
const installResult = await npmRunner.install({ cwd: dest });
365+
if (!installResult.ok) {
366+
return {
367+
success: false,
368+
error: `npm install failed: ${installResult.error}`,
369+
};
370+
}
371+
372+
this.plugin.settings.quartzRepoPath = dest;
373+
this.plugin.settings.enableSystemCommands = true;
374+
await this.plugin.saveSettings();
375+
376+
return { success: true, data: { path: dest } };
377+
}
378+
266379
private selectPublicationPaths(paths: string[]): ActionResult {
267380
const controller = this.getPublicationCenterController();
268381
if (!controller) {
269-
return { success: false, error: "Publication center unavailable" };
382+
return { success: false, error: "Publication Center unavailable" };
270383
}
271384
controller.setSelected(paths);
272385
return { success: true };
@@ -275,7 +388,7 @@ export class ActionRegistry {
275388
private deselectPublicationPaths(paths: string[]): ActionResult {
276389
const controller = this.getPublicationCenterController();
277390
if (!controller) {
278-
return { success: false, error: "Publication center unavailable" };
391+
return { success: false, error: "Publication Center unavailable" };
279392
}
280393
const remaining = new Set(controller.getSelected());
281394
for (const path of paths) {
@@ -288,7 +401,7 @@ export class ActionRegistry {
288401
private selectAllPublicationPaths(): ActionResult {
289402
const controller = this.getPublicationCenterController();
290403
if (!controller) {
291-
return { success: false, error: "Publication center unavailable" };
404+
return { success: false, error: "Publication Center unavailable" };
292405
}
293406
controller.selectAll();
294407
return { success: true };
@@ -297,7 +410,7 @@ export class ActionRegistry {
297410
private deselectAllPublicationPaths(): ActionResult {
298411
const controller = this.getPublicationCenterController();
299412
if (!controller) {
300-
return { success: false, error: "Publication center unavailable" };
413+
return { success: false, error: "Publication Center unavailable" };
301414
}
302415
controller.deselectAll();
303416
return { success: true };

src/operability/Assertions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ function parseExpectedCounts(
395395
const expected: Partial<Record<CountKey, number>> = {};
396396

397397
for (const key of COUNT_KEYS) {
398-
const value = (candidate as Record<string, unknown>)[key];
398+
const value = candidate[key];
399399
if (typeof value === "number") {
400400
expected[key] = value;
401401
}

src/operability/DomContract.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,18 @@ export type QSDomRole =
1818
| "settings-test-btn"
1919
| "settings-test-result"
2020
| "notice"
21-
| "diff-view";
21+
| "diff-view"
22+
| "hub"
23+
| "hub-tab"
24+
| "hub-status"
25+
| "hub-action"
26+
| "hub-path"
27+
| "hub-serve-status"
28+
| "hub-setup-link"
29+
| "hub-setup-link-path"
30+
| "hub-setup-clone"
31+
| "hub-setup-clone-url"
32+
| "hub-setup-clone-dest";
2233

2334
/**
2435
* Generates stable DOM attributes for the operability DOM contract.

src/operability/OperabilityFacade.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export class OperabilityFacadeImpl implements IOperabilityFacade {
3333
() => this.getPublicationService(),
3434
() => this.getOnboardingService(),
3535
() => plugin.getPublicationCenterManager(),
36+
() => plugin.getQuartzHubManager(),
3637
);
3738

3839
this.eventBuffer.emit("plugin.loaded", {
@@ -83,7 +84,9 @@ export class OperabilityFacadeImpl implements IOperabilityFacade {
8384
while (Date.now() < deadline) {
8485
const result = this.assert(condition, params);
8586
if (result.pass) return result;
86-
await new Promise((resolve) => setTimeout(resolve, interval));
87+
await new Promise((resolve) =>
88+
window.setTimeout(resolve, interval),
89+
);
8790
}
8891

8992
return {

0 commit comments

Comments
 (0)