Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
63 changes: 31 additions & 32 deletions apps/api/src/comment-preview-fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,50 @@
/**
* Static fallback items for the comment-settings preview endpoint (issue
* #307, Task 6) — used whenever a workspace has no recent `gh/`-prefixed
* attachments to render a realistic preview from. All three point at the
* same real, always-loadable static asset (`og/home.png`, the site-wide OG
* fallback served by apps/web) so the preview never depends on a workspace's
* own storage; only the filenames vary, since the renderer's per-item width
* heuristic (`attachmentImageWidth`) keys off filename patterns
* (landscape/portrait) and the before/after pairing keys off `meta.state`.
* attachments to render a realistic preview from.
*
* The images are purpose-drawn generic wireframes (`/preview/*.svg`, served
* by apps/web) rather than screenshots of uploads.sh itself: the preview's
* job is to show how the *comment* lays out, and a picture of our own UI
* reads as if it were the reader's own attachment. They're always-loadable
* static assets, so the preview never depends on a workspace's own storage.
* The before/after pair are two visibly different drawings — reusing one
* image for both made the pairing look broken.
*
* Three items (one wide dashboard + one before/after pair) land in the
* sparse density tier so the settings preview shows readable image sizes
* and path/state `<code>` captions — matching what a typical PR looks like.
* `pageUrl: null` throughout — fixtures never claim a real `/f/` file page.
* Filenames stay `.png` — they stand in for what a real uploaded screenshot
* is called, and the renderer's per-item width heuristic
* (`attachmentImageWidth`) keys off filename patterns while the before/after
* pairing keys off `meta.state`. `pageUrl: null` throughout — fixtures never
* claim a real `/f/` file page.
*/
import type { AttachmentItem } from "./github-comment-render";
import { webOrigin } from "./web-url";

/**
* Build the fixture items against `env.WEB_ORIGIN` (via `webOrigin`, the
* same single source of truth `filePageUrl` uses) instead of a hardcoded
* production origin, so the preview's fixture image resolves correctly in
* local/staging environments too. `/og/home.png` is the one path — only the
* origin varies.
* production origin, so the preview's fixture images resolve correctly in
* local/staging environments too — only the origin varies.
*/
export function previewFixtureItems(env: Env): AttachmentItem[] {
const fixtureImageUrl = `${webOrigin(env)}/og/home.png`;
const asset = (name: string): string => `${webOrigin(env)}/preview/${name}.svg`;
const item = (
filename: string,
assetName: string,
meta: AttachmentItem["meta"],
): AttachmentItem => ({
key: `gh/preview/pull/0/${filename}`,
url: asset(assetName),
embedUrl: asset(assetName),
pageUrl: null,
meta,
});
return [
{
key: "gh/preview/pull/0/dashboard-overview.png",
url: fixtureImageUrl,
embedUrl: fixtureImageUrl,
pageUrl: null,
meta: { path: "/dashboard", state: "after" },
},
{
key: "gh/preview/pull/0/settings-before.png",
url: fixtureImageUrl,
embedUrl: fixtureImageUrl,
pageUrl: null,
meta: { path: "/settings", state: "before" },
},
{
key: "gh/preview/pull/0/settings-after.png",
url: fixtureImageUrl,
embedUrl: fixtureImageUrl,
pageUrl: null,
meta: { path: "/settings", state: "after" },
},
item("dashboard-overview.png", "comment-dashboard", { path: "/dashboard", state: "after" }),
item("settings-before.png", "comment-settings-before", { path: "/settings", state: "before" }),
item("settings-after.png", "comment-settings-after", { path: "/settings", state: "after" }),
];
}
8 changes: 6 additions & 2 deletions apps/api/src/routes/me.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2254,8 +2254,12 @@ describe("GET /me/workspaces/:name/comment-preview", () => {
expect(body.body).toContain("dashboard-overview.png");
// Fixture image origin is derived from WEB_ORIGIN (previewEnv sets
// "https://uploads.test"), not hardcoded to production's uploads.sh.
expect(body.body).toContain('src="https://uploads.test/og/home.png"');
expect(body.body).not.toContain("uploads.sh/og/home.png");
expect(body.body).toContain('src="https://uploads.test/preview/comment-dashboard.svg"');
expect(body.body).not.toContain("uploads.sh/preview/");
// The before/after pair are two distinct drawings, not one image twice —
// a repeated image made the pairing look broken.
expect(body.body).toContain("/preview/comment-settings-before.svg");
expect(body.body).toContain("/preview/comment-settings-after.svg");
});

