diff --git a/src/components/connection-gate.test.tsx b/src/components/connection-gate.test.tsx index 171526d..14a3774 100644 --- a/src/components/connection-gate.test.tsx +++ b/src/components/connection-gate.test.tsx @@ -1,6 +1,8 @@ import { fireEvent, render, screen } from "@testing-library/react"; -import { WelcomePreview } from "./connection-gate"; +import type { MdbaseApplicationSessionSnapshot } from "@mdbase-dev/connect"; + +import { SetupReview, WelcomePreview } from "./connection-gate"; describe("WelcomePreview", () => { it("moves example tasks with pointer dragging", () => { @@ -26,3 +28,73 @@ describe("WelcomePreview", () => { expect(task).toHaveStyle("--preview-start: 45"); }); }); + +describe("SetupReview", () => { + it("shows the reviewed collection changes without implying record changes", () => { + const snapshot = { + status: "setup_review_required", + collectionId: "collection-1", + info: { + collectionId: "collection-1", + displayName: "Product", + operations: [], + scope: { kind: "collection" }, + authority: { kind: "connector", durability: "computer" }, + route: "relay", + directAccess: "unavailable", + }, + capabilities: { + requiredAvailable: true, + optionalAvailable: true, + available: [], + unavailable: [], + }, + connections: [], + update: { + status: "provision", + applicable: true, + assessmentDigest: "sha256:assessment", + collectionRevision: "revision-1", + provisionDigest: "sha256:provision", + canApply: true, + reason: "", + configuration: [ + { + requirement: "tasknotes-bases", + path: "x-obsidian.bases.include", + value: "TaskNotes/Views/**/*.base", + action: "add", + }, + ], + typePacks: [ + { + id: "tasknotes.task", + name: "TaskNotes task", + status: "install", + applicable: true, + assessmentDigest: "sha256:pack", + desiredVersion: "0.3.0-rc.10", + resources: [], + contractSetups: [], + canApply: true, + reason: "", + }, + ], + }, + } as unknown as Extract< + MdbaseApplicationSessionSnapshot, + { status: "setup_review_required" } + >; + + render(); + + expect(screen.getByText("Product")).toBeInTheDocument(); + expect(screen.getByText("TaskNotes task")).toBeInTheDocument(); + expect( + screen.getByText( + "Task records, custom types, and unrelated collection settings will stay as they are.", + ), + ).toBeInTheDocument(); + expect(screen.getByText("TaskNotes/Views/**/*.base")).toBeInTheDocument(); + }); +}); diff --git a/src/components/connection-gate.tsx b/src/components/connection-gate.tsx index 071d553..a5a0365 100644 --- a/src/components/connection-gate.tsx +++ b/src/components/connection-gate.tsx @@ -1,4 +1,4 @@ -import { Database, LoaderCircle } from "lucide-react"; +import { Check, Database, LoaderCircle } from "lucide-react"; import { useCallback, useEffect, @@ -13,6 +13,7 @@ import type { KeyboardEvent as ReactKeyboardEvent, PointerEvent as ReactPointerEvent, } from "react"; +import type { MdbaseApplicationSessionSnapshot } from "@mdbase-dev/connect"; import { isAuthorizationCallback, plannerSession } from "../data/connect"; import { MdbasePlannerRepository } from "../data/mdbase-repository"; @@ -77,6 +78,34 @@ export function ConnectionGate({ onDemo }: { onDemo(): void }) { } }, []); + const applySetup = useCallback(async () => { + setOpening(true); + setError(""); + try { + requireOutcome( + await plannerSession.applyCollectionSetup({ timeoutMs: 60_000 }), + ); + } catch (reason) { + setError(connectionErrorMessage(reason)); + } finally { + setOpening(false); + } + }, []); + + const reauthorize = useCallback(async () => { + setOpening(true); + setError(""); + try { + requireOutcome( + await plannerSession.authorize("selected", { timeoutMs: 60_000 }), + ); + } catch (reason) { + setError(connectionErrorMessage(reason)); + } finally { + setOpening(false); + } + }, []); + if (repository) return ( +
TaskNotes Planner
-

Plan tasks on a timeline.

-

- Open a TaskNotes collection to change dates and manage dependencies. -

+ {snapshot.status === "setup_review_required" ? ( + <> +

Finish opening this collection.

+

+ Review the TaskNotes files and settings Planner needs, then apply + them to continue. +

+ + ) : ( + <> +

Plan tasks on a timeline.

+

+ Open a TaskNotes collection to change dates and manage + dependencies. +

+ + )}
- + {snapshot.status === "setup_review_required" ? ( + + ) : snapshot.status === "authorization_required" ? ( + + ) : ( + + )} + {snapshot.status === "setup_review_required" ? ( + + ) : null}
{error || stateError ? (

@@ -127,11 +220,81 @@ export function ConnectionGate({ onDemo }: { onDemo(): void }) {

) : null}
- + {snapshot.status === "setup_review_required" ? ( + + ) : ( + + )}
); } +type SetupReviewSnapshot = Extract< + MdbaseApplicationSessionSnapshot, + { status: "setup_review_required" } +>; + +export function SetupReview({ snapshot }: { snapshot: SetupReviewSnapshot }) { + const changes = snapshot.update.configuration.filter( + (item) => item.action !== "current", + ); + + return ( +
+
+ COLLECTION SETUP + {snapshot.info.displayName} +
+
+

Changes to apply

+

+ Task records, custom types, and unrelated collection settings will + stay as they are. +

+
+
    + {snapshot.update.typePacks.map((pack) => ( +
  • + +
    + {pack.name} + + {pack.currentVersion + ? `${pack.currentVersion} → ${pack.desiredVersion}` + : `Install ${pack.desiredVersion}`} + +
    +
  • + ))} + {changes.map((item) => ( +
  • + +
    + + {item.action === "conflict" + ? "Collection setting needs attention" + : "Allow TaskNotes Base views"} + + + {item.action === "conflict" + ? item.conflict?.message + : String(item.value)} + +
    +
  • + ))} +
+ {!snapshot.update.canApply ? ( +

+ {snapshot.update.reason} +

+ ) : null} +
+ ); +} + const previewTasks = [ { label: "Research", start: 5, width: 26 }, { label: "Prototype", start: 18, width: 24 }, diff --git a/src/styles.css b/src/styles.css index fa1394e..444d687 100644 --- a/src/styles.css +++ b/src/styles.css @@ -175,6 +175,11 @@ select:focus-visible { line-height: 0.94; } +.setup-welcome h1 { + font-size: clamp(44px, 5vw, 72px); + line-height: 0.98; +} + .welcome-lede { max-width: 490px; margin: 0; @@ -240,6 +245,97 @@ select:focus-visible { transform: rotate(-1.2deg); } +.setup-review { + width: min(720px, 58vw); + min-width: 520px; + border: 1px solid var(--line-strong); + background: var(--paper-raised); + box-shadow: var(--shadow); +} + +.setup-review > header { + min-height: 63px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + padding: 15px 20px; + border-bottom: 1px solid var(--line-strong); +} + +.setup-review > header span, +.setup-change-list small { + color: var(--ink-muted); + font-family: "Azeret Mono", monospace; + font-size: 10px; + letter-spacing: 0.05em; +} + +.setup-review > header strong { + overflow: hidden; + font-size: 14px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.setup-summary { + padding: 27px 20px 22px; +} + +.setup-summary h2 { + margin: 0 0 7px; + font-size: 22px; +} + +.setup-summary p { + max-width: 60ch; + margin: 0; + color: var(--ink-soft); + line-height: 1.45; +} + +.setup-change-list { + margin: 0; + padding: 0; + list-style: none; + border-top: 1px solid var(--line); +} + +.setup-change-list li { + min-height: 68px; + display: grid; + grid-template-columns: 38px 1fr; + align-items: center; + gap: 4px; + padding: 12px 20px; + border-bottom: 1px solid var(--line); +} + +.setup-change-list li > span { + color: var(--accent); + font-family: "Azeret Mono", monospace; + font-size: 18px; +} + +.setup-change-list li > div { + min-width: 0; +} + +.setup-change-list strong, +.setup-change-list small { + display: block; +} + +.setup-change-list small { + margin-top: 5px; + overflow-wrap: anywhere; +} + +.setup-conflict { + margin: 18px 20px; + color: var(--danger); +} + .preview-topline, .preview-row { display: grid; @@ -1611,6 +1707,12 @@ select:focus-visible { transform: translateX(7vw) rotate(-1.2deg); } + .setup-review { + width: 100%; + min-width: 0; + margin-top: 56px; + } + .planner-header { grid-template-columns: 1fr auto; }