Skip to content

Commit

Permalink
Add Block Editing Functionality (#25)
Browse files Browse the repository at this point in the history
feat: Implement Block Editing System

- Add BlockEditor, FunctionEditor, IfEditor, and WhileEditor components
- Create BlockEditorContext for managing editing state
- Implement FunctionCombobox for function selection
- Add BlockAdder for nested block creation
- Update CodeGeneratorContext with codebaseInfo
- Integrate Shadcn UI components (Popover, Command, Separator, Label)
- Improve block manipulation (condition changes, nested function creation)
- Enhance UI with clear selection button and improved layouts
- Update dependencies and refactor imports
  • Loading branch information
ozhanefemeral authored Aug 4, 2024
1 parent 16dd4b4 commit 098c781
Show file tree
Hide file tree
Showing 21 changed files with 932 additions and 87 deletions.
24 changes: 21 additions & 3 deletions apps/next/app/features/code-generator/CodeGenerator.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
"use client";
import React from "react";
import React, { useEffect } from "react";
import { Button } from "@ui/button";
import { useCodeGenerator } from "@/contexts/CodeGeneratorContext";
import { useSavedFunctions } from "@/contexts/SavedFunctionsContext";
import { SortableBlockList } from "./SortableBlockList";
import { SaveDialog } from "./Dialogs";
import { CodeViewer } from "@/components/CodeViewer";
import { BlockEditor } from "@/components/editor";
import { useBlockEditor } from "@/contexts/BlockEditorContext";
import { CodebaseInfo } from "@ozhanefe/ts-codegenerator";

export const CodeGenerator: React.FC = () => {
const { state, setState, code } = useCodeGenerator();
interface CodeGeneratorProps {
codebaseInfo: CodebaseInfo;
}

export const CodeGenerator: React.FC<CodeGeneratorProps> = ({
codebaseInfo,
}) => {
const { state, setState, code, setCodebaseInfo } = useCodeGenerator();
const { setCurrentBlock } = useBlockEditor();
const { saveCurrentState } = useSavedFunctions();

const isEmpty = state.blocks.length === 0;
Expand All @@ -24,10 +34,18 @@ export const CodeGenerator: React.FC = () => {
variables: [],
isAsync: false,
});
setCurrentBlock(null);
};

useEffect(() => {
setCodebaseInfo(codebaseInfo);
}, [codebaseInfo]);

return (
<section className="h-fit p-4 border border-gray-200 border-dashed rounded-md">
<div>
<BlockEditor />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="col-span-1">
<SortableBlockList />
Expand Down
6 changes: 6 additions & 0 deletions apps/next/app/features/code-generator/SortableBlockList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import { CSS } from "@dnd-kit/utilities";
import { BlockViewRenderer } from "@/components/blocks";
import { Cross1Icon } from "@radix-ui/react-icons";
import { useBlockEditor } from "@/contexts/BlockEditorContext";

type SortableItemProps = {
block: CodeBlock;
Expand All @@ -36,6 +37,7 @@ const SortableItem: React.FC<SortableItemProps> = ({
}) => {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: index, animateLayoutChanges: () => false });
const { setCurrentBlock, currentBlock } = useBlockEditor();

const style = {
transform: CSS.Transform.toString(transform),
Expand All @@ -48,6 +50,10 @@ const SortableItem: React.FC<SortableItemProps> = ({
) => {
event.preventDefault();
onRemove(index);

if (currentBlock?.index === block.index) {
setCurrentBlock(null);
}
};

return (
Expand Down
4 changes: 2 additions & 2 deletions apps/next/app/features/code-generator/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ export default function CodeGeneratorPage() {
<main className="p-8 w-full">
<h1 className="text-3xl font-bold mb-6">Code Generator</h1>
<div className="flex gap-4 mb-4">
<SearchDialog codebaseInfo={codebaseInfo} />
<SearchDialog />
<LoadDialog />
<HelpDialog />
</div>
<CodeGenerator />
<CodeGenerator codebaseInfo={codebaseInfo} />
</main>
);
}
5 changes: 4 additions & 1 deletion apps/next/components/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@
import { DndContext } from "@dnd-kit/core";
import { CodeGeneratorProvider } from "@/contexts/CodeGeneratorContext";
import { SavedFunctionsProvider } from "@/contexts/SavedFunctionsContext";
import { BlockEditorProvider } from "@/contexts/BlockEditorContext";

export const Providers: React.FC<React.PropsWithChildren<{}>> = ({
children,
}) => {
return (
<DndContext>
<CodeGeneratorProvider>
<SavedFunctionsProvider>{children}</SavedFunctionsProvider>
<SavedFunctionsProvider>
<BlockEditorProvider>{children}</BlockEditorProvider>
</SavedFunctionsProvider>
</CodeGeneratorProvider>
</DndContext>
);
Expand Down
10 changes: 4 additions & 6 deletions apps/next/components/SearchDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,11 @@ import {
import { useCodeGenerator } from "@/contexts/CodeGeneratorContext";
import { KeyCombinationLabel } from "@ui/key-combination-label";

interface SearchDialogProps {
codebaseInfo: CodebaseInfo;
}

export const SearchDialog: React.FC<SearchDialogProps> = ({ codebaseInfo }) => {
export const SearchDialog: React.FC = () => {
const [open, setOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [searchResults, setSearchResults] = useState<FunctionInfo[]>([]);
const { state, setState } = useCodeGenerator();
const { state, setState, codebaseInfo } = useCodeGenerator();

const addFunction = useCallback(
(func: FunctionInfo) => {
Expand Down Expand Up @@ -62,6 +58,8 @@ export const SearchDialog: React.FC<SearchDialogProps> = ({ codebaseInfo }) => {
}, [open, searchResults, state]);

useEffect(() => {
if (!codebaseInfo) return;

if (searchQuery === "") {
setSearchResults([]);
} else {
Expand Down
129 changes: 80 additions & 49 deletions apps/next/components/blocks/BlockContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"use client";
import { CodeBlock } from '@ozhanefe/ts-codegenerator';
import { ChevronDownIcon, ChevronRightIcon } from '@radix-ui/react-icons';
import React from 'react';
import { IfBlockView } from './IfBlockView';
import { WhileBlockView } from './WhileBlockView';
import { FunctionCallBlockView } from './FunctionCallBlockView';
import { CodeBlock } from "@ozhanefe/ts-codegenerator";
import { ChevronDownIcon, ChevronRightIcon } from "@radix-ui/react-icons";
import React, { useCallback } from "react";
import { IfBlockView } from "./IfBlockView";
import { WhileBlockView } from "./WhileBlockView";
import { FunctionCallBlockView } from "./FunctionCallBlockView";
import { useBlockEditor } from "@/contexts/BlockEditorContext";

interface BlockContainerProps {
title: string;
Expand All @@ -17,65 +18,95 @@ interface BlockViewRendererProps {
block: CodeBlock;
}

export const BlockViewRenderer: React.FC<BlockViewRendererProps> = React.memo(({ block }) => {
switch (block.blockType) {
case 'if':
return (
<IfBlockView
condition={block.condition}
thenBlocks={block.thenBlocks.map((b, i) => <BlockViewRenderer key={i} block={b} />)}
elseIfBlocks={block.elseIfBlocks?.map((elseIf) => ({
condition: elseIf.condition,
blocks: elseIf.blocks.map((b, j) => <BlockViewRenderer key={j} block={b} />)
}))}
elseBlock={block.elseBlock && {
blocks: block.elseBlock.blocks.map((b, i) => <BlockViewRenderer key={i} block={b} />)
}}
/>
);
case 'while':
return (
<WhileBlockView condition={block.condition}>
{block.loopBlocks.map((b, i) => <BlockViewRenderer key={i} block={b} />)}
</WhileBlockView>
);
case 'functionCall':
return <FunctionCallBlockView block={block} />;
default:
return null;
}
});
export const BlockViewRenderer: React.FC<BlockViewRendererProps> = React.memo(
({ block }) => {
const { setCurrentBlock } = useBlockEditor();

const handleClick = useCallback(
(event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
setCurrentBlock(block);
},
[block, setCurrentBlock]
);

export const BlockContainer: React.FC<BlockContainerProps> = ({ title, type, children, isCollapsible = false }) => {
return (
<div className="cursor-pointer" onClick={handleClick}>
{block.blockType === "if" && (
<IfBlockView
condition={block.condition}
thenBlocks={block.thenBlocks.map((b, i) => (
<BlockViewRenderer key={i} block={b} />
))}
elseIfBlocks={block.elseIfBlocks?.map((elseIf) => ({
condition: elseIf.condition,
blocks: elseIf.blocks.map((b, j) => (
<BlockViewRenderer key={j} block={b} />
)),
}))}
elseBlock={
block.elseBlock && {
blocks: block.elseBlock.blocks.map((b, i) => (
<BlockViewRenderer key={i} block={b} />
)),
}
}
/>
)}
{block.blockType === "while" && (
<WhileBlockView condition={block.condition}>
{block.loopBlocks.map((b, i) => (
<BlockViewRenderer key={i} block={b} />
))}
</WhileBlockView>
)}
{block.blockType === "functionCall" && (
<FunctionCallBlockView block={block} />
)}
</div>
);
}
);

export const BlockContainer: React.FC<BlockContainerProps> = ({
title,
type,
children,
isCollapsible = false,
}) => {
const [isOpen, setIsOpen] = React.useState(true);

const blockColors = {
functionCall: 'bg-blue-100 border-blue-200',
if: 'bg-green-100 border-green-200',
elseif: 'bg-yellow-100 border-yellow-200',
else: 'bg-orange-100 border-orange-200',
while: 'bg-purple-100 border-purple-200',
default: 'bg-gray-100 border-gray-200'
functionCall: "bg-blue-100 border-blue-200",
if: "bg-green-100 border-green-200",
elseif: "bg-yellow-100 border-yellow-200",
else: "bg-orange-100 border-orange-200",
while: "bg-purple-100 border-purple-200",
default: "bg-gray-100 border-gray-200",
};

const color = blockColors[type] || blockColors.default;

const handleCollapse = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
setIsOpen(!isOpen);
};

return (
<div className={`${color} border-2 rounded-lg p-2 my-1`}>
<div className="flex items-center justify-between">
<span className="font-semibold">{title}</span>
{isCollapsible && (
<button onClick={() => setIsOpen(!isOpen)} className="focus:outline-none">
{isOpen ? <ChevronDownIcon width={16} /> : <ChevronRightIcon width={16} />}
<button onClick={handleCollapse} className="focus:outline-none">
{isOpen ? (
<ChevronDownIcon width={16} />
) : (
<ChevronRightIcon width={16} />
)}
</button>
)}
</div>
{(!isCollapsible || isOpen) && (
<div className="mt-1">
{children}
</div>
)}
{(!isCollapsible || isOpen) && <div className="mt-1">{children}</div>}
</div>
);
};
};
43 changes: 43 additions & 0 deletions apps/next/components/editor/BlockEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"use client";

import { useBlockEditor } from "@/contexts/BlockEditorContext";
import { FunctionEditor } from "./FunctionEditor";
import { IfEditor } from "./IfEditor";
import { WhileEditor } from "./WhileEditor";
import { BlockAdder } from "./components/BlockAdder";
import { Button } from "@ui/button";
import { XCircleIcon } from "lucide-react";

export const BlockEditor = () => {
const { currentBlock, setCurrentBlock } = useBlockEditor();

if (!currentBlock) return <EmptyBlockEditor />;

return (
<div className="flex justify-start w-full pb-4 gap-x-4 relative">
{currentBlock.blockType === "functionCall" && (
<FunctionEditor block={currentBlock} />
)}
{currentBlock.blockType === "if" && <IfEditor block={currentBlock} />}
{currentBlock.blockType === "while" && (
<WhileEditor block={currentBlock} />
)}
<Button
variant="secondary"
onClick={() => setCurrentBlock(null)}
className="absolute right-0 top-0"
>
<XCircleIcon className="h-6 w-6 mr-2" />
Clear Selection
</Button>
</div>
);
};

const EmptyBlockEditor = () => {
return (
<div className="pb-4">
<BlockAdder />
</div>
);
};
Loading

0 comments on commit 098c781

Please sign in to comment.