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 all 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
175 changes: 122 additions & 53 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ yarn add undo-redo-vuex
### Browser

```html
<script type="text/javascript" src="node_modules/undo-redo-vuex/dist/undo-redo-vuex.min.js"></script>
<script
type="text/javascript"
src="node_modules/undo-redo-vuex/dist/undo-redo-vuex.min.js"
></script>
```

### Module

```js
import undoRedo from 'undo-redo-vuex';
import undoRedo from "undo-redo-vuex";
```

## Usage
Expand All @@ -29,7 +32,7 @@ As a standard [plugin for Vuex](https://vuex.vuejs.org/guide/plugins.html), `und
The `scaffoldStore` helper function will bootstrap a vuex store to setup the `state`, `actions` and `mutations` to work with the plugin.

```js
import { scaffoldStore } from 'undo-redo-vuex';
import { scaffoldStore } from "undo-redo-vuex";

const state = {
list: [],
Expand All @@ -39,25 +42,25 @@ const actions = {
// Define vuex actions as normal
};
const mutations = {
/*
/*
* NB: The emptyState mutation HAS to be impemented.
* This mutation resets the state props to a "base" state,
* on top of which subsequent mutations are "replayed"
* whenever undo/redo is dispatched.
*/
emptyState: (state) => {
*/
emptyState: state => {
// Sets some state prop to an initial value
state.list = [];
},

// Define vuex mutations as normal
}
};

export default scaffoldStore({
state,
actions,
mutations,
namespaced: true // NB: do not include this is non-namespaced stores
namespaced: true, // NB: do not include this is non-namespaced stores
});
```

Expand All @@ -68,7 +71,7 @@ import {
scaffoldState,
scaffoldActions,
scaffoldMutations,
} from 'undo-redo-vuex';
} from "undo-redo-vuex";

const state = {
// Define vuex state properties as normal
Expand All @@ -78,24 +81,24 @@ const actions = {
};
const mutations = {
// Define vuex mutations as normal
}
};

export default {
// Use the respective helper function to scaffold state, actions and mutations
state: scaffoldState(state),
actions: scaffoldActions(actions),
mutations: scaffoldMutations(mutations),
namespaced: true // NB: do not include this is non-namespaced stores
}
namespaced: true, // NB: do not include this is non-namespaced stores
};
```

### store/index.js

- Namespaced modules

```js
import Vuex from 'vuex';
import undoRedo from 'undo-redo-vuex';
import Vuex from "vuex";
import undoRedo from "undo-redo-vuex";

// NB: The following config is used for namespaced store modules.
// Please see below for configuring a non-namespaced (basic) vuex store
Expand All @@ -105,31 +108,31 @@ export default new Vuex.Store({
// The config object for each store module is defined in the 'paths' array
paths: [
{
namespace: 'list',
namespace: "list",
// Any mutations that you want the undo/redo mechanism to ignore
ignoreMutations: ['addShadow', 'removeShadow'],
ignoreMutations: ["addShadow", "removeShadow"],
},
],
}),
],
/*
* For non-namespaced stores:
* state,
* actions,
* mutations,
*/
* For non-namespaced stores:
* state,
* actions,
* mutations,
*/
// Modules for namespaced stores:
modules: {
list,
}
},
});
```

- Non-namespaced (basic) vuex store

```js
import Vuex from 'vuex';
import undoRedo from 'undo-redo-vuex';
import Vuex from "vuex";
import undoRedo from "undo-redo-vuex";

