Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/clear undo redo #12

Merged
merged 7 commits into from
Sep 3, 2019
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,65 @@ undone = [
];
```

### Working with undo/redo on mutations produced by side effects (i.e. API/database calls

In Vue.js apps, vuex mutations are often committed inside actions, along side asynchronous calls to an API/database servive: e.g. `commit("list/addItem", item)` is called after an axios request to `PUT https://<your-rest-api>/v1/list`. When undoing the `commit("list/addItem", item)` mutation, an appropriate API call is required to `DELETE` this item. The inverse applies if the first API call is the `DELETE` method, i.e. `PUT` has to be called when the `commit("list/removeItem", item)` is undone. View the unit test for this feature [here](https://github.com/factorial-io/undo-redo-vuex/blob/b2a61ae92aad8c76ed9328021d2c6fc62ccc0774/tests/unit/test.basic.spec.ts#L66).

The scenario can be implemented by providing the respective actions names as the `undoCallback` and `redoCallback` fields in the mutation payload (NB: the payload object should be parameterized as an object literal):

```js
const actions = {
saveItem: async (({ commit }), item) => {
await axios.put(PUT_ITEM, item);
commit("addItem", {
item,
// dispatch("deleteItem", { item }) on undo()
undoCallback: "deleteItem",
// dispatch("saveItem", { item }) on redo()
redoCallback: "saveItem"
});
},
deleteItem: async (({ commit }), item) => {
await axios.delete(DELETE_ITEM, item);
commit("removeItem", {
item,
// dispatch("saveItem", { item }) on undo()
undoCallback: "saveItem",
// dispatch("deleteItem", { item }) on redo()
redoCallback: "deleteItem"
});
}
};

const mutations = {
// NB: payload parameters as object literal props
addItem: (state, { item }) => {
// adds the item to the list
},
removeItem: (state, { item }) => {
// removes the item from the list
}
};
```

### Clearing the undo/redo stacks with the `clear` action

