Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion example-app/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,10 @@ function App() {
{/*Form Component*/}
{/* See above how to adjust styles */}
<CreateCollectionForm
hideSidebar={true}
onPreviewFormOutput={(data) => {
console.log(data);
return <div>Preview</div>;
}}
onFinish={(data) => {
alert(JSON.stringify(data, null, 2));
// This is just an example of how to handle the form finish,
Expand Down
140 changes: 30 additions & 110 deletions package-lock.json

Large diffs are not rendered by default.

81 changes: 45 additions & 36 deletions src/CreateCollectionForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,28 @@ import TenantFieldSelectionStep from "./steps/TenantFieldSelectionStep.jsx";
import SimpleDenseEmbeddingStep from "./steps/SimpleDenseEmbeddingStep.jsx";
import SimpleHybridEmbeddingStep from "./steps/SimpleHybridEmbeddingStep.jsx";
import IndexFieldSelectionStep from "./steps/IndexFieldSelectionStep.jsx";
import { Box, Container, Grid, Typography } from "@mui/material";
import { CCFormButton, CCFormRoot, CCFormSidebar } from "./ThemedComponents";
import { Box, Grid } from "@mui/material";
import { CCFormButton, CCFormRoot } from "./ThemedComponents";
import GenericElementsStep from "./steps/GenericElementsStep.jsx";
import { prepareOutput } from "./prepareOutput.js";
import { ScrollableParentContext } from "./context/scrollable-parent-context.jsx";
import Sidebar from "./Sidebar.jsx";

/**
* CreateCollectionForm component
*
* @param {Object} props - Component props
* @param {() => Promise<any>} props.onFinish - Async function called on form finish. Must return a resolved value (not undefined), otherwise the form will not be cleared.
* @param {boolean} [props.hideSidebar=false] - Whether to hide the sidebar
* @param {() => Object} [props.scrollableParent] - Function that returns the parent element
* @param {Object} [props.sx] - Styles to be applied to the form
* @param {function} [props.onPreviewFormOutput] - Function to process the preview output data. Sidebar is shown only when this prop is provided and is a function.
* @returns {JSX.Element}
*/
export const CreateCollectionForm = function CreateCollectionForm({
onFinish,
hideSidebar = false,
scrollableParent,
sx,
onPreviewFormOutput,
...props
}) {
const resolvedScrollableParent = scrollableParent
Expand Down Expand Up @@ -153,39 +154,47 @@ export const CreateCollectionForm = function CreateCollectionForm({
value={{ scrollableParent: resolvedScrollableParent }}
>
<CCFormRoot>
<Container maxWidth="md">
{renderedSteps}

{isFinished &&
Object.values(formData).some(
(data) => typeof data === "object" && data?.completed,
) ? (
<Grid size={12} display="flex" justifyContent="flex-end">
<CCFormButton variant="text" onClick={handleClear}>
Clear
</CCFormButton>
<CCFormButton
disabled={!isAllCompleted}
variant="contained"
onClick={handleFinish}
sx={{ ml: 4 }}
>
Finish
</CCFormButton>
<Grid container spacing={4}>
<Grid
size={
onPreviewFormOutput && typeof onPreviewFormOutput === "function"
? 8
: 12
}
>
{renderedSteps}

{isFinished &&
Object.values(formData).some(
(data) => typeof data === "object" && data?.completed,
) ? (
<Grid size={12} display="flex" justifyContent="flex-end">
<CCFormButton variant="text" onClick={handleClear}>
Clear
</CCFormButton>
<CCFormButton
disabled={!isAllCompleted}
variant="contained"
onClick={handleFinish}
sx={{ ml: 4 }}
>
Finish
</CCFormButton>
</Grid>
) : (
<></>
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The empty fragment <></> is unnecessary. Consider using null instead for cleaner code when nothing needs to be rendered.

Suggested change
<></>
null

Copilot uses AI. Check for mistakes.
)}
</Grid>
{onPreviewFormOutput && typeof onPreviewFormOutput === "function" && (
<Grid size={4}>
<Sidebar
formData={formData}
path={path}
handleOutput={onPreviewFormOutput}
/>
</Grid>
) : (
<></>
)}
</Container>
{!hideSidebar && (
<CCFormSidebar>
<Typography variant="h6" sx={{ mb: 2 }}>
Estimated Price:
</Typography>
{/*todo: Price?*/}
<Typography variant="h4">200$</Typography>
</CCFormSidebar>
)}
</Grid>
</CCFormRoot>
</ScrollableParentContext.Provider>
);
Expand All @@ -195,9 +204,9 @@ export const CreateCollectionForm = function CreateCollectionForm({
CreateCollectionForm.propTypes = {
ref: PropTypes.object,
onFinish: PropTypes.func.isRequired,
hideSidebar: PropTypes.bool,
scrollableParent: PropTypes.func,
sx: PropTypes.object,
onPreviewFormOutput: PropTypes.func,
};

export default CreateCollectionForm;
44 changes: 44 additions & 0 deletions src/Sidebar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useEffect, useState } from "react";
import PropTypes from "prop-types";
import { previewOutput } from "./prepareOutput.js";
import { CCFormCard, CCFormSidebar } from "./ThemedComponents.jsx";
import { Typography } from "@mui/material";

const Sidebar = ({ formData, path, handleOutput }) => {
const [outputData, setOutputData] = useState(null);

useEffect(() => {
if (formData) {
// todo: should we show output if there is no function to process it?
if (handleOutput && typeof handleOutput === "function") {
setOutputData(handleOutput(previewOutput(formData, path)));
}
Comment on lines +14 to +15
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The useEffect hook is missing handleOutput cleanup logic. If handleOutput becomes null or undefined after initially being set, the outputData will retain its previous value instead of being cleared. Consider adding an else clause to set outputData to null when handleOutput is not a function.

Suggested change
setOutputData(handleOutput(previewOutput(formData, path)));
}
setOutputData(handleOutput(previewOutput(formData, path)));
} else {
setOutputData(null);
}
} else {
setOutputData(null);

Copilot uses AI. Check for mistakes.
}
}, [path, formData, handleOutput]);

return (
<CCFormSidebar>
<CCFormCard>
{outputData ? (
outputData
) : (
<Typography
variant="body2"
sx={{ marginTop: "8px", fontStyle: "italic" }}
>
No data to preview
</Typography>
)}
</CCFormCard>
</CCFormSidebar>
);
};

// prop types
Sidebar.propTypes = {
formData: PropTypes.object.isRequired,
path: PropTypes.array.isRequired,
handleOutput: PropTypes.func,
};

export default Sidebar;
22 changes: 8 additions & 14 deletions src/ThemedComponents.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -349,13 +349,7 @@ export const CCFormSlider = styled(Slider, {
export const CCFormSidebarInner = styled(Box, {
name: "MuiCreateCollectionForm",
slot: "sidebarStickyInner",
})(({ theme }) => ({
position: "sticky",
top: "2rem",
[theme.breakpoints.down("md")]: {
position: "static",
},
}));
})();
Comment on lines 349 to +352
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CCFormSidebarInner styled component now has an empty implementation () with no styles. However, it's still being used in the CCFormSidebar component (line 357) as a wrapper. Consider removing this component entirely if it serves no styling purpose, or document why it's kept as a structural component.

Copilot uses AI. Check for mistakes.

export const CCFormSidebar = styled(
(props) => (
Expand All @@ -368,23 +362,23 @@ export const CCFormSidebar = styled(
slot: "sidebar",
},
)(({ theme }) => ({
position: "sticky",
top: "2rem",
alignSelf: "flex-start",
borderRadius: 0,
position: "absolute",
bottom: 0,
right: 0,
top: 0,
width: "clamp(12.5rem, 25vw, 18.75rem)",
minWidth: "12.5rem",
padding: "2rem 1.5rem",
padding: "3rem 0 2rem",
flexShrink: 0,
background: theme.palette.background.default,
color: theme.palette.text.primary,
zIndex: 2,
maxHeight: "calc(100vh - 4rem)",
overflowY: "auto",
[theme.breakpoints.down("md")]: {
position: "fixed",
width: "100vw",
top: "auto",
padding: "1.5rem 1.5rem 2rem",
boxShadow: "0 0 10px 0 rgba(0,0,0,0.2)",
maxHeight: "none",
},
}));
6 changes: 3 additions & 3 deletions src/collection-schema.jtd.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,16 @@
"tokenizer": {
"enum": ["prefix", "whitespace", "word", "multilingual"]
},
"min_token_length": { "type": "uint32" },
"max_token_length": { "type": "uint32" },
"min_token_len": { "type": "uint32" },
"max_token_len": { "type": "uint32" },
"range": { "type": "boolean" },
"lookup": { "type": "boolean" },
"phrase_matching": { "type": "boolean" }
}
}
}
}
},
},
"tenant_field": {
"properties": {
"name": { "type": "string" },
Expand Down
7 changes: 4 additions & 3 deletions src/flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export const steps = {
size: 6,
"short-description": "Search across the whole collection",
description:
"Search across whole collection of data with optional filters. For example: <ul><li>e-commerce search,</li><li>website search,</li></ul>",
"Search across whole collection of data with optional filters. For example: <ul><li>e-commerce search</li><li>website search</li><li>documents search</li></ul>",
name: "global-search",
"on-select": {
"continue-step": "templates-selection-step",
Expand All @@ -106,7 +106,7 @@ export const steps = {
size: 6,
"short-description": "Many tenants, isolated data",
description:
"Search across multiple isolated tenants. For example: <ul><li>per-user documents,</li><li>chat history search,</li><li>organization-based isolation</li></ul>",
"Search across multiple isolated tenants. For example: <ul><li>per-user documents</li><li>chat history search</li><li>organization-based isolation</li></ul>",
name: "multitenancy",
"on-select": {
"continue-step": "tenant-field-selection-step",
Expand Down Expand Up @@ -578,7 +578,8 @@ export const steps = {
name: "phrase_matching",
type: "checkbox",
default: true,
description: "Allows phrase matching at the cost of extra index structure",
description:
"Allows phrase matching at the cost of extra index structure",
link: "https://qdrant.tech/documentation/concepts/filtering/#phrase-matching",
linkText: "Learn more",
size: 6,
Expand Down
4 changes: 2 additions & 2 deletions src/inputs/ButtonGroupWithInputs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,12 @@ Component capable of rendering the following configuration:
default: true,
},
{
name: "min_token_length",
name: "min_token_len",
type: "number",
default: null,
},
{
name: "max_token_length",
name: "max_token_len",
type: "number",
default: null,
},
Expand Down
4 changes: 2 additions & 2 deletions src/inputs/Inputs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,15 +148,15 @@ Checkbox.propTypes = {
};

export const NumberInput = ({ config, stepData, onChange }) => {
const value = stepData || 0;
const value = stepData || null;

const maxValue = config?.max;
const minValue = config?.min;

const handleChange = (e) => {
let valueNumber = parseInt(e.target.value);
if (isNaN(valueNumber)) {
valueNumber = 0;
valueNumber = null;
} else {
if (maxValue && valueNumber > maxValue) {
valueNumber = maxValue;
Expand Down
Loading