Skip to content

Commit

Permalink
feat: introduce UI overrides API
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisvxd committed Dec 11, 2023
1 parent e4f43c9 commit 8a7c325
Show file tree
Hide file tree
Showing 15 changed files with 521 additions and 356 deletions.
121 changes: 75 additions & 46 deletions packages/core/components/ComponentList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,88 @@ import styles from "./styles.module.css";
import getClassNameFactory from "../../lib/get-class-name-factory";
import { Draggable } from "../Draggable";
import { DragIcon } from "../DragIcon";
import { ReactNode } from "react";
import { ReactNode, useMemo } from "react";
import { useAppContext } from "../Puck/context";
import { ChevronDown, ChevronUp } from "react-feather";

const getClassName = getClassNameFactory("ComponentList", styles);
const getClassNameItem = getClassNameFactory("ComponentListItem", styles);

const ComponentListItem = ({
export const ComponentListDraggable = ({
children,
id,
index,
}: {
children: ReactNode;
id: string;
index: number;
}) => (
<Draggable key={id} id={id} index={index} showShadow disableAnimations>
{() => children}
</Draggable>
);

export const ComponentListItem = ({
component,
index,
id,
}: {
component: string;
index: number;
id: string;
}) => {
const { customUi } = useAppContext();

const CustomComponentListItem = useMemo(
() => customUi.componentListItem || "div",
[customUi]
);

return (
<div className={getClassNameItem()}>
<Draggable
key={component}
id={id}
index={index}
showShadow
disableAnimations
className={() => getClassNameItem("draggable")}
>
{() => (
<>
<div className={getClassNameItem("name")}>{component}</div>
<div className={getClassNameItem("icon")}>
<DragIcon />
<ComponentListDraggable id={component} index={index}>
<CustomComponentListItem name={component}>
<div className={getClassNameItem("draggableWrapper")}>
<div className={getClassNameItem("draggable")}>
<div className={getClassNameItem("name")}>{component}</div>
<div className={getClassNameItem("icon")}>
<DragIcon />
</div>
</div>
</>
)}
</Draggable>
</div>
</CustomComponentListItem>
</ComponentListDraggable>
</div>
);
};

export const ComponentListDroppable = ({
children,
droppableId = "component-list",
direction = "vertical",
}: {
children: ReactNode;
droppableId?: string;
direction?: "vertical" | "horizontal";
}) => {
return (
<Droppable droppableId={droppableId} isDropDisabled direction={direction}>
{(provided, snapshot) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
className={getClassName({
isDraggingFrom: !!snapshot.draggingFromThisWith,
})}
>
{children}

{/* Use different element so we don't clash with :last-of-type */}
<span style={{ display: "none" }}>{provided.placeholder}</span>
</div>
)}
</Droppable>
);
};

const ComponentList = ({
children,
title,
Expand Down Expand Up @@ -84,34 +127,20 @@ const ComponentList = ({
</div>
)}
<div className={getClassName("content")}>
<Droppable
<ComponentListDroppable
droppableId={`component-list${title ? `:${title}` : ""}`}
isDropDisabled
>
{(provided, snapshot) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
className={getClassName({
isDraggingFrom: !!snapshot.draggingFromThisWith,
})}
>
{children ||
Object.keys(config.components).map((componentKey, i) => {
return (
<ComponentListItem
key={componentKey}
component={componentKey}
index={i}
id={componentKey}
/>
);
})}
{/* Use different element so we don't clash with :last-of-type */}
<span style={{ display: "none" }}>{provided.placeholder}</span>
</div>
)}
</Droppable>
{children ||
Object.keys(config.components).map((componentKey, i) => {
return (
<ComponentListItem
key={componentKey}
component={componentKey}
index={i}
/>
);
})}
</ComponentListDroppable>
</div>
</div>
);
Expand Down
9 changes: 6 additions & 3 deletions packages/core/components/ComponentList/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@
margin-left: auto;
}

