You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Every untyped boundary uses TypeBox. Inside the boundary, code trusts the parsed value.
186
186
187
-
-**HTTP responses (client):**`@core/http` is a single three-layer stack — there is exactly ONE way to validate a response, expressed at the altitude you need:
187
+
-**HTTP responses (client):**`@core/http` is the single client stack — use the entry matching the response kind and altitude:
188
188
-**`apiRequest(path, { schema, … })`** — the canonical entry. Does the `fetch` itself: sets `credentials`, serializes a JSON body (FormData passes through untouched), validates the success body against `schema`, and throws a single `ApiError` (carrying the HTTP status) on failure. Detect cancellation with `isAbortError(err)`. **Default to this** — do NOT hand-roll `fetch` + `res.ok` + `res.json()` in admin code.
189
+
-**`apiBlobRequest(path, …)`** — the binary-response counterpart for authenticated image/file reads. It shares `apiRequest`'s credential, cancellation, and `ApiError` behavior, then returns a `Blob`; the caller validates the MIME type before use.
189
190
-**`readEnvelope(res, Schema, fallbackMessage)`** — for the persistence layer, which performs its own injectable `fetch` (test seam) and then hands the `Response` here. Checks `res.ok` (throws `ApiError` with status + the `{ error }` envelope message), then validates the body. Its no-body sibling is `assertOk(res, fallback)` for `void`/Blob/streaming/text responses.
190
191
-**`parseJsonResponse(res, Schema)`** — the low-level body-validation primitive that `apiRequest` and `readEnvelope` are *built on*. It validates a body with NO HTTP-status semantics. Reserved for genuine primitives only: the `@core/http` internals, the XHR upload path (`useUploadQueue`), and server-side fetches of external APIs. Do NOT reach for it in admin/persistence code — `assertOk(res, m); parseJsonResponse(res, S)` is exactly `readEnvelope(res, S, m)`; always write the latter.
191
192
-**`JSON.parse` of persisted data:**`safeParseJson(raw, Schema)` for hard, `parseJsonWithFallback(raw, Schema, default)` for soft.
Copy file name to clipboardExpand all lines: docs/features/media.md
+11-7Lines changed: 11 additions & 7 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -13,7 +13,7 @@ The workspace is canvas-style: it uses `AdminWorkspaceCanvasLayout`, the lighter
13
13
-**State:** one hook — `useMediaWorkspace()` — orchestrates folders, assets, selection, filters, upload queue, and folder moves. The editor store doesn't grow new slices; the Media page is self-contained.
14
14
-**Folders:** folders render as first-class grid/list items in the canvas. Opening a folder filters the canvas to its contents, and nested folders show a parent-folder entry to navigate back.
15
15
-**Drag/drop:** assets can be dragged into folders from the canvas or folder tree. A drop replaces the asset's folder memberships with the target folder, matching desktop file-manager move semantics.
16
-
-**Floating windows:** Asset viewer, upload queue, bulk edit. Each is `useDraggablePanel('mediaViewer' | 'mediaUploadQueue' | 'mediaBulkEdit')`. Position survives reload via `workspaceLayoutStorage`.
16
+
-**Floating windows:** Asset viewer, upload queue, bulk edit. Each uses the shared `@admin/shared/FloatingWindow` shell or its `useDraggablePanel` hook with a unique id (`mediaDetachedInspector`, `mediaUploadQueue`, `mediaBulkEdit`). Position survives reload via `workspaceLayoutStorage`.
17
17
-**Auto-open behavior:** upload queue opens when uploads start; bulk-edit opens at 2+ selected; viewer opens on primary selection.
18
18
-**Server side:**`media_assets`, `media_folders`, `media_asset_folders` tables. Handlers under `/admin/api/cms/media`, `/admin/api/cms/media/folders`, `/admin/api/cms/media/storage`. Repositories at `server/repositories/media*.ts`.
19
19
-**Storage adapters:** built-in local-disk plus plugin-registered adapters. Non-public-url adapters route through `/_instatic/media/<adapterId>/<storagePath>` for signed redirects.
@@ -25,6 +25,7 @@ The workspace is canvas-style: it uses `AdminWorkspaceCanvasLayout`, the lighter
├── smartFolders.ts — smart folder IDs, type guard, per-ID predicates
56
56
└── variants.ts — image variant URL helpers
57
+
58
+
src/admin/shared/FloatingWindow/ — shared portal shell + persisted drag hook used across admin workspaces
57
59
```
58
60
59
61
---
@@ -144,7 +146,7 @@ The count badge shown next to each smart folder in the sidebar is computed clien
144
146
145
147
### Floating windows
146
148
147
-
Each floating window has a unique `FloatingPanelId` (`'mediaViewer' | 'mediaUploadQueue' | 'mediaBulkEdit'`), uses `useDraggablePanel(id)` for position, and gets its position persisted via `workspaceLayoutStorage.ts`. Visibility differs by window:
149
+
Each floating window has a unique `FloatingPanelId` (`'mediaDetachedInspector' | 'mediaUploadQueue' | 'mediaBulkEdit'`), uses `useDraggablePanel(id)` for position, and gets its position persisted via `workspaceLayoutStorage.ts`. Visibility differs by window:
@@ -154,7 +156,7 @@ Each floating window has a unique `FloatingPanelId` (`'mediaViewer' | 'mediaUplo
154
156
155
157
The viewer and bulk-edit are derived rather than stored because "closed" is identical to "no selection" — every close path calls `workspace.clearSelection()`. Deriving avoids an extra render commit and the one-frame open lag that appeared with the old `setState`-in-effect approach.
156
158
157
-
The shared `FloatingWindow` shell at `components/FloatingWindow/` provides the chrome: drag handle, close button, position bounding.
159
+
The shared `FloatingWindow` shell at `src/admin/shared/FloatingWindow/` provides the portal, chrome, drag handle, close button, and position bounding without pulling media-specific editor code into other workspaces. Its dimension-aware clamp measures each panel and keeps at least a 50px header strip reachable on every viewport edge; stored positions are re-clamped when a window mounts, changes size, or the viewport resizes.
Uploads initiated outside the Media page use the same pipeline. In particular, the Agent Panel's explicit **Save to Media** image action resolves the private chat image, wraps it in a MIME-correct `File`, and calls `uploadCmsMediaAsset`; it does not create an AI-specific storage route. On success, `mediaAssetEvents.ts` upserts the new row into an already-mounted Site → Media explorer while the normal media cache is primed for canvas consumers.
@@ -325,8 +329,8 @@ The redirect handler is `tryServeMediaRedirect` in `server/router.ts`. The redir
325
329
### Add a new floating window
326
330
327
331
1. Add a unique id to `FloatingPanelId` in the layout storage module.
328
-
2. Create `components/<WindowName>/` with the window React component using the shared `FloatingWindow` shell.
329
-
3.Use `useDraggablePanel('newWindowId')` for position; track `open` in `MediaPage` local state.
332
+
2. Create `components/<WindowName>/` with the window React component using `FloatingWindow` from `@admin/shared/FloatingWindow`.
333
+
3.Pass the id to `FloatingWindow`; track `open` in `MediaPage` local state. Use the exported `useDraggablePanel` directly only for a custom shell such as `MediaViewerWindow`.
Copy file name to clipboardExpand all lines: docs/reference/typebox-patterns.md
+6-4Lines changed: 6 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -13,7 +13,7 @@ Schemas are the **source of truth**. Domain types come from `Static<typeof Schem
13
13
-**Helpers live in `src/core/utils/typeboxHelpers.ts`** — `Type`, `Value`, `Static`, `withFallback`, `parseValue`, `safeParseValue`, `filterArray`, `formatValueErrors`.
14
14
-**Compiled validators live in `src/core/utils/typeboxCompiler.ts`** — `compiled`, `compiledCheck`, `compiledDecode`, `compiledSafeParseValue`, `compiledFormatValueErrors`. Hot repeated `Check` / `Decode` / `Errors` paths should use these or helpers that already call them.
15
15
-**JSON boundary helpers live in `src/core/utils/jsonValidate.ts`** — `safeParseJson`, `parseJsonWithFallback`, `parseJsonResponse`.
16
-
-**Canonical client HTTP layer:**`@core/http` — `apiRequest(path, { schema, … })` (the default for browser→server calls), plus `ApiError`, `isAbortError`, `readEnvelope`, `responseErrorMessage`, `ErrorEnvelopeSchema`. All defined in `src/core/http/apiClient.ts`.
16
+
-**Canonical client HTTP layer:**`@core/http` — `apiRequest(path, { schema, … })` (the default for JSON browser→server calls), `apiBlobRequest(path, …)` for binary responses, plus `ApiError`, `isAbortError`, `readEnvelope`, `responseErrorMessage`, `ErrorEnvelopeSchema`. All defined in `src/core/http/apiClient.ts`.
17
17
-**Schemas are source of truth.**`type Foo = Static<typeof FooSchema>` — never a hand-rolled interface beside the schema.
18
18
-**Soft fallbacks** for corrupted local storage / optional config use `withFallback(schema, default)` + `parseJsonWithFallback`.
19
19
-**Hard fallbacks** for required documents throw and bubble to an error boundary.
|`apiRequest(path, { method, body, schema, query, signal, … })`|**The default for browser→server calls.** Sets `credentials`, JSON-serializes `body`, validates the success body against `schema` (returns `Static<schema>`; `void` without one), and throws `ApiError` on a non-OK status. |
97
+
|`apiBlobRequest(path, { signal, … })`| Binary-response counterpart for authenticated image/file reads. Uses the same transport and `ApiError` envelope handling, then returns a `Blob`; the caller validates the MIME type before use. |
97
98
|`readEnvelope(res, schema, fallbackMessage)`| For code that already holds a `Response` (the persistence layer, which injects its own `fetch`): check `res.ok` (throw `ApiError` with `responseErrorMessage(res, fallback)` if not), then validate body against `schema`|
98
99
|`assertOk(res, fallbackMessage)`| No-body counterpart to `readEnvelope`: throw `ApiError` if the `Response` is not OK, otherwise return (for void mutations / bodies parsed separately) |
99
100
|`responseErrorMessage(res, fallback)`| Extract a useful error message from a failed `Response` (reads `{ error: string }` envelope if present, then raw text, otherwise the fallback) |
100
-
|`ApiError`| The single error type thrown by `apiRequest`/`readEnvelope`; carries `.status` so UI can branch (403, 404, …) |
101
+
|`ApiError`| The single error type thrown by `apiRequest`/`apiBlobRequest`/`readEnvelope`; carries `.status` so UI can branch (403, 404, …) |
101
102
|`isAbortError(err)`| True for an aborted fetch (user cancellation / superseded request) — the uniform replacement for `(err as Error).name === 'AbortError'`|
102
103
103
104
---
@@ -286,7 +287,8 @@ Common boundaries already wrapped — extend the same pattern when you add a new
| HTTP response from a held `Response`|`readEnvelope(res, Schema, fallback)`|`src/core/http/apiClient.ts`|
291
293
| HTTP body-validation primitive (no status semantics; `@core/http` internals, XHR, server-side external APIs) |`parseJsonResponse(res, Schema)`|`src/core/utils/jsonValidate.ts`|
292
294
| Request body (server handler) |`readValidatedBody(req, Schema)` → typed value or `null` (return `badRequest` on null) |`server/http.ts` + per-handler |
@@ -333,7 +335,7 @@ Common boundaries already wrapped — extend the same pattern when you add a new
0 commit comments