Skip to content

Commit c10ee41

Browse files
committed
fix(plugin-git): paginate commit log in static builds; cover all panels with stories
The git:log snapshot baked only page one and served it for every request, so in a static deploy 'Load more' could never advance and was disabled outright. Bake the head of history (up to the 200-commit handler ceiling) as the snapshot instead, reporting hasMore: false since a static bundle has no further page — the dashboard now shows full history offline and live mode keeps paginating. Drops the now-unnecessary live-only gate on the button. Adds Storybook stories for the status, branches, and diff panel views alongside the existing log stories.
1 parent dbac198 commit c10ee41

8 files changed

Lines changed: 189 additions & 13 deletions

File tree

‎plugins/git/.storybook/shims.d.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Storybook config files live outside the app's tsconfig, so declare the CSS
2+
// side-effect import used by `preview.tsx`.
3+
declare module '*.css'

‎plugins/git/src/client/components/log-panel.tsx‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,6 @@ export function LogPanel() {
9797
await loadPage(rpc, skip, 'append')
9898
}, [rpc, skip, loadPage])
9999

100-
const liveBackend = rpc?.connectionMeta.backend === 'websocket'
101-
102100
return (
103101
<LogPanelView
104102
rpcConnected={!!rpc}
@@ -107,7 +105,6 @@ export function LogPanel() {
107105
hasMore={hasMore}
108106
loading={loading}
109107
error={error}
110-
liveBackend={!!liveBackend}
111108
currentBranch={currentBranch}
112109
workingChanges={workingChanges}
113110
onRefresh={refresh}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { Meta, StoryObj } from '@storybook/react-vite'
2+
import type { GitBranches } from '../../../index'
3+
import { BranchesPanelView } from './branches-panel-view'
4+
5+
const data: GitBranches = {
6+
isRepo: true,
7+
current: 'feat/plugin-git',
8+
branches: [
9+
{ name: 'feat/plugin-git', current: true, sha: 'af39698', upstream: 'origin/feat/plugin-git', subject: 'GitLens-style commit graph and log', ahead: 2, behind: 0, gone: false },
10+
{ name: 'main', current: false, sha: '524c6b6', upstream: 'origin/main', subject: 'add dock switcher UI to minimal hub examples', ahead: 0, behind: 12, gone: false },
11+
{ name: 'feature/onboard', current: false, sha: 'c03c3d4', upstream: 'origin/feature/onboard', subject: 'Fixes stash node icon alignment', ahead: 3, behind: 1, gone: false },
12+
{ name: 'bug/error-log', current: false, sha: 'c08b809', upstream: null, subject: 'Log error instead of throwing', ahead: 0, behind: 0, gone: false },
13+
{ name: 'feature/icons', current: false, sha: 'c10d0ab', upstream: 'origin/feature/icons', subject: 'Add file-diff icons, bump component version', ahead: 0, behind: 0, gone: true },
14+
],
15+
}
16+
17+
const meta = {
18+
title: 'Panels/Branches',
19+
component: BranchesPanelView,
20+
args: {
21+
data,
22+
loading: false,
23+
onRefresh: () => undefined,
24+
},
25+
} satisfies Meta<typeof BranchesPanelView>
26+
27+
export default meta
28+
type Story = StoryObj<typeof meta>
29+
30+
export const Default: Story = {}
31+
export const Loading: Story = { args: { data: null, loading: true } }
32+
export const NotARepo: Story = { args: { data: { isRepo: false, current: null, branches: [] } } }
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { Meta, StoryObj } from '@storybook/react-vite'
2+
import type { GitDiff } from '../../../index'
3+
import { useState } from 'react'
4+
import { DiffPanelView, DiffPatchView } from './diff-panel-view'
5+
6+
const PATCH = `diff --git a/src/rpc/functions/log.ts b/src/rpc/functions/log.ts
7+
index 1234567..89abcde 100644
8+
--- a/src/rpc/functions/log.ts
9+
+++ b/src/rpc/functions/log.ts
10+
@@ -72,7 +72,7 @@ export const log = defineRpcFunction({
11+
name: 'git:log',
12+
type: 'query',
13+
- snapshot: true,
14+
+ dump: async (_ctx, handler) => { /* bake head of history */ },
15+
jsonSerializable: true,
16+
setup: (ctx) => {`
17+
18+
const data: GitDiff = {
19+
isRepo: true,
20+
staged: false,
21+
path: null,
22+
files: [
23+
{ path: 'src/rpc/functions/log.ts', additions: 14, deletions: 3, binary: false },
24+
{ path: 'src/client/components/views/log-panel-view.tsx', additions: 162, deletions: 40, binary: false },
25+
{ path: 'src/client/lib/refs.ts', additions: 71, deletions: 0, binary: false },
26+
{ path: 'public/preview.png', additions: 0, deletions: 0, binary: true },
27+
],
28+
totalAdditions: 247,
29+
totalDeletions: 43,
30+
patch: null,
31+
truncated: false,
32+
}
33+
34+
// Wire the scope toggle + file selection so the panel is interactive, and feed
35+
// the selected file's patch through the `DiffPatchView` slot.
36+
function Harness(props: Partial<React.ComponentProps<typeof DiffPanelView>>) {
37+
const [staged, setStaged] = useState(false)
38+
const [selected, setSelected] = useState<string | null>('src/rpc/functions/log.ts')
39+
return (
40+
<DiffPanelView
41+
data={data}
42+
loading={false}
43+
staged={staged}
44+
selected={selected}
45+
onSelectScope={setStaged}
46+
onSelectFile={setSelected}
47+
onRefresh={() => undefined}
48+
patchSlot={<DiffPatchView patch={PATCH} loading={false} truncated={false} />}
49+
{...props}
50+
/>
51+
)
52+
}
53+
54+
const meta = {
55+
title: 'Panels/Diff',
56+
component: Harness,
57+
} satisfies Meta<typeof Harness>
58+
59+
export default meta
60+
type Story = StoryObj<typeof meta>
61+
62+
export const Default: Story = {}
63+
export const NoSelection: Story = { args: { selected: null } }
64+
export const Loading: Story = { args: { data: null, loading: true } }
65+
export const NoChanges: Story = { args: { data: { ...data, files: [], totalAdditions: 0, totalDeletions: 0 } } }
66+
export const NotARepo: Story = { args: { data: { ...data, isRepo: false } } }

‎plugins/git/src/client/components/views/log-panel-view.stories.tsx‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ const meta = {
3535
hasMore: false,
3636
loading: false,
3737
error: null,
38-
liveBackend: true,
3938
currentBranch: 'main',
4039
workingChanges: 0,
4140
onRefresh: () => undefined,
@@ -52,5 +51,5 @@ export const Connecting: Story = { args: { rpcConnected: false } }
5251
export const Empty: Story = { args: { commits: [] } }
5352
export const NotARepo: Story = { args: { isRepo: false, commits: [] } }
5453
export const HasMore: Story = { args: { hasMore: true } }
55-
export const StaticMode: Story = { args: { hasMore: true, liveBackend: false } }
54+
export const Loading: Story = { args: { hasMore: true, loading: true } }
5655
export const Error: Story = { args: { error: 'fatal: bad revision' } }

‎plugins/git/src/client/components/views/log-panel-view.tsx‎

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ export interface LogPanelViewProps {
2727
hasMore: boolean
2828
loading: boolean
2929
error: string | null
30-
liveBackend: boolean
3130
/** Active branch name, used to flag the checked-out ref. */
3231
currentBranch?: string | null
3332
/** Number of changed working-tree files; drives the "Work In Progress" row. */
@@ -254,7 +253,6 @@ export function LogPanelView(props: LogPanelViewProps) {
254253
hasMore,
255254
loading,
256255
error,
257-
liveBackend,
258256
currentBranch,
259257
workingChanges,
260258
onRefresh,
@@ -321,14 +319,10 @@ export function LogPanelView(props: LogPanelViewProps) {
321319
)}
322320

323321
{isRepo === true && hasMore && (
324-
<Button variant="outline" size="sm" className="w-full" onClick={onLoadMore} disabled={loading || !liveBackend}>
322+
<Button variant="outline" size="sm" className="w-full" onClick={onLoadMore} disabled={loading}>
325323
Load more
326324
</Button>
327325
)}
328-
329-
{isRepo === true && hasMore && !liveBackend && (
330-
<p className="text-muted-foreground text-xs">Load more is available in live mode.</p>
331-
)}
332326
</div>
333327
)
334328
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import type { Meta, StoryObj } from '@storybook/react-vite'
2+
import type { GitStatus } from '../../../index'
3+
import { useState } from 'react'
4+
import { StatusPanelView } from './status-panel-view'
5+
6+
const dirty: GitStatus = {
7+
isRepo: true,
8+
root: '/repo',
9+
branch: 'feat/plugin-git',
10+
detached: false,
11+
head: 'af39698',
12+
upstream: 'origin/feat/plugin-git',
13+
ahead: 2,
14+
behind: 1,
15+
staged: [
16+
{ path: 'src/client/components/views/log-panel-view.tsx', status: 'modified' },
17+
{ path: 'src/client/lib/refs.ts', status: 'added' },
18+
],
19+
unstaged: [
20+
{ path: 'src/rpc/functions/log.ts', status: 'modified' },
21+
{ path: 'README.md', status: 'modified' },
22+
],
23+
untracked: ['src/client/lib/refs.test.ts'],
24+
clean: false,
25+
canWrite: true,
26+
}
27+
28+
const clean: GitStatus = {
29+
...dirty,
30+
staged: [],
31+
unstaged: [],
32+
untracked: [],
33+
ahead: 0,
34+
behind: 0,
35+
clean: true,
36+
}
37+
38+
// Wrap so the commit textarea is interactive in Storybook.
39+
function Harness(props: Partial<React.ComponentProps<typeof StatusPanelView>>) {
40+
const [message, setMessage] = useState('')
41+
return (
42+
<StatusPanelView
43+
data={dirty}
44+
loading={false}
45+
busy={false}
46+
canWrite
47+
message={message}
48+
note={null}
49+
onRefresh={() => undefined}
50+
onStage={() => undefined}
51+
onUnstage={() => undefined}
52+
onCommit={() => undefined}
53+
onMessageChange={setMessage}
54+
{...props}
55+
/>
56+
)
57+
}
58+
59+
const meta = {
60+
title: 'Panels/Status',
61+
component: Harness,
62+
} satisfies Meta<typeof Harness>
63+
64+
export default meta
65+
type Story = StoryObj<typeof meta>
66+
67+
export const Dirty: Story = {}
68+
export const ReadOnly: Story = { args: { canWrite: false } }
69+
export const Clean: Story = { args: { data: clean } }
70+
export const Loading: Story = { args: { data: null, loading: true } }
71+
export const NotARepo: Story = { args: { data: { ...clean, isRepo: false } } }

‎plugins/git/src/rpc/functions/log.ts‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,25 @@ function parseLog(raw: string): Commit[] {
6969
})
7070
}
7171

72+
// Largest page git's handler will return (`limit` is clamped to 1–200). Static
73+
// builds bake this many commits so the dashboard shows real history offline.
74+
const SNAPSHOT_LIMIT = 200
75+
7276
export const log = defineRpcFunction({
7377
name: 'git:log',
7478
type: 'query',
75-
snapshot: true,
7679
jsonSerializable: true,
80+
// A static build can't run git on demand, so bake the head of history (up to
81+
// `SNAPSHOT_LIMIT`) as the snapshot. Every client call resolves to this baked
82+
// page via the fallback; since a static bundle has no further page to fetch,
83+
// it reports `hasMore: false` so the UI shows everything it has in one shot.
84+
dump: async (_ctx, handler: (args?: LogArgs) => Promise<GitLog>) => {
85+
const output = await handler({ limit: SNAPSHOT_LIMIT, skip: 0 })
86+
const baked: GitLog = { ...output, hasMore: false }
87+
// `RETURN` carries the handler's `Promise<GitLog>`, while dump records hold
88+
// the already-resolved value — assert past that wrapper mismatch.
89+
return { records: [{ inputs: [], output: baked }], fallback: baked } as any
90+
},
7791
setup: (ctx) => {
7892
const git = getGitContext(ctx)
7993
return {

0 commit comments

Comments
 (0)