export default new Vuex.Store({
state,
Expand All @@ -139,7 +142,7 @@ export default new Vuex.Store({
plugins: [
undoRedo({
// NB: Include 'ignoreMutations' at root level, and do not provide the list of 'paths'.
ignoreMutations: ['addShadow', 'removeShadow'],
ignoreMutations: ["addShadow", "removeShadow"],
}),
],
});
Expand All @@ -150,16 +153,16 @@ export default new Vuex.Store({
- Vue SFC (.vue)

```js
import { mapState } from 'vuex';
import { mapState } from "vuex";

const MyComponent = {
computed: {
...mapState({
undoButtonEnabled: 'canUndo',
redoButtonEnabled: 'canRedo',
undoButtonEnabled: "canUndo",
redoButtonEnabled: "canRedo",
}),
}
}
},
};
```

### Undoing actions with `actionGroups`
Expand All @@ -172,14 +175,14 @@ In certain scenarios, undo/redo is required on store actions which may consist o
const actions = {
myAction({ commit }, payload) {
// An arbitrary label to identify the group of mutations to undo/redo
const actionGroup = 'myAction';
const actionGroup = "myAction";

// All mutation payloads should contain the actionGroup property
commit('mutationA', {
commit("mutationA", {
...payload,
actionGroup,
});
commit('mutationB', {
commit("mutationB", {
someProp: true,
actionGroup,
});
Expand All @@ -192,19 +195,81 @@ const actions = {
```js
// After dispatching 'myAction' once
done = [
{ type: 'mutationA', payload: { ...payload, actionGroup: 'myAction' } },
{ type: 'mutationB', payload: { someProp: true, actionGroup: 'myAction' } },
{ type: "mutationA", payload: { ...payload, actionGroup: "myAction" } },
{ type: "mutationB", payload: { someProp: true, actionGroup: "myAction" } },
];
undone = [];

// After dispatching 'undo'
done = [];
undone = [
{ type: 'mutationA', payload: { ...payload, actionGroup: 'myAction' } },
{ type: 'mutationB', payload: { someProp: true, actionGroup: 'myAction' } },
{ type: "mutationA", payload: { ...payload, actionGroup: "myAction" } },
{ type: "mutationB", payload: { someProp: true, actionGroup: "myAction" } },
];
```

### 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 that contain asynchronous calls to an API/database service:
For instance, `commit("list/addItem", item)` could be 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 also applies if the first API call is the `DELETE` method (`PUT` would have 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).

This scenario can be implemented by providing the respective action 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 Expand Up @@ -240,25 +305,28 @@ stack by popping the done stack and &#39;replays&#39; all mutations in the done
<a name="store/plugins/undoRedo"></a>

### store/plugins/undoRedo(options) ⇒ <code>function</code>

The Undo-Redo plugin module

**Returns**: <code>function</code> - plugin - the plugin function which accepts the store parameter
**Returns**: <code>function</code> - plugin - the plugin function which accepts the store parameter

| Param | Type | Description |
| --- | --- | --- |
| options | <code>Object</code> | |
| options.namespace | <code>String</code> | The named vuex store module |
| Param | Type | Description |
| ----------------------- | --------------------------------- | ------------------------------------------------------------------- |
| options | <code>Object</code> | |
| options.namespace | <code>String</code> | The named vuex store module |
| options.ignoreMutations | <code>Array.&lt;String&gt;</code> | The list of store mutations (belonging to the module) to be ignored |

<a name="store/plugins/undoRedo_redo"></a>

### store/plugins/undoRedo:redo()

The Redo function - commits the latest undone mutation to the store,
and pushes it to the done stack

<a name="store/plugins/undoRedo_undo"></a>

### store/plugins/undoRedo:undo()

The Undo function - pushes the latest done mutation to the top of the undone
stack by popping the done stack and 'replays' all mutations in the done stack

Expand All @@ -282,34 +350,35 @@ to chain and initial, empty promise</p>
<a name="store/plugins/undoRedo_pipeActions"></a>

### store/plugins/undoRedo:pipeActions(actions)

Piping async action calls sequentially using Array.prototype.reduce
to chain and initial, empty promise


| Param | Type | Description |
| --- | --- | --- |
| Param | Type | Description |
| ------- | --------------------------------- | ------------------------------------------------------------------ |
| actions | <code>Array.&lt;Object&gt;</code> | The array of objects containing the each action's name and payload |

<a name="store/plugins/undoRedo_getConfig"></a>

### store/plugins/undoRedo:getConfig(namespace) ⇒ <code>Object</code>

Piping async action calls sequentially using Array.prototype.reduce
to chain and initial, empty promise

**Returns**: <code>Object</code> - config - The object containing the undo/redo stacks of the store module
**Returns**: <code>Object</code> - config - The object containing the undo/redo stacks of the store module

| Param | Type | Description |
| --- | --- | --- |
| Param | Type | Description |
| --------- | ------------------- | ---------------------------- |
| namespace | <code>String</code> | The name of the store module |

<a name="store/plugins/undoRedo_setConfig"></a>

### store/plugins/undoRedo:setConfig(namespace, config)

Piping async action calls sequentially using Array.prototype.reduce
to chain and initial, empty promise


| Param | Type | Description |
| --- | --- | --- |
| namespace | <code>String</code> | The name of the store module |
| config | <code>String</code> | The object containing the updated undo/redo stacks of the store module |
| Param | Type | Description |
| --------- | ------------------- | ---------------------------------------------------------------------- |
| namespace | <code>String</code> | The name of the store module |
| config | <code>String</code> | The object containing the updated undo/redo stacks of the store module |
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);
}
};
Loading