it("(e) repo not linked to this workspace -> 404", async () => {
Expand Down
20 changes: 20 additions & 0 deletions apps/web/public/preview/comment-dashboard.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions apps/web/public/preview/comment-settings-after.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions apps/web/public/preview/comment-settings-before.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 22 additions & 9 deletions apps/web/src/layouts/AccountLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import {
signedInCsp,
} from "../lib/signed-in-page";
import {
renderSettingsSubnavHtml,
WORKSPACE_NAV_TABS,
workspaceSettingsSubpageFromPathname,
workspaceTabFromPathname,
type WorkspaceNavTab,
} from "../lib/workspaces-nav";
Expand All @@ -38,9 +40,11 @@ interface Props {
creating?: boolean;
/** Optional document title override (e.g. workspace name). */
title?: string;
/** Widen the right rail (~430px) for the comment-settings 3-column layout. */
railWide?: boolean;
}

const { section, workspace = "", creating = false, title } = Astro.props;
const { section, workspace = "", creating = false, title, railWide = false } = Astro.props;
const hasRail = Astro.slots.has("rail");

const titles: Record<AccountSection, string> = {
Expand All @@ -54,6 +58,8 @@ const docTitle = `${pageHeading} · uploads.sh`;
const activeTab: WorkspaceNavTab | "" = workspace
? workspaceTabFromPathname(Astro.url.pathname) || "files"
: "";
const settingsSubpage =
activeTab === "settings" ? workspaceSettingsSubpageFromPathname(Astro.url.pathname) : "";
const workspaceBase = workspace ? `/account/workspaces/${encodeURIComponent(workspace)}` : "";
const switcherHeading = workspace || "workspaces";

Expand Down Expand Up @@ -90,7 +96,7 @@ applyAuthSecurityHeaders(Astro.response.headers, signedInCsp(authOrigin, apiOrig
<SiteHeader authOrigin={authOrigin} />

<div class="shell">
<div class="layout">
<div class="layout" data-rail-wide={railWide ? "" : undefined}>
<nav class="side" aria-label="Account sections">
<div class="ws-switcher" id="ws-switcher">
<button
Expand Down Expand Up @@ -149,13 +155,20 @@ applyAuthSecurityHeaders(Astro.response.headers, signedInCsp(authOrigin, apiOrig
{
workspace
? WORKSPACE_NAV_TABS.map((tab) => (
<a
href={`${workspaceBase}${tab.path}`}
class="side-link"
aria-current={activeTab === tab.id ? "page" : undefined}
>
{tab.label}
</a>
<Fragment>
<a
href={`${workspaceBase}${tab.path}`}
class="side-link"
aria-current={activeTab === tab.id ? "page" : undefined}
>
{tab.label}
</a>
{tab.id === "settings" && settingsSubpage && (
<Fragment
set:html={renderSettingsSubnavHtml(workspace, settingsSubpage)}
/>
)}
</Fragment>
))
: null
}
Expand Down
44 changes: 35 additions & 9 deletions apps/web/src/layouts/WorkspaceLayout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@
* `tip` is a per-page static hint, server-rendered (no JS, no placeholder):
* one short line, optionally linked. Omit it on pages with nothing worth
* saying (billing's usage meters already occupy the rail).
*
* The `preview` named slot puts page-specific content at the top of the
* rail, above connected work / usage / tip / quick actions — the comment-
* settings tab uses it for its live preview column. Filling it also hides
* the "connected work" section (a preview column and a work list are both
* "what's happening over here" content; showing both was noise) and, when
* paired with `wideRail`, widens the rail to ~430px for that column.
*/
import { GITHUB_APP_INSTALL_URL } from "../lib/github-app";
import { renderUsagePlaceholderHtml } from "../lib/workspace-ui";
Expand All @@ -28,25 +35,44 @@ interface Props {
showUsage?: boolean;
/** Static per-tab hint. `body` may include inline markup (e.g. `<code>`). */
tip?: { body: string; href?: string; linkLabel?: string; label?: string };
/**
* Widen the rail to ~430px for the `preview` slot's 3-column comment-
* settings layout (design's third column). Only takes effect when the
* `preview` slot is actually filled — a page passing this without content
* gets the normal-width rail.
*/
wideRail?: boolean;
}

const { workspace, showUsage = false, tip } = Astro.props;
const { workspace, showUsage = false, tip, wideRail = false } = Astro.props;
const hasPreview = Astro.slots.has("preview");

const invitePath = `/account/workspaces/${encodeURIComponent(workspace)}/people`;
---

<AccountLayout section="workspaces" workspace={workspace} title={workspace}>
<AccountLayout
section="workspaces"
workspace={workspace}
title={workspace}
railWide={wideRail && hasPreview}
>
<slot />

<Fragment slot="rail">
<div class="ws-rail" data-workspace-rail>
<section class="ws-rail__section" data-rail-connected hidden>
<div class="ws-rail__head">
<span class="ws-rail__label">connected work</span>
<span class="ws-rail__rule"></span>
</div>
<div class="ws-rail__connected-list" data-rail-connected-list></div>
</section>
{hasPreview && <slot name="preview" />}

{
!hasPreview && (
<section class="ws-rail__section" data-rail-connected hidden>
<div class="ws-rail__head">
<span class="ws-rail__label">connected work</span>
<span class="ws-rail__rule" />
</div>
<div class="ws-rail__connected-list" data-rail-connected-list />
</section>
)
}

{
showUsage && (
Expand Down
Loading
Loading