Skip to content

Commit

Permalink
feat: add APIs for setting undo/redo history
Browse files Browse the repository at this point in the history
This addresses #519.

This change exposes the history store's setHistories and setHistoryIndex
functions. When they are called the data is dispatched so that Puck
rerenders. The use case this supports is when a Puck consumer wants to
change or reset the history state manually without requiring the page to
be refreshed.

---------

Co-authored-by: Chris Villa <chrisvxd@users.noreply.github.com>
  • Loading branch information
mkilpatrick and chrisvxd authored Jul 23, 2024
1 parent 00a43cf commit 39e7f40
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 14 deletions.
32 changes: 26 additions & 6 deletions apps/docs/pages/docs/api-reference/functions/use-puck.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@ export function Editor() {

## Returns

| Param | Example | Type |
| ------------------------------- | ------------------------------------------------ | --------------------------------------------------- |
| [`appState`](#appstate) | `{ data: {}, ui: {} }` | [AppState](/docs/api-reference/app-state) |
| [`dispatch`](#dispatch) | `(action: PuckAction) => void` | Function |
| [`history`](#history) | `{}` | Object |
| [`selectedItem`](#selecteditem) | `{ type: "Heading", props: {id: "my-heading"} }` | [ComponentData](/docs/api-reference/data#content-1) |
| Param | Example | Type |
| ------------------------------------- | ------------------------------------------------ | --------------------------------------------------- |
| [`appState`](#appstate) | `{ data: {}, ui: {} }` | [AppState](/docs/api-reference/app-state) |
| [`dispatch`](#dispatch) | `(action: PuckAction) => void` | Function |
| [`history`](#history) | `{}` | Object |
| [`selectedItem`](#selecteditem) | `{ type: "Heading", props: {id: "my-heading"} }` | [ComponentData](/docs/api-reference/data#content-1) |
| [`setHistories`](#sethistories) | `setHistories: (histories) => {}` | Function |
| [`setHistoryIndex`](#sethistoryindex) | `setHistoryIndex: (index) => {}` | Function |

### `appState`

Expand Down Expand Up @@ -117,3 +119,21 @@ The currently selected item, as defined by `appState.ui.itemSelector`.
console.log(selectedItem);
// { type: "Heading", props: {id: "my-heading"} }
```

### `setHistories`

A function to set the history state.

```tsx
const { setHistories } = usePuck();
setHistories([]); // clears all history
```

### `setHistoryIndex`

A function to set current history index.

```tsx
const { setHistoryIndex } = usePuck();
setHistoryIndex(2);
```
32 changes: 32 additions & 0 deletions packages/core/lib/__tests__/use-history-store.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,38 @@ describe("use-history-store", () => {
expect(renderedHook.result.current.histories[1].data).toBe("Banana");
expect(renderedHook.result.current.currentHistory.data).toBe("Banana");
});

test("should reset histories and index on setHistories", () => {
act(() => renderedHook.result.current.record("Apples"));
act(() => renderedHook.result.current.record("Oranges"));
act(() =>
renderedHook.result.current.setHistories([
{
id: "1",
data: "Oreo",
},
])
);

expect(renderedHook.result.current.hasPast).toBe(true);
expect(renderedHook.result.current.hasFuture).toBe(false);
expect(renderedHook.result.current.histories.length).toBe(1);
expect(renderedHook.result.current.histories[0].data).toBe("Oreo");
expect(renderedHook.result.current.currentHistory.data).toBe("Oreo");
expect(renderedHook.result.current.index).toBe(0);
});

test("should update index on setHistoryIndex", () => {
act(() => renderedHook.result.current.record("Apples"));
act(() => renderedHook.result.current.record("Oranges"));
act(() => renderedHook.result.current.setHistoryIndex(0));

expect(renderedHook.result.current.hasPast).toBe(true);
expect(renderedHook.result.current.hasFuture).toBe(true);
expect(renderedHook.result.current.histories.length).toBe(2);
expect(renderedHook.result.current.currentHistory.data).toBe("Apples");
expect(renderedHook.result.current.index).toBe(0);
});
});

describe("use-history-store-prefilled", () => {
Expand Down
82 changes: 82 additions & 0 deletions packages/core/lib/__tests__/use-puck-history.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const historyStore = {
nextHistory: { data: null },
back: jest.fn(),
forward: jest.fn(),
setHistories: jest.fn(),
setHistoryIndex: jest.fn(),
} as unknown as HistoryStore;

const initialAppState = defaultAppState;
Expand Down Expand Up @@ -99,4 +101,84 @@ describe("use-puck-history", () => {
state: historyStore.nextHistory?.data,
});
});

test("setHistories calls dispatch to last history item", () => {
const { result } = renderHook(() =>
usePuckHistory({ dispatch, initialAppState, historyStore })
);

const updatedHistories = [
{
id: "1",
data: {
one: "foo 1",
two: "bar 1",
},
},
{
id: "2",
data: {
one: "foo 2",
two: "bar 2",
},
},
];

act(() => {
result.current.setHistories(updatedHistories);
});

expect(historyStore.setHistories).toHaveBeenCalled();
expect(dispatch).toHaveBeenCalledWith({
type: "set",
state: updatedHistories[1].data,
});
});

test("setHistoryIndex calls dispatch on the history at that index", () => {
const updatedHistories = [
{
id: "1",
data: {
one: "foo 1",
two: "bar 1",
},
},
{
id: "2",
data: {
one: "foo 2",
two: "bar 2",
},
},
];
historyStore.histories = updatedHistories;

const { result } = renderHook(() =>
usePuckHistory({ dispatch, initialAppState, historyStore })
);

act(() => {
result.current.setHistoryIndex(0);
});

expect(historyStore.setHistoryIndex).toHaveBeenCalled();
expect(dispatch).toHaveBeenCalledWith({
type: "set",
state: updatedHistories[0].data,
});
});

test("setHistoryIndex does not call dispatch when index out of bounds", () => {
const { result } = renderHook(() =>
usePuckHistory({ dispatch, initialAppState, historyStore })
);

act(() => {
result.current.setHistoryIndex(5);
});

expect(historyStore.setHistoryIndex).not.toHaveBeenCalled();
expect(dispatch).not.toHaveBeenCalled();
});
});
18 changes: 11 additions & 7 deletions packages/core/lib/use-history-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export type HistoryStore<D = any> = {
currentHistory: History;
nextHistory: History<D> | null;
prevHistory: History<D> | null;
setHistories: (histories: History[]) => void;
setHistoryIndex: (index: number) => void;
};

const EMPTY_HISTORY_INDEX = -1;
Expand All @@ -33,6 +35,12 @@ export function useHistoryStore<D = any>(initialHistory?: {
initialHistory?.histories ?? []
);

// Exported as setHistories so that the index gets automatically updated.
const updateHistories = (histories: History<D>[]) => {
setHistories(histories);
setIndex(histories.length - 1);
};

const [index, setIndex] = useState(
initialHistory?.index ?? EMPTY_HISTORY_INDEX
);
Expand All @@ -50,13 +58,7 @@ export function useHistoryStore<D = any>(initialHistory?: {
id: generateId("history"),
};

setHistories((prev) => {
const newVal = [...prev.slice(0, index + 1), history];

setIndex(newVal.length - 1);

return newVal;
});
updateHistories([...histories.slice(0, index + 1), history]);
}, 250);

const back = () => {
Expand All @@ -78,5 +80,7 @@ export function useHistoryStore<D = any>(initialHistory?: {
nextHistory,
prevHistory,
histories,
setHistories: updateHistories,
setHistoryIndex: setIndex,
};
}
27 changes: 26 additions & 1 deletion packages/core/lib/use-puck-history.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type { AppState } from "../types/Config";
import { PuckAction } from "../reducer";
import { useHotkeys } from "react-hotkeys-hook";
import { HistoryStore } from "./use-history-store";
import { History, HistoryStore } from "./use-history-store";

export type PuckHistory = {
back: VoidFunction;
forward: VoidFunction;
setHistories: (histories: History[]) => void;
setHistoryIndex: (index: number) => void;
historyStore: HistoryStore;
};

Expand Down Expand Up @@ -37,6 +39,27 @@ export function usePuckHistory({
}
};

const setHistories = (histories: History[]) => {
// dispatch the last history index or initial state
dispatch({
type: "set",
state: histories[histories.length - 1]?.data || initialAppState,
});

historyStore.setHistories(histories);
};

const setHistoryIndex = (index: number) => {
if (historyStore.histories.length > index) {
dispatch({
type: "set",
state: historyStore.histories[index]?.data || initialAppState,
});

historyStore.setHistoryIndex(index);
}
};

useHotkeys("meta+z", back, { preventDefault: true });
useHotkeys("meta+shift+z", forward, { preventDefault: true });
useHotkeys("meta+y", forward, { preventDefault: true });
Expand All @@ -45,5 +68,7 @@ export function usePuckHistory({
back,
forward,
historyStore,
setHistories,
setHistoryIndex,
};
}
2 changes: 2 additions & 0 deletions packages/core/lib/use-puck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export const usePuck = () => {
history: {
back: history.back!,
forward: history.forward!,
setHistories: history.setHistories!,
setHistoryIndex: history.setHistoryIndex!,
hasPast: history.historyStore!.hasPast,
hasFuture: history.historyStore!.hasFuture,
histories: history.historyStore!.histories,
Expand Down

0 comments on commit 39e7f40

Please sign in to comment.