Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ce81a43
refactor: remove custom drag preview from outline tree and simplify w…
Feb 22, 2026
7b56f2e
refactor: replace SquareIcon with BoxIcon in outline block type icon
Feb 23, 2026
d51684f
refactor: increase outline tree indent from 10 to 13 pixels
Feb 23, 2026
ce98860
refactor: add eslint-disable comment for exhaustive-deps in outline n…
Feb 23, 2026
d6472fd
refactor: increase outline tree indent from 13 to 14 pixels and add v…
Feb 23, 2026
88bfcda
refactor: add parent group hover state to outline tree hierarchy lines
Feb 23, 2026
88ae3a1
refactor: update UI libraries fetching to use `useWebsiteData` and ad…
amitdhiman5086 Feb 23, 2026
16a65df
Merge pull request #780 from chaibuilder/library-issue
surajair Feb 23, 2026
54109ea
refactor: improve outline tree visual hierarchy with better spacing
Feb 23, 2026
e89c12c
refactor: improve outline tree cursor positioning and add visual indi…
Feb 23, 2026
3b14500
Merge branch 'dev' of github.com-chaibuilder:chaibuilder/sdk into out…
Feb 23, 2026
62d472d
refactor: simplify outline tree hierarchy lines and adjust horizontal…
Feb 23, 2026
9cf294e
refactor: remove horizontal connector line from outline tree node hie…
Feb 23, 2026
f12f6ca
refactor: add isShown to outline node useEffect dependencies and remo…
Feb 23, 2026
3b2851e
Merge pull request #782 from chaibuilder/outline
surajair Feb 23, 2026
d00b13d
feat: page type search by removing the page type dropdown and impleme…
amitdhiman5086 Feb 24, 2026
3300e95
Merge pull request #784 from chaibuilder/search-form-pages
surajair Feb 24, 2026
46720e2
Merge branch 'main' into dev
surajair Feb 24, 2026
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useRegisteredChaiBlocks } from "@/runtime";
import { SquareIcon } from "@radix-ui/react-icons";
import { BoxIcon } from "@radix-ui/react-icons";
import { get } from "lodash-es";
import React from "react";

Expand All @@ -18,5 +18,5 @@ export const TypeIcon: React.FC<Props> = (props) => {
}

// * Fallback Icon
return <SquareIcon className={ICON_CLASS} />;
return <BoxIcon className={ICON_CLASS} />;
};
11 changes: 7 additions & 4 deletions src/core/components/sidepanels/panels/outline/default-cursor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@ const placeholderStyle = {
zIndex: 1,
};

