Merge remote-tracking branch 'public-gh/master' into paperclip-subissues
* public-gh/master: (51 commits) Use attachment-size limit for company logos Address Greptile company logo feedback Drop lockfile from PR branch Use asset-backed company logos fix: use appType "custom" for Vite dev server so worktree branding is applied docs: fix documentation drift — adapters, plugins, tech stack docs: update documentation for accuracy after plugin system launch chore: ignore superset artifacts Dark theme for CodeMirror code blocks in MDXEditor Remove duplicate @paperclipai/adapter-openclaw-gateway in server/package.json Fix code block styles with robust prose overrides Add Docker setup for untrusted PR review in isolated containers Fix org chart canvas height to fit viewport without scrolling Add doc-maintenance skill for periodic documentation accuracy audits Fix sidebar scrollbar: hide track background when not hovering Restyle markdown code blocks: dark background, smaller font, compact padding Add archive project button and filter archived projects from selectors fix: address review feedback — subscription cleanup, filter nullability, stale diagram fix: wire plugin event subscriptions from worker to host fix(ui): hide scrollbar track background when sidebar is not hovered ... # Conflicts: # packages/db/src/migrations/meta/0030_snapshot.json # packages/db/src/migrations/meta/_journal.json
This commit is contained in:
@@ -2,6 +2,7 @@ import { Link } from "@/lib/router";
|
||||
import { Menu } from "lucide-react";
|
||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||
import { useSidebar } from "../context/SidebarContext";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Breadcrumb,
|
||||
@@ -11,13 +12,46 @@ import {
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { Fragment } from "react";
|
||||
import { Fragment, useMemo } from "react";
|
||||
import { PluginSlotOutlet, usePluginSlots } from "@/plugins/slots";
|
||||
import { PluginLauncherOutlet, usePluginLaunchers } from "@/plugins/launchers";
|
||||
|
||||
type GlobalToolbarContext = { companyId: string | null; companyPrefix: string | null };
|
||||
|
||||
function GlobalToolbarPlugins({ context }: { context: GlobalToolbarContext }) {
|
||||
const { slots } = usePluginSlots({ slotTypes: ["globalToolbarButton"], companyId: context.companyId });
|
||||
const { launchers } = usePluginLaunchers({ placementZones: ["globalToolbarButton"], companyId: context.companyId, enabled: !!context.companyId });
|
||||
if (slots.length === 0 && launchers.length === 0) return null;
|
||||
return (
|
||||
<div className="flex items-center gap-1 ml-auto shrink-0 pl-2">
|
||||
<PluginSlotOutlet slotTypes={["globalToolbarButton"]} context={context} className="flex items-center gap-1" />
|
||||
<PluginLauncherOutlet placementZones={["globalToolbarButton"]} context={context} className="flex items-center gap-1" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BreadcrumbBar() {
|
||||
const { breadcrumbs } = useBreadcrumbs();
|
||||
const { toggleSidebar, isMobile } = useSidebar();
|
||||
const { selectedCompanyId, selectedCompany } = useCompany();
|
||||
|
||||
if (breadcrumbs.length === 0) return null;
|
||||
const globalToolbarSlotContext = useMemo(
|
||||
() => ({
|
||||
companyId: selectedCompanyId ?? null,
|
||||
companyPrefix: selectedCompany?.issuePrefix ?? null,
|
||||
}),
|
||||
[selectedCompanyId, selectedCompany?.issuePrefix],
|
||||
);
|
||||
|
||||
const globalToolbarSlots = <GlobalToolbarPlugins context={globalToolbarSlotContext} />;
|
||||
|
||||
if (breadcrumbs.length === 0) {
|
||||
return (
|
||||
<div className="border-b border-border px-4 md:px-6 h-12 shrink-0 flex items-center justify-end">
|
||||
{globalToolbarSlots}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const menuButton = isMobile && (
|
||||
<Button
|
||||
@@ -34,40 +68,46 @@ export function BreadcrumbBar() {
|
||||
// Single breadcrumb = page title (uppercase)
|
||||
if (breadcrumbs.length === 1) {
|
||||
return (
|
||||
<div className="border-b border-border px-4 md:px-6 h-12 shrink-0 flex items-center min-w-0 overflow-hidden">
|
||||
<div className="border-b border-border px-4 md:px-6 h-12 shrink-0 flex items-center">
|
||||
{menuButton}
|
||||
<h1 className="text-sm font-semibold uppercase tracking-wider truncate">
|
||||
{breadcrumbs[0].label}
|
||||
</h1>
|
||||
<div className="min-w-0 overflow-hidden flex-1">
|
||||
<h1 className="text-sm font-semibold uppercase tracking-wider truncate">
|
||||
{breadcrumbs[0].label}
|
||||
</h1>
|
||||
</div>
|
||||
{globalToolbarSlots}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Multiple breadcrumbs = breadcrumb trail
|
||||
return (
|
||||
<div className="border-b border-border px-4 md:px-6 h-12 shrink-0 flex items-center min-w-0 overflow-hidden">
|
||||
<div className="border-b border-border px-4 md:px-6 h-12 shrink-0 flex items-center">
|
||||
{menuButton}
|
||||
<Breadcrumb className="min-w-0 overflow-hidden">
|
||||
<BreadcrumbList className="flex-nowrap">
|
||||
{breadcrumbs.map((crumb, i) => {
|
||||
const isLast = i === breadcrumbs.length - 1;
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
{i > 0 && <BreadcrumbSeparator />}
|
||||
<BreadcrumbItem className={isLast ? "min-w-0" : "shrink-0"}>
|
||||
{isLast || !crumb.href ? (
|
||||
<BreadcrumbPage className="truncate">{crumb.label}</BreadcrumbPage>
|
||||
) : (
|
||||
<BreadcrumbLink asChild>
|
||||
<Link to={crumb.href}>{crumb.label}</Link>
|
||||
</BreadcrumbLink>
|
||||
)}
|
||||
</BreadcrumbItem>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<div className="min-w-0 overflow-hidden flex-1">
|
||||
<Breadcrumb className="min-w-0 overflow-hidden">
|
||||
<BreadcrumbList className="flex-nowrap">
|
||||
{breadcrumbs.map((crumb, i) => {
|
||||
const isLast = i === breadcrumbs.length - 1;
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
{i > 0 && <BreadcrumbSeparator />}
|
||||
<BreadcrumbItem className={isLast ? "min-w-0" : "shrink-0"}>
|
||||
{isLast || !crumb.href ? (
|
||||
<BreadcrumbPage className="truncate">{crumb.label}</BreadcrumbPage>
|
||||
) : (
|
||||
<BreadcrumbLink asChild>
|
||||
<Link to={crumb.href}>{crumb.label}</Link>
|
||||
</BreadcrumbLink>
|
||||
)}
|
||||
</BreadcrumbItem>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
{globalToolbarSlots}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -75,11 +75,15 @@ export function CommandPalette() {
|
||||
enabled: !!selectedCompanyId && open,
|
||||
});
|
||||
|
||||
const { data: projects = [] } = useQuery({
|
||||
const { data: allProjects = [] } = useQuery({
|
||||
queryKey: queryKeys.projects.list(selectedCompanyId!),
|
||||
queryFn: () => projectsApi.list(selectedCompanyId!),
|
||||
enabled: !!selectedCompanyId && open,
|
||||
});
|
||||
const projects = useMemo(
|
||||
() => allProjects.filter((p) => !p.archivedAt),
|
||||
[allProjects],
|
||||
);
|
||||
|
||||
function go(path: string) {
|
||||
setOpen(false);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
const BAYER_4X4 = [
|
||||
@@ -10,6 +10,7 @@ const BAYER_4X4 = [
|
||||
|
||||
interface CompanyPatternIconProps {
|
||||
companyName: string;
|
||||
logoUrl?: string | null;
|
||||
brandColor?: string | null;
|
||||
className?: string;
|
||||
}
|
||||
@@ -159,8 +160,18 @@ function makeCompanyPatternDataUrl(seed: string, brandColor?: string | null, log
|
||||
return canvas.toDataURL("image/png");
|
||||
}
|
||||
|
||||
export function CompanyPatternIcon({ companyName, brandColor, className }: CompanyPatternIconProps) {
|
||||
export function CompanyPatternIcon({
|
||||
companyName,
|
||||
logoUrl,
|
||||
brandColor,
|
||||
className,
|
||||
}: CompanyPatternIconProps) {
|
||||
const initial = companyName.trim().charAt(0).toUpperCase() || "?";
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const logo = !imageError && typeof logoUrl === "string" && logoUrl.trim().length > 0 ? logoUrl : null;
|
||||
useEffect(() => {
|
||||
setImageError(false);
|
||||
}, [logoUrl]);
|
||||
const patternDataUrl = useMemo(
|
||||
() => makeCompanyPatternDataUrl(companyName.trim().toLowerCase(), brandColor),
|
||||
[companyName, brandColor],
|
||||
@@ -173,7 +184,14 @@ export function CompanyPatternIcon({ companyName, brandColor, className }: Compa
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{patternDataUrl ? (
|
||||
{logo ? (
|
||||
<img
|
||||
src={logo}
|
||||
alt={`${companyName} logo`}
|
||||
onError={() => setImageError(true)}
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
) : patternDataUrl ? (
|
||||
<img
|
||||
src={patternDataUrl}
|
||||
alt=""
|
||||
@@ -184,9 +202,11 @@ export function CompanyPatternIcon({ companyName, brandColor, className }: Compa
|
||||
) : (
|
||||
<div className="absolute inset-0 bg-muted" />
|
||||
)}
|
||||
<span className="relative z-10 drop-shadow-[0_1px_2px_rgba(0,0,0,0.65)]">
|
||||
{initial}
|
||||
</span>
|
||||
{!logo && (
|
||||
<span className="relative z-10 drop-shadow-[0_1px_2px_rgba(0,0,0,0.65)]">
|
||||
{initial}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -122,6 +122,7 @@ function SortableCompanyItem({
|
||||
>
|
||||
<CompanyPatternIcon
|
||||
companyName={company.name}
|
||||
logoUrl={company.logoUrl}
|
||||
brandColor={company.brandColor}
|
||||
className={cn(
|
||||
isSelected
|
||||
|
||||
@@ -163,8 +163,12 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp
|
||||
queryFn: () => projectsApi.list(companyId!),
|
||||
enabled: !!companyId,
|
||||
});
|
||||
const activeProjects = useMemo(
|
||||
() => (projects ?? []).filter((p) => !p.archivedAt || p.id === issue.projectId),
|
||||
[projects, issue.projectId],
|
||||
);
|
||||
const { orderedProjects } = useProjectOrder({
|
||||
projects: projects ?? [],
|
||||
projects: activeProjects,
|
||||
companyId,
|
||||
userId: currentUserId,
|
||||
});
|
||||
|
||||
@@ -117,7 +117,7 @@ export function MarkdownBody({ children, className }: MarkdownBodyProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"paperclip-markdown prose prose-sm max-w-none break-words overflow-hidden prose-pre:whitespace-pre-wrap prose-pre:break-words prose-code:break-all",
|
||||
"paperclip-markdown prose prose-sm max-w-none break-words overflow-hidden",
|
||||
theme === "dark" && "prose-invert",
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -346,8 +346,12 @@ export function NewIssueDialog() {
|
||||
queryFn: () => authApi.getSession(),
|
||||
});
|
||||
const currentUserId = session?.user?.id ?? session?.session?.userId ?? null;
|
||||
const activeProjects = useMemo(
|
||||
() => (projects ?? []).filter((p) => !p.archivedAt),
|
||||
[projects],
|
||||
);
|
||||
const { orderedProjects } = useProjectOrder({
|
||||
projects: projects ?? [],
|
||||
projects: activeProjects,
|
||||
companyId: effectiveCompanyId,
|
||||
userId: currentUserId,
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Separator } from "@/components/ui/separator";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { AlertCircle, Check, ExternalLink, Github, Loader2, Plus, Trash2, X } from "lucide-react";
|
||||
import { AlertCircle, Archive, ArchiveRestore, Check, ExternalLink, Github, Loader2, Plus, Trash2, X } from "lucide-react";
|
||||
import { ChoosePathButton } from "./PathInstructionsModal";
|
||||
import { DraftInput } from "./agent-config-primitives";
|
||||
import { InlineEditor } from "./InlineEditor";
|
||||
@@ -32,6 +32,8 @@ interface ProjectPropertiesProps {
|
||||
onUpdate?: (data: Record<string, unknown>) => void;
|
||||
onFieldUpdate?: (field: ProjectConfigFieldKey, data: Record<string, unknown>) => void;
|
||||
getFieldSaveState?: (field: ProjectConfigFieldKey) => ProjectFieldSaveState;
|
||||
onArchive?: (archived: boolean) => void;
|
||||
archivePending?: boolean;
|
||||
}
|
||||
|
||||
export type ProjectFieldSaveState = "idle" | "saving" | "saved" | "error";
|
||||
@@ -148,7 +150,7 @@ function ProjectStatusPicker({ status, onChange }: { status: string; onChange: (
|
||||
);
|
||||
}
|
||||
|
||||
export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSaveState }: ProjectPropertiesProps) {
|
||||
export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSaveState, onArchive, archivePending }: ProjectPropertiesProps) {
|
||||
const { selectedCompanyId } = useCompany();
|
||||
const { enabled: showExperimentalWorkspaceUi } = useExperimentalWorkspacesEnabled();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -975,6 +977,45 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{onArchive && (
|
||||
<>
|
||||
<Separator className="my-4" />
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="text-xs font-medium text-destructive uppercase tracking-wide">
|
||||
Danger Zone
|
||||
</div>
|
||||
<div className="space-y-3 rounded-md border border-destructive/40 bg-destructive/5 px-4 py-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{project.archivedAt
|
||||
? "Unarchive this project to restore it in the sidebar and project selectors."
|
||||
: "Archive this project to hide it from the sidebar and project selectors."}
|
||||
</p>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
disabled={archivePending}
|
||||
onClick={() => {
|
||||
const action = project.archivedAt ? "Unarchive" : "Archive";
|
||||
const confirmed = window.confirm(
|
||||
`${action} project "${project.name}"?`,
|
||||
);
|
||||
if (!confirmed) return;
|
||||
onArchive(!project.archivedAt);
|
||||
}}
|
||||
>
|
||||
{archivePending ? (
|
||||
<><Loader2 className="h-3 w-3 animate-spin mr-1" />{project.archivedAt ? "Unarchiving..." : "Archiving..."}</>
|
||||
) : project.archivedAt ? (
|
||||
<><ArchiveRestore className="h-3 w-3 mr-1" />Unarchive project</>
|
||||
) : (
|
||||
<><Archive className="h-3 w-3 mr-1" />Archive project</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user