The internal `done` and `undone` stacks used to track mutations in the vuex store/modules can be cleared (i.e. emptied) with the `clear` action. This action is scaffolded when using `scaffoldActions(actions)` of `scaffoldStore(store)`. This enhancement is described further in [issue #7](https://github.com/factorial-io/undo-redo-vuex/issues/7#issuecomment-490073843), with accompanying [unit tests](https://github.com/factorial-io/undo-redo-vuex/commit/566a13214d0804ab09f63fcccf502cb854327f8e).

```js
/**
* Current done stack: [mutationA, mutation B]
* Current undone stack: [mutationC]
**/
this.$store.dispatch("list/clear");

await this.$nextTick();
/**
* Current done stack: []
* Current undone stack: []
**/
```

## Testing and test scenarios

Development tests are run using the [Jest](https://jestjs.io/) test runner. The `./tests/store` directory contains a basic Vuex store with a namespaced `list` module.
Expand Down
39 changes: 39 additions & 0 deletions src/clear.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { EMPTY_STATE } from "./constants";
import {
getConfig,
pipeActions,
setConfig,
updateCanUndoRedo
} from "./utils-undo-redo";

export default ({
paths,
store
}: {
paths: UndoRedoOptions[];
store: any;
}) => async (namespace: string) => {
const config = getConfig(paths)(namespace);

if (Object.keys(config).length) {
const undoCallbacks = config.done.map(({ payload }: { payload: any }) => ({
action: payload.undoCallback ? `${namespace}${payload.undoCallback}` : "",
payload
}));
await pipeActions(store)(undoCallbacks);

const done: [] = [];
const undone: [] = [];
config.newMutation = false;
store.commit(`${namespace}${EMPTY_STATE}`);

config.newMutation = true;
setConfig(paths)(namespace, {
...config,
done,
undone
});

updateCanUndoRedo({ paths, store })(namespace);
}
};
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export const EMPTY_STATE = "emptyState";
export const UPDATE_CAN_UNDO_REDO = "updateCanUndoRedo";
export const REDO = "redo";
export const UNDO = "undo";
export const CLEAR = "clear";
16 changes: 14 additions & 2 deletions src/undoRedo.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
/* eslint-disable no-param-reassign, no-shadow */
import { EMPTY_STATE, UPDATE_CAN_UNDO_REDO, REDO, UNDO } from "./constants";
import {
EMPTY_STATE,
UPDATE_CAN_UNDO_REDO,
REDO,
UNDO,
CLEAR
} from "./constants";
import { getConfig, setConfig, updateCanUndoRedo } from "./utils-undo-redo";
import execRedo from "./redo";
import execUndo from "./undo";
import execClear from "./clear";

// Logic based on: https://github.com/anthonygore/vuex-undo-redo

const noop = () => {};
export const undo = noop;
export const redo = noop;
export const clear = noop;

export const scaffoldState = (state: any) => ({
...state,
Expand All @@ -19,7 +27,8 @@ export const scaffoldState = (state: any) => ({
export const scaffoldActions = (actions: any) => ({
...actions,
undo,
redo
redo,
clear
});

export const scaffoldMutations = (mutations: any) => ({
Expand Down Expand Up @@ -147,6 +156,9 @@ export default (options: UndoRedoOptions = {}) => (store: any) => {
if (canUndo(paths)(namespace))
await execUndo({ paths, store })(namespace);
break;
case `${namespace}${CLEAR}`:
await execClear({ paths, store })(namespace);
break;
default:
break;
}
Expand Down
34 changes: 33 additions & 1 deletion tests/unit/test.basic.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Vue from "vue";
import store from "../store";
import { undo, redo } from "./utils-test";
import { undo, redo, clear } from "./utils-test";

const item = {
foo: "bar"
Expand Down Expand Up @@ -95,4 +95,36 @@ describe("Simple testing for undo/redo on a namespaced vuex store", () => {
// Check shadow: should contain no items
expect(state.list.shadow).toEqual([]);
});

it('"clear" should return the state to an empty list', async () => {
await clear(store)("list");
const expectedState: [] = [];

expect(state.list.list).toEqual(expectedState);
});

it('"canUndo" and "canRedo" should be reset', () => {
expect(state.list.canUndo).toBeFalsy();
expect(state.list.canUndo).toBeFalsy();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you meant canRedo here.

});

it("Add item to list", () => {
const expectedState = [{ ...item }];

// Commit the item to the store and assert
store.commit("list/addItem", { item });

expect(state.list.list).toEqual(expectedState);
});

it("Check 'canUndo' value; The undo function should remove the item", async () => {
expect(state.list.canUndo).toBeTruthy();

await undo(store)("list");
await Vue.nextTick();

// Check 'canUndo' value, Assert list items after undo
expect(state.list.canUndo).toBeFalsy();
expect(state.list.list).toEqual([]);
});
});
5 changes: 5 additions & 0 deletions tests/unit/utils-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ export const undo = (store: any) => async (namespace: string = "") => {
await store.dispatch(`${namespace ? `${namespace}/` : ""}undo`);
await Vue.nextTick();
};

export const clear = (store: any) => async (namespace: string = "") => {
await store.dispatch(`${namespace ? `${namespace}/` : ""}clear`);
await Vue.nextTick();
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div>
<template v-if="isAuthenticated">
<input v-model="newTodo" type="text" @keyup.enter.prevent="postNewTodo">
<todos v-bind="todoData" @postNewTodo="postNewTodo" @undo="undo" @redo="redo"/>
<todos v-bind="todoData" @postNewTodo="postNewTodo" @undo="undo" @redo="redo" @clear="clear" />
</template>
<button @click="() => (isAuthenticated ? logout() : login())">
{{ isAuthenticated ? 'Logout' : 'Login' }}
Expand All @@ -21,7 +21,7 @@ export default {
Todos
},
setup() {
const { useStore, list, canUndo, canRedo, label, undo, redo } = useList();
const { useStore, list, canUndo, canRedo, label, undo, redo, clear } = useList();
const { isAuthenticated, login, logout } = useAuth();
const newTodo = value("");
const store = useStore();
Expand All @@ -47,6 +47,7 @@ export default {
postNewTodo,
undo,
redo,
clear,
isAuthenticated,
login,
logout
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div>
<template v-if="isAuthenticated">
<input v-model="newTodo" type="text" @keyup.enter.prevent="postNewTodo">
<todos v-bind="todoData" @postNewTodo="postNewTodo" @undo="undo" @redo="redo"/>
<todos v-bind="todoData" @postNewTodo="postNewTodo" @undo="undo" @redo="redo" @clear="clear" />
</template>
<button @click="() => (isAuthenticated ? logout() : login())">
{{ isAuthenticated ? 'Logout' : 'Login' }}
Expand Down Expand Up @@ -48,7 +48,7 @@ export default Vue.extend({
},
methods: {
...mapMutations("list", ["addItem"]),
...mapActions("list", ["undo", "redo"]),
...mapActions("list", ["undo", "redo", "clear"]),
...mapActions("auth", ["login", "logout"]),
postNewTodo() {
// @ts-ignore
Expand Down
1 change: 1 addition & 0 deletions tests/vue-undo-redo-demo/src/components/Todos/Todos.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<button id="post" @click="$emit('postNewTodo')">Add</button>
<button id="undo" :disabled="!canUndo" @click="$emit('undo')">Undo</button>
<button id="redo" :disabled="!canRedo" @click="$emit('redo')">Redo</button>
<button id="clear" :disabled="!canUndo && !canRedo" @click="$emit('clear')">Clear undo/redo</button>
<h4>{{ label }}</h4>
<ol>
<li v-for="(todo, i) in list" :key="i">{{ todo }}</li>
Expand Down
4 changes: 3 additions & 1 deletion tests/vue-undo-redo-demo/src/utils/utils-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const useList = () => {

const undo = () => store.dispatch("list/undo");
const redo = () => store.dispatch("list/redo");
const clear = () => store.dispatch("list/clear");

return {
useStore,
Expand All @@ -24,7 +25,8 @@ export const useList = () => {
canRedo,
label,
undo,
redo
redo,
clear
};
};

Expand Down
2 changes: 1 addition & 1 deletion tests/vue-undo-redo-demo/src/views/Home.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

<script lang="ts">
import Vue from "vue";
import TodosContainer from "../components/Todos/Todos.container.function.based.vue";
import TodosContainer from "../components/Todos/Todos.container.vue";

export default Vue.extend({
name: "home",
Expand Down