export const DefaultCursor = React.memo(function DefaultCursor({ top, left }: CursorProps) {
export const DefaultCursor = React.memo(function DefaultCursor({ top, left, indent }: CursorProps) {
const style: CSSProperties = {
position: "absolute",
pointerEvents: "none",
top: top + "px",
left: left + "px",
top: top - 1 + "px",
left: "0px",
right: 0,
paddingLeft: left + indent + "px",
};

return (
<div style={{ ...placeholderStyle, ...style }}>
<div className="h-0.5 flex-1 rounded-[1px] bg-green-500"></div>
<div className="h-1 w-1 rounded-full bg-green-500"></div>
<div className="h-[1px] flex-1 rounded-[1px] border-t border-green-500"></div>
</div>
);
});

This file was deleted.

17 changes: 9 additions & 8 deletions src/core/components/sidepanels/panels/outline/list-tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { treeRefAtom } from "@/atoms/ui";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { DefaultCursor } from "@/core/components/sidepanels/panels/outline/default-cursor";
import { DefaultDragPreview } from "@/core/components/sidepanels/panels/outline/default-drag-preview";
import {
close,
defaultShortcuts,
Expand Down Expand Up @@ -259,7 +258,9 @@ const ListTree = () => {

return (
<>
<div className={cn("flex h-full select-none flex-col space-y-1")} onClick={() => clearSelection()}>
<div
className={cn("chai-outline group/parent parent-group flex h-full select-none flex-col space-y-1")}
onClick={() => clearSelection()}>
<div
id="outline-view "
className="no-scrollbar h-full overflow-y-auto text-sm"
Expand Down Expand Up @@ -302,10 +303,10 @@ const ListTree = () => {
aria-selected={true}
aria-expanded={false}
onClick={() => setIds([])}
className="flex h-full items-center border-b border-transparent">
className="flex min-h-[25px] items-center">
<div
className={cn(
"group flex w-full cursor-pointer items-center justify-between space-x-px !rounded p-1 py-0 outline-none",
"group flex h-[25px] w-full cursor-pointer items-center justify-between space-x-px px-2 py-0 outline-none",
ids.length === 0 ? "bg-primary/20" : "",
)}>
<div className="leading-1 flex items-center">
Expand All @@ -324,8 +325,8 @@ const ListTree = () => {
<Tree
ref={treeRef}
height={window.innerHeight - 160}
className="no-scrollbar !h-full max-w-full space-y-1 !overflow-y-auto !overflow-x-hidden"
rowClassName="flex items-center h-full border-b border-transparent"
className="no-scrollbar !h-full max-w-full !overflow-y-auto !overflow-x-hidden"
rowClassName="flex items-center h-full"
selection={ids[0] || ""}
onRename={onRename}
openByDefault={false}
Expand All @@ -336,8 +337,8 @@ const ListTree = () => {
childrenAccessor={(d: any) => d.children}
width={"100%"}
rowHeight={25}
renderDragPreview={DefaultDragPreview}
indent={10}
renderDragPreview={() => null}
indent={14}
onContextMenu={onContextMenu}
disableDrop={debouncedDisableDrop as any}
idAccessor={"_id"}>
Expand Down
67 changes: 46 additions & 21 deletions src/core/components/sidepanels/panels/outline/node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export const Node = memo(({ node, style, dragHandle }: NodeRendererProps<any>) =
}, 500);

return () => clearTimeout(timedToggle);
}, [willReceiveDrop, node, isDragging]);
}, [willReceiveDrop, node, isDragging, isShown]);

const setDropAttribute = (id: string, value: string) => {
const innerDoc = iframe.contentDocument || iframe.contentWindow?.document;
Expand Down Expand Up @@ -176,6 +176,20 @@ export const Node = memo(({ node, style, dragHandle }: NodeRendererProps<any>) =
}
};

const { librarySite } = useBuilderProp("flags", { librarySite: false });
const isLibBlock = useMemo(() => {
return (
librarySite &&
has(data, "_libBlockId") &&
!isEmpty(data._libBlockId) &&
(hasPermission(PERMISSIONS.CREATE_LIBRARY_BLOCK) || hasPermission(PERMISSIONS.EDIT_LIBRARY_BLOCK))
);
}, [data, hasPermission, librarySite]);

const isPartialBlock = useMemo(() => {
return data?._type === "PartialBlock" || data?._type === "GlobalBlock";
}, [data]);

if (id === ROOT_TEMP_KEY) {
return (
<div className="group relative mt-2 w-full cursor-pointer">
Expand All @@ -194,23 +208,11 @@ export const Node = memo(({ node, style, dragHandle }: NodeRendererProps<any>) =
</div>
);
}
const { librarySite } = useBuilderProp("flags", { librarySite: false });
const isLibBlock = useMemo(() => {
return (
librarySite &&
has(data, "_libBlockId") &&
!isEmpty(data._libBlockId) &&
(hasPermission(PERMISSIONS.CREATE_LIBRARY_BLOCK) || hasPermission(PERMISSIONS.EDIT_LIBRARY_BLOCK))
);
}, [data, hasPermission, librarySite]);

const isPartialBlock = useMemo(() => {
return data?._type === "PartialBlock" || data?._type === "GlobalBlock";
}, [data]);

return (
<div className={"w-full"}>
<div className={cn("relative flex h-full w-full items-center", isSelected ? "bg-primary/20" : "hover:bg-gray-100")}>
<div
className="w-full"
onMouseEnter={() => highlightBlock(id)}
onMouseLeave={() => clearHighlight()}
onClick={handleNodeClickWithoutPropagating}
Expand All @@ -231,6 +233,23 @@ export const Node = memo(({ node, style, dragHandle }: NodeRendererProps<any>) =
e.preventDefault();
setDropAttribute(id, "no");
}}>
{node.level > 0 && (
<div className="pointer-events-none absolute left-0 top-0 h-full">
{Array.from({ length: node.level }).map((_, index) => {
return (
<div
key={index}
className={
"absolute top-0 h-full border-l border-black/5 transition-colors group-hover/parent:border-black/30"
}
style={{
left: `${index * 14 + 10}px`,
}}
/>
);
})}
</div>
)}
{hasPermission(PERMISSIONS.ADD_BLOCK) &&
!isDragAndDropEnabled &&
node?.rowIndex !== null &&
Expand All @@ -253,15 +272,21 @@ export const Node = memo(({ node, style, dragHandle }: NodeRendererProps<any>) =
</div>
</div>
)}
<div className="absolute left-0 right-0 top-0 -z-10 h-full">
<div
className={cn(
"h-full transition-colors",
willReceiveDrop && canAcceptChildBlock(data._type, "Icon") ? "bg-green-200" : "",
node?.id === addSelectParentHighlight ? "bg-gray-100 dark:bg-gray-900" : "",
)}
/>
</div>
<div
className={cn(
"group flex w-full cursor-pointer items-center justify-between space-x-px !rounded p-1 py-0 outline-none",
isSelected ? "bg-primary/20" : "hover:bg-primary/10",
willReceiveDrop && canAcceptChildBlock(data._type, "Icon") ? "bg-green-200" : "",
node?.id === addSelectParentHighlight ? "bg-primary/10" : "",
"group relative flex w-full cursor-pointer items-center justify-between space-x-px p-1 py-0 outline-none",
isDragging && "opacity-20",
!isShown ? "line-through opacity-50" : "",
isLibBlock && isSelected && "bg-primary/20 text-primary",
isLibBlock && isSelected && "text-primary",
)}>
<div className="flex items-center">
<div
Expand All @@ -276,7 +301,7 @@ export const Node = memo(({ node, style, dragHandle }: NodeRendererProps<any>) =
</div>
<div
className={cn(
"leading-1 flex items-center",
"leading-1 flex w-full items-center",
isLibBlock && "text-orange-600/90",
isLibBlock && isSelected && "text-orange-800",
isPartialBlock && "text-purple-600/90",
Expand Down
70 changes: 22 additions & 48 deletions src/core/rjsf-widgets/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,16 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { DataBindingSelector } from "./data-binding-selector";

const PageTypeField = ({
href,
pageTypes,
onChange,
}: {
href: string;
pageTypes: any[];
onChange: (href: string) => void;
}) => {
const PageTypeField = ({ href, onChange }: { href: string; onChange: (href: string) => void }) => {
const { t } = useTranslation();
const searchPageTypeItems = useBuilderProp("searchPageTypeItems", (_: string, __: any) => []);
const [loading, setLoading] = useState(false);
const [isSearching, setIsSearching] = useState(false);
const [pageType, setPageType] = useState("page");
const [searchQuery, setSearchQuery] = useState("");
const [pageTypeItems, setPageTypeItems] = useState<any[]>([]);
const [selectedIndex, setSelectedIndex] = useState(-1);
const listRef = useRef<HTMLUListElement>(null);

const currentPageTypeName = pageTypes?.find((_pageType) => _pageType.key === pageType)?.name;

useEffect(() => {
setSearchQuery("");
setPageTypeItems([]);
Expand All @@ -38,11 +27,9 @@ const PageTypeField = ({

if (!href || loading || !startsWith(href, "pageType:")) return;
const initHref = split(href, ":");
const _pageType = get(initHref, 1, "page") || "page";
setPageType(_pageType);

(async () => {
const initalValue = await searchPageTypeItems(_pageType, [get(initHref, 2, "page")]);
const initalValue = await searchPageTypeItems("", [get(initHref, 2, "page")]);
if (initalValue && Array.isArray(initalValue)) {
setSearchQuery(get(initalValue, [0, "name"], ""));
}
Expand All @@ -55,18 +42,18 @@ const PageTypeField = ({
if (isEmpty(query)) {
setPageTypeItems([]);
} else {
const pageTypeItemResponse = await searchPageTypeItems(pageType, query);
const pageTypeItemResponse = await searchPageTypeItems("", query);
setPageTypeItems(pageTypeItemResponse);
}
setLoading(false);
setSelectedIndex(-1);
},
[pageType],
[],
300,
);

const handleSelect = (pageTypeItem: any) => {
const href = ["pageType", pageType, pageTypeItem.primaryPage ?? pageTypeItem.id];
const href = ["pageType", pageTypeItem.pageType || "page", pageTypeItem.primaryPage ?? pageTypeItem.id];
if (!href[1]) return;
onChange(href.join(":"));
setSearchQuery(pageTypeItem.name);
Expand Down Expand Up @@ -124,32 +111,23 @@ const PageTypeField = ({

return (
<div>
<select name="pageType" value={pageType} onChange={(e) => setPageType(e.target.value)}>
{map(pageTypes, (col) => (
<option key={col.key} value={col.key}>
{col.name}
</option>
))}
</select>
{pageType && (
<div className="group relative mt-2 flex items-center">
<input
type="text"
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={t(`Search ${currentPageTypeName ?? ""}`)}
className="w-full rounded-md border border-gray-300 p-2 pr-16"
/>
<div className="absolute bottom-2 right-2 top-3 flex items-center gap-1.5">
{searchQuery && (
<button onClick={clearSearch} className="text-gray-400 hover:text-gray-600" title={t("Clear search")}>
<Cross1Icon className="h-4 w-4" />
</button>
)}
</div>
<div className="group relative flex items-center">
<input
type="text"
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={t("Search pages")}
className="w-full rounded-md border border-gray-300 p-2 pr-16"
/>
<div className="absolute bottom-2 right-2 top-3 flex items-center gap-1.5">
{searchQuery && (
<button onClick={clearSearch} className="text-gray-400 hover:text-gray-600" title={t("Clear search")}>
<Cross1Icon className="h-4 w-4" />
</button>
)}
</div>
)}
</div>

{(loading || !isEmpty(pageTypeItems) || (isSearching && isEmpty(pageTypeItems))) && (
<div className="absolute z-40 mt-2 max-h-40 w-full max-w-[250px] overflow-y-auto rounded-md border border-border bg-background shadow-lg">
Expand Down Expand Up @@ -237,11 +215,7 @@ const LinkField = ({ schema, formData, onChange, name }: FieldProps) => {
)}
</select>
{linkType === "pageType" && !isEmpty(pageTypes) ? (
<PageTypeField
href={href}
pageTypes={pageTypes}
onChange={(href: string) => onChange({ ...formData, href })}
/>
<PageTypeField href={href} onChange={(href: string) => onChange({ ...formData, href })} />
) : null}
<input
id={`root.${name}.href`}
Expand Down
Loading
Loading