.ComponentListItem:last-of-type .ComponentListItem-draggable {
margin-bottom: 0px;
.ComponentListItem:last-of-type .ComponentListItem-draggableWrapper {
padding-bottom: 0px;
}

.ComponentListItem-draggableWrapper {
padding-bottom: 12px;
}

.ComponentListItem-draggable {
Expand All @@ -54,7 +58,6 @@
justify-content: space-between;
align-items: center;
cursor: grab;
margin-bottom: 12px;
}

.ComponentListItem-name {
Expand Down
47 changes: 20 additions & 27 deletions packages/core/components/InputOrGroup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { Lock } from "react-feather";
import { useDebouncedCallback } from "use-debounce";
import { ObjectField } from "./fields/ObjectField";
import { useAppContext } from "../Puck/context";

const getClassName = getClassNameFactory("Input", styles);

Expand Down Expand Up @@ -76,9 +77,9 @@ export const FieldLabelInternal = ({
);
};

export type InputProps = {
export type InputProps<F = Field<any>> = {
name: string;
field: Field<any>;
field: F;
value: any;
id: string;
label?: string;
Expand All @@ -88,6 +89,8 @@ export type InputProps = {
};

export const InputOrGroup = ({ onChange, ...props }: InputProps) => {
const { customUi } = useAppContext();

const { name, field, value, readOnly } = props;

const [localValue, setLocalValue] = useState(value);
Expand All @@ -114,30 +117,6 @@ export const InputOrGroup = ({ onChange, ...props }: InputProps) => {
onChange: onChangeLocal,
};

if (field.type === "array") {
return <ArrayField {...props} {...localProps} />;
}

if (field.type === "external") {
return <ExternalField {...props} {...localProps} />;
}

if (field.type === "object") {
return <ObjectField {...props} {...localProps} />;
}

if (field.type === "select") {
return <SelectField {...props} {...localProps} />;
}

if (field.type === "textarea") {
return <TextareaField {...props} {...localProps} />;
}

if (field.type === "radio") {
return <RadioField {...props} {...localProps} />;
}

if (field.type === "custom") {
if (!field.render) {
return null;
Expand All @@ -155,5 +134,19 @@ export const InputOrGroup = ({ onChange, ...props }: InputProps) => {
);
}

return <DefaultField {...props} {...localProps} />;
const render = {
...customUi.fields,
array: customUi.fields?.array || ArrayField,
external: customUi.fields?.external || ExternalField,
object: customUi.fields?.object || ObjectField,
select: customUi.fields?.select || SelectField,
textarea: customUi.fields?.textarea || TextareaField,
radio: customUi.fields?.radio || RadioField,
text: customUi.fields?.text || DefaultField,
number: customUi.fields?.number || DefaultField,
};

const Render = render[field.type] as (props: InputProps) => ReactNode;

return <Render {...props} {...localProps} field={field} />;
};
9 changes: 7 additions & 2 deletions packages/core/components/Puck/components/Components/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { useComponentList } from "../../../../lib/use-component-list";
import { useAppContext } from "../../context";
import { ComponentList } from "../../../ComponentList";
import { useMemo } from "react";

export const Components = () => {
const { config, state } = useAppContext();
const { config, state, customUi } = useAppContext();

const componentList = useComponentList(config, state.ui);

const Wrapper = useMemo(() => customUi.componentList || "div", [customUi]);

return (
<div>{componentList ? componentList : <ComponentList id="all" />}</div>
<Wrapper>
{componentList ? componentList : <ComponentList id="all" />}
</Wrapper>
);
};
47 changes: 38 additions & 9 deletions packages/core/components/Puck/components/Fields/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,43 @@ import { useAppContext } from "../../context";

import styles from "./styles.module.css";
import { getClassNameFactory } from "../../../../lib";
import { ReactNode, useMemo } from "react";

const getClassName = getClassNameFactory("PuckFields", styles);

const defaultPageFields: Record<string, Field> = {
title: { type: "text" },
};

const DefaultForm = ({
children,
isLoading,
}: {
children: ReactNode;
isLoading: boolean;
}) => {
return (
<div className={getClassName()}>
{children}
{isLoading && (
<div className={getClassName("loadingOverlay")}>
<ClipLoader />
</div>
)}
</div>
);
};

export const Fields = () => {
const { selectedItem, state, dispatch, config, resolveData, componentState } =
useAppContext();
const {
selectedItem,
state,
dispatch,
config,
resolveData,
componentState,
customUi,
} = useAppContext();
const { data, ui } = state;
const { itemSelector } = ui;

Expand All @@ -41,14 +68,21 @@ export const Fields = () => {
// DEPRECATED
const rootProps = data.root.props || data.root;

const Wrapper = useMemo(
() =>
(itemSelector ? customUi.form : customUi.rootForm || customUi.form) ||
DefaultForm,
[customUi, itemSelector]
);

return (
<form
className={getClassName()}
onSubmit={(e) => {
e.preventDefault();
}}
>
<div>
<Wrapper isLoading={isLoading}>
{Object.keys(fields).map((fieldName) => {
const field = fields[fieldName];

Expand Down Expand Up @@ -162,12 +196,7 @@ export const Fields = () => {
);
}
})}
</div>
{isLoading && (
<div className={getClassName("loadingOverlay")}>
<ClipLoader />
</div>
)}
</Wrapper>
</form>
);
};
10 changes: 6 additions & 4 deletions packages/core/components/Puck/components/Outline/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { rootDroppableId } from "../../../../lib/root-droppable-id";
import { LayerTree } from "../../../LayerTree";
import { useAppContext } from "../../context";
import { dropZoneContext } from "../../../DropZone";
import { useCallback } from "react";
import { useCallback, useMemo } from "react";
import { ItemSelector } from "../../../../lib/get-item";

export const Outline = () => {
const { dispatch, state } = useAppContext();
const { dispatch, state, customUi } = useAppContext();
const { data, ui } = state;
const { itemSelector } = ui;

Expand All @@ -22,8 +22,10 @@ export const Outline = () => {
[]
);

const Wrapper = useMemo(() => customUi.outline || "div", [customUi]);

return (
<div>
<Wrapper>
<dropZoneContext.Consumer>
{(ctx) => (
<>
Expand Down Expand Up @@ -54,6 +56,6 @@ export const Outline = () => {
</>
)}
</dropZoneContext.Consumer>
</div>
</Wrapper>
);
};
Loading

0 comments on commit 8a7c325

Please sign in to comment.