Skip to content

Commit

Permalink
Merge pull request #7 from EskiMojo14/pause
Browse files Browse the repository at this point in the history
add ability to pause history
  • Loading branch information
EskiMojo14 authored Apr 20, 2024
2 parents 2f45c0b + 8401830 commit 75ce360
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 10 deletions.
31 changes: 28 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,25 @@ console.log(tryUndoState.present); // [{ id: 1, title: "Dune" }]

Just like undoable functions, these methods will act mutably when passed an immer draft and immutably otherwise.

### Pausing history

If you need to make changes to the state without affecting the history, you can use the `pause` and `resume` methods.

```ts
const pausedState = booksHistoryAdapter.pause(resetState);

const withBook = addBook(pausedState, { id: 2, title: "Foundation" });

const resumedState = booksHistoryAdapter.resume(withBook);

const undoneState = booksHistoryAdapter.undo(resumedState);

// changes while paused cannot be undone
console.log(undoneState.present); // [{ id: 1, title: "Dune" }, { id: 2, title: "Foundation" }]
```

Changes will still be made to the data while paused (including `undo` and `redo`), but they won't be recorded in the history.

## Redux helper methods

If imported from `"history-adapter/redux"`, the history adapter will have additional methods to assist use with Redux, specifically with Redux Toolkit.
Expand Down Expand Up @@ -247,7 +266,7 @@ dispatch(addBook(book, true)); // action.meta.undoable === true
dispatch(addBook(book, false)); // action.meta.undoable === false
```

As a tip, `undo`, `redo` and `clearHistory` are all valid reducers due to not needing an argument. The version of `jump` on a Redux history adapter allows for either a number or payload action, making it also valid.
As a tip, `undo`, `redo`, `pause`, `resume` and `clearHistory` are all valid reducers due to not needing an argument. The version of `jump` on a Redux history adapter allows for either a number or payload action, making it also valid.

```ts
const booksSlice = createSlice({
Expand All @@ -257,6 +276,8 @@ const booksSlice = createSlice({
undo: booksHistoryAdapter.undo,
redo: booksHistoryAdapter.redo,
jump: booksHistoryAdapter.jump,
pause: booksHistoryAdapter.pause,
resume: booksHistoryAdapter.resume,
clearHistory: booksHistoryAdapter.clearHistory,
addBook: {
prepare: booksHistoryAdapter.withPayload<Book>(),
Expand All @@ -275,20 +296,24 @@ const booksSlice = createSlice({
A method which returns some useful selectors.

```ts
const { selectCanUndo, selectCanRedo, selectPresent } =
const { selectCanUndo, selectCanRedo, selectPresent, selectPaused } =
booksHistoryAdapter.getSelectors();

console.log(
selectPresent(initialState), // []
selectCanUndo(initialState), // false
selectCanRedo(initialState), // false
selectPaused(initialState), // false
);

console.log(
selectPresent(nextState), // [{ id: 1, title: "Dune" }]
selectCanUndo(nextState), // true
selectCanRedo(nextState), // false
selectPaused(nextState), // false
);

console.log(selectPaused(pausedState)); // true
```

If an input selector is provided, the selectors will be combined using [reselect](https://github.com/reduxjs/reselect).
Expand All @@ -301,7 +326,7 @@ const { selectPresent } = booksHistoryAdapter.getSelectors(
console.log(selectPresent({ books: initialState })); // []
```

The instance of `createSelector` used can be customised:
The instance of `createSelector` used can be customised, and defaults to RTK's [`createDraftSafeSelector`](https://redux-toolkit.js.org/api/createSelector#createdraftsafeselector):

```ts
import { createSelectorCreator, lruMemoize } from "reselect";
Expand Down
22 changes: 22 additions & 0 deletions src/creator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ describe("Slice creators", () => {
undo,
redo,
jump,
pause,
resume,
clearHistory,
reset,
addBook,
Expand Down Expand Up @@ -109,6 +111,16 @@ describe("Slice creators", () => {
store.dispatch(reset());

expect(selectLastBook(store.getState())).toBeUndefined();

store.dispatch(pause());

store.dispatch(addBook(book1));

store.dispatch(resume());

store.dispatch(undo());

expect(selectLastBook(store.getState())).toEqual(book1);
});
it("works with nested state", () => {
const bookSlice = createAppSlice({
Expand Down Expand Up @@ -144,6 +156,8 @@ describe("Slice creators", () => {
undo,
redo,
jump,
pause,
resume,
clearHistory,
reset,
addBook,
Expand Down Expand Up @@ -195,6 +209,14 @@ describe("Slice creators", () => {
store.dispatch(reset());

expect(selectLastBook(store.getState())).toBeUndefined();

store.dispatch(pause());

store.dispatch(addBook(book1));

store.dispatch(resume());

expect(selectLastBook(store.getState())).toEqual(book1);
});
it("can be destructured", () => {
const bookSlice = createAppSlice({
Expand Down
6 changes: 5 additions & 1 deletion src/creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const undoableCreatorsCreatorType = Symbol("undoableCreatorsCreator");
interface HistoryReducers<State> {
undo: CaseReducerDefinition<State, PayloadAction>;
redo: CaseReducerDefinition<State, PayloadAction>;
pause: CaseReducerDefinition<State, PayloadAction>;
resume: CaseReducerDefinition<State, PayloadAction>;
jump: CaseReducerDefinition<State, PayloadAction<number>>;
clearHistory: CaseReducerDefinition<State, PayloadAction>;
reset: ReducerDefinition<typeof historyMethodsCreatorType> & {
Expand Down Expand Up @@ -153,12 +155,14 @@ export const historyMethodsCreator: ReducerCreator<
{
selectHistoryState = (state) => state as HistoryState<Data>,
}: HistoryMethodsCreatorConfig<State, Data> = {},
) {
): HistoryReducers<State> {
const createReducer = makeScopedReducerCreator(selectHistoryState);
return {
undo: createReducer(adapter.undo),
redo: createReducer(adapter.redo),
jump: createReducer(adapter.jump),
pause: createReducer(adapter.pause),
resume: createReducer(adapter.resume),
clearHistory: createReducer(adapter.clearHistory),
reset: {
_reducerDefinitionType: historyMethodsCreatorType,
Expand Down
101 changes: 101 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ describe("createHistoryAdapter", () => {
past: [],
present: [],
future: [],
paused: false,
});
});
});
Expand All @@ -52,6 +53,7 @@ describe("createHistoryAdapter", () => {
past: [aPatchState],
present: [book1],
future: [],
paused: false,
});
});
it("handles nothing value to return undefined", () => {
Expand All @@ -63,6 +65,7 @@ describe("createHistoryAdapter", () => {
past: [aPatchState],
present: undefined,
future: [],
paused: false,
});
});
it("allows deriving from arguments whether update should be undoable", () => {
Expand All @@ -78,6 +81,7 @@ describe("createHistoryAdapter", () => {
past: [],
present: [book1],
future: [],
paused: false,
});
});
it("can be used as a mutator if already working with drafts", () => {
Expand All @@ -92,6 +96,7 @@ describe("createHistoryAdapter", () => {
past: [aPatchState],
present: [book1],
future: [],
paused: false,
});
});
it("can be provided with a selector if working with nested state", () => {
Expand All @@ -113,6 +118,7 @@ describe("createHistoryAdapter", () => {
past: [aPatchState],
present: [book1],
future: [],
paused: false,
},
});
});
Expand All @@ -130,6 +136,7 @@ describe("createHistoryAdapter", () => {
past: [],
present: [],
future: [aPatchState],
paused: false,
});
});
it("can be used as a mutator if already working with drafts", () => {
Expand All @@ -141,6 +148,7 @@ describe("createHistoryAdapter", () => {
past: [],
present: [],
future: [aPatchState],
paused: false,
});
});
});
Expand All @@ -159,6 +167,7 @@ describe("createHistoryAdapter", () => {
past: [aPatchState],
present: [book1],
future: [],
paused: false,
});
});
it("can be used as a mutator if already working with drafts", () => {
Expand All @@ -170,6 +179,7 @@ describe("createHistoryAdapter", () => {
past: [aPatchState],
present: [book1],
future: [],
paused: false,
});
});
});
Expand All @@ -187,6 +197,7 @@ describe("createHistoryAdapter", () => {
past: [],
present: [],
future: [aPatchState, aPatchState],
paused: false,
});
const jumpedForwardState = booksHistoryAdapter.jump(jumpedState, 2);
expect(jumpedForwardState).toEqual(secondState);
Expand All @@ -200,6 +211,96 @@ describe("createHistoryAdapter", () => {
past: [],
present: [],
future: [aPatchState, aPatchState],
paused: false,
});
});
});
describe("pause", () => {
it("can be used to pause history tracking", () => {
const initialState = booksHistoryAdapter.getInitialState([]);
const pausedState = booksHistoryAdapter.pause(initialState);
expect(pausedState).toEqual<HistoryState<Array<Book>>>({
past: [],
present: [],
future: [],
paused: true,
});
});
it("can be used as a mutator if already working with drafts", () => {
const initialState = booksHistoryAdapter.getInitialState([]);
expect(
produce(initialState, (draft) => {
booksHistoryAdapter.pause(draft);
}),
).toEqual<HistoryState<Array<Book>>>({
past: [],
present: [],
future: [],
paused: true,
});
});
it("is respected by undoable functions", () => {
const addBook = booksHistoryAdapter.undoable((books, book: Book) => {
books.push(book);
});
const initialState = booksHistoryAdapter.getInitialState([]);
const pausedState = booksHistoryAdapter.pause(initialState);
const nextState = addBook(pausedState, book1);
expect(nextState).toEqual<HistoryState<Array<Book>>>({
past: [],
present: [book1],
future: [],
paused: true,
});
});
});
describe("resume", () => {
it("can be used to resume history tracking", () => {
const initialState = booksHistoryAdapter.getInitialState([]);
const pausedState = booksHistoryAdapter.pause(initialState);
const resumedState = booksHistoryAdapter.resume(pausedState);
expect(resumedState).toEqual<HistoryState<Array<Book>>>({
past: [],
present: [],
future: [],
paused: false,
});
});
it("can be used as a mutator if already working with drafts", () => {
const initialState = booksHistoryAdapter.getInitialState([]);
const pausedState = booksHistoryAdapter.pause(initialState);
expect(
produce(pausedState, (draft) => {
booksHistoryAdapter.resume(draft);
}),
).toEqual<HistoryState<Array<Book>>>({
past: [],
present: [],
future: [],
paused: false,
});
});
it("is respected by undoable functions", () => {
const addBook = booksHistoryAdapter.undoable((books, book: Book) => {
books.push(book);
});
const initialState = booksHistoryAdapter.getInitialState([]);
const pausedState = booksHistoryAdapter.pause(initialState);
const withBook = addBook(pausedState, book1);
expect(withBook).toEqual<HistoryState<Array<Book>>>({
past: [],
present: [book1],
future: [],
paused: true,
});

const resumedState = booksHistoryAdapter.resume(withBook);
const nextState = addBook(resumedState, book2);
expect(nextState).toEqual<HistoryState<Array<Book>>>({
past: [aPatchState],
present: [book1, book2],
future: [],
paused: false,
});
});
});
Expand Down
22 changes: 22 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface HistoryState<Data> {
past: Array<PatchState>;
present: Data;
future: Array<PatchState>;
paused: boolean;
}

export type MaybeDraftHistoryState<Data> =
Expand Down Expand Up @@ -130,6 +131,17 @@ export interface HistoryAdapter<Data> {
state: State,
...args: Args
) => State;

/**
* Pauses the history, preventing any new patches from being added.
* @param state History state shape, with patches
*/
pause<State extends MaybeDraftHistoryState<Data>>(state: State): State;
/**
* Resumes the history, allowing new patches to be added.
* @param state History state shape, with patches
*/
resume<State extends MaybeDraftHistoryState<Data>>(state: State): State;
}

/**
Expand All @@ -142,6 +154,7 @@ export function getInitialState<Data>(initialData: Data): HistoryState<Data> {
past: [],
present: initialData,
future: [],
paused: false,
};
}

Expand Down Expand Up @@ -210,6 +223,9 @@ export function createHistoryAdapter<Data>({
});
state.present = present;

// if paused, don't add to history
if (state.paused) return;

const undoable = isUndoable?.(...args) ?? true;
if (undoable) {
const lengthWithoutFuture = state.past.length + 1;
Expand All @@ -221,5 +237,11 @@ export function createHistoryAdapter<Data>({
}
});
},
pause: makeStateOperator<HistoryState<Data>>((state) => {
state.paused = true;
}),
resume: makeStateOperator<HistoryState<Data>>((state) => {
state.paused = false;
}),
};
}
Loading

0 comments on commit 75ce360

Please sign in to comment.