Skip to content

Commit

Permalink
feat: infer Data type from user config
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisvxd committed Sep 3, 2024
1 parent 025ccae commit 50045bb
Show file tree
Hide file tree
Showing 18 changed files with 182 additions and 105 deletions.
10 changes: 5 additions & 5 deletions apps/demo/app/[...puckPath]/client.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"use client";

import { Button, Data, Puck, Render } from "@/core";
import { Button, Puck, Render } from "@/core";
import headingAnalyzer from "@/plugin-heading-analyzer/src/HeadingAnalyzer";
import config, { UserConfig } from "../../config";
import config from "../../config";
import { useDemoData } from "../../lib/use-demo-data";

export function Client({ path, isEdit }: { path: string; isEdit: boolean }) {
Expand All @@ -14,10 +14,10 @@ export function Client({ path, isEdit }: { path: string; isEdit: boolean }) {
if (isEdit) {
return (
<div>
<Puck<UserConfig>
<Puck
config={config}
data={data}
onPublish={async (data: Data) => {
onPublish={async (data) => {
localStorage.setItem(key, JSON.stringify(data));
}}
plugins={[headingAnalyzer]}
Expand All @@ -41,7 +41,7 @@ export function Client({ path, isEdit }: { path: string; isEdit: boolean }) {
}

if (data) {
return <Render<UserConfig> config={config} data={resolvedData} />;
return <Render config={config} data={resolvedData} />;
}

return (
Expand Down
2 changes: 1 addition & 1 deletion apps/demo/app/custom-ui/[...puckPath]/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { IconButton, usePuck } from "@/core";
import { ReactNode, useEffect, useRef, useState } from "react";
import { Drawer } from "@/core/components/Drawer";
import { ChevronUp, ChevronDown, Globe, Bug } from "lucide-react";
import { Overrides } from "@/core/types/Overrides";
import { Overrides } from "@/core/types";

const CustomHeader = ({ onPublish }: { onPublish: (data: Data) => void }) => {
const { appState, dispatch } = usePuck();
Expand Down
14 changes: 6 additions & 8 deletions packages/core/components/MenuBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,24 @@ import styles from "./styles.module.css";

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

export const MenuBar = ({
export function MenuBar<UserData extends Data>({
appState,
data = { content: [], root: {} },
dispatch,
menuOpen = false,
onPublish,
renderHeaderActions,
setMenuOpen,
}: {
appState: AppState;
data: Data;
appState: AppState<UserData>;
dispatch: (action: PuckAction) => void;
onPublish?: (data: Data) => void;
onPublish?: (data: UserData) => void;
menuOpen: boolean;
renderHeaderActions?: (props: {
state: AppState;
state: AppState<UserData>;
dispatch: (action: PuckAction) => void;
}) => ReactElement;
setMenuOpen: Dispatch<SetStateAction<boolean>>;
}) => {
}) {
const {
history: { back, forward, historyStore },
} = useAppContext();
Expand Down Expand Up @@ -74,4 +72,4 @@ export const MenuBar = ({
</div>
</div>
);
};
}
49 changes: 31 additions & 18 deletions packages/core/components/Puck/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import type {
Data,
UiState,
Permissions,
ExtractPropsFromConfig,
ExtractRootPropsFromConfig,
} from "../../types/Config";
import type { OnAction } from "../../types/OnAction";
import { Button } from "../Button";
Expand Down Expand Up @@ -60,10 +62,18 @@ import { DefaultOverride } from "../DefaultOverride";
const getClassName = getClassNameFactory("Puck", styles);
const getLayoutClassName = getClassNameFactory("PuckLayout", styles);

export function Puck<UserConfig extends Config = Config>({
export function Puck<
UserConfig extends Config = Config,
UserProps extends ExtractPropsFromConfig<UserConfig> = ExtractPropsFromConfig<UserConfig>,
UserRootProps extends ExtractRootPropsFromConfig<UserConfig> = ExtractRootPropsFromConfig<UserConfig>,
UserData extends Data<UserProps, UserRootProps> | Data = Data<
UserProps,
UserRootProps
>
>({
children,
config,
data: initialData = { content: [], root: {} },
data: initialData,
ui: initialUi,
onChange,
onPublish,
Expand All @@ -84,21 +94,21 @@ export function Puck<UserConfig extends Config = Config>({
}: {
children?: ReactNode;
config: UserConfig;
data: Partial<Data>;
data: Partial<UserData>;
ui?: Partial<UiState>;
onChange?: (data: Data) => void;
onPublish?: (data: Data) => void;
onAction?: OnAction;
onChange?: (data: UserData) => void;
onPublish?: (data: UserData) => void;
onAction?: OnAction<UserData>;
permissions?: Partial<Permissions>;
plugins?: Plugin[];
overrides?: Partial<Overrides>;
renderHeader?: (props: {
children: ReactNode;
dispatch: (action: PuckAction) => void;
state: AppState;
state: AppState<UserData>;
}) => ReactElement;
renderHeaderActions?: (props: {
state: AppState;
state: AppState<UserData>;
dispatch: (action: PuckAction) => void;
}) => ReactElement;
headerTitle?: string;
Expand All @@ -116,13 +126,17 @@ export function Puck<UserConfig extends Config = Config>({
const historyStore = useHistoryStore(initialHistory);

const [reducer] = useState(() =>
createReducer<UserConfig>({ config, record: historyStore.record, onAction })
createReducer<UserConfig, UserData>({
config,
record: historyStore.record,
onAction,
})
);

const [initialAppState] = useState<AppState>(() => {
const [initialAppState] = useState<AppState<UserData>>(() => {
const initial = { ...defaultAppState.ui, ...initialUi };

let clientUiState: Partial<AppState["ui"]> = {};
let clientUiState: Partial<AppState<UserData>["ui"]> = {};

if (typeof window !== "undefined") {
// Hide side bars on mobile
Expand Down Expand Up @@ -211,12 +225,12 @@ export function Puck<UserConfig extends Config = Config>({
)
: {},
},
};
} as AppState<UserData>;
});

const [appState, dispatch] = useReducer<StateReducer>(
const [appState, dispatch] = useReducer<StateReducer<UserData>>(
reducer,
flushZones(initialAppState)
flushZones<UserData>(initialAppState)
);

const { data, ui } = appState;
Expand Down Expand Up @@ -249,7 +263,7 @@ export function Puck<UserConfig extends Config = Config>({
const selectedItem = itemSelector ? getItem(itemSelector, data) : null;

useEffect(() => {
if (onChange) onChange(data);
if (onChange) onChange(data as UserData);
}, [data]);

const { onDragStartOrUpdate, placeholderStyle } = usePlaceholderStyle();
Expand Down Expand Up @@ -500,7 +514,7 @@ export function Puck<UserConfig extends Config = Config>({
<CustomHeaderActions>
<Button
onClick={() => {
onPublish && onPublish(data);
onPublish && onPublish(data as UserData);
}}
icon={<Globe size="14px" />}
>
Expand Down Expand Up @@ -572,9 +586,8 @@ export function Puck<UserConfig extends Config = Config>({
)}
</IconButton>
</div>
<MenuBar
<MenuBar<UserData>
appState={appState}
data={data}
dispatch={dispatch}
onPublish={onPublish}
menuOpen={menuOpen}
Expand Down
23 changes: 15 additions & 8 deletions packages/core/components/Render/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
"use client";

import { rootDroppableId } from "../../lib/root-droppable-id";
import { Config, Data } from "../../types/Config";
import {
Config,
Data,
ExtractPropsFromConfig,
ExtractRootPropsFromConfig,
} from "../../types/Config";
import { DropZone, DropZoneProvider } from "../DropZone";

export function Render<UserConfig extends Config = Config>({
config,
data,
}: {
config: UserConfig;
data: Partial<Data>;
}) {
export function Render<
UserConfig extends Config = Config,
UserProps extends ExtractPropsFromConfig<UserConfig> = ExtractPropsFromConfig<UserConfig>,
UserRootProps extends ExtractRootPropsFromConfig<UserConfig> = ExtractRootPropsFromConfig<UserConfig>,
UserData extends Data<UserProps, UserRootProps> | Data = Data<
UserProps,
UserRootProps
>
>({ config, data }: { config: UserConfig; data: Partial<UserData> }) {
const defaultedData = {
...data,
root: data.root || {},
Expand Down
6 changes: 4 additions & 2 deletions packages/core/lib/flush-zones.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AppState } from "../types/Config";
import type { AppState, Data } from "../types/Config";
import { addToZoneCache } from "../reducer/data";

/**
Expand All @@ -7,7 +7,9 @@ import { addToZoneCache } from "../reducer/data";
* @param appState initial app state
* @returns appState with zones removed from data
*/
export const flushZones = (appState: AppState): AppState => {
export const flushZones = <UserData extends Data>(
appState: AppState<UserData>
): AppState<UserData> => {
const containsZones = typeof appState.data.zones !== "undefined";

if (containsZones) {
Expand Down
8 changes: 4 additions & 4 deletions packages/core/lib/get-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ export type ItemSelector = {
zone?: string;
};

export const getItem = (
export function getItem<UserData extends Data>(
selector: ItemSelector,
data: Data,
data: UserData,
dynamicProps: Record<string, any> = {}
): Data["content"][0] | undefined => {
): UserData["content"][0] | undefined {
if (!selector.zone || selector.zone === rootDroppableId) {
const item = data.content[selector.index];

Expand All @@ -27,4 +27,4 @@ export const getItem = (
return item?.props
? { ...item, props: dynamicProps[item.props.id] || item.props }
: undefined;
};
}
3 changes: 1 addition & 2 deletions packages/core/lib/load-overrides.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Overrides } from "../types/Overrides";
import { Plugin } from "../types/Plugin";
import { Overrides, Plugin } from "../types";

export const loadOverrides = ({
overrides,
Expand Down
31 changes: 17 additions & 14 deletions packages/core/lib/reduce-related-zones.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ import { Data } from "../types/Config";
import { generateId } from "./generate-id";
import { getZoneId } from "./get-zone-id";

export const reduceRelatedZones = (
item: Data["content"][0],
data: Data,
export function reduceRelatedZones<UserData extends Data>(
item: UserData["content"][0],
data: UserData,
fn: (
zones: Required<Data>["zones"],
zones: Required<UserData>["zones"],
key: string,
zone: Required<Data>["zones"][0]
) => Required<Data>["zones"]
) => {
) => Required<UserData>["zones"]
) {
return {
...data,
zones: Object.keys(data.zones || {}).reduce<Required<Data>["zones"]>(
zones: Object.keys(data.zones || {}).reduce<Required<UserData>["zones"]>(
(acc, key) => {
const [parentId] = getZoneId(key);

Expand All @@ -27,7 +27,7 @@ export const reduceRelatedZones = (
{}
),
};
};
}

const findRelatedByZoneId = (
zoneId: string,
Expand Down Expand Up @@ -73,7 +73,10 @@ const findRelatedByItem = (item: Data["content"][0], data: Data) => {
/**
* Remove all related zones
*/
export const removeRelatedZones = (item: Data["content"][0], data: Data) => {
export const removeRelatedZones = <UserData extends Data>(
item: UserData["content"][0],
data: UserData
) => {
const newData = { ...data };

const related = findRelatedByItem(item, data);
Expand All @@ -85,11 +88,11 @@ export const removeRelatedZones = (item: Data["content"][0], data: Data) => {
return newData;
};

export const duplicateRelatedZones = (
item: Data["content"][0],
data: Data,
export function duplicateRelatedZones<UserData extends Data>(
item: UserData["content"][0],
data: UserData,
newId: string
) => {
): UserData {
return reduceRelatedZones(item, data, (acc, key, zone) => {
const dupedZone = zone.map((zoneItem) => ({
...zoneItem,
Expand All @@ -113,4 +116,4 @@ export const duplicateRelatedZones = (
[`${newId}:${zoneId}`]: dupedZone,
};
});
};
}
2 changes: 1 addition & 1 deletion packages/core/lib/resolve-all-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,5 @@ export async function resolveAllData<
onResolveEnd
)) as Content<Props>,
zones: resolvedZones,
};
} as Data<Props, RootProps>;
}
27 changes: 21 additions & 6 deletions packages/core/lib/setup-zone.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
import { Data } from "../types/Config";
import { rootDroppableId } from "./root-droppable-id";

export const setupZone = (data: Data, zoneKey: string): Required<Data> => {
// Force 'zones' to always be present and non-undefined
type WithZones<T extends Data> = T & { zones: NonNullable<T["zones"]> };

// Ensuring zones is non-undefined and part of the final type
function ensureZones<UserData extends Data>(
data: UserData
): WithZones<UserData> {
return {
...data,
zones: data.zones || {},
} as WithZones<UserData>;
}

export const setupZone = <UserData extends Data>(
data: UserData,
zoneKey: string
): Required<WithZones<UserData>> => {
if (zoneKey === rootDroppableId) {
return data as Required<Data>;
return data as Required<WithZones<UserData>>;
}

const newData = { ...data };

newData.zones = data.zones || {};
// Preprocess to ensure zones is not undefined
const newData = ensureZones(data);

newData.zones[zoneKey] = newData.zones[zoneKey] || [];

return newData as Required<Data>;
return newData as Required<WithZones<UserData>>;
};
Loading

0 comments on commit 50045bb

Please sign in to comment.