Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tanstack-query-webapp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tumaet/webapp": patch
---

No visible change to the editor. Under the hood, the web app now loads and caches version history, version snapshots, and shared diagrams through one shared data layer instead of hand-written fetching, so the version panel refreshes and reconciles more consistently across tabs and collaborators.
59 changes: 59 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions standalone/webapp/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { DocsContainer } from "@storybook/addon-docs/blocks"
import { themes } from "storybook/theming"
import { addons } from "storybook/preview-api"
import { withTanStackRouter } from "../src/stories/_support/webapp"
import { QueryClientProvider } from "@tanstack/react-query"
import { storybookQueryClient } from "../src/stories/_support/queryClient"
import { VersionRepositoryProvider } from "../src/contexts/VersionRepositoryContext"

type DocsContainerCtx = ComponentProps<typeof DocsContainer>["context"]

Expand Down Expand Up @@ -213,7 +216,24 @@ const preview: Preview = {
},
},
tags: ["autodocs"],
// One clean query cache per story: stories that share a query key but inject
// different data would otherwise read each other's cached results.
beforeEach: () => {
storybookQueryClient.clear()
},
decorators: [
// TanStack Query context for components that read server state through
// the query hooks (versioning UI, share flow, legal pages). The shared
// client lives in _support/queryClient so beforeEach hooks can reset it.
// Query cache + the version backend the story's UI talks to (the editor
// routes supply the latter in production).
(Story) => (
<QueryClientProvider client={storybookQueryClient}>
<VersionRepositoryProvider kind="remote">
<Story />
</VersionRepositoryProvider>
</QueryClientProvider>
),
// TanStack router context so any component using <Link>/useNavigate/
// useLocation renders without crashing. Per-story routes and the active
// location are set via the `tanstackRouter` parameter.
Expand Down
19 changes: 18 additions & 1 deletion standalone/webapp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,24 @@ Part of the Apollon monorepo — run it from the repo root with `pnpm dev`, not

## Stack

React 19, TypeScript, Vite, the shadcn-style [`@tumaet/ui`](../../packages/ui) design system (Base UI primitives + Tailwind v4), Storybook, Vitest, Playwright (visual + e2e).
React 19, TypeScript, Vite, the shadcn-style [`@tumaet/ui`](../../packages/ui) design system (Base UI primitives + Tailwind v4), [TanStack Query](https://tanstack.com/query) for server state, Storybook, Vitest, Playwright (visual + e2e).

## Debugging server state

HTTP data (diagram loads, version history, version snapshots) goes through
Comment thread
FelixTJDietrich marked this conversation as resolved.
Outdated
TanStack Query — see [`src/queries`](src/queries) and the boundary note in
[`src/queryClient.ts`](src/queryClient.ts).

The Query Devtools are **off by default**: their floating button sits
bottom-right, on top of the editor's minimap, and every other corner is taken
by the editor's own chrome. Enable them per browser from the console, then
reload:

```js
localStorage.setItem("apollon:query-devtools", "1")
```

They are stripped from production builds regardless.

## Scripts

Expand Down
4 changes: 4 additions & 0 deletions standalone/webapp/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import pluginJs from "@eslint/js"
import tseslint from "typescript-eslint"
import eslintReact from "@eslint-react/eslint-plugin"
import reactHooks from "eslint-plugin-react-hooks"
import pluginQuery from "@tanstack/eslint-plugin-query"

/** @type {import('eslint').Linter.Config[]} */
export default [
Expand All @@ -26,6 +27,9 @@ export default [
{ languageOptions: { globals: globals.browser } },
pluginJs.configs.recommended,
...tseslint.configs.recommended,
// TanStack Query correctness rules (exhaustive query keys, stable
// QueryClient, no misuse of mutation results).
...pluginQuery.configs["flat/recommended"],
// recommended-typescript disables the prop-types rules TypeScript already enforces.
eslintReact.configs["recommended-typescript"],
{
Expand Down
3 changes: 3 additions & 0 deletions standalone/webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"@resvg/resvg-wasm": "catalog:",
"@tailwindcss/vite": "catalog:",
"@tanstack/history": "1.162.0",
"@tanstack/react-query": "5.101.2",
"@tanstack/react-router": "1.170.16",
"@tumaet/apollon": "workspace:*",
"@tumaet/ui": "workspace:*",
Expand Down Expand Up @@ -80,6 +81,8 @@
"@storybook/addon-themes": "catalog:",
"@storybook/addon-vitest": "catalog:",
"@storybook/react-vite": "catalog:",
"@tanstack/eslint-plugin-query": "5.100.4",
"@tanstack/react-query-devtools": "5.101.2",
"@tanstack/router-plugin": "1.168.18",
"@testing-library/react": "catalog:",
"@testing-library/user-event": "14.6.1",
Expand Down
34 changes: 31 additions & 3 deletions standalone/webapp/src/AppProviders.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,42 @@
import React, { ReactNode } from "react"
import { QueryClientProvider } from "@tanstack/react-query"
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
import { EditorProvider, ModalProvider } from "@/contexts"
import { queryClient } from "@/queryClient"

interface Props {
children: ReactNode
}

/**
* Query Devtools are opt-in: they float a toggle button over the editor's own
* floating chrome, which is in the way far more often than it is useful. Turn
* them on per browser with
*
* localStorage.setItem("apollon:query-devtools", "1")
*
* and reload. Read once at module load — the flag is a debugging switch, not
* reactive state. Production is unaffected either way: the package swaps
* itself for a no-op export when `NODE_ENV !== "development"`.
*/
const SHOW_QUERY_DEVTOOLS =
import.meta.env.DEV &&
(() => {
try {
return localStorage.getItem("apollon:query-devtools") === "1"
} catch {
// Storage throws when cookies / site data are blocked.
return false
}
})()

export const AppProviders: React.FC<Props> = ({ children }) => {
return (
<EditorProvider>
<ModalProvider>{children}</ModalProvider>
</EditorProvider>
<QueryClientProvider client={queryClient}>
<EditorProvider>
<ModalProvider>{children}</ModalProvider>
</EditorProvider>
{SHOW_QUERY_DEVTOOLS && <ReactQueryDevtools initialIsOpen={false} />}
</QueryClientProvider>
)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { beforeEach, describe, expect, it, vi } from "vitest"
import { fireEvent, screen, waitFor } from "@testing-library/react"
import { ShareDashboardModal } from "./ShareDashboardModal"
import { QueryClientProvider } from "@tanstack/react-query"
import { renderWithRouter } from "@/test/renderWithRouter"
import { createTestQueryClient } from "@/test/queryTestUtils"
import { usePersistenceModelStore } from "@/stores/usePersistenceModelStore"
import { DiagramView } from "@/types"

Expand Down Expand Up @@ -89,7 +91,14 @@ describe("ShareDashboardModal", () => {

const { router } = renderWithRouter(
<ShareDashboardModal modelId="diagram-1" />,
{ routePaths: ["/", "/shared/$diagramId"] }
{
routePaths: ["/", "/shared/$diagramId"],
wrapper: (children) => (
<QueryClientProvider client={createTestQueryClient()}>
{children}
</QueryClientProvider>
),
}
)

fireEvent.click(await screen.findByRole("button", { name: "Create" }))
Expand Down
7 changes: 4 additions & 3 deletions standalone/webapp/src/components/modals/ShareModal.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { beforeEach, describe, expect, it, vi } from "vitest"
import { fireEvent, render, screen } from "@testing-library/react"
import { fireEvent, screen } from "@testing-library/react"
import { renderWithQuery } from "@/test/queryTestUtils"

const { sharedIdRef, createMock } = vi.hoisted(() => ({
sharedIdRef: { value: undefined as string | undefined },
Expand Down Expand Up @@ -46,7 +47,7 @@ beforeEach(() => {
describe("ShareModal", () => {
it("opens straight on the link for an already-shared diagram, with embed", () => {
sharedIdRef.value = "shared-xyz"
render(<ShareModal />)
renderWithQuery(<ShareModal />)

expect(screen.getByLabelText("Copy link")).toBeTruthy()
expect(screen.getByText("Embed")).toBeTruthy()
Expand All @@ -56,7 +57,7 @@ describe("ShareModal", () => {

it("creates the shared diagram exactly once, then shows the link", async () => {
createMock.mockResolvedValue({ id: "new-1" })
render(<ShareModal />)
renderWithQuery(<ShareModal />)

fireEvent.click(screen.getByRole("button", { name: "Create share link" }))
// After creation the link appears and the create button is gone.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from "react"
import { useMutation } from "@tanstack/react-query"
import { toast } from "react-toastify"
import type { UMLModel } from "@tumaet/apollon"
import { DiagramView } from "@/types"
Expand Down Expand Up @@ -40,6 +41,10 @@ export function useShareableDiagram(

const link = diagramId ? buildSharedDiagramUrl(diagramId, mode) : ""

const createDiagramMutation = useMutation({
mutationFn: (model: UMLModel) => DiagramApiClient.createDiagram(model),
})

const create = async (name: string) => {
if (!modelData) {
toast.error("This diagram can't be shared right now.")
Expand All @@ -52,7 +57,7 @@ export function useShareableDiagram(
trimmed && trimmed !== modelData.title
? { ...modelData, title: trimmed }
: modelData
const { id } = await DiagramApiClient.createDiagram(model)
const { id } = await createDiagramMutation.mutateAsync(model)
addSharedDiagramEntry(id)
setDiagramId(id)
setMode(DiagramView.COLLABORATE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ interface VersionHistoryButtonProps {
* Discoverable navbar entry point for the version-history sidebar.
*
* Renders on any route with an active diagram that has a version backend:
* `/shared/:id` (collab, RemoteVersionRepository) and `/local/:id`
* (standalone, LocalVersionRepository). Legacy `/:id` redirects to
* `/shared/:id` (collab, the collab backend) and `/local/:id`
* (standalone, the local backend). Legacy `/:id` redirects to
* `/shared/:id`, so it is also covered. The gallery (`/`) and the
* playground have no active diagram, so the button is hidden.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
AlertDialogFooter,
} from "@tumaet/ui/components/alert-dialog"
import { useModalContext } from "@/contexts"
import type { PendingVersion } from "@/stores/useVersionStore"
import type { PendingVersion } from "@/types"
import { log } from "@/logger"
import { versioningStrings as t } from "./strings"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,8 @@ import {
} from "lucide-react"
import type { CSSProperties, FC } from "react"
import { cn } from "@tumaet/ui/lib/utils"
import {
selectScopedPreview,
useVersionStore,
type PendingVersion,
} from "@/stores/useVersionStore"
import { selectScopedPreview, useVersionStore } from "@/stores/useVersionStore"
import type { PendingVersion } from "@/types"
import { useClosePreview } from "@/hooks/useVersionPreviewUrlSync"
import { relativeTime } from "./relativeTime"
import {
Expand Down
Loading
Loading