Skip to content

Commit ce0a21d

Browse files
dan-leeCopilot
andauthored
Fix anyOf variants (#1469)
Co-authored-by: Copilot <[email protected]>
1 parent f98e47d commit ce0a21d

File tree

2 files changed

+78
-37
lines changed

2 files changed

+78
-37
lines changed
Lines changed: 48 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { useState } from "react";
12
import type { SchemaObject } from "../../../oas/parser/index.js";
23
import { Badge } from "../../../ui/Badge.js";
34
import { Card } from "../../../ui/Card.js";
4-
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../../ui/Tabs.js";
5+
import { cn } from "../../../util/cn.js";
56
import { SchemaView } from "./SchemaView.js";
67
import {
78
decideExclusivity,
@@ -13,9 +14,13 @@ import {
1314
const DecisionTable = ({
1415
variants,
1516
schema,
17+
selectedVariant,
18+
onSelectVariant,
1619
}: {
1720
variants: SchemaObject[];
1821
schema: SchemaObject;
22+
selectedVariant: string;
23+
onSelectVariant: (label: string) => void;
1924
}) => {
2025
const rows = variants.map((v, i) => ({
2126
label: labelForVariant(i, v),
@@ -36,7 +41,18 @@ const DecisionTable = ({
3641
<tbody className="divide-y">
3742
{rows.map((row) => (
3843
<tr key={row.label} className="hover:bg-muted/30">
39-
<td className="p-2 font-medium">{row.label}</td>
44+
<td className="p-2 font-medium">
45+
<button
46+
type="button"
47+
className={cn(
48+
"hover:underline",
49+
selectedVariant === row.label && "text-primary",
50+
)}
51+
onClick={() => onSelectVariant(row.label)}
52+
>
53+
{row.label}
54+
</button>
55+
</td>
4056
<td className="p-2 text-muted-foreground text-xs">
4157
{row.guards.length > 0
4258
? row.guards.join(" · ")
@@ -51,14 +67,16 @@ const DecisionTable = ({
5167
);
5268
};
5369

54-
const VariantPanel = ({ variant }: { variant: SchemaObject }) => (
55-
<div className="my-4">
56-
{variant.description && (
57-
<p className="text-sm text-muted-foreground">{variant.description}</p>
58-
)}
59-
<SchemaView schema={variant} />
60-
</div>
61-
);
70+
const VariantPanel = ({ variant }: { variant: SchemaObject }) => {
71+
return (
72+
<div className="space-y-2">
73+
{variant.description && (
74+
<p className="text-sm text-muted-foreground">{variant.description}</p>
75+
)}
76+
<SchemaView schema={variant} />
77+
</div>
78+
);
79+
};
6280

6381
export const UnionView = ({ schema }: { schema: SchemaObject }) => {
6482
const mode = Array.isArray(schema.oneOf)
@@ -67,10 +85,14 @@ export const UnionView = ({ schema }: { schema: SchemaObject }) => {
6785
? "anyOf"
6886
: undefined;
6987

88+
const variants = mode ? unionVariants(schema) : [];
89+
const [selectedVariant, setSelectedVariant] = useState(() =>
90+
variants[0] ? labelForVariant(0, variants[0]) : "",
91+
);
92+
7093
if (!mode) return null;
7194

7295
const exclusivity = decideExclusivity(schema);
73-
const variants = unionVariants(schema);
7496

7597
const semanticsMessage =
7698
exclusivity === "exactly-one" ? (
@@ -84,8 +106,14 @@ export const UnionView = ({ schema }: { schema: SchemaObject }) => {
84106
</>
85107
);
86108

109+
const currentVariantIndex = variants.findIndex(
110+
(v, i) => labelForVariant(i, v) === selectedVariant,
111+
);
112+
const currentVariant =
113+
currentVariantIndex >= 0 ? variants[currentVariantIndex] : null;
114+
87115
return (
88-
<Card className="overflow-hidden">
116+
<Card className="overflow-hidden text-sm">
89117
<div className="flex flex-col gap-4 p-4">
90118
<div className="flex items-center gap-2">
91119
<Badge variant="outline">{mode}</Badge>
@@ -94,30 +122,15 @@ export const UnionView = ({ schema }: { schema: SchemaObject }) => {
94122
</div>
95123
</div>
96124

97-
<DecisionTable variants={variants} schema={schema} />
125+
<DecisionTable
126+
variants={variants}
127+
schema={schema}
128+
selectedVariant={selectedVariant}
129+
onSelectVariant={setSelectedVariant}
130+
/>
131+
<strong>Properties for {selectedVariant}:</strong>
132+
{currentVariant && <VariantPanel variant={currentVariant} />}
98133
</div>
99-
<Tabs defaultValue="0" className="w-full px-4">
100-
<TabsList className="flex w-full">
101-
{variants.map((v, i) => (
102-
<TabsTrigger
103-
key={labelForVariant(i, v)}
104-
value={String(i)}
105-
className="flex-1"
106-
>
107-
{labelForVariant(i, v)}
108-
</TabsTrigger>
109-
))}
110-
</TabsList>
111-
{variants.map((v, i) => (
112-
<TabsContent
113-
key={labelForVariant(i, v)}
114-
value={String(i)}
115-
className="px-2"
116-
>
117-
<VariantPanel variant={v} />
118-
</TabsContent>
119-
))}
120-
</Tabs>
121134
</Card>
122135
);
123136
};

packages/zudoku/src/lib/plugins/openapi/schema/union-helpers.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,36 @@ export type FieldDoc = {
2525
requiredInAll: boolean;
2626
};
2727

28-
export const unionVariants = (schema: SchemaObject) =>
29-
(schema.oneOf ?? schema.anyOf ?? []) as SchemaObject[];
28+
export const unionVariants = (schema: SchemaObject): SchemaObject[] => {
29+
const variants = (schema.oneOf ?? schema.anyOf ?? []) as SchemaObject[];
30+
31+
// If parent schema has properties that variants don't, merge them
32+
// This handles the pattern where anyOf/oneOf is used just for required field combinations
33+
if (schema.properties && Object.keys(schema.properties).length > 0) {
34+
return variants.map((variant) => {
35+
// If variant doesn't define its own properties or type, inherit from parent
36+
const shouldInherit =
37+
!variant.properties &&
38+
!variant.type &&
39+
!variant.allOf &&
40+
!variant.oneOf &&
41+
!variant.anyOf;
42+
43+
if (shouldInherit) {
44+
return {
45+
...variant,
46+
type: "object" as const,
47+
properties: schema.properties,
48+
required: variant.required ?? schema.required,
49+
};
50+
}
51+
52+
return variant;
53+
});
54+
}
55+
56+
return variants;
57+
};
3058

3159
export const decideExclusivity = (
3260
schema: SchemaObject,

0 commit comments

Comments
 (0)