mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +01:00
Fix workspace changes toolbar defaults
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
43c4abaf8d
commit
986df210a3
6 files changed
+173
-7
No files matched your search
@@ -119,6 +119,7 @@ export const workspaceDiffResponseSchema = z.object({
|
||||
companyId: z.string(),
|
||||
view: workspaceDiffViewSchema,
|
||||
baseRef: z.string().nullable(),
|
||||
defaultBaseRef: z.string().nullable(),
|
||||
headSha: z.string().nullable(),
|
||||
includeUntracked: z.boolean(),
|
||||
paths: z.array(z.string()),
|
||||
|
||||
@@ -3,7 +3,7 @@ import { usePluginData, usePluginToast } from "@paperclipai/plugin-sdk/ui";
|
||||
import { DIFFS_TAG_NAME, getSingularPatch } from "@pierre/diffs";
|
||||
import type { PatchDiffProps } from "@pierre/diffs/react";
|
||||
import { useFileDiffInstance } from "@pierre/diffs/react";
|
||||
import { createElement, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { createElement, type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
diffSummary,
|
||||
fileName,
|
||||
@@ -21,6 +21,37 @@ type WorkspaceDiffData = WorkspaceDiffResponse;
|
||||
type WorkspacePatchDiffOptions = PatchDiffProps<undefined>["options"];
|
||||
type DiffViewMode = "working-tree" | "head";
|
||||
|
||||
type LucideIconProps = { size?: number };
|
||||
|
||||
function makeLucideIcon(paths: ReactNode) {
|
||||
return function LucideIcon({ size = 16 }: LucideIconProps) {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
style={{ width: size, height: size, display: "block" }}
|
||||
>
|
||||
{paths}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// Plugin bundles cannot import host-only lucide-react; this mirrors lucide RefreshCw.
|
||||
const RefreshCwIcon = makeLucideIcon(
|
||||
<>
|
||||
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
|
||||
<path d="M21 3v5h-5" />
|
||||
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
|
||||
<path d="M8 16H3v5" />
|
||||
</>,
|
||||
);
|
||||
|
||||
function readInitialView(): DiffViewMode {
|
||||
if (typeof window === "undefined") return "working-tree";
|
||||
return new URLSearchParams(window.location.search).get("diffView") === "head" ? "head" : "working-tree";
|
||||
@@ -314,6 +345,7 @@ export function ChangesTab({ context }: PluginDetailTabProps) {
|
||||
const [mode, setMode] = useState<DiffRenderMode>("split");
|
||||
const [view, setView] = useState<DiffViewMode>(() => readInitialView());
|
||||
const [baseRef, setBaseRef] = useState(() => readInitialBaseRef());
|
||||
const baseRefTouchedRef = useRef(Boolean(baseRef.trim()));
|
||||
const [includeUntracked, setIncludeUntracked] = useState(false);
|
||||
const [expandedFiles, setExpandedFiles] = useState<Set<string>>(() => new Set());
|
||||
const [selectedPath, setSelectedPath] = useState<string | null>(null);
|
||||
@@ -382,6 +414,12 @@ export function ChangesTab({ context }: PluginDetailTabProps) {
|
||||
});
|
||||
}, [syncSelectedPathFromScroll]);
|
||||
|
||||
useEffect(() => {
|
||||
const defaultBaseRef = data?.defaultBaseRef?.trim();
|
||||
if (!defaultBaseRef || baseRef.trim() || baseRefTouchedRef.current) return;
|
||||
setBaseRef(defaultBaseRef);
|
||||
}, [baseRef, data?.defaultBaseRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (files.length === 0) {
|
||||
setExpandedFiles(new Set());
|
||||
@@ -450,7 +488,10 @@ export function ChangesTab({ context }: PluginDetailTabProps) {
|
||||
key="base-ref"
|
||||
className="h-8 w-40 rounded-md border border-border bg-background px-2.5 font-mono text-xs outline-none transition-colors placeholder:text-muted-foreground focus:border-foreground/40"
|
||||
value={baseRef}
|
||||
onChange={(event) => setBaseRef(event.target.value)}
|
||||
onChange={(event) => {
|
||||
baseRefTouchedRef.current = true;
|
||||
setBaseRef(event.target.value);
|
||||
}}
|
||||
placeholder="origin/master"
|
||||
aria-label="Base ref"
|
||||
/>
|
||||
@@ -465,8 +506,15 @@ export function ChangesTab({ context }: PluginDetailTabProps) {
|
||||
{includeUntracked ? "Untracked shown" : "Show untracked"}
|
||||
</button>
|
||||
) : null}
|
||||
<button key="refresh" type="button" className={buttonClass(false)} onClick={() => refresh()}>
|
||||
Refresh
|
||||
<button
|
||||
key="refresh"
|
||||
type="button"
|
||||
className={iconButtonClass(false)}
|
||||
onClick={() => refresh()}
|
||||
title="Refresh changes"
|
||||
aria-label="Refresh changes"
|
||||
>
|
||||
<RefreshCwIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,21 @@ function readString(value: unknown): string {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function readOptionalString(value: unknown): string | null {
|
||||
const trimmed = readString(value);
|
||||
return trimmed || null;
|
||||
}
|
||||
|
||||
export function resolveDefaultBaseRef(input: {
|
||||
workspaceBaseRef?: unknown;
|
||||
projectWorkspaceDefaultRef?: unknown;
|
||||
projectWorkspaceRepoRef?: unknown;
|
||||
}): string | null {
|
||||
return readOptionalString(input.workspaceBaseRef)
|
||||
?? readOptionalString(input.projectWorkspaceDefaultRef)
|
||||
?? readOptionalString(input.projectWorkspaceRepoRef);
|
||||
}
|
||||
|
||||
const plugin = definePlugin({
|
||||
async setup(ctx) {
|
||||
ctx.logger.info(`${PLUGIN_NAME} plugin setup`);
|
||||
@@ -34,7 +49,10 @@ const plugin = definePlugin({
|
||||
id: workspace.id,
|
||||
companyId,
|
||||
cwd: workspace.path,
|
||||
baseRef: workspace.defaultRef ?? workspace.repoRef ?? null,
|
||||
baseRef: resolveDefaultBaseRef({
|
||||
projectWorkspaceDefaultRef: workspace.defaultRef,
|
||||
projectWorkspaceRepoRef: workspace.repoRef,
|
||||
}),
|
||||
}, workspaceDiffQuerySchema.parse(params));
|
||||
}
|
||||
|
||||
@@ -42,7 +60,25 @@ const plugin = definePlugin({
|
||||
if (!workspace) {
|
||||
throw new Error("Workspace not found");
|
||||
}
|
||||
return workspaceDiff.getDiff(workspace, workspaceDiffQuerySchema.parse(params));
|
||||
let projectWorkspaceDefaultBaseRef: string | null = null;
|
||||
if (!readOptionalString(workspace.baseRef) && workspace.projectWorkspaceId) {
|
||||
const workspaces = await ctx.projects.listWorkspaces(workspace.projectId, companyId);
|
||||
const projectWorkspace = workspaces.find((candidate) => candidate.id === workspace.projectWorkspaceId);
|
||||
projectWorkspaceDefaultBaseRef = projectWorkspace
|
||||
? resolveDefaultBaseRef({
|
||||
projectWorkspaceDefaultRef: projectWorkspace.defaultRef,
|
||||
projectWorkspaceRepoRef: projectWorkspace.repoRef,
|
||||
})
|
||||
: null;
|
||||
}
|
||||
|
||||
return workspaceDiff.getDiff({
|
||||
...workspace,
|
||||
baseRef: resolveDefaultBaseRef({
|
||||
workspaceBaseRef: workspace.baseRef,
|
||||
projectWorkspaceDefaultRef: projectWorkspaceDefaultBaseRef,
|
||||
}),
|
||||
}, workspaceDiffQuerySchema.parse(params));
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -676,6 +676,7 @@ export function workspaceDiffService() {
|
||||
companyId: workspace.companyId,
|
||||
view: query.view,
|
||||
baseRef,
|
||||
defaultBaseRef: workspace.baseRef,
|
||||
headSha: await resolveHeadSha(cwd),
|
||||
includeUntracked: query.includeUntracked,
|
||||
paths,
|
||||
|
||||
@@ -49,6 +49,7 @@ export function diffResponse(overrides: Partial<WorkspaceDiffResponse> = {}): Wo
|
||||
companyId: "22222222-2222-4222-8222-222222222222",
|
||||
view: "working-tree",
|
||||
baseRef: null,
|
||||
defaultBaseRef: null,
|
||||
headSha: null,
|
||||
includeUntracked: true,
|
||||
paths: [],
|
||||
|
||||
@@ -6,7 +6,7 @@ import { promisify } from "node:util";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { createTestHarness } from "@paperclipai/plugin-sdk/testing";
|
||||
import manifest from "../src/manifest.js";
|
||||
import plugin from "../src/worker.js";
|
||||
import plugin, { resolveDefaultBaseRef } from "../src/worker.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const tempRoots: string[] = [];
|
||||
@@ -143,6 +143,85 @@ describe("workspace diff plugin", () => {
|
||||
|
||||
expect(result).toMatchObject({
|
||||
baseRef: "main",
|
||||
defaultBaseRef: "main",
|
||||
stats: { fileCount: 1 },
|
||||
files: [expect.objectContaining({ path: "src/app.ts" })],
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves the default base ref from workspace and project workspace metadata", () => {
|
||||
expect(resolveDefaultBaseRef({
|
||||
workspaceBaseRef: " release/main ",
|
||||
projectWorkspaceDefaultRef: "origin/main",
|
||||
projectWorkspaceRepoRef: "feature",
|
||||
})).toBe("release/main");
|
||||
expect(resolveDefaultBaseRef({
|
||||
workspaceBaseRef: null,
|
||||
projectWorkspaceDefaultRef: " origin/main ",
|
||||
projectWorkspaceRepoRef: "feature",
|
||||
})).toBe("origin/main");
|
||||
expect(resolveDefaultBaseRef({
|
||||
workspaceBaseRef: "",
|
||||
projectWorkspaceDefaultRef: null,
|
||||
projectWorkspaceRepoRef: " feature ",
|
||||
})).toBe("feature");
|
||||
expect(resolveDefaultBaseRef({
|
||||
workspaceBaseRef: "",
|
||||
projectWorkspaceDefaultRef: null,
|
||||
projectWorkspaceRepoRef: "",
|
||||
})).toBeNull();
|
||||
});
|
||||
|
||||
it("uses project workspace default refs for execution workspace head diffs", async () => {
|
||||
const root = await createGitWorkspace();
|
||||
await git(root, ["checkout", "-b", "feature"]);
|
||||
await fs.writeFile(path.join(root, "src/app.ts"), "export const value = 4;\n");
|
||||
await git(root, ["add", "src/app.ts"]);
|
||||
await git(root, ["commit", "-m", "feature change"]);
|
||||
const harness = createTestHarness({ manifest });
|
||||
harness.seed({
|
||||
executionWorkspaces: [{
|
||||
id: "workspace-1",
|
||||
companyId: "company-1",
|
||||
projectId: "project-1",
|
||||
projectWorkspaceId: "project-workspace-1",
|
||||
path: root,
|
||||
cwd: root,
|
||||
repoUrl: null,
|
||||
baseRef: null,
|
||||
branchName: "feature",
|
||||
providerType: "git_worktree",
|
||||
providerMetadata: null,
|
||||
}],
|
||||
});
|
||||
harness.ctx.projects.listWorkspaces = async (projectId, companyId) => {
|
||||
expect(projectId).toBe("project-1");
|
||||
expect(companyId).toBe("company-1");
|
||||
return [{
|
||||
id: "project-workspace-1",
|
||||
projectId: "project-1",
|
||||
name: "Primary",
|
||||
path: root,
|
||||
repoUrl: null,
|
||||
repoRef: "feature",
|
||||
defaultRef: "main",
|
||||
isPrimary: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
}];
|
||||
};
|
||||
await plugin.definition.setup(harness.ctx);
|
||||
|
||||
const result = await harness.getData("workspace-diff", {
|
||||
workspaceId: "workspace-1",
|
||||
companyId: "company-1",
|
||||
view: "head",
|
||||
includeUntracked: false,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
baseRef: "main",
|
||||
defaultBaseRef: "main",
|
||||
stats: { fileCount: 1 },
|
||||
files: [expect.objectContaining({ path: "src/app.ts" })],
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user