Skip to content

Commit

Permalink
feat: call resolveData when new item inserted
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisvxd authored Aug 13, 2024
1 parent 6386bd1 commit 3298831
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 42 deletions.
18 changes: 7 additions & 11 deletions packages/core/components/Puck/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { defaultViewports } from "../ViewportControls/default-viewports";
import { Viewports } from "../../types/Viewports";
import { DragDropContext } from "../DragDropContext";
import { IframeConfig } from "../../types/IframeConfig";
import { insertComponent } from "../../lib/insert-component";

const getClassName = getClassNameFactory("Puck", styles);
const getLayoutClassName = getClassNameFactory("PuckLayout", styles);
Expand Down Expand Up @@ -426,17 +427,12 @@ export function Puck<UserConfig extends Config = Config>({
) {
const [_, componentType] = droppedItem.draggableId.split("::");

dispatch({
type: "insert",
componentType: componentType || droppedItem.draggableId,
destinationIndex: droppedItem.destination!.index,
destinationZone: droppedItem.destination.droppableId,
});

setItemSelector({
index: droppedItem.destination!.index,
zone: droppedItem.destination.droppableId,
});
insertComponent(
componentType || droppedItem.draggableId,
droppedItem.destination.droppableId,
droppedItem.destination!.index,
{ config, dispatch, resolveData, state: appState }
);

return;
} else {
Expand Down
123 changes: 123 additions & 0 deletions packages/core/lib/__tests__/insert-component.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { cleanup } from "@testing-library/react";
import { AppState, Config, Data } from "../../types/Config";
import { PuckAction } from "../../reducer";
import { defaultAppState } from "../../components/Puck/context";
import { insertComponent } from "../insert-component";
import { rootDroppableId } from "../root-droppable-id";

const item1 = { type: "MyComponent", props: { id: "MyComponent-1" } };
const item2 = { type: "MyComponent", props: { id: "MyComponent-2" } };
const item3 = { type: "MyComponent", props: { id: "MyComponent-3" } };

const data: Data = {
root: { props: { title: "" } },
content: [item1],
zones: {
"MyComponent-1:zone": [item2],
"MyComponent-2:zone": [item3],
},
};

const state: AppState = {
data,
ui: defaultAppState.ui,
};

const config: Config = {
components: {
MyComponent: {
defaultProps: { prop: "example" },
resolveData: ({ props }) => {
return {
props: {
...props,
prop: "Hello, world",
},
readOnly: {
prop: true,
},
};
},
render: () => <div />,
},
},
};

describe("use-insert-component", () => {
describe("insert-component", () => {
let dispatchedEvents: PuckAction[] = [];
let resolvedDataEvents: AppState[] = [];

const ctx = {
config,
dispatch: (action: PuckAction) => {
dispatchedEvents.push(action);
},
resolveData: (appState: AppState) => {
resolvedDataEvents.push(appState);
},
state,
};

afterEach(() => {
cleanup();
dispatchedEvents = [];
resolvedDataEvents = [];
});

it("should dispatch the insert action", async () => {
insertComponent("MyComponent", rootDroppableId, 0, ctx);

expect(dispatchedEvents[0]).toEqual<PuckAction>({
type: "insert",
componentType: "MyComponent",
destinationZone: rootDroppableId,
destinationIndex: 0,
id: expect.stringContaining("MyComponent-"),
recordHistory: false,
});
});

it("should dispatch the setUi action, and select the item", async () => {
insertComponent("MyComponent", rootDroppableId, 0, ctx);

expect(dispatchedEvents[1]).toEqual<PuckAction>({
type: "setUi",
ui: {
itemSelector: {
zone: rootDroppableId,
index: 0,
},
},
});
});

it("should run any resolveData methods on the inserted item", async () => {
insertComponent("MyComponent", rootDroppableId, 0, ctx);

expect(resolvedDataEvents[0]).toEqual<AppState>({
...state,
data: {
...state.data,
content: [
{
type: "MyComponent",
props: {
id: expect.stringContaining("MyComponent-"),
prop: "example",
},
},
...state.data.content,
],
},
ui: {
...state.ui,
itemSelector: {
index: 0,
zone: rootDroppableId,
},
},
});
});
});
});
54 changes: 54 additions & 0 deletions packages/core/lib/insert-component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { insertAction, InsertAction, PuckAction } from "../reducer";
import { AppState, Config } from "../types/Config";
import { generateId } from "./generate-id";

// Makes testing easier without mocks
export const insertComponent = (
componentType: string,
zone: string,
index: number,
{
config,
dispatch,
resolveData,
state,
}: {
config: Config;
dispatch: (action: PuckAction) => void;
resolveData: (newAppState: AppState) => void;
state: AppState;
}
) => {
// Reuse newData so ID retains parity between dispatch and resolver
const id = generateId(componentType);

const insertActionData: InsertAction = {
type: "insert",
componentType,
destinationIndex: index,
destinationZone: zone,
id,
};

const insertedData = insertAction(state.data, insertActionData, config);

// Dispatch the insert, immediately
dispatch({
...insertActionData, // Dispatch insert rather set, as user's may rely on this via onAction
recordHistory: false,
});

const itemSelector = {
index,
zone,
};

// Select the item, immediately
dispatch({ type: "setUi", ui: { itemSelector } });

// Run any resolvers, async
resolveData({
data: insertedData,
ui: { ...state.ui, itemSelector },
});
};
1 change: 1 addition & 0 deletions packages/core/reducer/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type InsertAction = {
componentType: string;
destinationIndex: number;
destinationZone: string;
id?: string;
};

export type DuplicateAction = {
Expand Down
70 changes: 39 additions & 31 deletions packages/core/reducer/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
removeRelatedZones,
} from "../lib/reduce-related-zones";
import { generateId } from "../lib/generate-id";
import { PuckAction, ReplaceAction } from "./actions";
import { InsertAction, PuckAction, ReplaceAction } from "./actions";

// Restore unregistered zones when re-registering in same session
export const zoneCache = {};
Expand Down Expand Up @@ -43,42 +43,50 @@ export const replaceAction = (data: Data, action: ReplaceAction) => {
};
};

export const reduceData = (data: Data, action: PuckAction, config: Config) => {
if (action.type === "insert") {
const emptyComponentData = {
type: action.componentType,
props: {
...(config.components[action.componentType].defaultProps || {}),
id: generateId(action.componentType),
},
};

if (action.destinationZone === rootDroppableId) {
return {
...data,
content: insert(
data.content,
action.destinationIndex,
emptyComponentData
),
};
}

const newData = setupZone(data, action.destinationZone);
export const insertAction = (
data: Data,
action: InsertAction,
config: Config
) => {
const emptyComponentData = {
type: action.componentType,
props: {
...(config.components[action.componentType].defaultProps || {}),
id: action.id || generateId(action.componentType),
},
};

if (action.destinationZone === rootDroppableId) {
return {
...data,
zones: {
...newData.zones,
[action.destinationZone]: insert(
newData.zones[action.destinationZone],
action.destinationIndex,
emptyComponentData
),
},
content: insert(
data.content,
action.destinationIndex,
emptyComponentData
),
};
}

const newData = setupZone(data, action.destinationZone);

return {
...data,
zones: {
...newData.zones,
[action.destinationZone]: insert(
newData.zones[action.destinationZone],
action.destinationIndex,
emptyComponentData
),
},
};
};

export const reduceData = (data: Data, action: PuckAction, config: Config) => {
if (action.type === "insert") {
return insertAction(data, action, config);
}

if (action.type === "duplicate") {
const item = getItem(
{ index: action.sourceIndex, zone: action.sourceZone },
Expand Down

0 comments on commit 3298831

Please sign in to comment.