Skip to content

Commit f2442e1

Browse files
authored
enable multi-file recipe edit in jumble (#1341)
1 parent fcc3ddf commit f2442e1

File tree

1 file changed

+123
-25
lines changed

1 file changed

+123
-25
lines changed

packages/jumble/src/views/CharmDetailView.tsx

Lines changed: 123 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import {
22
Charm,
33
charmId,
44
compileAndRunRecipe,
5+
compileRecipe,
56
extractVersionTag,
67
getIframeRecipe,
78
getRecipeIdFromCharm,
89
modifyCharm,
910
} from "@commontools/charm";
1011
import { useCharmReferences } from "@/hooks/use-charm-references.ts";
11-
import { isCell, isStream, Runtime } from "@commontools/runner";
12+
import { isCell, isStream, Runtime, RuntimeProgram } from "@commontools/runner";
1213
import { isObject } from "@commontools/utils/types";
1314
import {
1415
CheckboxToggle,
@@ -719,8 +720,20 @@ const CodeTab = () => {
719720
const [showFullCode, setShowFullCode] = useState(false);
720721
const { loading, setLoading } = useCharmOperationContext();
721722
const [regularRecipeSource, setRegularRecipeSource] = useState<string>("");
722-
const [workingRegularRecipeSource, setWorkingRegularRecipeSource] = useState<string>("");
723+
const [workingRegularRecipeSource, setWorkingRegularRecipeSource] = useState<
724+
string
725+
>("");
723726
const [isRegularRecipe, setIsRegularRecipe] = useState(false);
727+
const [isMultiFileRecipe, setIsMultiFileRecipe] = useState(false);
728+
const [recipeFiles, setRecipeFiles] = useState<
729+
Array<{ name: string; contents: string }>
730+
>([]);
731+
const [workingRecipeFiles, setWorkingRecipeFiles] = useState<
732+
Array<{ name: string; contents: string }>
733+
>([]);
734+
const [selectedFileIndex, setSelectedFileIndex] = useState(0);
735+
const [mainFile, setMainFile] = useState<string>("/main.tsx");
736+
const [mainExport, setMainExport] = useState<string | undefined>();
724737
const navigate = useNavigate();
725738
const { replicaName } = useParams<CharmRouteParams>();
726739

@@ -729,17 +742,29 @@ const CodeTab = () => {
729742
function loadRegularRecipeSource() {
730743
if (!charm || iframeRecipe) {
731744
setIsRegularRecipe(false);
745+
setIsMultiFileRecipe(false);
732746
return;
733747
}
734-
748+
735749
// This is a regular recipe
736750
setIsRegularRecipe(true);
737-
751+
738752
try {
739753
const recipeId = getRecipeIdFromCharm(charm);
740754
if (recipeId) {
741755
const recipeMeta = runtime.recipeManager.getRecipeMeta({ recipeId });
742-
if (recipeMeta?.src) {
756+
757+
// Check if it's a multi-file recipe
758+
if (recipeMeta?.program) {
759+
setIsMultiFileRecipe(true);
760+
setRecipeFiles(recipeMeta.program.files);
761+
setWorkingRecipeFiles([...recipeMeta.program.files]);
762+
setMainFile(recipeMeta.program.main);
763+
setMainExport(recipeMeta.program.mainExport);
764+
setSelectedFileIndex(0);
765+
} else if (recipeMeta?.src) {
766+
// Single file recipe
767+
setIsMultiFileRecipe(false);
743768
setRegularRecipeSource(recipeMeta.src);
744769
setWorkingRegularRecipeSource(recipeMeta.src);
745770
}
@@ -748,7 +773,7 @@ const CodeTab = () => {
748773
console.error("Error loading regular recipe source:", error);
749774
}
750775
}
751-
776+
752777
loadRegularRecipeSource();
753778
}, [charm, iframeRecipe, runtime]);
754779

@@ -823,14 +848,21 @@ const CodeTab = () => {
823848
return activeSchema === "argument" ? "argumentSchema" : "resultSchema";
824849
};
825850

826-
// For regular recipes, just show a full code editor
851+
// For regular recipes, show either single or multi-file editor
827852
if (isRegularRecipe) {
853+
// Check if changes have been made
854+
const hasChanges = isMultiFileRecipe
855+
? JSON.stringify(workingRecipeFiles) !== JSON.stringify(recipeFiles)
856+
: workingRegularRecipeSource !== regularRecipeSource;
857+
828858
return (
829859
<div className="h-full flex flex-col overflow-hidden">
830860
<div className="flex items-center justify-between p-4 pb-2">
831-
<div className="text-sm font-semibold">Recipe Code</div>
861+
<div className="text-sm font-semibold">
862+
{isMultiFileRecipe ? "Recipe Files" : "Recipe Code"}
863+
</div>
832864
{/* Save button for regular recipes */}
833-
{workingRegularRecipeSource !== regularRecipeSource && (
865+
{hasChanges && (
834866
<button
835867
type="button"
836868
onClick={async () => {
@@ -839,22 +871,52 @@ const CodeTab = () => {
839871
try {
840872
// Get the recipe spec and argument from the current charm
841873
const recipeId = getRecipeIdFromCharm(charm);
842-
const recipeMeta = runtime.recipeManager.getRecipeMeta({ recipeId });
874+
const recipeMeta = runtime.recipeManager.getRecipeMeta({
875+
recipeId,
876+
});
843877
const spec = recipeMeta?.spec || "";
844878
const argument = charmManager.getArgument(charm);
845-
846-
// Compile and run the updated recipe
847-
const newCharm = await compileAndRunRecipe(
848-
charmManager,
849-
workingRegularRecipeSource,
850-
spec,
851-
argument,
852-
recipeId ? [recipeId] : undefined,
853-
);
854-
879+
880+
let newCharm;
881+
if (isMultiFileRecipe) {
882+
// For multi-file recipes, we need to use compileRecipe directly
883+
const program: RuntimeProgram = {
884+
main: mainFile,
885+
files: workingRecipeFiles,
886+
...(mainExport && { mainExport }),
887+
};
888+
889+
// Use compileRecipe which accepts RuntimeProgram
890+
const recipe = await compileRecipe(
891+
program,
892+
spec,
893+
runtime,
894+
charmManager.getSpace(),
895+
recipeId ? [recipeId] : undefined,
896+
);
897+
898+
// Run the recipe
899+
newCharm = await charmManager.runPersistent(
900+
recipe,
901+
argument,
902+
);
903+
} else {
904+
// Single file recipe - use the existing compileAndRunRecipe
905+
newCharm = await compileAndRunRecipe(
906+
charmManager,
907+
workingRegularRecipeSource,
908+
spec,
909+
argument,
910+
recipeId ? [recipeId] : undefined,
911+
);
912+
}
913+
855914
if (newCharm && replicaName) {
856915
navigate(
857-
createPath("charmShow", { charmId: charmId(newCharm)!, replicaName }),
916+
createPath("charmShow", {
917+
charmId: charmId(newCharm)!,
918+
replicaName,
919+
}),
858920
);
859921
}
860922
} catch (error) {
@@ -886,15 +948,51 @@ const CodeTab = () => {
886948
</button>
887949
)}
888950
</div>
889-
951+
952+
{/* File tabs for multi-file recipes */}
953+
{isMultiFileRecipe && (
954+
<div className="px-4 pb-2">
955+
<div className="flex gap-2 overflow-x-auto">
956+
{workingRecipeFiles.map((file, index) => (
957+
<button
958+
key={file.name}
959+
type="button"
960+
onClick={() => setSelectedFileIndex(index)}
961+
className={`px-3 py-1 text-sm border ${
962+
selectedFileIndex === index
963+
? "bg-black text-white border-black"
964+
: "bg-white text-black border-gray-300 hover:border-black"
965+
}`}
966+
>
967+
{file.name.substring(1)} {/* Remove leading slash */}
968+
{file.name === mainFile && " (main)"}
969+
</button>
970+
))}
971+
</div>
972+
</div>
973+
)}
974+
890975
<div className="px-4 flex-grow flex flex-col overflow-hidden">
891976
<CharmCodeEditor
892977
docs={[
893978
{
894979
key: "code",
895-
label: "Recipe Code",
896-
value: workingRegularRecipeSource,
897-
onChange: setWorkingRegularRecipeSource,
980+
label: isMultiFileRecipe
981+
? workingRecipeFiles[selectedFileIndex]?.name.substring(1)
982+
: "Recipe Code",
983+
value: isMultiFileRecipe
984+
? workingRecipeFiles[selectedFileIndex]?.contents || ""
985+
: workingRegularRecipeSource,
986+
onChange: isMultiFileRecipe
987+
? (newContent: string) => {
988+
const updatedFiles = [...workingRecipeFiles];
989+
updatedFiles[selectedFileIndex] = {
990+
...updatedFiles[selectedFileIndex],
991+
contents: newContent,
992+
};
993+
setWorkingRecipeFiles(updatedFiles);
994+
}
995+
: setWorkingRegularRecipeSource,
898996
language: "javascript" as const,
899997
readOnly: false,
900998
},

0 commit comments

Comments
 (0)