├── .eslintignore ├── demo ├── .browserslistrc ├── babel.config.js ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── assets │ │ └── logo.png │ ├── main.js │ ├── App.vue │ ├── store │ │ └── index.js │ └── components │ │ ├── CTodos.vue │ │ └── HelloWorld.vue ├── .gitignore ├── README.md ├── .eslintrc.js └── package.json ├── .browserslistrc ├── vue.config.js ├── tests ├── unit │ ├── .eslintrc.js │ ├── utils-test.ts │ ├── test.multiple-undo.spec.ts │ ├── test.action-group.spec.ts │ ├── test.advance-action-groups.spec.ts │ ├── test.basic.spec.ts │ ├── test.expose-undo-redo-stacks.spec.ts │ ├── test.non-namespaced.spec.ts │ └── test.reset.spec.ts ├── store │ ├── index.ts │ └── modules │ │ ├── auth.ts │ │ └── list.ts └── store-non-namespaced │ └── index.ts ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── coverage ├── lcov-report │ ├── sort-arrow-sprite.png │ ├── prettify.css │ ├── src │ │ ├── constants.ts.html │ │ ├── index.html │ │ ├── utils-undo-redo.ts.html │ │ ├── redo.ts.html │ │ └── undo.ts.html │ ├── tests │ │ ├── unit │ │ │ ├── utils-test.ts.html │ │ │ └── index.html │ │ ├── store │ │ │ ├── index.html │ │ │ ├── modules │ │ │ │ ├── index.html │ │ │ │ ├── list.ts.html │ │ │ │ └── auth.ts.html │ │ │ └── index.ts.html │ │ └── store-non-namespaced │ │ │ ├── index.html │ │ │ └── index.ts.html │ ├── sorter.js │ ├── base.css │ └── index.html └── lcov.info ├── babel.config.js ├── docs ├── guide │ ├── demo.md │ ├── testing.md │ ├── installation.md │ └── usage.md ├── README.md ├── .vuepress │ └── config.js └── api │ └── README.md ├── .gitignore ├── src ├── constants.ts ├── types.d.ts ├── reset.ts ├── clear.ts ├── utils-undo-redo.ts ├── redo.ts ├── undo.ts └── undoRedo.ts ├── .eslintrc.js ├── deploy.sh ├── jest.config.js ├── tsconfig.json ├── LICENSE ├── rollup.config.js └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | tests/vue-undo-redo-demo -------------------------------------------------------------------------------- /demo/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | lintOnSave: false 3 | }; 4 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/factorial-io/undo-redo-vuex/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /demo/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/factorial-io/undo-redo-vuex/HEAD/demo/public/favicon.ico -------------------------------------------------------------------------------- /demo/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/factorial-io/undo-redo-vuex/HEAD/demo/src/assets/logo.png -------------------------------------------------------------------------------- /coverage/lcov-report/sort-arrow-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/factorial-io/undo-redo-vuex/HEAD/coverage/lcov-report/sort-arrow-sprite.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ["@babel/preset-env", { modules: false, useBuiltIns: "usage" }], 4 | "@babel/typescript" 5 | ] 6 | }; 7 | -------------------------------------------------------------------------------- /demo/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import store from './store' 4 | 5 | Vue.config.productionTip = false 6 | 7 | new Vue({ 8 | store, 9 | render: h => h(App) 10 | }).$mount('#app') 11 | -------------------------------------------------------------------------------- /docs/guide/demo.md: -------------------------------------------------------------------------------- 1 | --- 2 | prev: ./testing 3 | --- 4 | 5 | # Demo Vue.js app 6 | 7 | A demo Vue.js TODO application featuring this plugin is included in the `/demo` directory. 8 | 9 | Simply run a `yarn install` and then `yarn serve` in the `/demo` directory. -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroImage: http://logo.factorial.io/color.svg 4 | heroText: "Undo/Redo Vuex" 5 | tagline: Undo/Redo Vuex is a Vuex Store Plugin for module namespaced undo and redo functionality. 6 | actionText: Get Started → 7 | actionLink: /guide/installation/ 8 | footer: A Factorial Open Source Project 9 | --- -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | 4 | # local env files 5 | .env.local 6 | .env.*.local 7 | 8 | # Log files 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Editor directories and files 14 | .idea 15 | .vscode 16 | *.suo 17 | *.ntvs* 18 | *.njsproj 19 | *.sln 20 | *.sw* 21 | 22 | docs/.vuepress/dist 23 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const EMPTY_STATE = "emptyState"; 2 | export const RESET_STATE = "resetState"; 3 | export const UPDATE_CAN_UNDO_REDO = "updateCanUndoRedo"; 4 | export const REDO = "redo"; 5 | export const UNDO = "undo"; 6 | export const CLEAR = "clear"; 7 | export const RESET = "reset"; 8 | export const UPDATE_UNDO_REDO_CONFIG = "updateUndoRedoConfig"; 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | 4 | env: { 5 | node: true 6 | }, 7 | 8 | extends: ["plugin:vue/essential", "@vue/prettier", "@vue/typescript"], 9 | 10 | rules: { 11 | "no-console": "off", 12 | "no-debugger": "off" 13 | }, 14 | 15 | parserOptions: { 16 | parser: "typescript-eslint-parser" 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # demo 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | yarn lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /demo/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | 'eslint:recommended' 9 | ], 10 | parserOptions: { 11 | parser: 'babel-eslint' 12 | }, 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare interface UndoRedoOptions { 2 | namespace?: string; 3 | ignoreMutations?: Array; 4 | paths?: Array; 5 | newMutation?: boolean; 6 | done?: Array | []; 7 | undone?: Array | []; 8 | exposeUndoRedoConfig?: boolean; 9 | } 10 | 11 | declare interface Mutation { 12 | type: string; 13 | payload: any; 14 | } 15 | 16 | declare interface Action { 17 | type: string; 18 | payload: any; 19 | } 20 | -------------------------------------------------------------------------------- /docs/guide/testing.md: -------------------------------------------------------------------------------- 1 | --- 2 | prev: ./usage 3 | next: ./demo 4 | --- 5 | 6 | # Testing and test scenarios 7 | 8 | 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. 9 | 10 | The test blocks (each `it()` declaration) in `./tests/unit` directory are grouped to mimic certain user interactions with the store, making it possible to track the change in state over time. 11 | 12 | ```js 13 | yarn test 14 | ``` -------------------------------------------------------------------------------- /demo/src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | 19 | 29 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # abort on errors 4 | set -e 5 | 6 | # build 7 | yarn run docs:build 8 | 9 | # navigate into the build output directory 10 | cd docs/.vuepress/dist 11 | 12 | # if you are deploying to a custom domain 13 | # echo 'www.example.com' > CNAME 14 | 15 | git init 16 | git add -A 17 | git commit -m 'deploy' 18 | 19 | # if you are deploying to https://.github.io 20 | # git push -f git@github.com:/.github.io.git master 21 | 22 | # if you are deploying to https://.github.io/ 23 | git push -f git@github.com:factorial-io/undo-redo-vuex.git master:gh-pages 24 | 25 | cd - -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ["js", "jsx", "json", "vue", "ts", "tsx"], 3 | transform: { 4 | "^.+\\.vue$": "vue-jest", 5 | ".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$": 6 | "jest-transform-stub", 7 | "^.+\\.tsx?$": "ts-jest" 8 | }, 9 | moduleNameMapper: { 10 | "^@/(.*)$": "/src/$1" 11 | }, 12 | snapshotSerializers: ["jest-serializer-vue"], 13 | testMatch: [ 14 | "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)" 15 | ], 16 | testPathIgnorePatterns: ["/tests/vue-undo-redo-demo/"], 17 | testURL: "http://localhost/" 18 | }; 19 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | undo-redo-vuex 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /demo/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import {default as undoRedo, scaffoldStore} from 'undo-redo-vuex'; 4 | 5 | Vue.use(Vuex); 6 | 7 | const state = { 8 | todos: [] 9 | }; 10 | 11 | const actions = {}; 12 | 13 | const mutations = { 14 | addTodo(state, payload) { 15 | state.todos = [...state.todos, payload]; 16 | }, 17 | emptyState(state) { 18 | state.todos = []; 19 | } 20 | }; 21 | 22 | export default new Vuex.Store( 23 | scaffoldStore({ 24 | state, 25 | actions, 26 | mutations, 27 | plugins: [ 28 | undoRedo() 29 | ] 30 | }) 31 | ); 32 | 33 | -------------------------------------------------------------------------------- /coverage/lcov-report/prettify.css: -------------------------------------------------------------------------------- 1 | .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} 2 | -------------------------------------------------------------------------------- /demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.6.4", 12 | "undo-redo-vuex": "^1.2.0", 13 | "vue": "^2.6.11", 14 | "vuex": "^3.1.2" 15 | }, 16 | "devDependencies": { 17 | "@vue/cli-plugin-babel": "~4.2.0", 18 | "@vue/cli-plugin-eslint": "~4.2.0", 19 | "@vue/cli-plugin-vuex": "~4.2.0", 20 | "@vue/cli-service": "~4.2.0", 21 | "babel-eslint": "^10.0.3", 22 | "eslint": "^6.7.2", 23 | "eslint-plugin-vue": "^6.1.2", 24 | "vue-template-compiler": "^2.6.11" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docs/guide/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | next: ./usage 3 | --- 4 | 5 | # Installation 6 | 7 | First add the package to your project by adding it via yarn or npm: 8 | 9 | **Install it with Yarn:** 10 | ```shell script 11 | yarn add undo-redo-vuex 12 | ``` 13 | 14 | **or with NPM:** 15 | ```shell script 16 | npm i undo-redo-vuex 17 | ``` 18 | 19 | ## Import as Module 20 | 21 | Simply import the module in your project: 22 | 23 | ```javascript 24 | import undoRedo from "undo-redo-vuex"; 25 | ``` 26 | 27 | ## Use in Browser 28 | 29 | Or you can also load the plugin in the browser by using the following snippet: 30 | 31 | ```html 32 | 36 | ``` 37 | -------------------------------------------------------------------------------- /tests/unit/utils-test.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | 3 | export const redo = (store: any) => async (namespace: string = "") => { 4 | await store.dispatch(`${namespace ? `${namespace}/` : ""}redo`); 5 | await Vue.nextTick(); 6 | }; 7 | 8 | export const undo = (store: any) => async (namespace: string = "") => { 9 | await store.dispatch(`${namespace ? `${namespace}/` : ""}undo`); 10 | await Vue.nextTick(); 11 | }; 12 | 13 | export const clear = (store: any) => async (namespace: string = "") => { 14 | await store.dispatch(`${namespace ? `${namespace}/` : ""}clear`); 15 | await Vue.nextTick(); 16 | }; 17 | 18 | export const reset = (store: any) => async (namespace: string = "") => { 19 | await store.dispatch(`${namespace ? `${namespace}/` : ""}reset`); 20 | await Vue.nextTick(); 21 | }; 22 | -------------------------------------------------------------------------------- /src/reset.ts: -------------------------------------------------------------------------------- 1 | import { RESET_STATE } from "./constants"; 2 | import { getConfig, setConfig, updateCanUndoRedo } from "./utils-undo-redo"; 3 | 4 | export default ({ 5 | paths, 6 | store 7 | }: { 8 | paths: UndoRedoOptions[]; 9 | store: any; 10 | }) => async (namespace: string) => { 11 | const config = getConfig(paths)(namespace); 12 | 13 | if (Object.keys(config).length) { 14 | const done: [] = []; 15 | const undone: [] = []; 16 | config.newMutation = false; 17 | store.commit(`${namespace}${RESET_STATE}`); 18 | 19 | config.newMutation = true; 20 | 21 | setConfig(paths)( 22 | namespace, 23 | { 24 | ...config, 25 | done, 26 | undone 27 | }, 28 | store 29 | ); 30 | 31 | updateCanUndoRedo({ paths, store })(namespace); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "sourceMap": true, 12 | "baseUrl": ".", 13 | "types": [ 14 | "node", 15 | "jest" 16 | ], 17 | "paths": { 18 | "@/*": [ 19 | "src/*" 20 | ] 21 | }, 22 | "lib": [ 23 | "esnext", 24 | "dom", 25 | "dom.iterable", 26 | "scripthost" 27 | ] 28 | }, 29 | "include": [ 30 | "src/**/*.ts", 31 | "src/**/*.tsx", 32 | "src/**/*.vue", 33 | "tests/**/*.ts", 34 | "tests/**/*.tsx" 35 | ], 36 | "exclude": [ 37 | "node_modules" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /src/clear.ts: -------------------------------------------------------------------------------- 1 | import { EMPTY_STATE } from "./constants"; 2 | import { 3 | getConfig, 4 | pipeActions, 5 | setConfig, 6 | updateCanUndoRedo 7 | } from "./utils-undo-redo"; 8 | 9 | export default ({ 10 | paths, 11 | store 12 | }: { 13 | paths: UndoRedoOptions[]; 14 | store: any; 15 | }) => async (namespace: string) => { 16 | const config = getConfig(paths)(namespace); 17 | 18 | if (Object.keys(config).length) { 19 | const undoCallbacks = config.done.map(({ payload }: { payload: any }) => ({ 20 | action: payload.undoCallback ? `${namespace}${payload.undoCallback}` : "", 21 | payload 22 | })); 23 | await pipeActions(store)(undoCallbacks); 24 | 25 | const done: [] = []; 26 | const undone: [] = []; 27 | config.newMutation = false; 28 | store.commit(`${namespace}${EMPTY_STATE}`); 29 | 30 | config.newMutation = true; 31 | setConfig(paths)( 32 | namespace, 33 | { 34 | ...config, 35 | done, 36 | undone 37 | }, 38 | store 39 | ); 40 | 41 | updateCanUndoRedo({ paths, store })(namespace); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /tests/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vuex from "vuex"; 2 | import Vue from "vue"; 3 | import list from "./modules/list"; 4 | import auth from "./modules/auth"; 5 | import undoRedo from "../../src/undoRedo"; 6 | 7 | Vue.use(Vuex); 8 | 9 | const debug = process.env.NODE_ENV !== "production"; 10 | 11 | const listUndoRedoConfig = { 12 | namespace: "list", 13 | ignoreMutations: ["addShadow", "removeShadow"] 14 | }; 15 | 16 | export const store = { 17 | plugins: [ 18 | undoRedo({ 19 | paths: [listUndoRedoConfig] 20 | }) 21 | ], 22 | modules: { 23 | list, 24 | auth 25 | }, 26 | strict: debug 27 | }; 28 | 29 | export const getExposedConfigStore = () => { 30 | return new Vuex.Store({ 31 | plugins: [ 32 | undoRedo({ 33 | paths: [ 34 | { 35 | ...listUndoRedoConfig, 36 | exposeUndoRedoConfig: true 37 | } 38 | ] 39 | }) 40 | ], 41 | modules: { 42 | list, 43 | auth 44 | }, 45 | strict: false 46 | }); 47 | }; 48 | 49 | export default new Vuex.Store({ 50 | ...store, 51 | strict: false 52 | }); 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Factorial GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/unit/test.multiple-undo.spec.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import store from "../store-non-namespaced"; 3 | import { undo, redo } from "./utils-test"; 4 | 5 | const state: any = store.state; 6 | 7 | const item = { 8 | foo: "bar" 9 | }; 10 | 11 | describe("Testing multiple undo/redo in a vuex store", () => { 12 | it("Add 3 items to list and undo once", async () => { 13 | const expectedState = [{ ...item }, { ...item }, { ...item }]; 14 | 15 | // Commit the item to the store and assert 16 | store.commit("addItem", { item }); 17 | store.commit("addItem", { item }); 18 | store.commit("addItem", { item }); 19 | expect(state.list).toEqual(expectedState); 20 | 21 | // The undo function should remove the item 22 | // Undo twice 23 | await undo(store)(); 24 | await Vue.nextTick(); 25 | 26 | await undo(store)(); 27 | await Vue.nextTick(); 28 | 29 | // Assert list items after undos 30 | expect(state.list).toEqual([{ ...item }]); 31 | }); 32 | 33 | it("Assert list items after redo", async () => { 34 | // Redo twice 35 | await redo(store)(); 36 | await redo(store)(); 37 | expect(state.list).toEqual([{ ...item }, { ...item }, { ...item }]); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | base: "/undo-redo-vuex/", 3 | title: "Undo/Redo Vuex", 4 | description: "A Vuex plugin for module namespaced undo and redo functionality.", 5 | theme: require.resolve("@factorial/vuepress-theme"), 6 | themeConfig: { 7 | repo: "factorial-io/undo-redo-vuex", 8 | editLinks: true, 9 | editLinkText: "Help us improve this page!", 10 | sidebarDepth: 0, 11 | displayAllHeaders: true, 12 | docsDir: "docs", 13 | sidebar: "auto", 14 | nav: [ 15 | { 16 | text: "Guide", 17 | items: [ 18 | { 19 | text: "Installation", 20 | link: "/guide/installation/" 21 | }, 22 | { 23 | text: "Usage", 24 | link: "/guide/usage/" 25 | }, 26 | { 27 | text: "Testing", 28 | link: "/guide/testing/" 29 | }, 30 | { 31 | text: "Demo", 32 | link: "/guide/demo/" 33 | } 34 | ] 35 | }, 36 | { 37 | text: "API", 38 | link: "/api/" 39 | } 40 | ] 41 | } 42 | }; -------------------------------------------------------------------------------- /demo/src/components/CTodos.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /tests/store/modules/auth.ts: -------------------------------------------------------------------------------- 1 | const debug = process.env.NODE_ENV !== "production"; 2 | 3 | const state = { 4 | token: "", 5 | status: "" 6 | }; 7 | 8 | const validateToken = (token: string) => token === "login-token"; 9 | 10 | const getters = { 11 | isAuthenticated: (state: any) => validateToken(state.token) 12 | }; 13 | 14 | const apiLogin = (): Promise<{ token: string }> => 15 | new Promise(resolve => { 16 | setTimeout(() => { 17 | resolve({ token: "login-token" }); 18 | }, 200); 19 | }); 20 | const apiLogout = () => 21 | new Promise(resolve => { 22 | setTimeout(() => { 23 | resolve(); 24 | }, 200); 25 | }); 26 | 27 | const actions = { 28 | async login({ commit }: { commit: any }) { 29 | try { 30 | commit("setStatusLoading"); 31 | const { token } = await apiLogin(); 32 | validateToken(token); 33 | commit("setStatusSuccess", { token }); 34 | } catch (e) { 35 | console.error(e); 36 | commit("setStatusError"); 37 | } 38 | }, 39 | async logout({ commit }: { commit: any }) { 40 | try { 41 | commit("setStatusLoading"); 42 | await apiLogout(); 43 | commit("setStatusLoggedOut"); 44 | } catch (e) { 45 | console.error(e); 46 | commit("setStatusError"); 47 | } 48 | } 49 | }; 50 | 51 | const mutations = { 52 | setStatusLoading(state: any) { 53 | state.status = "loading"; 54 | }, 55 | 56 | setStatusSuccess(state: any, { token }: { token: string }) { 57 | state.status = "success"; 58 | state.token = token; 59 | }, 60 | 61 | setToken(state: any, { token }: { token: string }) { 62 | state.token = token; 63 | }, 64 | 65 | setStatusError(state: any) { 66 | state.status = "error"; 67 | state.token = ""; 68 | }, 69 | 70 | setStatusLoggedOut(state: any) { 71 | state.status = "success"; 72 | state.token = ""; 73 | } 74 | }; 75 | 76 | export default { 77 | state, 78 | getters, 79 | actions, 80 | mutations, 81 | namespaced: true, 82 | debug 83 | }; 84 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from "rollup-plugin-babel"; 2 | import commonjs from "rollup-plugin-commonjs"; 3 | import resolve from "rollup-plugin-node-resolve"; 4 | import { terser } from "rollup-plugin-terser"; 5 | import pkg from "./package.json"; 6 | 7 | const extensions = [".js", ".jsx", ".ts", ".tsx"]; 8 | 9 | const name = "undoRedo"; 10 | 11 | export default [ 12 | { 13 | input: "./src/undoRedo.ts", 14 | 15 | // Specify here external modules which you don"t want to include in your bundle (for instance: "lodash", "moment" etc.) 16 | // https://rollupjs.org/guide/en#external-e-external 17 | external: [], 18 | 19 | plugins: [ 20 | // Compile TypeScript/JavaScript files 21 | resolve({ extensions }), 22 | 23 | commonjs(), 24 | 25 | babel({ 26 | extensions, 27 | include: ["src/**/*"], 28 | runtimeHelpers: true 29 | }), 30 | 31 | terser() 32 | ], 33 | 34 | output: [ 35 | { 36 | file: pkg.main, 37 | format: "umd", 38 | name, 39 | exports: "named" 40 | }, 41 | { 42 | file: pkg.module, 43 | format: "es" 44 | } 45 | ] 46 | }, 47 | { 48 | input: "./src/undoRedo.ts", 49 | 50 | // Specify here external modules which you don"t want to include in your bundle (for instance: "lodash", "moment" etc.) 51 | // https://rollupjs.org/guide/en#external-e-external 52 | external: [], 53 | 54 | plugins: [ 55 | // Compile TypeScript/JavaScript files 56 | resolve({ extensions }), 57 | 58 | commonjs(), 59 | 60 | babel({ 61 | extensions, 62 | include: ["src/**/*"], 63 | runtimeHelpers: true 64 | }) 65 | ], 66 | 67 | output: [ 68 | { 69 | file: pkg.main.replace(".min", ""), 70 | format: "umd", 71 | name, 72 | exports: "named" 73 | }, 74 | { 75 | file: pkg.module.replace(".min", ""), 76 | format: "es" 77 | } 78 | ] 79 | } 80 | ]; 81 | -------------------------------------------------------------------------------- /tests/store/modules/list.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign, no-shadow */ 2 | import deepEqual from "fast-deep-equal"; 3 | import { scaffoldStore } from "../../../src/undoRedo"; 4 | 5 | const debug = process.env.NODE_ENV !== "production"; 6 | 7 | const state = { 8 | list: [], 9 | shadow: [], 10 | resetList: [] 11 | }; 12 | 13 | const getters = { 14 | getList: ({ list }: { list: Array }) => list, 15 | getItem: (state: any) => ({ item }: { item: any }) => 16 | state.list.find((i: any) => deepEqual(i, item)), 17 | getShadow: ({ shadow }: { shadow: Array }) => shadow 18 | }; 19 | 20 | interface Context { 21 | commit: Function; 22 | state: any; 23 | getters: any; 24 | rootState: any; 25 | rootGetters: any; 26 | } 27 | 28 | interface Payload { 29 | index?: number; 30 | item?: any; 31 | } 32 | 33 | const actions = { 34 | // NB: add/remove shadow actions to test undo/redo callback actions 35 | addShadow({ commit }: Context, { item }: Payload) { 36 | commit("addShadow", { item }); 37 | }, 38 | removeShadow({ commit }: Context, { index }: Payload) { 39 | commit("removeShadow", { index }); 40 | } 41 | }; 42 | 43 | export const mutations = { 44 | emptyState: (state: any) => { 45 | state.list = [...state.resetList]; 46 | }, 47 | resetState: (state: any) => { 48 | state.resetList = [...state.list]; 49 | }, 50 | addItem: (state: any, { item }: { item: any }) => { 51 | state.list = [...state.list, item]; 52 | }, 53 | updateItem: (state: any, { item, index }: Payload) => { 54 | state.list.splice(index, 1, item); 55 | }, 56 | removeItem: (state: any, { index }: Payload) => { 57 | state.list.splice(index, 1); 58 | }, 59 | addShadow: (state: any, { item }: Payload) => { 60 | state.shadow = [...state.shadow, item]; 61 | }, 62 | removeShadow: (state: any, { index }: Payload) => { 63 | state.shadow.splice(index, 1); 64 | } 65 | }; 66 | 67 | export default scaffoldStore( 68 | { 69 | state, 70 | getters, 71 | actions, 72 | mutations, 73 | namespaced: true, 74 | debug 75 | }, 76 | true 77 | ); 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "undo-redo-vuex", 3 | "version": "1.4.0", 4 | "scripts": { 5 | "build": "rollup --compact -c", 6 | "test:unit": "vue-cli-service test:unit", 7 | "docs:dev": "vuepress dev docs", 8 | "docs:build": "vuepress build docs", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "dependencies": {}, 12 | "devDependencies": { 13 | "@babel/preset-typescript": "^7.3.3", 14 | "@factorial/vuepress-theme": "^1.0.0-alpha.1", 15 | "@types/jest": "^23.3.14", 16 | "@vue/cli-plugin-babel": "^3.0.1", 17 | "@vue/cli-plugin-eslint": "^3.0.1", 18 | "@vue/cli-plugin-typescript": "^3.0.1", 19 | "@vue/cli-plugin-unit-jest": "^3.0.1", 20 | "@vue/cli-service": "^3.0.1", 21 | "@vue/eslint-config-prettier": "^3.0.3", 22 | "@vue/eslint-config-typescript": "^3.0.3", 23 | "@vue/test-utils": "^1.0.0-beta.20", 24 | "babel-core": "7.0.0-bridge.0", 25 | "babel-preset-minify": "^0.5.0", 26 | "clean-webpack-plugin": "^3.0.0", 27 | "husky": "^4.2.5", 28 | "lint-staged": "^7.2.2", 29 | "node-sass": "^4.13.1", 30 | "rollup": "^1.19.4", 31 | "rollup-plugin-babel": "^4.3.3", 32 | "rollup-plugin-commonjs": "^10.0.2", 33 | "rollup-plugin-node-resolve": "^5.2.0", 34 | "rollup-plugin-terser": "^5.1.1", 35 | "ts-jest": "^23.0.0", 36 | "typescript": "^3.0.0", 37 | "vue": "^2.6.10", 38 | "vue-template-compiler": "^2.5.17", 39 | "vuepress": "^1.3.0", 40 | "vuex": "^3.1.1", 41 | "webpack-cli": "^3.3.6" 42 | }, 43 | "husky": { 44 | "hooks": { 45 | "pre-commit": "yarn run lint", 46 | "pre-push": "yarn run lint && yarn test:unit" 47 | } 48 | }, 49 | "lint-staged": { 50 | "*.js": [ 51 | "vue-cli-service lint", 52 | "git add" 53 | ], 54 | "*.vue": [ 55 | "vue-cli-service lint", 56 | "git add" 57 | ] 58 | }, 59 | "keywords": [ 60 | "vuex", 61 | "vue", 62 | "undo/redo" 63 | ], 64 | "repository": { 65 | "type": "git", 66 | "url": "https://github.com/andrewbeng89/undo-redo-vuex.git" 67 | }, 68 | "main": "dist/undo-redo-vuex.umd.min.js", 69 | "module": "dist/undo-redo-vuex.esm.min.js" 70 | } 71 | -------------------------------------------------------------------------------- /demo/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 42 | 43 | 44 | 60 | -------------------------------------------------------------------------------- /tests/store-non-namespaced/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign, no-shadow */ 2 | 3 | import Vuex from "vuex"; 4 | import Vue from "vue"; 5 | import deepEqual from "fast-deep-equal"; 6 | import undoRedo, { scaffoldStore } from "@/undoRedo"; 7 | 8 | Vue.use(Vuex); 9 | 10 | const debug = process.env.NODE_ENV !== "production"; 11 | 12 | interface State { 13 | list: Array; 14 | shadow: Array; 15 | resetList?: Array; 16 | } 17 | 18 | interface Payload { 19 | index: number; 20 | item?: any; 21 | } 22 | 23 | interface Context { 24 | commit: Function; 25 | state: any; 26 | getters: any; 27 | rootState: any; 28 | rootGetters: any; 29 | } 30 | 31 | const state: State = { 32 | list: [], 33 | shadow: [], 34 | resetList: undefined 35 | }; 36 | 37 | const getters = { 38 | getList: ({ list }: State) => list, 39 | getItem: (state: State) => ({ item }: Payload) => 40 | state.list.find(i => deepEqual(i, item)), 41 | getShadow: ({ shadow }: State) => shadow 42 | }; 43 | 44 | const actions = { 45 | // NB: add/remove shadow actions to test undo/redo callback actions 46 | addShadow({ commit }: Context, { item }: Payload) { 47 | commit("addShadow", { item }); 48 | }, 49 | removeShadow({ commit }: Context, { index }: Payload) { 50 | commit("removeShadow", { index }); 51 | } 52 | }; 53 | 54 | const mutations = { 55 | emptyState: (state: State) => { 56 | state.list = []; 57 | }, 58 | resetState: (state: State) => { 59 | state.resetList = [...state.list]; 60 | }, 61 | addItem: (state: State, { item }: Payload) => { 62 | state.list = [...state.list, item]; 63 | }, 64 | updateItem: (state: State, { item, index }: Payload) => { 65 | state.list.splice(index, 1, item); 66 | }, 67 | removeItem: (state: State, { index }: Payload) => { 68 | state.list.splice(index, 1); 69 | }, 70 | addShadow: (state: State, { item }: Payload) => { 71 | state.shadow = [...state.shadow, item]; 72 | }, 73 | removeShadow: (state: State, { index }: Payload) => { 74 | state.shadow.splice(index, 1); 75 | } 76 | }; 77 | 78 | const exposeUndoRedoConfig = true; 79 | 80 | export default new Vuex.Store( 81 | scaffoldStore( 82 | { 83 | plugins: [ 84 | undoRedo({ 85 | ignoreMutations: ["addShadow", "removeShadow"], 86 | exposeUndoRedoConfig 87 | }) 88 | ], 89 | strict: debug, 90 | state, 91 | getters, 92 | actions, 93 | mutations 94 | }, 95 | exposeUndoRedoConfig 96 | ) 97 | ); 98 | -------------------------------------------------------------------------------- /tests/unit/test.action-group.spec.ts: -------------------------------------------------------------------------------- 1 | import store from "../store-non-namespaced"; 2 | import { undo, redo } from "./utils-test"; 3 | 4 | const item = { 5 | foo: "bar" 6 | }; 7 | 8 | const other = { 9 | cool: "idea" 10 | }; 11 | 12 | const state: any = store.state; 13 | 14 | describe("Testing undo/redo of grouped mutations (i.e. Actions)", () => { 15 | it("Add 4 items to list", async () => { 16 | // Commit the items with action groups to the store and assert 17 | store.commit("addItem", { item }); 18 | store.commit("addItem", { item, actionGroup: "firstGroup" }); 19 | store.commit("addItem", { item: other, actionGroup: "firstGroup" }); 20 | store.commit("addItem", { item, actionGroup: "secondGroup" }); 21 | 22 | // Assert items: should contain 4 items 23 | const expectedState = [{ ...item }, { ...item }, { ...other }, { ...item }]; 24 | expect(state.list).toEqual(expectedState); 25 | }); 26 | 27 | it("Undo secondGroup", async () => { 28 | await undo(store)(); 29 | 30 | // Assert items: should contain 3 items 31 | expect(state.list).toEqual([{ ...item }, { ...item }, { ...other }]); 32 | }); 33 | 34 | it("Undo firstGroup", async () => { 35 | await undo(store)(); 36 | 37 | // Assert items: should contain 1 item 38 | expect(state.list).toEqual([{ ...item }]); 39 | }); 40 | 41 | it("Redo firstGroup", async () => { 42 | await redo(store)(); 43 | 44 | // Assert items: should contain 3 items 45 | expect(state.list).toEqual([{ ...item }, { ...item }, { ...other }]); 46 | }); 47 | 48 | it("Redo secondGroup", async () => { 49 | await redo(store)(); 50 | 51 | // Assert items: should contain 4 items 52 | expect(state.list).toEqual([ 53 | { ...item }, 54 | { ...item }, 55 | { ...other }, 56 | { ...item } 57 | ]); 58 | }); 59 | 60 | it('Repeat action: "firstGroup"', () => { 61 | store.commit("addItem", { item, actionGroup: "firstGroup" }); 62 | store.commit("addItem", { item: other, actionGroup: "firstGroup" }); 63 | 64 | // Assert items: should contain 6 items 65 | expect(state.list).toEqual([ 66 | { ...item }, 67 | { ...item }, 68 | { ...other }, 69 | { ...item }, 70 | { ...item }, 71 | { ...other } 72 | ]); 73 | }); 74 | 75 | it("Undo firstGroup", async () => { 76 | await undo(store)(); 77 | 78 | // Assert items: should contain 6 items 79 | expect(state.list).toEqual([ 80 | { ...item }, 81 | { ...item }, 82 | { ...other }, 83 | { ...item } 84 | ]); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /coverage/lcov-report/src/constants.ts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for src/constants.ts 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files / src constants.ts 20 |

