]> git.ipfire.org Git - thirdparty/vuejs/pinia.git/commitdiff
feat(devtools): load/save state
authorEduardo San Martin Morote <posva13@gmail.com>
Sun, 30 May 2021 17:39:27 +0000 (19:39 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Sun, 30 May 2021 17:39:27 +0000 (19:39 +0200)
package.json
src/createPinia.ts
src/devtools/plugin.ts
src/global.d.ts
yarn.lock

index abfb3dfd02f2a13d3760bd4dc07fdf72cbd2392f..33ddaa1f465495fef415cfbff9c5287d72cb0184 100644 (file)
@@ -68,6 +68,7 @@
     "@rollup/plugin-node-resolve": "^13.0.0",
     "@rollup/plugin-replace": "^2.4.2",
     "@sucrase/jest-plugin": "^2.1.0",
+    "@types/file-saver": "^2.0.2",
     "@types/jest": "^26.0.23",
     "@types/node": "^15.6.1",
     "@vue/server-renderer": "^3.0.11",
@@ -76,6 +77,7 @@
     "brotli": "^1.3.2",
     "codecov": "^3.8.2",
     "conventional-changelog-cli": "^2.1.1",
+    "file-saver": "^2.0.5",
     "jest": "^26.6.3",
     "jest-mock-warn": "^1.1.0",
     "lint-staged": "^11.0.0",
index 11b4af5cfa338a6e80ce4f97dc541963157b7ada..587928aaf4775a30aee6dbe54fc76493fe194f16 100644 (file)
@@ -4,7 +4,7 @@ import {
   setActivePinia,
   piniaSymbol,
 } from './rootStore'
-import { ref, App } from 'vue'
+import { ref, App, markRaw } from 'vue'
 import { devtoolsPlugin } from './devtools'
 import { IS_CLIENT } from './env'
 
@@ -21,7 +21,7 @@ export function createPinia(): Pinia {
   // plugins added before calling app.use(pinia)
   const toBeInstalled: PiniaStorePlugin[] = []
 
-  const pinia: Pinia = {
+  const pinia: Pinia = markRaw({
     install(app: App) {
       pinia._a = localApp = app
       app.provide(piniaSymbol, pinia)
@@ -48,7 +48,7 @@ export function createPinia(): Pinia {
     _a: localApp!,
 
     state,
-  }
+  })
 
   // pinia devtools rely on dev only features so they cannot be forced unless
   // the dev build of Vue is used
index 827a4bebd05f16ca188a55860145799ea7e83bf3..5f6860137395696b32aeb7fdee6f0abd17b495a9 100644 (file)
@@ -15,6 +15,7 @@ import {
   formatStoreForInspectorState,
   formatStoreForInspectorTree,
 } from './formatting'
+import { saveAs } from 'file-saver'
 
 /**
  * Registered stores used for devtools.
@@ -36,12 +37,23 @@ function checkClipboardAccess() {
   }
 }
 
+function checkNotFocusedError(error: Error) {
+  if (error.message.toLowerCase().includes('document is not focused')) {
+    toastMessage(
+      'You need to activate the "Emulate a focused page" setting in the "Rendering" panel of devtools.',
+      'warn'
+    )
+    return true
+  }
+}
+
 async function actionGlobalCopyState(pinia: Pinia) {
   if (checkClipboardAccess()) return
   try {
     await navigator.clipboard.writeText(JSON.stringify(pinia.state.value))
     toastMessage('Global state copied to clipboard.')
   } catch (error) {
+    if (checkNotFocusedError(error)) return
     toastMessage(
       `Failed to serialize the state. Check the console for more details.`,
       'error'
@@ -56,6 +68,7 @@ async function actionGlobalPasteState(pinia: Pinia) {
     pinia.state.value = JSON.parse(await navigator.clipboard.readText())
     toastMessage('Global state pasted from clipboard.')
   } catch (error) {
+    if (checkNotFocusedError(error)) return
     toastMessage(
       `Failed to deserialize the state from clipboard. Check the console for more details.`,
       'error'
@@ -64,6 +77,64 @@ async function actionGlobalPasteState(pinia: Pinia) {
   }
 }
 
+async function actionGlobalSaveState(pinia: Pinia) {
+  try {
+    saveAs(
+      new Blob([JSON.stringify(pinia.state.value)], {
+        type: 'text/plain;charset=utf-8',
+      }),
+      'pinia-state.json'
+    )
+  } catch (error) {
+    toastMessage(
+      `Failed to export the state as JSON. Check the console for more details.`,
+      'error'
+    )
+    console.error(error)
+  }
+}
+
+let fileInput: HTMLInputElement | undefined
+function getFileOpener() {
+  if (!fileInput) {
+    fileInput = document.createElement('input')
+    fileInput.type = 'file'
+    fileInput.accept = '.json'
+  }
+
+  function openFile(): Promise<null | { text: string; file: File }> {
+    return new Promise((resolve, reject) => {
+      fileInput!.onchange = async () => {
+        const files = fileInput!.files
+        if (!files) return resolve(null)
+        const file = files.item(0)
+        if (!file) return resolve(null)
+        return resolve({ text: await file.text(), file })
+      }
+      fileInput!.oncancel = () => resolve(null)
+      fileInput!.click()
+    })
+  }
+  return openFile
+}
+
+async function actionGlobalOpenStateFile(pinia: Pinia) {
+  try {
+    const open = await getFileOpener()
+    const result = await open()
+    if (!result) return
+    const { text, file } = result
+    pinia.state.value = JSON.parse(text)
+    toastMessage(`Global state imported from "${file.name}".`)
+  } catch (error) {
+    toastMessage(
+      `Failed to export the state as JSON. Check the console for more details.`,
+      'error'
+    )
+    console.error(error)
+  }
+}
+
 export function addDevtools(app: App, store: Store) {
   // TODO: we probably need to ensure the latest version of the store is kept:
   // without effectScope, multiple stores will be created and will have a
@@ -100,19 +171,37 @@ export function addDevtools(app: App, store: Store) {
           treeFilterPlaceholder: 'Search stores',
           actions: [
             {
-              icon: 'content-copy',
+              icon: 'content_copy',
               action: () => {
                 actionGlobalCopyState(store._p)
               },
               tooltip: 'Serialize and copy the state',
             },
             {
-              icon: 'content-paste',
-              action: () => {
-                actionGlobalPasteState(store._p)
+              icon: 'content_paste',
+              action: async () => {
+                await actionGlobalPasteState(store._p)
+                api.sendInspectorTree(INSPECTOR_ID)
+                api.sendInspectorState(INSPECTOR_ID)
               },
               tooltip: 'Replace the state with the content of your clipboard',
             },
+            {
+              icon: 'save',
+              action: () => {
+                actionGlobalSaveState(store._p)
+              },
+              tooltip: 'Save the state as a JSON file',
+            },
+            {
+              icon: 'folder_open',
+              action: async () => {
+                await actionGlobalOpenStateFile(store._p)
+                api.sendInspectorTree(INSPECTOR_ID)
+                api.sendInspectorState(INSPECTOR_ID)
+              },
+              tooltip: 'Import the state from a JSON file',
+            },
           ],
         })
 
@@ -413,7 +502,7 @@ export function devtoolsPlugin<
  */
 function toastMessage(
   message: string,
-  type?: 'normal' | 'error' | 'warning' | undefined
+  type?: 'normal' | 'error' | 'warn' | undefined
 ) {
   const piniaMessage = '🍍 ' + message
 
@@ -421,7 +510,7 @@ function toastMessage(
     __VUE_DEVTOOLS_TOAST__(piniaMessage, type)
   } else if (type === 'error') {
     console.error(piniaMessage)
-  } else if (type === 'warning') {
+  } else if (type === 'warn') {
     console.warn(piniaMessage)
   } else {
     console.log(piniaMessage)
index 9481230be02389b3bd8677f7b8e32942de26df21..ecff968b79667bcd966af53c4f84c98b44757fca 100644 (file)
@@ -6,5 +6,5 @@ declare var __BROWSER__: boolean
 declare var __CI__: boolean
 declare var __VUE_DEVTOOLS_TOAST__: (
   message: string,
-  type?: 'normal' | 'error' | 'warning'
+  type?: 'normal' | 'error' | 'warn'
 ) => void
index 57fa1e8ba59608b121bfaf4c268998cc930ed42b..48dc20b2d4340c4ffb854fae6c0efe58a61da871 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
   resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
   integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
 
+"@types/file-saver@^2.0.2":
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.2.tgz#bd593ccfaee42ff94a5c1c83bf69ae9be83493b9"
+  integrity sha512-xbqnZmGrCEqi/KUzOkeUSe77p7APvLuyellGaAoeww3CHJ1AbjQWjPSCFtKIzZn8L7LpEax4NXnC+gfa6nM7IA==
+
 "@types/graceful-fs@^4.1.2":
   version "4.1.3"
   resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.3.tgz#039af35fe26bec35003e8d86d2ee9c586354348f"
@@ -2482,6 +2487,11 @@ figures@^3.2.0:
   dependencies:
     escape-string-regexp "^1.0.5"
 
+file-saver@^2.0.5:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
+  integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==
+
 fill-range@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"