21 |
22 |
23 | 100% 24 | Statements 25 | 4/4 26 |
27 |
28 | 100% 29 | Branches 30 | 0/0 31 |
32 |
33 | 100% 34 | Functions 35 | 0/0 36 |
37 |
38 | 100% 39 | Lines 40 | 4/4 41 |
42 |
43 |
44 |
45 |

46 | 
59 | 
1 47 | 2 48 | 3 49 | 4 50 | 55x 51 | 5x 52 | 5x 53 | 5x 54 |  
export const EMPTY_STATE = "emptyState";
55 | export const UPDATE_CAN_UNDO_REDO = "updateCanUndoRedo";
56 | export const REDO = "redo";
57 | export const UNDO = "undo";
58 |  
60 |
61 |
62 | 66 | 67 | 68 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/utils-undo-redo.ts: -------------------------------------------------------------------------------- 1 | import { UPDATE_CAN_UNDO_REDO, UPDATE_UNDO_REDO_CONFIG } from "./constants"; 2 | 3 | /** 4 | * Piping async action calls secquentially using Array.prototype.reduce 5 | * to chain and initial, empty promise 6 | * 7 | * @module store/plugins/undoRedo:getConfig 8 | * @function 9 | * @param {String} namespace - The name of the store module 10 | * @returns {Object} config - The object containing the undo/redo stacks of the store module 11 | */ 12 | export const getConfig: UndoRedoOptions | any = (paths: UndoRedoOptions[]) => ( 13 | namespace: string 14 | ): object => paths.find(path => path.namespace === namespace) || {}; 15 | 16 | // Based on https://gist.github.com/anvk/5602ec398e4fdc521e2bf9940fd90f84 17 | /** 18 | * Piping async action calls secquentially using Array.prototype.reduce 19 | * to chain and initial, empty promise 20 | * 21 | * @module store/plugins/undoRedo:pipeActions 22 | * @function 23 | * @param {Array} actions - The array of objects containing the each 24 | * action's name and payload 25 | */ 26 | export const pipeActions = (store: any) => (actions: Array) => 27 | actions 28 | .filter(({ action }) => !!action) 29 | .reduce( 30 | (promise, { action, payload }) => 31 | promise.then(() => store.dispatch(action, payload)), 32 | Promise.resolve() 33 | ); 34 | 35 | /** 36 | * Piping async action calls secquentially using Array.prototype.reduce 37 | * to chain and initial, empty promise 38 | * 39 | * @module store/plugins/undoRedo:setConfig 40 | * @function 41 | * @param {String} [namespace] - The name of the store module 42 | * @param {Object} config - The object containing the updated undo/redo stacks of the store module 43 | */ 44 | export const setConfig = (paths: UndoRedoOptions[]) => { 45 | return (namespace: string, config: any, store: any = undefined) => { 46 | const pathIndex = paths.findIndex(path => path.namespace === namespace); 47 | paths.splice(pathIndex, 1, config); 48 | 49 | const { exposeUndoRedoConfig } = config; 50 | if (exposeUndoRedoConfig) { 51 | store.commit(`${namespace}${UPDATE_UNDO_REDO_CONFIG}`, config); 52 | } 53 | }; 54 | }; 55 | 56 | const canRedo = (paths: UndoRedoOptions[]) => (namespace: string) => { 57 | const config = getConfig(paths)(namespace); 58 | if (Object.keys(config).length) { 59 | return config.undone.length > 0; 60 | } 61 | return false; 62 | }; 63 | 64 | const canUndo = (paths: UndoRedoOptions[]) => (namespace: string) => { 65 | const config = getConfig(paths)(namespace); 66 | if (config) { 67 | return config.done.length > 0; 68 | } 69 | return false; 70 | }; 71 | 72 | export const updateCanUndoRedo = ({ 73 | paths, 74 | store 75 | }: { 76 | paths: UndoRedoOptions[]; 77 | store: any; 78 | }) => (namespace: string) => { 79 | const undoEnabled = canUndo(paths)(namespace); 80 | const redoEnabled = canRedo(paths)(namespace); 81 | 82 | store.commit(`${namespace}${UPDATE_CAN_UNDO_REDO}`, { 83 | canUndo: undoEnabled 84 | }); 85 | store.commit(`${namespace}${UPDATE_CAN_UNDO_REDO}`, { 86 | canRedo: redoEnabled 87 | }); 88 | }; 89 | -------------------------------------------------------------------------------- /tests/unit/test.advance-action-groups.spec.ts: -------------------------------------------------------------------------------- 1 | import store from "../store-non-namespaced"; 2 | import { undo, redo } from "./utils-test"; 3 | 4 | const state: any = store.state; 5 | 6 | const item = (id: number, label: string) => ({ id, label }); 7 | const firstGroupItems = [ 8 | item(0, "First group item one"), 9 | item(1, "First group item two") 10 | ]; 11 | 12 | const itemWithoutGroup = item(2, "Item without group"); 13 | 14 | const secondGroupItems = [ 15 | item(3, "Second group item one"), 16 | item(4, "Second group item two"), 17 | item(5, "Second group item three") 18 | ]; 19 | 20 | describe("Testing more advanced combinations of grouped mutations", () => { 21 | it("Add all items", async () => { 22 | // Commit the items with action groups to the store and assert 23 | await store.commit("addItem", { 24 | item: firstGroupItems[0], 25 | actionGroup: "firstGroup" 26 | }); 27 | await store.commit("addItem", { 28 | item: firstGroupItems[1], 29 | actionGroup: "firstGroup" 30 | }); 31 | await store.commit("addItem", { item: itemWithoutGroup }); // no group here 32 | await store.commit("addItem", { 33 | item: secondGroupItems[0], 34 | actionGroup: "secondGroup" 35 | }); 36 | await store.commit("addItem", { 37 | item: secondGroupItems[1], 38 | actionGroup: "secondGroup" 39 | }); 40 | await store.commit("addItem", { 41 | item: secondGroupItems[2], 42 | actionGroup: "secondGroup" 43 | }); 44 | 45 | expect(state.list).toEqual([ 46 | ...firstGroupItems, 47 | itemWithoutGroup, 48 | ...secondGroupItems 49 | ]); 50 | }); 51 | 52 | it("Undo secondGroup should remove all secondGroup items", async () => { 53 | await undo(store)(); 54 | expect(state.list).toEqual([...firstGroupItems, itemWithoutGroup]); 55 | }); 56 | 57 | it("Redo secondGroup should restore full list", async () => { 58 | await redo(store)(); 59 | expect(state.list).toEqual([ 60 | ...firstGroupItems, 61 | itemWithoutGroup, 62 | ...secondGroupItems 63 | ]); 64 | }); 65 | 66 | it("Undo secondGroup", async () => { 67 | await undo(store)(); 68 | expect(state.list).toEqual([...firstGroupItems, itemWithoutGroup]); 69 | }); 70 | 71 | it("Undo secondGroup and mutation without group", async () => { 72 | await undo(store)(); 73 | expect(state.list).toEqual(firstGroupItems); 74 | }); 75 | 76 | it("Undo firstGroup as well should leave list empty", async () => { 77 | await undo(store)(); 78 | expect(state.list).toEqual([]); 79 | }); 80 | 81 | it("Redo firstGroup should restore items from firstGroup", async () => { 82 | await redo(store)(); 83 | expect(state.list).toEqual(firstGroupItems); 84 | }); 85 | 86 | it("Redo mutation without group should restore that item", async () => { 87 | await redo(store)(); 88 | expect(state.list).toEqual([...firstGroupItems, itemWithoutGroup]); 89 | }); 90 | 91 | it("Redo secondGroup should restore full list again", async () => { 92 | await redo(store)(); 93 | expect(state.list).toEqual([ 94 | ...firstGroupItems, 95 | itemWithoutGroup, 96 | ...secondGroupItems 97 | ]); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /docs/api/README.md: -------------------------------------------------------------------------------- 1 | # API documentation and reference 2 | 3 | ## undoRedo(options) ⇒ function 4 | 5 | The Undo-Redo plugin module 6 | 7 | **Returns**: function - plugin - the plugin function which accepts the store parameter 8 | 9 | | Param | Type | Description | 10 | | ---------------------------- | --------------------------------- | ------------------------------------------------------------------- | 11 | | options | Object | | 12 | | options.namespace | String | The named vuex store module | 13 | | options.ignoreMutations | Array.<String> | The list of store mutations (belonging to the module) to be ignored | 14 | | options.exposeUndoRedoConfig | Boolean | (Optional) Flag to expose the `done` and `undone` mutation stacks | 15 | 16 | ## undoRedo:redo() 17 | 18 | The Redo function - commits the latest undone mutation to the store, 19 | and pushes it to the done stack 20 | 21 | ## undoRedo:undo() 22 | 23 | The Undo function - pushes the latest done mutation to the top of the undone 24 | stack by popping the done stack and 'replays' all mutations in the done stack 25 | 26 | ## undoRedo:clear() 27 | 28 | The Clear function - empties the done and undone stacks, and re-initializes 29 | the store's state by executing the `emptyState` mutation 30 | 31 | ## undoRedo:reset() 32 | 33 | The Reset function - empties the done and undone stacks, and resets the 34 | store's initial state to the current by executing the `resetState` mutation 35 | 36 | ## undoRedo:pipeActions(actions) 37 | 38 | Piping async action calls sequentially using Array.prototype.reduce 39 | to chain and initial, empty promise 40 | 41 | | Param | Type | Description | 42 | | ------- | --------------------------------- | ------------------------------------------------------------------ | 43 | | actions | Array.<Object> | The array of objects containing the each action's name and payload | 44 | 45 | ## undoRedo:getConfig(namespace) ⇒ Object 46 | 47 | Piping async action calls sequentially using Array.prototype.reduce 48 | to chain and initial, empty promise 49 | 50 | **Returns**: Object - config - The object containing the undo/redo stacks of the store module 51 | 52 | | Param | Type | Description | 53 | | --------- | ------------------- | ---------------------------- | 54 | | namespace | String | The name of the store module | 55 | 56 | ## undoRedo:setConfig(namespace, config) 57 | 58 | Piping async action calls sequentially using Array.prototype.reduce 59 | to chain and initial, empty promise 60 | 61 | | Param | Type | Description | 62 | | --------- | ------------------- | ---------------------------------------------------------------------- | 63 | | namespace | String | The name of the store module | 64 | | config | String | The object containing the updated undo/redo stacks of the store module | 65 | -------------------------------------------------------------------------------- /coverage/lcov-report/tests/unit/utils-test.ts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for tests/unit/utils-test.ts 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files / tests/unit utils-test.ts 20 |

21 |
22 |
23 | 100% 24 | Statements 25 | 9/9 26 |
27 |
28 | 100% 29 | Branches 30 | 6/6 31 |
32 |
33 | 100% 34 | Functions 35 | 4/4 36 |
37 |
38 | 100% 39 | Lines 40 | 7/7 41 |
42 |
43 |
44 |
45 |

46 | 
80 | 
1 47 | 2 48 | 3 49 | 4 50 | 5 51 | 6 52 | 7 53 | 8 54 | 9 55 | 10 56 | 11 57 | 125x 58 |   59 | 15x 60 | 15x 61 | 15x 62 |   63 |   64 | 15x 65 | 15x 66 | 15x 67 |   68 |  
import Vue from "vue";
69 |  
70 | export const redo = (store: any) => async (namespace: string = "") => {
71 |   await store.dispatch(`${namespace ? `${namespace}/` : ""}redo`);
72 |   await Vue.nextTick();
73 | };
74 |  
75 | export const undo = (store: any) => async (namespace: string = "") => {
76 |   await store.dispatch(`${namespace ? `${namespace}/` : ""}undo`);
77 |   await Vue.nextTick();
78 | };
79 |  
81 |
82 |
83 | 87 | 88 | 89 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/redo.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getConfig, 3 | pipeActions, 4 | setConfig, 5 | updateCanUndoRedo 6 | } from "./utils-undo-redo"; 7 | 8 | /** 9 | * The Redo function - commits the latest undone mutation to the store, 10 | * and pushes it to the done stack 11 | * 12 | * @module store/plugins/undoRedo:redo 13 | * @function 14 | */ 15 | export default ({ 16 | paths, 17 | store 18 | }: { 19 | paths: UndoRedoOptions[]; 20 | store: any; 21 | }) => async (namespace: string) => { 22 | const config = getConfig(paths)(namespace); 23 | if (Object.keys(config).length) { 24 | /** 25 | * @var {Array} undone - The updated undone stack 26 | * @var {Array} commits - The list of mutations to be redone 27 | * NB: The reduceRight operation is used to identify the mutation(s) from the 28 | * top of the undone stack to be redone 29 | */ 30 | const { undone, commits } = config.undone.reduceRight( 31 | ( 32 | { 33 | commits, 34 | undone, 35 | proceed 36 | }: { 37 | commits: Array | []; 38 | undone: Array | []; 39 | proceed: boolean; 40 | }, 41 | m: Mutation 42 | ) => { 43 | if (!commits.length) { 44 | // The "topmost" mutation 45 | commits = [m]; 46 | // Do not find more mutations if the mutations does not belong to a group 47 | proceed = !!m.payload.actionGroup; 48 | } else if (!proceed) { 49 | // The mutation(s) to redo have been identified 50 | undone = [m, ...undone]; 51 | } else { 52 | // Find mutations belonging to the same actionGroup 53 | const lastCommit = commits[commits.length - 1]; 54 | const { actionGroup } = lastCommit.payload; 55 | // Stop finding more mutations if the current mutation belongs to 56 | // another actionGroup, or does not have an actionGroup 57 | proceed = 58 | m.payload.actionGroup && m.payload.actionGroup === actionGroup; 59 | commits = [...(proceed ? [m] : []), ...commits]; 60 | undone = [...(proceed ? [] : [m]), ...undone]; 61 | } 62 | 63 | return { commits, undone, proceed }; 64 | }, 65 | { 66 | commits: [], 67 | undone: [], 68 | proceed: true 69 | } 70 | ); 71 | 72 | config.newMutation = false; 73 | // NB: The array of redoCallbacks and respective action payloads 74 | const redoCallbacks = commits 75 | .filter((mutation: Mutation) => mutation.type && mutation.payload) 76 | .map(async ({ type, payload }: Mutation) => { 77 | // NB: Commit each mutation in the redo stack 78 | store.commit( 79 | type, 80 | Array.isArray(payload) ? [...payload] : payload.constructor(payload) 81 | ); 82 | 83 | // Check if there is an redo callback action 84 | const { redoCallback } = payload; 85 | // NB: The object containing the redoCallback action and payload 86 | return { 87 | action: redoCallback ? `${namespace}${redoCallback}` : "", 88 | payload 89 | }; 90 | }); 91 | await pipeActions(store)(await Promise.all(redoCallbacks)); 92 | config.done = [...config.done, ...commits]; 93 | config.newMutation = true; 94 | setConfig(paths)( 95 | namespace, 96 | { 97 | ...config, 98 | undone 99 | }, 100 | store 101 | ); 102 | 103 | updateCanUndoRedo({ paths, store })(namespace); 104 | } 105 | }; 106 | -------------------------------------------------------------------------------- /src/undo.ts: -------------------------------------------------------------------------------- 1 | import { EMPTY_STATE } from "./constants"; 2 | import { 3 | getConfig, 4 | pipeActions, 5 | setConfig, 6 | updateCanUndoRedo 7 | } from "./utils-undo-redo"; 8 | 9 | /** 10 | * The Undo function - pushes the latest done mutation to the top of the undone 11 | * stack by popping the done stack and 'replays' all mutations in the done stack 12 | * 13 | * @module store/plugins/undoRedo:undo 14 | * @function 15 | */ 16 | export default ({ 17 | paths, 18 | store 19 | }: { 20 | paths: UndoRedoOptions[]; 21 | store: any; 22 | }) => async (namespace: string) => { 23 | const config = getConfig(paths)(namespace); 24 | 25 | if (Object.keys(config).length) { 26 | /** 27 | * @var {Array} done - The updated done stack 28 | * @var {Array} commits - The list of mutations which are undone 29 | * NB: The reduceRight operation is used to identify the mutation(s) from the 30 | * top of the done stack to be undone 31 | */ 32 | const { done, commits } = config.done.reduceRight( 33 | ( 34 | { 35 | commits, 36 | done, 37 | proceed 38 | }: { 39 | commits: Array | []; 40 | done: Array | []; 41 | proceed: boolean; 42 | }, 43 | m: Mutation 44 | ) => { 45 | if (!commits.length) { 46 | // The "topmost" mutation from the done stack 47 | commits = [m]; 48 | // Do not find more mutations if the mutations does not belong to a group 49 | proceed = !!m.payload.actionGroup; 50 | } else if (!proceed) { 51 | // Unshift the mutation to the done stack 52 | done = [m, ...done]; 53 | } else { 54 | const lastUndone = commits[commits.length - 1]; 55 | const { actionGroup } = lastUndone.payload; 56 | // Unshift to commits if mutation belongs to the same actionGroup, 57 | // otherwise unshift to the done stack 58 | proceed = 59 | m.payload.actionGroup && m.payload.actionGroup === actionGroup; 60 | commits = [...(proceed ? [m] : []), ...commits]; 61 | done = [...(proceed ? [] : [m]), ...done]; 62 | } 63 | 64 | return { done, commits, proceed }; 65 | }, 66 | { 67 | done: [], 68 | commits: [], 69 | proceed: true 70 | } 71 | ); 72 | 73 | // Check if there are any undo callback actions 74 | const undoCallbacks = commits.map(({ payload }: { payload: any }) => ({ 75 | action: payload.undoCallback ? `${namespace}${payload.undoCallback}` : "", 76 | payload 77 | })); 78 | await pipeActions(store)(undoCallbacks); 79 | 80 | const undone = [...config.undone, ...commits]; 81 | config.newMutation = false; 82 | store.commit(`${namespace}${EMPTY_STATE}`); 83 | const redoCallbacks = done 84 | .filter((mutation: Mutation) => mutation.type && mutation.payload) 85 | .map(async (mutation: Mutation) => { 86 | store.commit( 87 | mutation.type, 88 | Array.isArray(mutation.payload) 89 | ? [...mutation.payload] 90 | : mutation.payload.constructor(mutation.payload) 91 | ); 92 | 93 | // Check if there is an undo callback action 94 | const { redoCallback } = mutation.payload; 95 | return { 96 | action: redoCallback ? `${namespace}${redoCallback}` : "", 97 | payload: mutation.payload 98 | }; 99 | }); 100 | await pipeActions(store)(await Promise.all(redoCallbacks)); 101 | config.newMutation = true; 102 | setConfig(paths)( 103 | namespace, 104 | { 105 | ...config, 106 | done, 107 | undone 108 | }, 109 | store 110 | ); 111 | 112 | updateCanUndoRedo({ paths, store })(namespace); 113 | } 114 | }; 115 | -------------------------------------------------------------------------------- /coverage/lcov-report/tests/store/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for tests/store 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files tests/store 20 |

21 |
22 |
23 | 100% 24 | Statements 25 | 9/9 26 |
27 |
28 | 100% 29 | Branches 30 | 0/0 31 |
32 |
33 | 100% 34 | Functions 35 | 0/0 36 |
37 |
38 | 100% 39 | Lines 40 | 9/9 41 |
42 |
43 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
FileStatementsBranchesFunctionsLines
index.ts
100%9/9100%0/0100%0/0100%9/9
76 |
77 |
78 | 82 | 83 | 84 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /coverage/lcov-report/tests/unit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for tests/unit 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files tests/unit 20 |

21 |
22 |
23 | 100% 24 | Statements 25 | 9/9 26 |
27 |
28 | 100% 29 | Branches 30 | 6/6 31 |
32 |
33 | 100% 34 | Functions 35 | 4/4 36 |
37 |
38 | 100% 39 | Lines 40 | 7/7 41 |
42 |
43 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
FileStatementsBranchesFunctionsLines
utils-test.ts
100%9/9100%6/6100%4/4100%7/7
76 |
77 |
78 | 82 | 83 | 84 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /coverage/lcov-report/tests/store-non-namespaced/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for tests/store-non-namespaced 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files tests/store-non-namespaced 20 |

21 |
22 |
23 | 70.83% 24 | Statements 25 | 17/24 26 |
27 |
28 | 100% 29 | Branches 30 | 0/0 31 |
32 |
33 | 36.36% 34 | Functions 35 | 4/11 36 |
37 |
38 | 73.91% 39 | Lines 40 | 17/23 41 |
42 |
43 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
FileStatementsBranchesFunctionsLines
index.ts
70.83%17/24100%0/036.36%4/1173.91%17/23
76 |
77 |
78 | 82 | 83 | 84 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /tests/unit/test.basic.spec.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import store from "../store"; 3 | import { undo, redo, clear } from "./utils-test"; 4 | 5 | const item = { 6 | foo: "bar" 7 | }; 8 | 9 | const state: any = store.state; 10 | 11 | describe("Simple testing for undo/redo on a namespaced vuex store", () => { 12 | it("Add item to list", () => { 13 | const expectedState = [{ ...item }]; 14 | 15 | // Commit the item to the store and assert 16 | store.commit("list/addItem", { item }); 17 | 18 | expect(state.list.list).toEqual(expectedState); 19 | }); 20 | 21 | it("Check 'canUndo' value; The undo function should remove the item", async () => { 22 | expect(state.list.canUndo).toBeTruthy(); 23 | 24 | await undo(store)("list"); 25 | await Vue.nextTick(); 26 | 27 | // Check 'canUndo' value, Assert list items after undo 28 | expect(state.list.canUndo).toBeFalsy(); 29 | expect(state.list.list).toEqual([]); 30 | }); 31 | 32 | it("Redo 'addItem' commit", async () => { 33 | expect(state.list.canRedo).toBeTruthy(); 34 | await redo(store)("list"); 35 | }); 36 | 37 | it("Grouped mutations: adding two items to the list", async () => { 38 | const anotherItem = { foo: "baz" }; 39 | const expectedState = [{ ...item }, { ...item }, { ...anotherItem }]; 40 | const actionGroup = "myAction"; 41 | 42 | // Commit the items to the store and assert 43 | store.commit("list/addItem", { item, actionGroup }); 44 | store.commit("list/addItem", { item: anotherItem, actionGroup }); 45 | expect(state.list.list).toEqual(expectedState); 46 | }); 47 | 48 | it("Dispatch undo", async () => { 49 | // The undo function should remove the item 50 | await undo(store)("list"); 51 | 52 | // Assert list items after undo: should contain 1 item 53 | expect(state.list.list).toEqual([{ ...item }]); 54 | }); 55 | 56 | it("Redo 'addItem' twice (grouped mutations)", async () => { 57 | // Redo 'addItem' 58 | await redo(store)("list"); 59 | const anotherItem = { foo: "baz" }; 60 | const expectedState = [{ ...item }, { ...item }, { ...anotherItem }]; 61 | 62 | // Assert list items after redo: should contain 3 items 63 | expect(state.list.list).toEqual(expectedState); 64 | }); 65 | 66 | it('"addShadow" action should be dispatched on undo', async () => { 67 | let expectedState = [...state.list.list, item]; 68 | 69 | store.commit("list/addItem", { 70 | index: 0, 71 | item, 72 | undoCallback: "addShadow", 73 | redoCallback: "removeShadow" 74 | }); 75 | expect(state.list.list).toEqual(expectedState); 76 | 77 | await undo(store)("list"); 78 | expectedState = [{ ...item }]; 79 | expect(state.list.shadow).toEqual(expectedState); 80 | }); 81 | 82 | it("Check shadow: should contain 1 item", () => {}); 83 | 84 | it('"removeShadow" should be dispatched on redo', async () => { 85 | // Redo 'addItem' 86 | await redo(store)("list"); 87 | const expectedState = [ 88 | { foo: "bar" }, 89 | { foo: "bar" }, 90 | { foo: "baz" }, 91 | { foo: "bar" } 92 | ]; 93 | 94 | expect(state.list.list).toEqual(expectedState); 95 | // Check shadow: should contain no items 96 | expect(state.list.shadow).toEqual([]); 97 | }); 98 | 99 | it('"clear" should return the state to an empty list', async () => { 100 | await clear(store)("list"); 101 | const expectedState: [] = []; 102 | 103 | expect(state.list.list).toEqual(expectedState); 104 | }); 105 | 106 | it('"canUndo" and "canRedo" should be reset', () => { 107 | expect(state.list.canUndo).toBeFalsy(); 108 | expect(state.list.canRedo).toBeFalsy(); 109 | }); 110 | 111 | it("Add item to list", () => { 112 | const expectedState = [{ ...item }]; 113 | 114 | // Commit the item to the store and assert 115 | store.commit("list/addItem", { item }); 116 | 117 | expect(state.list.list).toEqual(expectedState); 118 | }); 119 | 120 | it("Check 'canUndo' value; The undo function should remove the item", async () => { 121 | expect(state.list.canUndo).toBeTruthy(); 122 | 123 | await undo(store)("list"); 124 | await Vue.nextTick(); 125 | 126 | // Check 'canUndo' value, Assert list items after undo 127 | expect(state.list.canUndo).toBeFalsy(); 128 | expect(state.list.list).toEqual([]); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /tests/unit/test.expose-undo-redo-stacks.spec.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import { getExposedConfigStore } from "../store"; 3 | import { undo, redo, clear } from "./utils-test"; 4 | 5 | const item = { 6 | foo: "bar" 7 | }; 8 | 9 | const store = getExposedConfigStore(); 10 | const state: any = store.state; 11 | 12 | /** 13 | * Check exposed done and undone stacks in the store's state 14 | * after mutation|undo|redo 15 | */ 16 | const getDoneUndone = () => { 17 | const { done, undone } = state.list.undoRedoConfig; 18 | 19 | return { done, undone }; 20 | }; 21 | 22 | describe("Simple testing for undo/redo on a namespaced vuex store", () => { 23 | it("Add item to list", () => { 24 | const expectedState = [{ ...item }]; 25 | 26 | // Commit the item to the store and assert 27 | store.commit("list/addItem", { item }); 28 | 29 | const { done } = getDoneUndone(); 30 | 31 | expect(done).toEqual([ 32 | { 33 | type: "list/addItem", 34 | payload: { 35 | item 36 | } 37 | } 38 | ]); 39 | }); 40 | 41 | it("Check 'canUndo' value; The undo function should remove the item", async () => { 42 | expect(state.list.canUndo).toBeTruthy(); 43 | 44 | await undo(store)("list"); 45 | await Vue.nextTick(); 46 | 47 | const { done, undone } = getDoneUndone(); 48 | 49 | expect(undone).toEqual([ 50 | { 51 | type: "list/addItem", 52 | payload: { 53 | item 54 | } 55 | } 56 | ]); 57 | expect(done).toEqual([]); 58 | }); 59 | 60 | it("Redo 'addItem' commit", async () => { 61 | await redo(store)("list"); 62 | 63 | const { done, undone } = getDoneUndone(); 64 | 65 | expect(done).toEqual([ 66 | { 67 | type: "list/addItem", 68 | payload: { 69 | item 70 | } 71 | } 72 | ]); 73 | expect(undone).toEqual([]); 74 | }); 75 | 76 | it("Grouped mutations: adding two items to the list", async () => { 77 | const anotherItem = { foo: "baz" }; 78 | const actionGroup = "myAction"; 79 | 80 | // Commit the items to the store and assert 81 | store.commit("list/addItem", { item, actionGroup }); 82 | store.commit("list/addItem", { item: anotherItem, actionGroup }); 83 | 84 | const { done, undone } = getDoneUndone(); 85 | 86 | expect(done).toEqual([ 87 | { 88 | type: "list/addItem", 89 | payload: { 90 | item 91 | } 92 | }, 93 | { 94 | type: "list/addItem", 95 | payload: { 96 | item, 97 | actionGroup 98 | } 99 | }, 100 | { 101 | type: "list/addItem", 102 | payload: { 103 | item: anotherItem, 104 | actionGroup 105 | } 106 | } 107 | ]); 108 | expect(undone).toEqual([]); 109 | }); 110 | 111 | it("Dispatch undo", async () => { 112 | const actionGroup = "myAction"; 113 | const anotherItem = { foo: "baz" }; 114 | 115 | // The undo function should remove the item 116 | await undo(store)("list"); 117 | 118 | const { done, undone } = getDoneUndone(); 119 | 120 | expect(done).toEqual([ 121 | { 122 | type: "list/addItem", 123 | payload: { 124 | item 125 | } 126 | } 127 | ]); 128 | expect(undone).toEqual([ 129 | { 130 | type: "list/addItem", 131 | payload: { 132 | item, 133 | actionGroup 134 | } 135 | }, 136 | { 137 | type: "list/addItem", 138 | payload: { 139 | item: anotherItem, 140 | actionGroup 141 | } 142 | } 143 | ]); 144 | }); 145 | 146 | it("Redo 'addItem' twice (grouped mutations)", async () => { 147 | // Redo 'addItem' 148 | await redo(store)("list"); 149 | const actionGroup = "myAction"; 150 | const anotherItem = { foo: "baz" }; 151 | 152 | const { done, undone } = getDoneUndone(); 153 | 154 | expect(done).toEqual([ 155 | { 156 | type: "list/addItem", 157 | payload: { 158 | item 159 | } 160 | }, 161 | { 162 | type: "list/addItem", 163 | payload: { 164 | item, 165 | actionGroup 166 | } 167 | }, 168 | { 169 | type: "list/addItem", 170 | payload: { 171 | item: anotherItem, 172 | actionGroup 173 | } 174 | } 175 | ]); 176 | expect(undone).toEqual([]); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /tests/unit/test.non-namespaced.spec.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import store from "../store-non-namespaced"; 3 | import { undo, redo } from "./utils-test"; 4 | 5 | const state: any = store.state; 6 | 7 | const item = { 8 | foo: "bar" 9 | }; 10 | 11 | const getDoneUndone = () => { 12 | const { done, undone } = state.undoRedoConfig; 13 | 14 | return { done, undone }; 15 | }; 16 | 17 | describe("Testing undo/redo in a non-namespaced vuex store", () => { 18 | it("Add item to list and undo", async () => { 19 | const expectedState = [{ ...item }]; 20 | 21 | // Commit the item to the store and assert 22 | store.commit("addItem", { item }); 23 | expect(state.list).toEqual(expectedState); 24 | 25 | const { done } = getDoneUndone(); 26 | 27 | expect(done).toEqual([ 28 | { 29 | type: "addItem", 30 | payload: { 31 | item 32 | } 33 | } 34 | ]); 35 | }); 36 | 37 | it("Check 'canUndo' value; The undo function should remove the item", async () => { 38 | expect(state.canUndo).toBeTruthy(); 39 | await undo(store)(); 40 | await Vue.nextTick(); 41 | 42 | // Check 'canUndo' value, Assert list items after undo 43 | expect(state.canUndo).toBeFalsy(); 44 | expect(state.list).toEqual([]); 45 | 46 | const { done, undone } = getDoneUndone(); 47 | 48 | expect(undone).toEqual([ 49 | { 50 | type: "addItem", 51 | payload: { 52 | item 53 | } 54 | } 55 | ]); 56 | expect(done).toEqual([]); 57 | }); 58 | 59 | it("Redo 'addItem' commit", async () => { 60 | await redo(store)(); 61 | 62 | // Assert list items after redo 63 | const expectedState = [{ ...item }]; 64 | expect(state.list).toEqual(expectedState); 65 | 66 | const { done, undone } = getDoneUndone(); 67 | 68 | expect(done).toEqual([ 69 | { 70 | type: "addItem", 71 | payload: { 72 | item 73 | } 74 | } 75 | ]); 76 | expect(undone).toEqual([]); 77 | }); 78 | 79 | it("Grouped mutations: adding two items to the list", async () => { 80 | const anotherItem = { foo: "baz" }; 81 | const expectedState = [{ ...item }, { ...item }, { ...anotherItem }]; 82 | const actionGroup = "myAction"; 83 | 84 | // Commit the items to the store and assert 85 | store.commit("addItem", { item, actionGroup }); 86 | store.commit("addItem", { item: anotherItem, actionGroup }); 87 | expect(state.list).toEqual(expectedState); 88 | 89 | const { done, undone } = getDoneUndone(); 90 | 91 | expect(done).toEqual([ 92 | { 93 | type: "addItem", 94 | payload: { 95 | item 96 | } 97 | }, 98 | { 99 | type: "addItem", 100 | payload: { 101 | item, 102 | actionGroup 103 | } 104 | }, 105 | { 106 | type: "addItem", 107 | payload: { 108 | item: anotherItem, 109 | actionGroup 110 | } 111 | } 112 | ]); 113 | expect(undone).toEqual([]); 114 | 115 | // The undo function should remove the item 116 | await undo(store)(); 117 | 118 | // Assert list items after undo: should contain 1 item 119 | expect(state.list).toEqual([{ ...item }]); 120 | }); 121 | 122 | it("Assert list items after redos: should contain 3 items", async () => { 123 | // Redo "addItem" twice 124 | await redo(store)(); 125 | await redo(store)(); 126 | const anotherItem = { foo: "baz" }; 127 | const expectedState = [{ ...item }, { ...item }, { ...anotherItem }]; 128 | 129 | expect(state.list).toEqual(expectedState); 130 | }); 131 | 132 | it("'addShadow' action should be dispatched on undo", async () => { 133 | let expectedState = [...state.list, item]; 134 | 135 | // Redo "addItem" 136 | store.commit("addItem", { 137 | index: 0, 138 | item, 139 | undoCallback: "addShadow", 140 | redoCallback: "removeShadow" 141 | }); 142 | expect(state.list).toEqual(expectedState); 143 | 144 | await undo(store)(); 145 | expectedState = [{ ...item }]; 146 | expect(state.shadow).toEqual(expectedState); 147 | }); 148 | 149 | it("'removeShadow' should be dispatched on redo", async () => { 150 | // Redo "addItem" 151 | await redo(store)(); 152 | const expectedState = [ 153 | { foo: "bar" }, 154 | { foo: "bar" }, 155 | { foo: "baz" }, 156 | { foo: "bar" } 157 | ]; 158 | 159 | // Check shadow: should contain no items 160 | expect(state.list).toEqual(expectedState); 161 | expect(state.shadow).toEqual([]); 162 | }); 163 | }); 164 | -------------------------------------------------------------------------------- /coverage/lcov-report/tests/store/modules/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for tests/store/modules 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files tests/store/modules 20 |

21 |
22 |
23 | 38.98% 24 | Statements 25 | 23/59 26 |
27 |
28 | 100% 29 | Branches 30 | 0/0 31 |
32 |
33 | 21.05% 34 | Functions 35 | 4/19 36 |
37 |
38 | 40.35% 39 | Lines 40 | 23/57 41 |
42 |
43 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
FileStatementsBranchesFunctionsLines
auth.ts
23.68%9/38100%0/00%0/824.32%9/37
list.ts
66.67%14/21100%0/036.36%4/1170%14/20
89 |
90 |
91 | 95 | 96 | 97 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /tests/unit/test.reset.spec.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import store from "../store"; 3 | import { undo, redo, reset } from "./utils-test"; 4 | 5 | const item = { 6 | foo: "bar" 7 | }; 8 | 9 | const state: any = store.state; 10 | 11 | describe("Simple testing for undo/redo on a namespaced vuex store", () => { 12 | it("Add item to list", () => { 13 | const expectedState = [{ ...item }]; 14 | 15 | // Commit the item to the store and assert 16 | store.commit("list/addItem", { item }); 17 | 18 | expect(state.list.list).toEqual(expectedState); 19 | }); 20 | 21 | it("Check 'canUndo' value; The undo function should remove the item", async () => { 22 | expect(state.list.canUndo).toBeTruthy(); 23 | 24 | await undo(store)("list"); 25 | await Vue.nextTick(); 26 | 27 | // Check 'canUndo' value, Assert list items after undo 28 | expect(state.list.canUndo).toBeFalsy(); 29 | expect(state.list.list).toEqual([]); 30 | }); 31 | 32 | it("Redo 'addItem' commit", async () => { 33 | expect(state.list.canRedo).toBeTruthy(); 34 | await redo(store)("list"); 35 | }); 36 | 37 | it("Grouped mutations: adding two items to the list", async () => { 38 | const anotherItem = { foo: "baz" }; 39 | const expectedState = [{ ...item }, { ...item }, { ...anotherItem }]; 40 | const actionGroup = "myAction"; 41 | 42 | // Commit the items to the store and assert 43 | store.commit("list/addItem", { item, actionGroup }); 44 | store.commit("list/addItem", { item: anotherItem, actionGroup }); 45 | expect(state.list.list).toEqual(expectedState); 46 | }); 47 | 48 | it("Dispatch undo", async () => { 49 | // The undo function should remove the item 50 | await undo(store)("list"); 51 | 52 | // Assert list items after undo: should contain 1 item 53 | expect(state.list.list).toEqual([{ ...item }]); 54 | }); 55 | 56 | it("Redo 'addItem' twice (grouped mutations)", async () => { 57 | // Redo 'addItem' 58 | await redo(store)("list"); 59 | const anotherItem = { foo: "baz" }; 60 | const expectedState = [{ ...item }, { ...item }, { ...anotherItem }]; 61 | 62 | // Assert list items after redo: should contain 3 items 63 | expect(state.list.list).toEqual(expectedState); 64 | }); 65 | 66 | it('"addShadow" action should be dispatched on undo', async () => { 67 | let expectedState = [...state.list.list, item]; 68 | 69 | store.commit("list/addItem", { 70 | index: 0, 71 | item, 72 | undoCallback: "addShadow", 73 | redoCallback: "removeShadow" 74 | }); 75 | expect(state.list.list).toEqual(expectedState); 76 | 77 | await undo(store)("list"); 78 | expectedState = [{ ...item }]; 79 | expect(state.list.shadow).toEqual(expectedState); 80 | }); 81 | 82 | it("Check shadow: should contain 1 item", () => {}); 83 | 84 | it('"removeShadow" should be dispatched on redo', async () => { 85 | // Redo 'addItem' 86 | await redo(store)("list"); 87 | const expectedState = [ 88 | { foo: "bar" }, 89 | { foo: "bar" }, 90 | { foo: "baz" }, 91 | { foo: "bar" } 92 | ]; 93 | 94 | expect(state.list.list).toEqual(expectedState); 95 | // Check shadow: should contain no items 96 | expect(state.list.shadow).toEqual([]); 97 | }); 98 | 99 | it('"reset" should empty the undo/redo stacks, but retain the current state', async () => { 100 | await reset(store)("list"); 101 | const expectedState = [ 102 | { foo: "bar" }, 103 | { foo: "bar" }, 104 | { foo: "baz" }, 105 | { foo: "bar" } 106 | ]; 107 | 108 | expect(state.list.list).toEqual(expectedState); 109 | }); 110 | 111 | it('"canUndo" and "canRedo" should be reset', () => { 112 | expect(state.list.canUndo).toBeFalsy(); 113 | expect(state.list.canRedo).toBeFalsy(); 114 | }); 115 | 116 | it("Add item to list", () => { 117 | const expectedState = [ 118 | { foo: "bar" }, 119 | { foo: "bar" }, 120 | { foo: "baz" }, 121 | { foo: "bar" }, 122 | { foo: "bar" } 123 | ]; 124 | 125 | // Commit the item to the store and assert 126 | store.commit("list/addItem", { item }); 127 | 128 | expect(state.list.list).toEqual(expectedState); 129 | }); 130 | 131 | it("Check 'canUndo' value; The undo function should remove the item", async () => { 132 | const expectedState = [ 133 | { foo: "bar" }, 134 | { foo: "bar" }, 135 | { foo: "baz" }, 136 | { foo: "bar" } 137 | ]; 138 | 139 | expect(state.list.canUndo).toBeTruthy(); 140 | 141 | await undo(store)("list"); 142 | await Vue.nextTick(); 143 | 144 | // Check 'canUndo' value, Assert list items after undo 145 | expect(state.list.canUndo).toBeFalsy(); 146 | expect(state.list.list).toEqual(expectedState); 147 | }); 148 | }); 149 | -------------------------------------------------------------------------------- /coverage/lcov-report/tests/store/index.ts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for tests/store/index.ts 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files / tests/store index.ts 20 |

21 |
22 |
23 | 100% 24 | Statements 25 | 9/9 26 |
27 |
28 | 100% 29 | Branches 30 | 0/0 31 |
32 |
33 | 100% 34 | Functions 35 | 0/0 36 |
37 |
38 | 100% 39 | Lines 40 | 9/9 41 |
42 |
43 |
44 |
45 |

 46 | 
143 | 
1 47 | 2 48 | 3 49 | 4 50 | 5 51 | 6 52 | 7 53 | 8 54 | 9 55 | 10 56 | 11 57 | 12 58 | 13 59 | 14 60 | 15 61 | 16 62 | 17 63 | 18 64 | 19 65 | 20 66 | 21 67 | 22 68 | 23 69 | 24 70 | 25 71 | 26 72 | 27 73 | 28 74 | 29 75 | 30 76 | 31 77 | 32 78 | 331x 79 | 1x 80 | 1x 81 | 1x 82 | 1x 83 |   84 | 1x 85 |   86 | 1x 87 |   88 | 1x 89 |   90 |   91 |   92 |   93 |   94 |   95 |   96 |   97 |   98 |   99 |   100 |   101 |   102 |   103 |   104 |   105 |   106 | 1x 107 |   108 |   109 |   110 |  
import Vuex from "vuex";
111 | import Vue from "vue";
112 | import list from "./modules/list";
113 | import auth from "./modules/auth";
114 | import undoRedo from "../../src/undoRedo";
115 |  
116 | Vue.use(Vuex);
117 |  
118 | const debug = process.env.NODE_ENV !== "production";
119 |  
120 | export const store = {
121 |   plugins: [
122 |     undoRedo({
123 |       paths: [
124 |         {
125 |           namespace: "list",
126 |           ignoreMutations: ["addShadow", "removeShadow"]
127 |         }
128 |       ]
129 |     })
130 |   ],
131 |   modules: {
132 |     list,
133 |     auth
134 |   },
135 |   strict: debug
136 | };
137 |  
138 | export default new Vuex.Store({
139 |   ...store,
140 |   strict: false
141 | });
142 |  
144 |
145 |
146 | 150 | 151 | 152 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /coverage/lcov-report/sorter.js: -------------------------------------------------------------------------------- 1 | var addSorting = (function () { 2 | "use strict"; 3 | var cols, 4 | currentSort = { 5 | index: 0, 6 | desc: false 7 | }; 8 | 9 | // returns the summary table element 10 | function getTable() { return document.querySelector('.coverage-summary'); } 11 | // returns the thead element of the summary table 12 | function getTableHeader() { return getTable().querySelector('thead tr'); } 13 | // returns the tbody element of the summary table 14 | function getTableBody() { return getTable().querySelector('tbody'); } 15 | // returns the th element for nth column 16 | function getNthColumn(n) { return getTableHeader().querySelectorAll('th')[n]; } 17 | 18 | // loads all columns 19 | function loadColumns() { 20 | var colNodes = getTableHeader().querySelectorAll('th'), 21 | colNode, 22 | cols = [], 23 | col, 24 | i; 25 | 26 | for (i = 0; i < colNodes.length; i += 1) { 27 | colNode = colNodes[i]; 28 | col = { 29 | key: colNode.getAttribute('data-col'), 30 | sortable: !colNode.getAttribute('data-nosort'), 31 | type: colNode.getAttribute('data-type') || 'string' 32 | }; 33 | cols.push(col); 34 | if (col.sortable) { 35 | col.defaultDescSort = col.type === 'number'; 36 | colNode.innerHTML = colNode.innerHTML + ''; 37 | } 38 | } 39 | return cols; 40 | } 41 | // attaches a data attribute to every tr element with an object 42 | // of data values keyed by column name 43 | function loadRowData(tableRow) { 44 | var tableCols = tableRow.querySelectorAll('td'), 45 | colNode, 46 | col, 47 | data = {}, 48 | i, 49 | val; 50 | for (i = 0; i < tableCols.length; i += 1) { 51 | colNode = tableCols[i]; 52 | col = cols[i]; 53 | val = colNode.getAttribute('data-value'); 54 | if (col.type === 'number') { 55 | val = Number(val); 56 | } 57 | data[col.key] = val; 58 | } 59 | return data; 60 | } 61 | // loads all row data 62 | function loadData() { 63 | var rows = getTableBody().querySelectorAll('tr'), 64 | i; 65 | 66 | for (i = 0; i < rows.length; i += 1) { 67 | rows[i].data = loadRowData(rows[i]); 68 | } 69 | } 70 | // sorts the table using the data for the ith column 71 | function sortByIndex(index, desc) { 72 | var key = cols[index].key, 73 | sorter = function (a, b) { 74 | a = a.data[key]; 75 | b = b.data[key]; 76 | return a < b ? -1 : a > b ? 1 : 0; 77 | }, 78 | finalSorter = sorter, 79 | tableBody = document.querySelector('.coverage-summary tbody'), 80 | rowNodes = tableBody.querySelectorAll('tr'), 81 | rows = [], 82 | i; 83 | 84 | if (desc) { 85 | finalSorter = function (a, b) { 86 | return -1 * sorter(a, b); 87 | }; 88 | } 89 | 90 | for (i = 0; i < rowNodes.length; i += 1) { 91 | rows.push(rowNodes[i]); 92 | tableBody.removeChild(rowNodes[i]); 93 | } 94 | 95 | rows.sort(finalSorter); 96 | 97 | for (i = 0; i < rows.length; i += 1) { 98 | tableBody.appendChild(rows[i]); 99 | } 100 | } 101 | // removes sort indicators for current column being sorted 102 | function removeSortIndicators() { 103 | var col = getNthColumn(currentSort.index), 104 | cls = col.className; 105 | 106 | cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); 107 | col.className = cls; 108 | } 109 | // adds sort indicators for current column being sorted 110 | function addSortIndicators() { 111 | getNthColumn(currentSort.index).className += currentSort.desc ? ' sorted-desc' : ' sorted'; 112 | } 113 | // adds event listeners for all sorter widgets 114 | function enableUI() { 115 | var i, 116 | el, 117 | ithSorter = function ithSorter(i) { 118 | var col = cols[i]; 119 | 120 | return function () { 121 | var desc = col.defaultDescSort; 122 | 123 | if (currentSort.index === i) { 124 | desc = !currentSort.desc; 125 | } 126 | sortByIndex(i, desc); 127 | removeSortIndicators(); 128 | currentSort.index = i; 129 | currentSort.desc = desc; 130 | addSortIndicators(); 131 | }; 132 | }; 133 | for (i =0 ; i < cols.length; i += 1) { 134 | if (cols[i].sortable) { 135 | // add the click event handler on the th so users 136 | // dont have to click on those tiny arrows 137 | el = getNthColumn(i).querySelector('.sorter').parentElement; 138 | if (el.addEventListener) { 139 | el.addEventListener('click', ithSorter(i)); 140 | } else { 141 | el.attachEvent('onclick', ithSorter(i)); 142 | } 143 | } 144 | } 145 | } 146 | // adds sorting functionality to the UI 147 | return function () { 148 | if (!getTable()) { 149 | return; 150 | } 151 | cols = loadColumns(); 152 | loadData(cols); 153 | addSortIndicators(); 154 | enableUI(); 155 | }; 156 | })(); 157 | 158 | window.addEventListener('load', addSorting); 159 | -------------------------------------------------------------------------------- /coverage/lcov-report/base.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin:0; padding: 0; 3 | height: 100%; 4 | } 5 | body { 6 | font-family: Helvetica Neue, Helvetica, Arial; 7 | font-size: 14px; 8 | color:#333; 9 | } 10 | .small { font-size: 12px; } 11 | *, *:after, *:before { 12 | -webkit-box-sizing:border-box; 13 | -moz-box-sizing:border-box; 14 | box-sizing:border-box; 15 | } 16 | h1 { font-size: 20px; margin: 0;} 17 | h2 { font-size: 14px; } 18 | pre { 19 | font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; 20 | margin: 0; 21 | padding: 0; 22 | -moz-tab-size: 2; 23 | -o-tab-size: 2; 24 | tab-size: 2; 25 | } 26 | a { color:#0074D9; text-decoration:none; } 27 | a:hover { text-decoration:underline; } 28 | .strong { font-weight: bold; } 29 | .space-top1 { padding: 10px 0 0 0; } 30 | .pad2y { padding: 20px 0; } 31 | .pad1y { padding: 10px 0; } 32 | .pad2x { padding: 0 20px; } 33 | .pad2 { padding: 20px; } 34 | .pad1 { padding: 10px; } 35 | .space-left2 { padding-left:55px; } 36 | .space-right2 { padding-right:20px; } 37 | .center { text-align:center; } 38 | .clearfix { display:block; } 39 | .clearfix:after { 40 | content:''; 41 | display:block; 42 | height:0; 43 | clear:both; 44 | visibility:hidden; 45 | } 46 | .fl { float: left; } 47 | @media only screen and (max-width:640px) { 48 | .col3 { width:100%; max-width:100%; } 49 | .hide-mobile { display:none!important; } 50 | } 51 | 52 | .quiet { 53 | color: #7f7f7f; 54 | color: rgba(0,0,0,0.5); 55 | } 56 | .quiet a { opacity: 0.7; } 57 | 58 | .fraction { 59 | font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; 60 | font-size: 10px; 61 | color: #555; 62 | background: #E8E8E8; 63 | padding: 4px 5px; 64 | border-radius: 3px; 65 | vertical-align: middle; 66 | } 67 | 68 | div.path a:link, div.path a:visited { color: #333; } 69 | table.coverage { 70 | border-collapse: collapse; 71 | margin: 10px 0 0 0; 72 | padding: 0; 73 | } 74 | 75 | table.coverage td { 76 | margin: 0; 77 | padding: 0; 78 | vertical-align: top; 79 | } 80 | table.coverage td.line-count { 81 | text-align: right; 82 | padding: 0 5px 0 20px; 83 | } 84 | table.coverage td.line-coverage { 85 | text-align: right; 86 | padding-right: 10px; 87 | min-width:20px; 88 | } 89 | 90 | table.coverage td span.cline-any { 91 | display: inline-block; 92 | padding: 0 5px; 93 | width: 100%; 94 | } 95 | .missing-if-branch { 96 | display: inline-block; 97 | margin-right: 5px; 98 | border-radius: 3px; 99 | position: relative; 100 | padding: 0 4px; 101 | background: #333; 102 | color: yellow; 103 | } 104 | 105 | .skip-if-branch { 106 | display: none; 107 | margin-right: 10px; 108 | position: relative; 109 | padding: 0 4px; 110 | background: #ccc; 111 | color: white; 112 | } 113 | .missing-if-branch .typ, .skip-if-branch .typ { 114 | color: inherit !important; 115 | } 116 | .coverage-summary { 117 | border-collapse: collapse; 118 | width: 100%; 119 | } 120 | .coverage-summary tr { border-bottom: 1px solid #bbb; } 121 | .keyline-all { border: 1px solid #ddd; } 122 | .coverage-summary td, .coverage-summary th { padding: 10px; } 123 | .coverage-summary tbody { border: 1px solid #bbb; } 124 | .coverage-summary td { border-right: 1px solid #bbb; } 125 | .coverage-summary td:last-child { border-right: none; } 126 | .coverage-summary th { 127 | text-align: left; 128 | font-weight: normal; 129 | white-space: nowrap; 130 | } 131 | .coverage-summary th.file { border-right: none !important; } 132 | .coverage-summary th.pct { } 133 | .coverage-summary th.pic, 134 | .coverage-summary th.abs, 135 | .coverage-summary td.pct, 136 | .coverage-summary td.abs { text-align: right; } 137 | .coverage-summary td.file { white-space: nowrap; } 138 | .coverage-summary td.pic { min-width: 120px !important; } 139 | .coverage-summary tfoot td { } 140 | 141 | .coverage-summary .sorter { 142 | height: 10px; 143 | width: 7px; 144 | display: inline-block; 145 | margin-left: 0.5em; 146 | background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; 147 | } 148 | .coverage-summary .sorted .sorter { 149 | background-position: 0 -20px; 150 | } 151 | .coverage-summary .sorted-desc .sorter { 152 | background-position: 0 -10px; 153 | } 154 | .status-line { height: 10px; } 155 | /* dark red */ 156 | .red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } 157 | .low .chart { border:1px solid #C21F39 } 158 | /* medium red */ 159 | .cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } 160 | /* light red */ 161 | .low, .cline-no { background:#FCE1E5 } 162 | /* light green */ 163 | .high, .cline-yes { background:rgb(230,245,208) } 164 | /* medium green */ 165 | .cstat-yes { background:rgb(161,215,106) } 166 | /* dark green */ 167 | .status-line.high, .high .cover-fill { background:rgb(77,146,33) } 168 | .high .chart { border:1px solid rgb(77,146,33) } 169 | 170 | 171 | .medium .chart { border:1px solid #666; } 172 | .medium .cover-fill { background: #666; } 173 | 174 | .cbranch-no { background: yellow !important; color: #111; } 175 | 176 | .cstat-skip { background: #ddd; color: #111; } 177 | .fstat-skip { background: #ddd; color: #111 !important; } 178 | .cbranch-skip { background: #ddd !important; color: #111; } 179 | 180 | span.cline-neutral { background: #eaeaea; } 181 | .medium { background: #eaeaea; } 182 | 183 | .cover-fill, .cover-empty { 184 | display:inline-block; 185 | height: 12px; 186 | } 187 | .chart { 188 | line-height: 0; 189 | } 190 | .cover-empty { 191 | background: white; 192 | } 193 | .cover-full { 194 | border-right: none !important; 195 | } 196 | pre.prettyprint { 197 | border: none !important; 198 | padding: 0 !important; 199 | margin: 0 !important; 200 | } 201 | .com { color: #999 !important; } 202 | .ignore-none { color: #999; font-weight: normal; } 203 | 204 | .wrapper { 205 | min-height: 100%; 206 | height: auto !important; 207 | height: 100%; 208 | margin: 0 auto -48px; 209 | } 210 | .footer, .push { 211 | height: 48px; 212 | } 213 | -------------------------------------------------------------------------------- /src/undoRedo.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign, no-shadow */ 2 | import { 3 | EMPTY_STATE, 4 | UPDATE_CAN_UNDO_REDO, 5 | REDO, 6 | UNDO, 7 | CLEAR, 8 | RESET, 9 | UPDATE_UNDO_REDO_CONFIG 10 | } from "./constants"; 11 | import { getConfig, setConfig, updateCanUndoRedo } from "./utils-undo-redo"; 12 | import execRedo from "./redo"; 13 | import execUndo from "./undo"; 14 | import execClear from "./clear"; 15 | import execReset from "./reset"; 16 | 17 | // Logic based on: https://github.com/anthonygore/vuex-undo-redo 18 | 19 | const noop = () => {}; 20 | export const undo = noop; 21 | export const redo = noop; 22 | export const clear = noop; 23 | export const reset = noop; 24 | export const getUndoRedoConfig = noop; 25 | 26 | export const scaffoldState = (state: any, exposeUndoRedoConfig = false) => ({ 27 | ...state, 28 | canUndo: false, 29 | canRedo: false, 30 | ...(exposeUndoRedoConfig 31 | ? { 32 | undoRedoConfig: {} 33 | } 34 | : {}) 35 | }); 36 | 37 | export const scaffoldActions = ( 38 | actions: any, 39 | exposeUndoRedoConfig = false 40 | ) => ({ 41 | ...actions, 42 | undo, 43 | redo, 44 | clear, 45 | reset, 46 | ...(exposeUndoRedoConfig 47 | ? { 48 | getUndoRedoConfig 49 | } 50 | : {}) 51 | }); 52 | 53 | export const scaffoldMutations = ( 54 | mutations: any, 55 | exposeUndoRedoConfig = false 56 | ) => ({ 57 | ...mutations, 58 | [UPDATE_CAN_UNDO_REDO]: (state: any, payload: any) => { 59 | if (payload.canUndo !== undefined) state.canUndo = payload.canUndo; 60 | if (payload.canRedo !== undefined) state.canRedo = payload.canRedo; 61 | }, 62 | [UPDATE_UNDO_REDO_CONFIG]: exposeUndoRedoConfig 63 | ? (state: any, { done, undone }: { done: any[]; undone: any[] }) => { 64 | state.undoRedoConfig.done = [...done]; 65 | state.undoRedoConfig.undone = [...undone]; 66 | } 67 | : noop 68 | }); 69 | 70 | export const scaffoldStore = (store: any, exposeUndoRedoConfig = false) => ({ 71 | ...store, 72 | state: scaffoldState(store.state || {}, exposeUndoRedoConfig), 73 | actions: scaffoldActions(store.actions || {}, exposeUndoRedoConfig), 74 | mutations: scaffoldMutations(store.mutations || {}, exposeUndoRedoConfig) 75 | }); 76 | 77 | const createPathConfig = ({ 78 | namespace = "", 79 | ignoreMutations = [], 80 | exposeUndoRedoConfig = false 81 | }: UndoRedoOptions): UndoRedoOptions => ({ 82 | namespace, 83 | ignoreMutations, 84 | done: [], 85 | undone: [], 86 | newMutation: true, 87 | exposeUndoRedoConfig 88 | }); 89 | 90 | const mapIgnoreMutations = ({ 91 | namespace, 92 | ignoreMutations 93 | }: UndoRedoOptions) => ({ 94 | ignoreMutations: (ignoreMutations || []) 95 | .map(mutation => `${namespace}/${mutation}`) 96 | .concat([ 97 | `${namespace}/${UPDATE_CAN_UNDO_REDO}`, 98 | `${namespace}/${UPDATE_UNDO_REDO_CONFIG}` 99 | ]) 100 | }); 101 | 102 | const mapPaths = (paths: UndoRedoOptions[]) => 103 | paths.map(({ namespace, ignoreMutations, exposeUndoRedoConfig }) => 104 | createPathConfig({ 105 | namespace: `${namespace}/`, 106 | ...(ignoreMutations 107 | ? mapIgnoreMutations({ namespace, ignoreMutations }) 108 | : {}), 109 | exposeUndoRedoConfig 110 | }) 111 | ); 112 | 113 | const canRedo = (paths: UndoRedoOptions[]) => (namespace: string) => { 114 | const config = getConfig(paths)(namespace); 115 | if (Object.keys(config).length) { 116 | return config.undone.length > 0; 117 | } 118 | return false; 119 | }; 120 | 121 | const canUndo = (paths: UndoRedoOptions[]) => (namespace: string) => { 122 | const config = getConfig(paths)(namespace); 123 | if (config) { 124 | return config.done.length > 0; 125 | } 126 | return false; 127 | }; 128 | 129 | /** 130 | * The Undo-Redo plugin module 131 | * 132 | * @module store/plugins/undoRedo 133 | * @function 134 | * @param {Object} options 135 | * @param {String} options.namespace - The named vuex store module 136 | * @param {Array} options.ignoreMutations - The list of store mutations 137 | * (belonging to the module) to be ignored 138 | * @returns {Function} plugin - the plugin function which accepts the store parameter 139 | */ 140 | export default (options: UndoRedoOptions = {}) => (store: any) => { 141 | const paths = options.paths 142 | ? mapPaths(options.paths) 143 | : [ 144 | createPathConfig({ 145 | ignoreMutations: [ 146 | ...(options.ignoreMutations || []), 147 | UPDATE_CAN_UNDO_REDO, 148 | UPDATE_UNDO_REDO_CONFIG 149 | ], 150 | exposeUndoRedoConfig: options.exposeUndoRedoConfig 151 | }) 152 | ]; 153 | 154 | store.subscribe((mutation: Mutation) => { 155 | const isStoreNamespaced = mutation.type.split("/").length > 1; 156 | const namespace = isStoreNamespaced 157 | ? `${mutation.type.split("/")[0]}/` 158 | : ""; 159 | const config = getConfig(paths)(namespace); 160 | 161 | if (Object.keys(config).length) { 162 | const { 163 | ignoreMutations, 164 | newMutation, 165 | done, 166 | exposeUndoRedoConfig 167 | } = config; 168 | 169 | if ( 170 | mutation.type !== `${namespace}${EMPTY_STATE}` && 171 | mutation.type !== `${namespace}${UPDATE_CAN_UNDO_REDO}` && 172 | mutation.type !== `${namespace}${UPDATE_UNDO_REDO_CONFIG}` && 173 | ignoreMutations.indexOf(mutation.type) === -1 && 174 | mutation.type.includes(namespace) && 175 | newMutation 176 | ) { 177 | done.push(mutation); 178 | 179 | setConfig(paths)( 180 | namespace, 181 | { 182 | ...config, 183 | done 184 | }, 185 | store 186 | ); 187 | 188 | updateCanUndoRedo({ paths, store })(namespace); 189 | } 190 | } 191 | }); 192 | 193 | // NB: Watch all actions to intercept the undo/redo NOOP actions 194 | store.subscribeAction(async (action: Action) => { 195 | const isStoreNamespaced = action.type.split("/").length > 1; 196 | const namespace = isStoreNamespaced ? `${action.type.split("/")[0]}/` : ""; 197 | 198 | switch (action.type) { 199 | case `${namespace}${REDO}`: 200 | if (canRedo(paths)(namespace)) 201 | await execRedo({ paths, store })(namespace); 202 | break; 203 | case `${namespace}${UNDO}`: 204 | if (canUndo(paths)(namespace)) 205 | await execUndo({ paths, store })(namespace); 206 | break; 207 | case `${namespace}${CLEAR}`: 208 | await execClear({ paths, store })(namespace); 209 | break; 210 | case `${namespace}${RESET}`: 211 | await execReset({ paths, store })(namespace); 212 | break; 213 | default: 214 | break; 215 | } 216 | }); 217 | }; 218 | -------------------------------------------------------------------------------- /coverage/lcov-report/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for src 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files src 20 |

21 |
22 |
23 | 97.5% 24 | Statements 25 | 156/160 26 |
27 |
28 | 77.38% 29 | Branches 30 | 65/84 31 |
32 |
33 | 100% 34 | Functions 35 | 45/45 36 |
37 |
38 | 97.2% 39 | Lines 40 | 139/143 41 |
42 |
43 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 |
FileStatementsBranchesFunctionsLines
constants.ts
100%4/4100%0/0100%0/0100%4/4
redo.ts
100%27/2787.5%14/16100%4/4100%27/27
undo.ts
100%32/3283.33%15/18100%5/5100%31/31
undoRedo.ts
96.83%61/6375%33/44100%19/1996.3%52/54
utils-undo-redo.ts
94.12%32/3450%3/6100%17/1792.59%25/27
128 |
129 |
130 | 134 | 135 | 136 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /coverage/lcov-report/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for All files 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files 20 |

21 |
22 |
23 | 81.99% 24 | Statements 25 | 214/261 26 |
27 |
28 | 78.89% 29 | Branches 30 | 71/90 31 |
32 |
33 | 72.15% 34 | Functions 35 | 57/79 36 |
37 |
38 | 81.59% 39 | Lines 40 | 195/239 41 |
42 |
43 |
44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 |
FileStatementsBranchesFunctionsLines
src
97.5%156/16077.38%65/84100%45/4597.2%139/143
tests/store
100%9/9100%0/0100%0/0100%9/9
tests/store-non-namespaced
70.83%17/24100%0/036.36%4/1173.91%17/23
tests/store/modules
38.98%23/59100%0/021.05%4/1940.35%23/57
tests/unit
100%9/9100%6/6100%4/4100%7/7
128 |
129 |
130 | 134 | 135 | 136 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /coverage/lcov-report/tests/store/modules/list.ts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for tests/store/modules/list.ts 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files / tests/store/modules list.ts 20 |

21 |
22 |
23 | 66.67% 24 | Statements 25 | 14/21 26 |
27 |
28 | 100% 29 | Branches 30 | 0/0 31 |
32 |
33 | 36.36% 34 | Functions 35 | 4/11 36 |
37 |
38 | 70% 39 | Lines 40 | 14/20 41 |
42 |
43 |
44 |
45 |

 46 | 
257 | 
1 47 | 2 48 | 3 49 | 4 50 | 5 51 | 6 52 | 7 53 | 8 54 | 9 55 | 10 56 | 11 57 | 12 58 | 13 59 | 14 60 | 15 61 | 16 62 | 17 63 | 18 64 | 19 65 | 20 66 | 21 67 | 22 68 | 23 69 | 24 70 | 25 71 | 26 72 | 27 73 | 28 74 | 29 75 | 30 76 | 31 77 | 32 78 | 33 79 | 34 80 | 35 81 | 36 82 | 37 83 | 38 84 | 39 85 | 40 86 | 41 87 | 42 88 | 43 89 | 44 90 | 45 91 | 46 92 | 47 93 | 48 94 | 49 95 | 50 96 | 51 97 | 52 98 | 53 99 | 54 100 | 55 101 | 56 102 | 57 103 | 58 104 | 59 105 | 60 106 | 61 107 | 62 108 | 63 109 | 64 110 | 65 111 | 66 112 | 67 113 | 68 114 | 69 115 | 70 116 | 71  117 | 1x 118 | 1x 119 |   120 | 1x 121 |   122 | 1x 123 |   124 |   125 |   126 |   127 | 1x 128 |   129 |   130 |   131 |   132 |   133 |   134 |   135 |   136 |   137 |   138 |   139 |   140 |   141 |   142 |   143 |   144 |   145 |   146 |   147 | 1x 148 |   149 |   150 | 1x 151 |   152 |   153 | 1x 154 |   155 |   156 |   157 | 1x 158 |   159 | 3x 160 |   161 |   162 | 12x 163 |   164 |   165 |   166 |   167 |   168 |   169 |   170 |   171 | 1x 172 |   173 |   174 | 1x 175 |   176 |   177 |   178 | 1x 179 |   180 |   181 |   182 |   183 |   184 |   185 |   186 |  
/* eslint-disable no-param-reassign, no-shadow */
187 | import deepEqual from "fast-deep-equal";
188 | import { scaffoldStore } from "../../../src/undoRedo";
189 |  
190 | const debug = process.env.NODE_ENV !== "production";
191 |  
192 | const state = {
193 |   list: [],
194 |   shadow: []
195 | };
196 |  
197 | const getters = {
198 |   getList: ({ list }: { list: Array<any> }) => list,
199 |   getItem: (state: any) => ({ item }: { item: any }) =>
200 |     state.list.find((i: any) => deepEqual(i, item)),
201 |   getShadow: ({ shadow }: { shadow: Array<any> }) => shadow
202 | };
203 |  
204 | interface Context {
205 |   commit: Function;
206 |   state: any;
207 |   getters: any;
208 |   rootState: any;
209 |   rootGetters: any;
210 | }
211 |  
212 | interface Payload {
213 |   index?: number;
214 |   item?: any;
215 | }
216 |  
217 | const actions = {
218 |   // NB: add/remove shadow actions to test undo/redo callback actions
219 |   addShadow({ commit }: Context, { item }: Payload) {
220 |     commit("addShadow", { item });
221 |   },
222 |   removeShadow({ commit }: Context, { index }: Payload) {
223 |     commit("removeShadow", { index });
224 |   }
225 | };
226 |  
227 | export const mutations = {
228 |   emptyState: (state: any) => {
229 |     state.list = [];
230 |   },
231 |   addItem: (state: any, { item }: { item: any }) => {
232 |     state.list = [...state.list, item];
233 |   },
234 |   updateItem: (state: any, { item, index }: Payload) => {
235 |     state.list.splice(index, 1, item);
236 |   },
237 |   removeItem: (state: any, { index }: Payload) => {
238 |     state.list.splice(index, 1);
239 |   },
240 |   addShadow: (state: any, { item }: Payload) => {
241 |     state.shadow = [...state.shadow, item];
242 |   },
243 |   removeShadow: (state: any, { index }: Payload) => {
244 |     state.shadow.splice(index, 1);
245 |   }
246 | };
247 |  
248 | export default scaffoldStore({
249 |   state,
250 |   getters,
251 |   actions,
252 |   mutations,
253 |   namespaced: true,
254 |   debug
255 | });
256 |  
258 |
259 |
260 | 264 | 265 | 266 | 273 | 274 | 275 | 276 | -------------------------------------------------------------------------------- /coverage/lcov-report/src/utils-undo-redo.ts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for src/utils-undo-redo.ts 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files / src utils-undo-redo.ts 20 |

21 |
22 |
23 | 94.12% 24 | Statements 25 | 32/34 26 |
27 |
28 | 50% 29 | Branches 30 | 3/6 31 |
32 |
33 | 100% 34 | Functions 35 | 17/17 36 |
37 |
38 | 92.59% 39 | Lines 40 | 25/27 41 |
42 |
43 |
44 |
45 |

 46 | 
296 | 
1 47 | 2 48 | 3 49 | 4 50 | 5 51 | 6 52 | 7 53 | 8 54 | 9 55 | 10 56 | 11 57 | 12 58 | 13 59 | 14 60 | 15 61 | 16 62 | 17 63 | 18 64 | 19 65 | 20 66 | 21 67 | 22 68 | 23 69 | 24 70 | 25 71 | 26 72 | 27 73 | 28 74 | 29 75 | 30 76 | 31 77 | 32 78 | 33 79 | 34 80 | 35 81 | 36 82 | 37 83 | 38 84 | 39 85 | 40 86 | 41 87 | 42 88 | 43 89 | 44 90 | 45 91 | 46 92 | 47 93 | 48 94 | 49 95 | 50 96 | 51 97 | 52 98 | 53 99 | 54 100 | 55 101 | 56 102 | 57 103 | 58 104 | 59 105 | 60 106 | 61 107 | 62 108 | 63 109 | 64 110 | 65 111 | 66 112 | 67 113 | 68 114 | 69 115 | 70 116 | 71 117 | 72 118 | 73 119 | 74 120 | 75 121 | 76 122 | 77 123 | 78 124 | 79 125 | 80 126 | 81 127 | 82 128 | 83 129 | 845x 130 |   131 |   132 |   133 |   134 |   135 |   136 |   137 |   138 |   139 |   140 | 358x 141 |   142 | 358x 143 |   144 |   145 |   146 |   147 |   148 |   149 |   150 |   151 |   152 |   153 |   154 | 44x 155 | 44x 156 | 73x 157 |   158 |   159 | 4x 160 |   161 |   162 |   163 |   164 |   165 |   166 |   167 |   168 |   169 |   170 |   171 |   172 | 5x 173 | 52x 174 | 52x 175 | 52x 176 |   177 |   178 |   179 | 52x 180 | 52x 181 | 52x 182 | 52x 183 |   184 |   185 |   186 |   187 | 52x 188 | 52x 189 | 52x 190 | 52x 191 |   192 |   193 |   194 |   195 | 5x 196 |   197 |   198 |   199 |   200 |   201 | 52x 202 | 52x 203 | 52x 204 |   205 | 52x 206 |   207 |   208 | 52x 209 |   210 |   211 |   212 |  
import { UPDATE_CAN_UNDO_REDO } from "./constants";
213 |  
214 | /**
215 |  * Piping async action calls secquentially using Array.prototype.reduce
216 |  * to chain and initial, empty promise
217 |  *
218 |  * @module store/plugins/undoRedo:getConfig
219 |  * @function
220 |  * @param {String} namespace - The name of the store module
221 |  * @returns {Object} config - The object containing the undo/redo stacks of the store module
222 |  */
223 | export const getConfig: UndoRedoOptions | any = (paths: UndoRedoOptions[]) => (
224 |   namespace: string
225 | ): object => paths.find(path => path.namespace === namespace) || {};
226 |  
227 | // Based on https://gist.github.com/anvk/5602ec398e4fdc521e2bf9940fd90f84
228 | /**
229 |  * Piping async action calls secquentially using Array.prototype.reduce
230 |  * to chain and initial, empty promise
231 |  *
232 |  * @module store/plugins/undoRedo:pipeActions
233 |  * @function
234 |  * @param {Array<Object>} actions - The array of objects containing the each
235 |  * action's name and payload
236 |  */
237 | export const pipeActions = (store: any) => (actions: Array<any>) =>
238 |   actions
239 |     .filter(({ action }) => !!action)
240 |     .reduce(
241 |       (promise, { action, payload }) =>
242 |         promise.then(() => store.dispatch(action, payload)),
243 |       Promise.resolve()
244 |     );
245 |  
246 | /**
247 |  * Piping async action calls secquentially using Array.prototype.reduce
248 |  * to chain and initial, empty promise
249 |  *
250 |  * @module store/plugins/undoRedo:setConfig
251 |  * @function
252 |  * @param {String} [namespace] - The name of the store module
253 |  * @param {Object} config - The object containing the updated undo/redo stacks of the store module
254 |  */
255 | export const setConfig = (paths: UndoRedoOptions[]) => {
256 |   return (namespace: string, config: any) => {
257 |     const pathIndex = paths.findIndex(path => path.namespace === namespace);
258 |     paths.splice(pathIndex, 1, config);
259 |   };
260 | };
261 |  
262 | const canRedo = (paths: UndoRedoOptions[]) => (namespace: string) => {
263 |   const config = getConfig(paths)(namespace);
264 |   Eif (Object.keys(config).length) {
265 |     return config.undone.length > 0;
266 |   }
267 |   return false;
268 | };
269 |  
270 | const canUndo = (paths: UndoRedoOptions[]) => (namespace: string) => {
271 |   const config = getConfig(paths)(namespace);
272 |   Eif (config) {
273 |     return config.done.length > 0;
274 |   }
275 |   return false;
276 | };
277 |  
278 | export const updateCanUndoRedo = ({
279 |   paths,
280 |   store
281 | }: {
282 |   paths: UndoRedoOptions[];
283 |   store: any;
284 | }) => (namespace: string) => {
285 |   const undoEnabled = canUndo(paths)(namespace);
286 |   const redoEnabled = canRedo(paths)(namespace);
287 |  
288 |   store.commit(`${namespace}${UPDATE_CAN_UNDO_REDO}`, {
289 |     canUndo: undoEnabled
290 |   });
291 |   store.commit(`${namespace}${UPDATE_CAN_UNDO_REDO}`, {
292 |     canRedo: redoEnabled
293 |   });
294 | };
295 |  
297 |
298 |
299 | 303 | 304 | 305 | 312 | 313 | 314 | 315 | -------------------------------------------------------------------------------- /coverage/lcov.info: -------------------------------------------------------------------------------- 1 | TN: 2 | SF:/Users/andrew/Documents/multibasebox/projects/undo-redo-vuex/src/constants.ts 3 | FNF:0 4 | FNH:0 5 | DA:1,5 6 | DA:2,5 7 | DA:3,5 8 | DA:4,5 9 | LF:4 10 | LH:4 11 | BRF:0 12 | BRH:0 13 | end_of_record 14 | TN: 15 | SF:/Users/andrew/Documents/multibasebox/projects/undo-redo-vuex/src/redo.ts 16 | FN:15,(anonymous_0) 17 | FN:21,(anonymous_1) 18 | FN:31,(anonymous_2) 19 | FN:74,(anonymous_3) 20 | FNF:4 21 | FNH:4 22 | FNDA:14,(anonymous_0) 23 | FNDA:14,(anonymous_1) 24 | FNDA:31,(anonymous_2) 25 | FNDA:22,(anonymous_3) 26 | DA:1,5 27 | DA:15,5 28 | DA:21,14 29 | DA:22,14 30 | DA:23,14 31 | DA:30,14 32 | DA:43,31 33 | DA:45,14 34 | DA:47,14 35 | DA:48,17 36 | DA:50,7 37 | DA:53,10 38 | DA:54,10 39 | DA:57,10 40 | DA:59,10 41 | DA:60,10 42 | DA:63,31 43 | DA:72,14 44 | DA:74,14 45 | DA:76,22 46 | DA:82,22 47 | DA:84,22 48 | DA:89,14 49 | DA:90,14 50 | DA:91,14 51 | DA:92,14 52 | DA:97,14 53 | LF:27 54 | LH:27 55 | BRDA:23,0,0,14 56 | BRDA:23,0,1,0 57 | BRDA:43,1,0,14 58 | BRDA:43,1,1,17 59 | BRDA:48,2,0,7 60 | BRDA:48,2,1,10 61 | BRDA:58,3,0,10 62 | BRDA:58,3,1,9 63 | BRDA:59,4,0,8 64 | BRDA:59,4,1,2 65 | BRDA:60,5,0,8 66 | BRDA:60,5,1,2 67 | BRDA:78,6,0,0 68 | BRDA:78,6,1,22 69 | BRDA:85,7,0,2 70 | BRDA:85,7,1,20 71 | BRF:16 72 | BRH:14 73 | end_of_record 74 | TN: 75 | SF:/Users/andrew/Documents/multibasebox/projects/undo-redo-vuex/src/undo.ts 76 | FN:16,(anonymous_0) 77 | FN:22,(anonymous_1) 78 | FN:33,(anonymous_2) 79 | FN:74,(anonymous_3) 80 | FN:83,(anonymous_4) 81 | FNF:5 82 | FNH:5 83 | FNDA:15,(anonymous_0) 84 | FNDA:15,(anonymous_1) 85 | FNDA:51,(anonymous_2) 86 | FNDA:24,(anonymous_3) 87 | FNDA:27,(anonymous_4) 88 | DA:1,5 89 | DA:2,5 90 | DA:16,5 91 | DA:22,15 92 | DA:23,15 93 | DA:25,15 94 | DA:32,15 95 | DA:45,51 96 | DA:47,15 97 | DA:49,15 98 | DA:50,36 99 | DA:52,20 100 | DA:54,16 101 | DA:55,16 102 | DA:58,16 103 | DA:60,16 104 | DA:61,16 105 | DA:64,51 106 | DA:74,24 107 | DA:78,15 108 | DA:80,15 109 | DA:81,15 110 | DA:82,15 111 | DA:83,15 112 | DA:84,27 113 | DA:92,27 114 | DA:93,27 115 | DA:98,15 116 | DA:99,15 117 | DA:100,15 118 | DA:106,15 119 | LF:31 120 | LH:31 121 | BRDA:25,0,0,15 122 | BRDA:25,0,1,0 123 | BRDA:45,1,0,15 124 | BRDA:45,1,1,36 125 | BRDA:50,2,0,20 126 | BRDA:50,2,1,16 127 | BRDA:59,3,0,16 128 | BRDA:59,3,1,11 129 | BRDA:60,4,0,9 130 | BRDA:60,4,1,7 131 | BRDA:61,5,0,9 132 | BRDA:61,5,1,7 133 | BRDA:75,6,0,2 134 | BRDA:75,6,1,22 135 | BRDA:87,7,0,0 136 | BRDA:87,7,1,27 137 | BRDA:94,8,0,0 138 | BRDA:94,8,1,27 139 | BRF:18 140 | BRH:15 141 | end_of_record 142 | TN: 143 | SF:/Users/andrew/Documents/multibasebox/projects/undo-redo-vuex/src/undoRedo.ts 144 | FN:9,(anonymous_0) 145 | FN:13,(anonymous_1) 146 | FN:19,(anonymous_2) 147 | FN:25,(anonymous_3) 148 | FN:27,(anonymous_4) 149 | FN:33,(anonymous_5) 150 | FN:40,(anonymous_6) 151 | FN:51,(anonymous_7) 152 | FN:56,(anonymous_8) 153 | FN:60,(anonymous_9) 154 | FN:61,(anonymous_10) 155 | FN:70,(anonymous_11) 156 | FN:70,(anonymous_12) 157 | FN:78,(anonymous_13) 158 | FN:78,(anonymous_14) 159 | FN:97,(anonymous_15) 160 | FN:97,(anonymous_16) 161 | FN:109,(anonymous_17) 162 | FN:137,(anonymous_18) 163 | FNF:19 164 | FNH:19 165 | FNDA:30,(anonymous_0) 166 | FNDA:5,(anonymous_1) 167 | FNDA:5,(anonymous_2) 168 | FNDA:5,(anonymous_3) 169 | FNDA:104,(anonymous_4) 170 | FNDA:5,(anonymous_5) 171 | FNDA:5,(anonymous_6) 172 | FNDA:1,(anonymous_7) 173 | FNDA:2,(anonymous_8) 174 | FNDA:1,(anonymous_9) 175 | FNDA:1,(anonymous_10) 176 | FNDA:15,(anonymous_11) 177 | FNDA:15,(anonymous_12) 178 | FNDA:15,(anonymous_13) 179 | FNDA:15,(anonymous_14) 180 | FNDA:5,(anonymous_15) 181 | FNDA:5,(anonymous_16) 182 | FNDA:195,(anonymous_17) 183 | FNDA:34,(anonymous_18) 184 | DA:2,5 185 | DA:3,5 186 | DA:4,5 187 | DA:5,5 188 | DA:9,5 189 | DA:10,5 190 | DA:11,5 191 | DA:13,5 192 | DA:19,5 193 | DA:25,5 194 | DA:28,104 195 | DA:29,104 196 | DA:33,5 197 | DA:40,5 198 | DA:43,5 199 | DA:51,5 200 | DA:54,1 201 | DA:56,2 202 | DA:60,5 203 | DA:61,1 204 | DA:62,1 205 | DA:70,15 206 | DA:71,15 207 | DA:72,15 208 | DA:73,15 209 | DA:75,0 210 | DA:78,15 211 | DA:79,15 212 | DA:80,15 213 | DA:81,15 214 | DA:83,0 215 | DA:97,5 216 | DA:98,5 217 | DA:109,5 218 | DA:110,195 219 | DA:111,195 220 | DA:114,195 221 | DA:116,195 222 | DA:117,195 223 | DA:119,195 224 | DA:126,23 225 | DA:127,23 226 | DA:131,23 227 | DA:137,5 228 | DA:138,34 229 | DA:139,34 230 | DA:141,34 231 | DA:143,15 232 | DA:144,14 233 | DA:145,15 234 | DA:147,15 235 | DA:148,15 236 | DA:149,15 237 | DA:151,4 238 | LF:54 239 | LH:52 240 | BRDA:28,0,0,52 241 | BRDA:28,0,1,52 242 | BRDA:29,1,0,52 243 | BRDA:29,1,1,52 244 | BRDA:35,2,0,5 245 | BRDA:35,2,1,0 246 | BRDA:36,3,0,5 247 | BRDA:36,3,1,0 248 | BRDA:37,4,0,5 249 | BRDA:37,4,1,0 250 | BRDA:41,5,0,4 251 | BRDA:42,6,0,0 252 | BRDA:55,7,0,1 253 | BRDA:55,7,1,0 254 | BRDA:65,8,0,1 255 | BRDA:65,8,1,0 256 | BRDA:72,9,0,15 257 | BRDA:72,9,1,0 258 | BRDA:80,10,0,15 259 | BRDA:80,10,1,0 260 | BRDA:99,11,0,1 261 | BRDA:99,11,1,4 262 | BRDA:103,12,0,4 263 | BRDA:103,12,1,0 264 | BRDA:112,13,0,37 265 | BRDA:112,13,1,158 266 | BRDA:116,14,0,195 267 | BRDA:116,14,1,0 268 | BRDA:119,15,0,23 269 | BRDA:119,15,1,172 270 | BRDA:120,16,0,195 271 | BRDA:120,16,1,180 272 | BRDA:120,16,2,76 273 | BRDA:120,16,3,72 274 | BRDA:120,16,4,72 275 | BRDA:139,17,0,8 276 | BRDA:139,17,1,26 277 | BRDA:142,18,0,15 278 | BRDA:142,18,1,15 279 | BRDA:142,18,2,4 280 | BRDA:143,19,0,14 281 | BRDA:143,19,1,1 282 | BRDA:147,20,0,15 283 | BRDA:147,20,1,0 284 | BRF:44 285 | BRH:33 286 | end_of_record 287 | TN: 288 | SF:/Users/andrew/Documents/multibasebox/projects/undo-redo-vuex/src/utils-undo-redo.ts 289 | FN:12,(anonymous_0) 290 | FN:12,(anonymous_1) 291 | FN:14,(anonymous_2) 292 | FN:26,(anonymous_3) 293 | FN:26,(anonymous_4) 294 | FN:28,(anonymous_5) 295 | FN:30,(anonymous_6) 296 | FN:31,(anonymous_7) 297 | FN:44,(anonymous_8) 298 | FN:45,(anonymous_9) 299 | FN:46,(anonymous_10) 300 | FN:51,(anonymous_11) 301 | FN:51,(anonymous_12) 302 | FN:59,(anonymous_13) 303 | FN:59,(anonymous_14) 304 | FN:67,(anonymous_15) 305 | FN:73,(anonymous_16) 306 | FNF:17 307 | FNH:17 308 | FNDA:358,(anonymous_0) 309 | FNDA:358,(anonymous_1) 310 | FNDA:358,(anonymous_2) 311 | FNDA:44,(anonymous_3) 312 | FNDA:44,(anonymous_4) 313 | FNDA:73,(anonymous_5) 314 | FNDA:4,(anonymous_6) 315 | FNDA:4,(anonymous_7) 316 | FNDA:52,(anonymous_8) 317 | FNDA:52,(anonymous_9) 318 | FNDA:52,(anonymous_10) 319 | FNDA:52,(anonymous_11) 320 | FNDA:52,(anonymous_12) 321 | FNDA:52,(anonymous_13) 322 | FNDA:52,(anonymous_14) 323 | FNDA:52,(anonymous_15) 324 | FNDA:52,(anonymous_16) 325 | DA:1,5 326 | DA:12,358 327 | DA:14,358 328 | DA:26,44 329 | DA:27,44 330 | DA:28,73 331 | DA:31,4 332 | DA:44,5 333 | DA:45,52 334 | DA:46,52 335 | DA:47,52 336 | DA:51,52 337 | DA:52,52 338 | DA:53,52 339 | DA:54,52 340 | DA:56,0 341 | DA:59,52 342 | DA:60,52 343 | DA:61,52 344 | DA:62,52 345 | DA:64,0 346 | DA:67,5 347 | DA:73,52 348 | DA:74,52 349 | DA:75,52 350 | DA:77,52 351 | DA:80,52 352 | LF:27 353 | LH:25 354 | BRDA:14,0,0,358 355 | BRDA:14,0,1,0 356 | BRDA:53,1,0,52 357 | BRDA:53,1,1,0 358 | BRDA:61,2,0,52 359 | BRDA:61,2,1,0 360 | BRF:6 361 | BRH:3 362 | end_of_record 363 | TN: 364 | SF:/Users/andrew/Documents/multibasebox/projects/undo-redo-vuex/tests/store/index.ts 365 | FNF:0 366 | FNH:0 367 | DA:1,1 368 | DA:2,1 369 | DA:3,1 370 | DA:4,1 371 | DA:5,1 372 | DA:7,1 373 | DA:9,1 374 | DA:11,1 375 | DA:29,1 376 | LF:9 377 | LH:9 378 | BRF:0 379 | BRH:0 380 | end_of_record 381 | TN: 382 | SF:/Users/andrew/Documents/multibasebox/projects/undo-redo-vuex/tests/store-non-namespaced/index.ts 383 | FN:36,(anonymous_0) 384 | FN:37,(anonymous_1) 385 | FN:37,(anonymous_2) 386 | FN:38,(anonymous_3) 387 | FN:39,(anonymous_4) 388 | FN:53,(anonymous_5) 389 | FN:56,(anonymous_6) 390 | FN:59,(anonymous_7) 391 | FN:62,(anonymous_8) 392 | FN:65,(anonymous_9) 393 | FN:68,(anonymous_10) 394 | FNF:11 395 | FNH:4 396 | FNDA:0,(anonymous_0) 397 | FNDA:0,(anonymous_1) 398 | FNDA:0,(anonymous_2) 399 | FNDA:0,(anonymous_3) 400 | FNDA:0,(anonymous_4) 401 | FNDA:12,(anonymous_5) 402 | FNDA:60,(anonymous_6) 403 | FNDA:0,(anonymous_7) 404 | FNDA:0,(anonymous_8) 405 | FNDA:1,(anonymous_9) 406 | FNDA:1,(anonymous_10) 407 | DA:3,4 408 | DA:4,4 409 | DA:5,4 410 | DA:6,4 411 | DA:8,4 412 | DA:10,4 413 | DA:30,4 414 | DA:35,4 415 | DA:36,0 416 | DA:37,0 417 | DA:38,0 418 | DA:39,0 419 | DA:42,4 420 | DA:45,1 421 | DA:48,1 422 | DA:52,4 423 | DA:54,12 424 | DA:57,60 425 | DA:60,0 426 | DA:63,0 427 | DA:66,1 428 | DA:69,1 429 | DA:73,4 430 | LF:23 431 | LH:17 432 | BRF:0 433 | BRH:0 434 | end_of_record 435 | TN: 436 | SF:/Users/andrew/Documents/multibasebox/projects/undo-redo-vuex/tests/store/modules/auth.ts 437 | FN:8,(anonymous_0) 438 | FN:11,(anonymous_1) 439 | FN:14,(anonymous_2) 440 | FN:15,(anonymous_3) 441 | FN:16,(anonymous_4) 442 | FN:20,(anonymous_5) 443 | FN:21,(anonymous_6) 444 | FN:22,(anonymous_7) 445 | FNF:8 446 | FNH:0 447 | FNDA:0,(anonymous_0) 448 | FNDA:0,(anonymous_1) 449 | FNDA:0,(anonymous_2) 450 | FNDA:0,(anonymous_3) 451 | FNDA:0,(anonymous_4) 452 | FNDA:0,(anonymous_5) 453 | FNDA:0,(anonymous_6) 454 | FNDA:0,(anonymous_7) 455 | DA:1,1 456 | DA:3,1 457 | DA:8,1 458 | DA:10,1 459 | DA:11,0 460 | DA:14,1 461 | DA:15,0 462 | DA:16,0 463 | DA:17,0 464 | DA:20,1 465 | DA:21,0 466 | DA:22,0 467 | DA:23,0 468 | DA:27,1 469 | DA:29,0 470 | DA:30,0 471 | DA:31,0 472 | DA:32,0 473 | DA:33,0 474 | DA:35,0 475 | DA:36,0 476 | DA:40,0 477 | DA:41,0 478 | DA:42,0 479 | DA:43,0 480 | DA:45,0 481 | DA:46,0 482 | DA:51,1 483 | DA:53,0 484 | DA:57,0 485 | DA:58,0 486 | DA:62,0 487 | DA:66,0 488 | DA:67,0 489 | DA:71,0 490 | DA:72,0 491 | DA:76,1 492 | LF:37 493 | LH:9 494 | BRF:0 495 | BRH:0 496 | end_of_record 497 | TN: 498 | SF:/Users/andrew/Documents/multibasebox/projects/undo-redo-vuex/tests/store/modules/list.ts 499 | FN:13,(anonymous_0) 500 | FN:14,(anonymous_1) 501 | FN:14,(anonymous_2) 502 | FN:15,(anonymous_3) 503 | FN:16,(anonymous_4) 504 | FN:43,(anonymous_5) 505 | FN:46,(anonymous_6) 506 | FN:49,(anonymous_7) 507 | FN:52,(anonymous_8) 508 | FN:55,(anonymous_9) 509 | FN:58,(anonymous_10) 510 | FNF:11 511 | FNH:4 512 | FNDA:0,(anonymous_0) 513 | FNDA:0,(anonymous_1) 514 | FNDA:0,(anonymous_2) 515 | FNDA:0,(anonymous_3) 516 | FNDA:0,(anonymous_4) 517 | FNDA:3,(anonymous_5) 518 | FNDA:12,(anonymous_6) 519 | FNDA:0,(anonymous_7) 520 | FNDA:0,(anonymous_8) 521 | FNDA:1,(anonymous_9) 522 | FNDA:1,(anonymous_10) 523 | DA:2,1 524 | DA:3,1 525 | DA:5,1 526 | DA:7,1 527 | DA:12,1 528 | DA:13,0 529 | DA:14,0 530 | DA:15,0 531 | DA:16,0 532 | DA:32,1 533 | DA:35,1 534 | DA:38,1 535 | DA:42,1 536 | DA:44,3 537 | DA:47,12 538 | DA:50,0 539 | DA:53,0 540 | DA:56,1 541 | DA:59,1 542 | DA:63,1 543 | LF:20 544 | LH:14 545 | BRF:0 546 | BRH:0 547 | end_of_record 548 | TN: 549 | SF:/Users/andrew/Documents/multibasebox/projects/undo-redo-vuex/tests/unit/utils-test.ts 550 | FN:3,(anonymous_0) 551 | FN:3,(anonymous_1) 552 | FN:8,(anonymous_2) 553 | FN:8,(anonymous_3) 554 | FNF:4 555 | FNH:4 556 | FNDA:15,(anonymous_0) 557 | FNDA:15,(anonymous_1) 558 | FNDA:15,(anonymous_2) 559 | FNDA:15,(anonymous_3) 560 | DA:1,5 561 | DA:3,15 562 | DA:4,15 563 | DA:5,15 564 | DA:8,15 565 | DA:9,15 566 | DA:10,15 567 | LF:7 568 | LH:7 569 | BRDA:3,0,0,12 570 | BRDA:4,1,0,3 571 | BRDA:4,1,1,12 572 | BRDA:8,2,0,12 573 | BRDA:9,3,0,3 574 | BRDA:9,3,1,12 575 | BRF:6 576 | BRH:6 577 | end_of_record 578 | -------------------------------------------------------------------------------- /coverage/lcov-report/tests/store-non-namespaced/index.ts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for tests/store-non-namespaced/index.ts 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files / tests/store-non-namespaced index.ts 20 |

21 |
22 |
23 | 70.83% 24 | Statements 25 | 17/24 26 |
27 |
28 | 100% 29 | Branches 30 | 0/0 31 |
32 |
33 | 36.36% 34 | Functions 35 | 4/11 36 |
37 |
38 | 73.91% 39 | Lines 40 | 17/23 41 |
42 |
43 |
44 |
45 |

 46 | 
305 | 
1 47 | 2 48 | 3 49 | 4 50 | 5 51 | 6 52 | 7 53 | 8 54 | 9 55 | 10 56 | 11 57 | 12 58 | 13 59 | 14 60 | 15 61 | 16 62 | 17 63 | 18 64 | 19 65 | 20 66 | 21 67 | 22 68 | 23 69 | 24 70 | 25 71 | 26 72 | 27 73 | 28 74 | 29 75 | 30 76 | 31 77 | 32 78 | 33 79 | 34 80 | 35 81 | 36 82 | 37 83 | 38 84 | 39 85 | 40 86 | 41 87 | 42 88 | 43 89 | 44 90 | 45 91 | 46 92 | 47 93 | 48 94 | 49 95 | 50 96 | 51 97 | 52 98 | 53 99 | 54 100 | 55 101 | 56 102 | 57 103 | 58 104 | 59 105 | 60 106 | 61 107 | 62 108 | 63 109 | 64 110 | 65 111 | 66 112 | 67 113 | 68 114 | 69 115 | 70 116 | 71 117 | 72 118 | 73 119 | 74 120 | 75 121 | 76 122 | 77 123 | 78 124 | 79 125 | 80 126 | 81 127 | 82 128 | 83 129 | 84 130 | 85 131 | 86 132 | 87  133 |   134 | 4x 135 | 4x 136 | 4x 137 | 4x 138 |   139 | 4x 140 |   141 | 4x 142 |   143 |   144 |   145 |   146 |   147 |   148 |   149 |   150 |   151 |   152 |   153 |   154 |   155 |   156 |   157 |   158 |   159 |   160 |   161 | 4x 162 |   163 |   164 |   165 |   166 | 4x 167 |   168 |   169 |   170 |   171 |   172 |   173 | 4x 174 |   175 |   176 | 1x 177 |   178 |   179 | 1x 180 |   181 |   182 |   183 | 4x 184 |   185 | 12x 186 |   187 |   188 | 60x 189 |   190 |   191 |   192 |   193 |   194 |   195 |   196 |   197 | 1x 198 |   199 |   200 | 1x 201 |   202 |   203 |   204 | 4x 205 |   206 |   207 |   208 |   209 |   210 |   211 |   212 |   213 |   214 |   215 |   216 |   217 |   218 |  
/* eslint-disable no-param-reassign, no-shadow */
219 |  
220 | import Vuex from "vuex";
221 | import Vue from "vue";
222 | import deepEqual from "fast-deep-equal";
223 | import undoRedo, { scaffoldStore } from "@/undoRedo";
224 |  
225 | Vue.use(Vuex);
226 |  
227 | const debug = process.env.NODE_ENV !== "production";
228 |  
229 | interface State {
230 |   list: Array<any>;
231 |   shadow: Array<any>;
232 | }
233 |  
234 | interface Payload {
235 |   index: number;
236 |   item?: any;
237 | }
238 |  
239 | interface Context {
240 |   commit: Function;
241 |   state: any;
242 |   getters: any;
243 |   rootState: any;
244 |   rootGetters: any;
245 | }
246 |  
247 | const state: State = {
248 |   list: [],
249 |   shadow: []
250 | };
251 |  
252 | const getters = {
253 |   getList: ({ list }: State) => list,
254 |   getItem: (state: State) => ({ item }: Payload) =>
255 |     state.list.find(i => deepEqual(i, item)),
256 |   getShadow: ({ shadow }: State) => shadow
257 | };
258 |  
259 | const actions = {
260 |   // NB: add/remove shadow actions to test undo/redo callback actions
261 |   addShadow({ commit }: Context, { item }: Payload) {
262 |     commit("addShadow", { item });
263 |   },
264 |   removeShadow({ commit }: Context, { index }: Payload) {
265 |     commit("removeShadow", { index });
266 |   }
267 | };
268 |  
269 | const mutations = {
270 |   emptyState: (state: State) => {
271 |     state.list = [];
272 |   },
273 |   addItem: (state: State, { item }: Payload) => {
274 |     state.list = [...state.list, item];
275 |   },
276 |   updateItem: (state: State, { item, index }: Payload) => {
277 |     state.list.splice(index, 1, item);
278 |   },
279 |   removeItem: (state: State, { index }: Payload) => {
280 |     state.list.splice(index, 1);
281 |   },
282 |   addShadow: (state: State, { item }: Payload) => {
283 |     state.shadow = [...state.shadow, item];
284 |   },
285 |   removeShadow: (state: State, { index }: Payload) => {
286 |     state.shadow.splice(index, 1);
287 |   }
288 | };
289 |  
290 | export default new Vuex.Store(
291 |   scaffoldStore({
292 |     plugins: [
293 |       undoRedo({
294 |         ignoreMutations: ["addShadow", "removeShadow"]
295 |       })
296 |     ],
297 |     strict: debug,
298 |     state,
299 |     getters,
300 |     actions,
301 |     mutations
302 |   })
303 | );
304 |  
306 |
307 |
308 | 312 | 313 | 314 | 321 | 322 | 323 | 324 | -------------------------------------------------------------------------------- /coverage/lcov-report/tests/store/modules/auth.ts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for tests/store/modules/auth.ts 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files / tests/store/modules auth.ts 20 |

21 |
22 |
23 | 23.68% 24 | Statements 25 | 9/38 26 |
27 |
28 | 100% 29 | Branches 30 | 0/0 31 |
32 |
33 | 0% 34 | Functions 35 | 0/8 36 |
37 |
38 | 24.32% 39 | Lines 40 | 9/37 41 |
42 |
43 |
44 |
45 |

 46 | 
296 | 
1 47 | 2 48 | 3 49 | 4 50 | 5 51 | 6 52 | 7 53 | 8 54 | 9 55 | 10 56 | 11 57 | 12 58 | 13 59 | 14 60 | 15 61 | 16 62 | 17 63 | 18 64 | 19 65 | 20 66 | 21 67 | 22 68 | 23 69 | 24 70 | 25 71 | 26 72 | 27 73 | 28 74 | 29 75 | 30 76 | 31 77 | 32 78 | 33 79 | 34 80 | 35 81 | 36 82 | 37 83 | 38 84 | 39 85 | 40 86 | 41 87 | 42 88 | 43 89 | 44 90 | 45 91 | 46 92 | 47 93 | 48 94 | 49 95 | 50 96 | 51 97 | 52 98 | 53 99 | 54 100 | 55 101 | 56 102 | 57 103 | 58 104 | 59 105 | 60 106 | 61 107 | 62 108 | 63 109 | 64 110 | 65 111 | 66 112 | 67 113 | 68 114 | 69 115 | 70 116 | 71 117 | 72 118 | 73 119 | 74 120 | 75 121 | 76 122 | 77 123 | 78 124 | 79 125 | 80 126 | 81 127 | 82 128 | 83 129 | 841x 130 |   131 | 1x 132 |   133 |   134 |   135 |   136 | 1x 137 |   138 | 1x 139 |   140 |   141 |   142 | 1x 143 |   144 |   145 |   146 |   147 |   148 | 1x 149 |   150 |   151 |   152 |   153 |   154 |   155 | 1x 156 |   157 |   158 |   159 |   160 |   161 |   162 |   163 |   164 |   165 |   166 |   167 |   168 |   169 |   170 |   171 |   172 |   173 |   174 |   175 |   176 |   177 |   178 |   179 | 1x 180 |   181 |   182 |   183 |   184 |   185 |   186 |   187 |   188 |   189 |   190 |   191 |   192 |   193 |   194 |   195 |   196 |   197 |   198 |   199 |   200 |   201 |   202 |   203 |   204 | 1x 205 |   206 |   207 |   208 |   209 |   210 |   211 |   212 |  
const debug = process.env.NODE_ENV !== "production";
213 |  
214 | const state = {
215 |   token: "",
216 |   status: ""
217 | };
218 |  
219 | const validateToken = (token: string) => token === "login-token";
220 |  
221 | const getters = {
222 |   isAuthenticated: (state: any) => validateToken(state.token)
223 | };
224 |  
225 | const apiLogin = (): Promise<{ token: string }> =>
226 |   new Promise(resolve => {
227 |     setTimeout(() => {
228 |       resolve({ token: "login-token" });
229 |     }, 200);
230 |   });
231 | const apiLogout = () =>
232 |   new Promise(resolve => {
233 |     setTimeout(() => {
234 |       resolve();
235 |     }, 200);
236 |   });
237 |  
238 | const actions = {
239 |   async login({ commit }: { commit: any }) {
240 |     try {
241 |       commit("setStatusLoading");
242 |       const { token } = await apiLogin();
243 |       validateToken(token);
244 |       commit("setStatusSuccess", { token });
245 |     } catch (e) {
246 |       console.error(e);
247 |       commit("setStatusError");
248 |     }
249 |   },
250 |   async logout({ commit }: { commit: any }) {
251 |     try {
252 |       commit("setStatusLoading");
253 |       await apiLogout();
254 |       commit("setStatusLoggedOut");
255 |     } catch (e) {
256 |       console.error(e);
257 |       commit("setStatusError");
258 |     }
259 |   }
260 | };
261 |  
262 | const mutations = {
263 |   setStatusLoading(state: any) {
264 |     state.status = "loading";
265 |   },
266 |  
267 |   setStatusSuccess(state: any, { token }: { token: string }) {
268 |     state.status = "success";
269 |     state.token = token;
270 |   },
271 |  
272 |   setToken(state: any, { token }: { token: string }) {
273 |     state.token = token;
274 |   },
275 |  
276 |   setStatusError(state: any) {
277 |     state.status = "error";
278 |     state.token = "";
279 |   },
280 |  
281 |   setStatusLoggedOut(state: any) {
282 |     state.status = "success";
283 |     state.token = "";
284 |   }
285 | };
286 |  
287 | export default {
288 |   state,
289 |   getters,
290 |   actions,
291 |   mutations,
292 |   namespaced: true,
293 |   debug
294 | };
295 |  
297 |
298 |
299 | 303 | 304 | 305 | 312 | 313 | 314 | 315 | -------------------------------------------------------------------------------- /docs/guide/usage.md: -------------------------------------------------------------------------------- 1 | --- 2 | prev: ./installation 3 | next: ./testing 4 | --- 5 | 6 | # Usage 7 | 8 | As a standard [plugin for Vuex](https://vuex.vuejs.org/guide/plugins.html), `undo-redo-vuex` can be used with the following setup: 9 | 10 | ## How to use it in your store module 11 | 12 | The `scaffoldStore` helper function will bootstrap a vuex store to setup the `state`, `actions` and `mutations` to work with the plugin. 13 | 14 | ```js 15 | import { scaffoldStore } from "undo-redo-vuex"; 16 | 17 | const state = { 18 | list: [], 19 | /** 20 | * 'resetList' is a placeholder (initially the same as 'list') to 21 | * fast-forward 'list' during a 'reset()' 22 | */ 23 | resetList: [], 24 | // Define vuex state properties as normal 25 | }; 26 | const actions = { 27 | // Define vuex actions as normal 28 | }; 29 | const mutations = { 30 | /* 31 | * NB: The emptyState mutation HAS to be implemented. 32 | * This mutation resets the state props to a "base" state, 33 | * on top of which subsequent mutations are "replayed" 34 | * whenever undo/redo is dispatched. 35 | */ 36 | emptyState: state => { 37 | // Sets some state prop to the 'reset placeholder' value 38 | state.list = [...state.resetList]; 39 | }, 40 | 41 | resetState: state => { 42 | // Sets the 'reset placeholder' (see state.resetList) prop to the current state 43 | state.resetList = [...state.list]; 44 | }, 45 | 46 | // Define vuex mutations as normal 47 | }; 48 | 49 | export default scaffoldStore({ 50 | state, 51 | actions, 52 | mutations, 53 | namespaced: true, // NB: do not include this is non-namespaced stores 54 | }); 55 | ``` 56 | 57 | ### Setting up `emptyState` and `resetState` mutations 58 | 59 | The undo and redo functionality requires the `emptyState` mutation (in the example above) to be defined by the developer. This mutation (which is not tracked in the undo and redo stacks) allows state to be 'replayed' in the chronological order mutations are executed. The `clear()` action also commits `emptyState` to re-initialize the store to its original state. The `reset()` action additionally requires the `resetState` mutation to be defined, allowing the state to 'fast-forward' to its point when `reset()` was called before other mutations are 'replayed'. 60 | 61 | Alternatively, the `scaffoldState`, `scaffoldActions`, and `scaffoldMutations` helper functions can be individually required to bootstrap the vuex store. This will expose `canUndo` and `canRedo` as vuex state properties which can be used to enable/disable UI controls (e.g. undo/redo buttons). 62 | 63 | ```js 64 | import { 65 | scaffoldState, 66 | scaffoldActions, 67 | scaffoldMutations, 68 | } from "undo-redo-vuex"; 69 | 70 | const state = { 71 | list: [], 72 | resetList: [], 73 | // Define vuex state properties as normal 74 | }; 75 | const actions = { 76 | // Define vuex actions as normal 77 | }; 78 | const mutations = { 79 | emptyState: state => { 80 | state.list = [...(state.resetList || [])]; 81 | }, 82 | resetState: state => { 83 | // Sets the 'reset placeholder' (see state.resetList) prop to the current state 84 | state.resetList = [...state.list]; 85 | }, 86 | // Define vuex mutations as normal 87 | }; 88 | 89 | export default { 90 | // Use the respective helper function to scaffold state, actions and mutations 91 | state: scaffoldState(state), 92 | actions: scaffoldActions(actions), 93 | mutations: scaffoldMutations(mutations), 94 | namespaced: true, // NB: do not include this is non-namespaced stores 95 | }; 96 | ``` 97 | 98 | ## Setup the store plugin 99 | 100 | - Namespaced modules 101 | 102 | ```js 103 | import Vuex from "vuex"; 104 | import undoRedo from "undo-redo-vuex"; 105 | 106 | // NB: The following config is used for namespaced store modules. 107 | // Please see below for configuring a non-namespaced (basic) vuex store 108 | export default new Vuex.Store({ 109 | plugins: [ 110 | undoRedo({ 111 | // The config object for each store module is defined in the 'paths' array 112 | paths: [ 113 | { 114 | namespace: "list", 115 | // Any mutations that you want the undo/redo mechanism to ignore 116 | ignoreMutations: ["addShadow", "removeShadow"], 117 | }, 118 | ], 119 | }), 120 | ], 121 | /* 122 | * For non-namespaced stores: 123 | * state, 124 | * actions, 125 | * mutations, 126 | */ 127 | // Modules for namespaced stores: 128 | modules: { 129 | list, 130 | }, 131 | }); 132 | ``` 133 | 134 | - Non-namespaced (basic) vuex store 135 | 136 | ```js 137 | import Vuex from "vuex"; 138 | import undoRedo from "undo-redo-vuex"; 139 | 140 | export default new Vuex.Store({ 141 | state, 142 | getters, 143 | actions, 144 | mutations, 145 | plugins: [ 146 | undoRedo({ 147 | // NB: Include 'ignoreMutations' at root level, and do not provide the list of 'paths'. 148 | ignoreMutations: ["addShadow", "removeShadow"], 149 | }), 150 | ], 151 | }); 152 | ``` 153 | 154 | ## Accessing `canUndo` and `canRedo` properties 155 | 156 | - Vue SFC (.vue) 157 | 158 | ```js 159 | import { mapState } from "vuex"; 160 | 161 | const MyComponent = { 162 | computed: { 163 | ...mapState({ 164 | undoButtonEnabled: "canUndo", 165 | redoButtonEnabled: "canRedo", 166 | }), 167 | }, 168 | }; 169 | ``` 170 | 171 | ## Undoing actions with `actionGroups` 172 | 173 | In certain scenarios, undo/redo is required on store actions which may consist of one or more mutations. This feature is accessible by including a `actionGroup` property in the `payload` object of the associated vuex action. Please refer to `test/test-action-group-undo.js` for more comprehensive scenarios. 174 | 175 | - vuex module 176 | 177 | ```js 178 | const actions = { 179 | myAction({ commit }, payload) { 180 | // An arbitrary label to identify the group of mutations to undo/redo 181 | const actionGroup = "myAction"; 182 | 183 | // All mutation payloads should contain the actionGroup property 184 | commit("mutationA", { 185 | ...payload, 186 | actionGroup, 187 | }); 188 | commit("mutationB", { 189 | someProp: true, 190 | actionGroup, 191 | }); 192 | }, 193 | }; 194 | ``` 195 | 196 | - Undo/redo stack illustration 197 | 198 | ```js 199 | // After dispatching 'myAction' once 200 | done = [ 201 | { type: "mutationA", payload: { ...payload, actionGroup: "myAction" } }, 202 | { type: "mutationB", payload: { someProp: true, actionGroup: "myAction" } }, 203 | ]; 204 | undone = []; 205 | 206 | // After dispatching 'undo' 207 | done = []; 208 | undone = [ 209 | { type: "mutationA", payload: { ...payload, actionGroup: "myAction" } }, 210 | { type: "mutationB", payload: { someProp: true, actionGroup: "myAction" } }, 211 | ]; 212 | ``` 213 | 214 | ## Working with undo/redo on mutations produced by side effects (i.e. API/database calls) 215 | 216 | In Vue.js apps, Vuex mutations are often committed inside actions that contain asynchronous calls to an API/database service: 217 | For instance, `commit("list/addItem", item)` could be called after an axios request to `PUT https:///v1/list`. 218 | 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). 219 | 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). 220 | 221 | 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): 222 | 223 | ```js 224 | const actions = { 225 | saveItem: async (({ commit }), item) => { 226 | await axios.put(PUT_ITEM, item); 227 | commit("addItem", { 228 | item, 229 | // dispatch("deleteItem", { item }) on undo() 230 | undoCallback: "deleteItem", 231 | // dispatch("saveItem", { item }) on redo() 232 | redoCallback: "saveItem" 233 | }); 234 | }, 235 | deleteItem: async (({ commit }), item) => { 236 | await axios.delete(DELETE_ITEM, item); 237 | commit("removeItem", { 238 | item, 239 | // dispatch("saveItem", { item }) on undo() 240 | undoCallback: "saveItem", 241 | // dispatch("deleteItem", { item }) on redo() 242 | redoCallback: "deleteItem" 243 | }); 244 | } 245 | }; 246 | 247 | const mutations = { 248 | // NB: payload parameters as object literal props 249 | addItem: (state, { item }) => { 250 | // adds the item to the list 251 | }, 252 | removeItem: (state, { item }) => { 253 | // removes the item from the list 254 | } 255 | }; 256 | ``` 257 | 258 | ## Clearing the undo/redo stacks with the `clear` action 259 | 260 | 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). 261 | 262 | ```js 263 | /** 264 | * Current done stack: [mutationA, mutation B] 265 | * Current undone stack: [mutationC] 266 | **/ 267 | this.$store.dispatch("list/clear"); 268 | 269 | await this.$nextTick(); 270 | /** 271 | * Current done stack: [] 272 | * Current undone stack: [] 273 | **/ 274 | ``` 275 | 276 | ## Resetting the current state with the `reset` action 277 | 278 | Unlike the `clear` action, `reset` empties the `done` and `undone` stacks, but maintains the state of the store up to this particular point. This action is scaffolded when using `scaffoldActions(actions)` of `scaffoldStore(store)`. This enhancement is described further in [issue #13](https://github.com/factorial-io/undo-redo-vuex/issues/13), with accompanying [unit tests](https://github.com/factorial-io/undo-redo-vuex/tree/master/tests/unit/test.reset.spec.ts). 279 | 280 | ```js 281 | /** 282 | * Current done stack: [mutationA, mutation B] 283 | * Current undone stack: [mutationC] 284 | **/ 285 | this.$store.dispatch("list/reset"); 286 | 287 | await this.$nextTick(); 288 | /** 289 | * Current done stack: [] 290 | * Current undone stack: [] 291 | * state: resetState (i.e. initial state + mutationA + mutationB) 292 | **/ 293 | ``` 294 | 295 | ## Inspecting `done` and `undone` mutations 296 | 297 | Some vuex powered applications may require knowledge of the `done` and `undone` stacks, e.g. to preserve undo/redo functionality between page loads. The following configuration exposes the stacks by scaffoling a `undoRedoConfig` object in the store or module which uses the plugin: 298 | 299 | ```js 300 | import Vuex from "vuex"; 301 | import undoRedo, { scaffoldStore } from "undo-redo-vuex"; 302 | 303 | // state, getters, actions & mutations ... 304 | 305 | // boolean flag to expose done and undone stacks 306 | const exposeUndoRedoConfig = true; 307 | 308 | const storeConfig = scaffoldStore({ 309 | state, 310 | actions, 311 | mutations 312 | }, exposeUndoRedoConfig); // boolean flag as the second optional param 313 | 314 | /** 315 | * NB: When configuring state, actions or mutations with scaffoldState, 316 | * scaffoldActions or scaffoldMutations, the exposeUndoRedoConfig = true 317 | * flag should be passed as the second param. 318 | */ 319 | 320 | const store = new Vuex.Store({ 321 | ...storeConfig, 322 | plugins: [ 323 | // Pass boolean flag as an named option 324 | undoRedo({ exposeUndoRedoConfig }) 325 | ] 326 | }); 327 | ``` 328 | 329 | To access the exposed `done` and `undone` stacks, e.g. in a component: 330 | 331 | ```js 332 | const { done, undone } = this.$store.state; 333 | ``` 334 | 335 | This enhancement is described further in [issue #45](https://github.com/factorial-io/undo-redo-vuex/issues/45), with accompanying [unit tests](https://github.com/factorial-io/undo-redo-vuex/tree/master/tests/unit/test.expose-undo-redo-stacks.spec.ts). 336 | -------------------------------------------------------------------------------- /coverage/lcov-report/src/redo.ts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for src/redo.ts 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files / src redo.ts 20 |

21 |
22 |
23 | 100% 24 | Statements 25 | 27/27 26 |
27 |
28 | 87.5% 29 | Branches 30 | 14/16 31 |
32 |
33 | 100% 34 | Functions 35 | 4/4 36 |
37 |
38 | 100% 39 | Lines 40 | 27/27 41 |
42 |
43 |
44 |
45 |

 46 | 
344 | 
1 47 | 2 48 | 3 49 | 4 50 | 5 51 | 6 52 | 7 53 | 8 54 | 9 55 | 10 56 | 11 57 | 12 58 | 13 59 | 14 60 | 15 61 | 16 62 | 17 63 | 18 64 | 19 65 | 20 66 | 21 67 | 22 68 | 23 69 | 24 70 | 25 71 | 26 72 | 27 73 | 28 74 | 29 75 | 30 76 | 31 77 | 32 78 | 33 79 | 34 80 | 35 81 | 36 82 | 37 83 | 38 84 | 39 85 | 40 86 | 41 87 | 42 88 | 43 89 | 44 90 | 45 91 | 46 92 | 47 93 | 48 94 | 49 95 | 50 96 | 51 97 | 52 98 | 53 99 | 54 100 | 55 101 | 56 102 | 57 103 | 58 104 | 59 105 | 60 106 | 61 107 | 62 108 | 63 109 | 64 110 | 65 111 | 66 112 | 67 113 | 68 114 | 69 115 | 70 116 | 71 117 | 72 118 | 73 119 | 74 120 | 75 121 | 76 122 | 77 123 | 78 124 | 79 125 | 80 126 | 81 127 | 82 128 | 83 129 | 84 130 | 85 131 | 86 132 | 87 133 | 88 134 | 89 135 | 90 136 | 91 137 | 92 138 | 93 139 | 94 140 | 95 141 | 96 142 | 97 143 | 98 144 | 99 145 | 1005x 146 |   147 |   148 |   149 |   150 |   151 |   152 |   153 |   154 |   155 |   156 |   157 |   158 |   159 | 5x 160 |   161 |   162 |   163 |   164 |   165 | 14x 166 | 14x 167 | 14x 168 |   169 |   170 |   171 |   172 |   173 |   174 | 14x 175 |   176 |   177 |   178 |   179 |   180 |   181 |   182 |   183 |   184 |   185 |   186 |   187 | 31x 188 |   189 | 14x 190 |   191 | 14x 192 | 17x 193 |   194 | 7x 195 |   196 |   197 | 10x 198 | 10x 199 |   200 |   201 | 10x 202 |   203 | 10x 204 | 10x 205 |   206 |   207 | 31x 208 |   209 |   210 |   211 |   212 |   213 |   214 |   215 |   216 | 14x 217 |   218 | 14x 219 |   220 | 22x 221 |   222 |   223 |   224 |   225 |   226 | 22x 227 |   228 | 22x 229 |   230 |   231 |   232 |   233 | 14x 234 | 14x 235 | 14x 236 | 14x 237 |   238 |   239 |   240 |   241 | 14x 242 |   243 |   244 |  
import {
245 |   getConfig,
246 |   pipeActions,
247 |   setConfig,
248 |   updateCanUndoRedo
249 | } from "./utils-undo-redo";
250 |  
251 | /**
252 |  * The Redo function - commits the latest undone mutation to the store,
253 |  * and pushes it to the done stack
254 |  *
255 |  * @module store/plugins/undoRedo:redo
256 |  * @function
257 |  */
258 | export default ({
259 |   paths,
260 |   store
261 | }: {
262 |   paths: UndoRedoOptions[];
263 |   store: any;
264 | }) => async (namespace: string) => {
265 |   const config = getConfig(paths)(namespace);
266 |   Eif (Object.keys(config).length) {
267 |     /**
268 |      * @var {Array} undone - The updated undone stack
269 |      * @var {Array} commits - The list of mutations to be redone
270 |      * NB: The reduceRight operation is used to identify the mutation(s) from the
271 |      * top of the undone stack to be redone
272 |      */
273 |     const { undone, commits } = config.undone.reduceRight(
274 |       (
275 |         {
276 |           commits,
277 |           undone,
278 |           proceed
279 |         }: {
280 |           commits: Array<Mutation> | [];
281 |           undone: Array<Mutation> | [];
282 |           proceed: boolean;
283 |         },
284 |         m: Mutation
285 |       ) => {
286 |         if (!commits.length) {
287 |           // The "topmost" mutation
288 |           commits = [m];
289 |           // Do not find more mutations if the mutations does not belong to a group
290 |           proceed = !!m.payload.actionGroup;
291 |         } else if (!proceed) {
292 |           // The mutation(s) to redo have been identified
293 |           undone = [m, ...undone];
294 |         } else {
295 |           // Find mutations belonging to the same actionGroup
296 |           const lastCommit = commits[commits.length - 1];
297 |           const { actionGroup } = lastCommit.payload;
298 |           // Stop finding more mutations if the current mutation belongs to
299 |           // another actionGroup, or does not have an actionGroup
300 |           proceed =
301 |             m.payload.actionGroup && m.payload.actionGroup === actionGroup;
302 |           commits = [...(proceed ? [m] : []), ...commits];
303 |           undone = [...(proceed ? [] : [m]), ...undone];
304 |         }
305 |  
306 |         return { commits, undone, proceed };
307 |       },
308 |       {
309 |         commits: [],
310 |         undone: [],
311 |         proceed: true
312 |       }
313 |     );
314 |  
315 |     config.newMutation = false;
316 |     // NB: The array of redoCallbacks and respective action payloads
317 |     const redoCallbacks = commits.map(async ({ type, payload }: Mutation) => {
318 |       // NB: Commit each mutation in the redo stack
319 |       store.commit(
320 |         type,
321 |         Array.isArray(payload) ? [...payload] : payload.constructor(payload)
322 |       );
323 |  
324 |       // Check if there is an redo callback action
325 |       const { redoCallback } = payload;
326 |       // NB: The object containing the redoCallback action and payload
327 |       return {
328 |         action: redoCallback ? `${namespace}${redoCallback}` : "",
329 |         payload
330 |       };
331 |     });
332 |     await pipeActions(store)(await Promise.all(redoCallbacks));
333 |     config.done = [...config.done, ...commits];
334 |     config.newMutation = true;
335 |     setConfig(paths)(namespace, {
336 |       ...config,
337 |       undone
338 |     });
339 |  
340 |     updateCanUndoRedo({ paths, store })(namespace);
341 |   }
342 | };
343 |  
345 |
346 |
347 | 351 | 352 | 353 | 360 | 361 | 362 | 363 | -------------------------------------------------------------------------------- /coverage/lcov-report/src/undo.ts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for src/undo.ts 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files / src undo.ts 20 |

21 |
22 |
23 | 100% 24 | Statements 25 | 32/32 26 |
27 |
28 | 83.33% 29 | Branches 30 | 15/18 31 |
32 |
33 | 100% 34 | Functions 35 | 5/5 36 |
37 |
38 | 100% 39 | Lines 40 | 31/31 41 |
42 |
43 |
44 |
45 |

 46 | 
371 | 
1 47 | 2 48 | 3 49 | 4 50 | 5 51 | 6 52 | 7 53 | 8 54 | 9 55 | 10 56 | 11 57 | 12 58 | 13 59 | 14 60 | 15 61 | 16 62 | 17 63 | 18 64 | 19 65 | 20 66 | 21 67 | 22 68 | 23 69 | 24 70 | 25 71 | 26 72 | 27 73 | 28 74 | 29 75 | 30 76 | 31 77 | 32 78 | 33 79 | 34 80 | 35 81 | 36 82 | 37 83 | 38 84 | 39 85 | 40 86 | 41 87 | 42 88 | 43 89 | 44 90 | 45 91 | 46 92 | 47 93 | 48 94 | 49 95 | 50 96 | 51 97 | 52 98 | 53 99 | 54 100 | 55 101 | 56 102 | 57 103 | 58 104 | 59 105 | 60 106 | 61 107 | 62 108 | 63 109 | 64 110 | 65 111 | 66 112 | 67 113 | 68 114 | 69 115 | 70 116 | 71 117 | 72 118 | 73 119 | 74 120 | 75 121 | 76 122 | 77 123 | 78 124 | 79 125 | 80 126 | 81 127 | 82 128 | 83 129 | 84 130 | 85 131 | 86 132 | 87 133 | 88 134 | 89 135 | 90 136 | 91 137 | 92 138 | 93 139 | 94 140 | 95 141 | 96 142 | 97 143 | 98 144 | 99 145 | 100 146 | 101 147 | 102 148 | 103 149 | 104 150 | 105 151 | 106 152 | 107 153 | 108 154 | 1095x 155 | 5x 156 |   157 |   158 |   159 |   160 |   161 |   162 |   163 |   164 |   165 |   166 |   167 |   168 |   169 | 5x 170 |   171 |   172 |   173 |   174 |   175 | 15x 176 | 15x 177 |   178 | 15x 179 |   180 |   181 |   182 |   183 |   184 |   185 | 15x 186 |   187 |   188 |   189 |   190 |   191 |   192 |   193 |   194 |   195 |   196 |   197 |   198 | 51x 199 |   200 | 15x 201 |   202 | 15x 203 | 36x 204 |   205 | 20x 206 |   207 | 16x 208 | 16x 209 |   210 |   211 | 16x 212 |   213 | 16x 214 | 16x 215 |   216 |   217 | 51x 218 |   219 |   220 |   221 |   222 |   223 |   224 |   225 |   226 |   227 | 24x 228 |   229 |   230 |   231 | 15x 232 |   233 | 15x 234 | 15x 235 | 15x 236 | 15x 237 | 27x 238 |   239 |   240 |   241 |   242 |   243 |   244 |   245 | 27x 246 | 27x 247 |   248 |   249 |   250 |   251 | 15x 252 | 15x 253 | 15x 254 |   255 |   256 |   257 |   258 |   259 | 15x 260 |   261 |   262 |  
import { EMPTY_STATE } from "./constants";
263 | import {
264 |   getConfig,
265 |   pipeActions,
266 |   setConfig,
267 |   updateCanUndoRedo
268 | } from "./utils-undo-redo";
269 |  
270 | /**
271 |  * The Undo function - pushes the latest done mutation to the top of the undone
272 |  * stack by popping the done stack and 'replays' all mutations in the done stack
273 |  *
274 |  * @module store/plugins/undoRedo:undo
275 |  * @function
276 |  */
277 | export default ({
278 |   paths,
279 |   store
280 | }: {
281 |   paths: UndoRedoOptions[];
282 |   store: any;
283 | }) => async (namespace: string) => {
284 |   const config = getConfig(paths)(namespace);
285 |  
286 |   Eif (Object.keys(config).length) {
287 |     /**
288 |      * @var {Array} done - The updated done stack
289 |      * @var {Array} commits - The list of mutations which are undone
290 |      * NB: The reduceRight operation is used to identify the mutation(s) from the
291 |      * top of the done stack to be undone
292 |      */
293 |     const { done, commits } = config.done.reduceRight(
294 |       (
295 |         {
296 |           commits,
297 |           done,
298 |           proceed
299 |         }: {
300 |           commits: Array<Mutation> | [];
301 |           done: Array<Mutation> | [];
302 |           proceed: boolean;
303 |         },
304 |         m: Mutation
305 |       ) => {
306 |         if (!commits.length) {
307 |           // The "topmost" mutation from the done stack
308 |           commits = [m];
309 |           // Do not find more mutations if the mutations does not belong to a group
310 |           proceed = !!m.payload.actionGroup;
311 |         } else if (!proceed) {
312 |           // Unshift the mutation to the done stack
313 |           done = [m, ...done];
314 |         } else {
315 |           const lastUndone = commits[commits.length - 1];
316 |           const { actionGroup } = lastUndone.payload;
317 |           // Unshift to commits if mutation belongs to the same actionGroup,
318 |           // otherwise unshift to the done stack
319 |           proceed =
320 |             m.payload.actionGroup && m.payload.actionGroup === actionGroup;
321 |           commits = [...(proceed ? [m] : []), ...commits];
322 |           done = [...(proceed ? [] : [m]), ...done];
323 |         }
324 |  
325 |         return { done, commits, proceed };
326 |       },
327 |       {
328 |         done: [],
329 |         commits: [],
330 |         proceed: true
331 |       }
332 |     );
333 |  
334 |     // Check if there are any undo callback actions
335 |     const undoCallbacks = commits.map(({ payload }: { payload: any }) => ({
336 |       action: payload.undoCallback ? `${namespace}${payload.undoCallback}` : "",
337 |       payload
338 |     }));
339 |     await pipeActions(store)(undoCallbacks);
340 |  
341 |     const undone = [...config.undone, ...commits];
342 |     config.newMutation = false;
343 |     store.commit(`${namespace}${EMPTY_STATE}`);
344 |     const redoCallbacks = done.map(async (mutation: Mutation) => {
345 |       store.commit(
346 |         mutation.type,
347 |         Array.isArray(mutation.payload)
348 |           ? [...mutation.payload]
349 |           : mutation.payload.constructor(mutation.payload)
350 |       );
351 |  
352 |       // Check if there is an undo callback action
353 |       const { redoCallback } = mutation.payload;
354 |       return {
355 |         action: redoCallback ? `${namespace}${redoCallback}` : "",
356 |         payload: mutation.payload
357 |       };
358 |     });
359 |     await pipeActions(store)(await Promise.all(redoCallbacks));
360 |     config.newMutation = true;
361 |     setConfig(paths)(namespace, {
362 |       ...config,
363 |       done,
364 |       undone
365 |     });
366 |  
367 |     updateCanUndoRedo({ paths, store })(namespace);
368 |   }
369 | };
370 |  
372 |
373 |
374 | 378 | 379 | 380 | 387 | 388 | 389 | 390 | --------------------------------------------------------------------------------