diff --git a/package-lock.json b/package-lock.json index 87f7dba..ef7d569 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1976,10 +1976,20 @@ "tslib": "~1.9.0" } }, + "@buttercup/app-env": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@buttercup/app-env/-/app-env-0.1.1.tgz", + "integrity": "sha512-nNx7UWZ9T/2ut/pJGxT7xey8saxfvIHhG42UWlWBGAHqrmuMxsCXjqivzWwMHtVEVlE1bXFjQRPBBLcvJR+22g==", + "dev": true, + "requires": { + "gzip-js": "^0.3.2", + "iocane": "^3.0.0" + } + }, "@buttercup/channel-queue": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@buttercup/channel-queue/-/channel-queue-0.3.0.tgz", - "integrity": "sha512-yXIBH4w0Hc1+ERkv7fMYFLhVxzgcleupT6bNTdmicIFZ4JRDwMzvB0IDAi5/u8Mi1q7xr3thak+XpteN5gQxPA==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@buttercup/channel-queue/-/channel-queue-0.5.0.tgz", + "integrity": "sha512-JsKKbbVIUqMWM5TnJNlMDjdzvBaRWrj2aVorVAmN0b1pHcgSI94ZCWSH6+j4q90GsCkkBC/tXxvl8e2vjdGHhw==", "dev": true, "requires": { "eventemitter3": "~3.0.1" @@ -1994,9 +2004,9 @@ } }, "@buttercup/credentials": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@buttercup/credentials/-/credentials-1.2.0.tgz", - "integrity": "sha512-d64WFbUNj5WwErtCgBflqDtfGSUgUVDln7vg0ade+sBy7rtpnPGn9ipy/yY5XEpqk0PEwova5sOg4CVyL0GVZA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@buttercup/credentials/-/credentials-2.0.0.tgz", + "integrity": "sha512-9I+0vOf7haINXR5265a60JgYknXpqeYvCPIUgXLF7USzaRKShrvap+rdD+blVwG7giIMmvbi0UEROrnjjqANCA==", "dev": true, "requires": { "@buttercup/signing": "^0.1.0", @@ -2004,17 +2014,16 @@ } }, "@buttercup/datasources": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@buttercup/datasources/-/datasources-3.2.4.tgz", - "integrity": "sha512-yq8OJ0J99JbvY5FMaFxjhZXNXBfh8gVIjqrJdZTLIPDrcPToTGza/6PtIVC5OcNkuMnR0gc8c4tAF/Zc4WoVKw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@buttercup/datasources/-/datasources-4.0.1.tgz", + "integrity": "sha512-CkbbTr5egi0C9fjs+BB7Oq1zCorUbWrCVr+849uBJ8C1nfcCd/n5vFCugTTMUPEpVFg8I3+ClLPcg3dLiEQIEw==", "dev": true, "requires": { - "@buttercup/dropbox-client": "^0.3.2", - "@buttercup/googledrive-client": "^0.5.0", + "@buttercup/dropbox-client": "^0.4.0", + "@buttercup/googledrive-client": "^0.8.0", "@buttercup/signing": "^0.1.0", "foreachasync": "^5.1.3", "global": "^4.4.0", - "gzip-js": "^0.3.2", "hash.js": "^1.1.7", "pify": "^4.0.1", "url-join": "^4.0.1", @@ -2047,30 +2056,40 @@ } }, "@buttercup/dropbox-client": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@buttercup/dropbox-client/-/dropbox-client-0.3.2.tgz", - "integrity": "sha512-78hQ88JSpskJEgX9nESe9iXNov4xvilRh7v1P3Iam6oAJvtfNUiVwUzTD4XWKo2kYYzSJq7VPxjWT/Jiz3Eh/w==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@buttercup/dropbox-client/-/dropbox-client-0.4.0.tgz", + "integrity": "sha512-KPhm0xJJRFCG4PE/phel9wHLpT2x3I0r61qiXAWfsVbQ1hRbDAATyoV7DJsJkourFpaorNgHc3uJ0C2oTHxcaA==", "dev": true, "requires": { "cowl": "^0.5.0", "hot-patcher": "^0.5.0" + }, + "dependencies": { + "cowl": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cowl/-/cowl-0.5.0.tgz", + "integrity": "sha512-atPoI4NTUh6KhsXp75fqKm/iIcBvI7ICAB4yiLtpgbMBaYeAJQrGMy9GOqkttiNFSw/Y6cPESf6Q7LZjQTv3Pg==", + "dev": true, + "requires": { + "arraybuffer-to-buffer": "0.0.6", + "caseless": "^0.12.0", + "get-headers": "^1.0.5", + "is-array-buffer": "^1.0.1", + "is-in-browser": "^1.1.3", + "query-string": "^6.8.1", + "xhr2": "^0.2.0" + } + } } }, "@buttercup/facades": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@buttercup/facades/-/facades-1.1.0.tgz", - "integrity": "sha512-dCr/qenfR2ID9S+vVWW9HV5A6wN1ONUV4dPtRPrXkZoOesoa655TPd/b+ieoJj0OqUTIjXneyYgyxZQtuNKSLA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@buttercup/facades/-/facades-1.3.4.tgz", + "integrity": "sha512-zGsG4WtqdFoOmmOawx7mfLv48s16WORKqnNP/NmyZGlg9+hex7kFYko/tgZbIAx/lKc5Zo8pIwqClMdzGoix0g==", "dev": true, "requires": { + "hash-sum": "^2.0.0", "uuid": "^3.3.3" - }, - "dependencies": { - "uuid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", - "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", - "dev": true - } } }, "@buttercup/generator": { @@ -2083,14 +2102,14 @@ } }, "@buttercup/googledrive-client": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@buttercup/googledrive-client/-/googledrive-client-0.5.0.tgz", - "integrity": "sha512-ReowjJc6Ho1WevRl7IVfsv8UkFZP4EDAEE2q3uVCBad2ttaAIpze5BDYFo2LjX7rqpUI5uAasGKs+LFAG+JiUQ==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@buttercup/googledrive-client/-/googledrive-client-0.8.0.tgz", + "integrity": "sha512-RpT11pxpNN8ANs/8rPEq38ZRVi+JrdSugqamFphWx0O+eGZV3sWsqDamiOh2lZy9FtokNWD2hhSVBYhsBo5SHQ==", "dev": true, "requires": { - "buffer": "^5.2.1", "cowl": "^0.6.0", "hot-patcher": "^0.5.0", + "safe-buffer": "^5.2.0", "verror": "^1.10.0" }, "dependencies": { @@ -2108,6 +2127,12 @@ "query-string": "^6.8.1", "xhr2": "^0.2.0" } + }, + "safe-buffer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", + "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", + "dev": true } } }, @@ -4325,21 +4350,12 @@ "dev": true }, "axios": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", - "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.1.tgz", + "integrity": "sha512-Yl+7nfreYKaLRvAvjNPkvfjnQHJM1yLBY3zhqAwcJSwR/6ETkanUgylgtIvkvz0xJ+p/vZuNw8X7Hnb7Whsbpw==", "dev": true, "requires": { - "follow-redirects": "1.5.10", - "is-buffer": "^2.0.2" - }, - "dependencies": { - "is-buffer": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", - "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==", - "dev": true - } + "follow-redirects": "1.5.10" } }, "babel-code-frame": { @@ -5622,16 +5638,6 @@ "node-int64": "^0.4.0" } }, - "buffer": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.2.tgz", - "integrity": "sha512-iy9koArjAFCzGnx3ZvNA6Z0clIbbFgbdWQ0mKD3hO0krOrZh8UgA6qMKcZvwLJxS+D6iVR76+5/pV56yMNYTag==", - "dev": true, - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -5657,22 +5663,21 @@ "dev": true }, "buttercup": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/buttercup/-/buttercup-2.15.4.tgz", - "integrity": "sha512-ddK6EhtGt5ouVX+zBDnCyytw7B+ebYZxp1o5rINz4ehwaRPTXAY1OqqEGhFLWJ1owEawfHgpotFX4jOi86Iuug==", + "version": "3.0.0-rc3.0", + "resolved": "https://registry.npmjs.org/buttercup/-/buttercup-3.0.0-rc3.0.tgz", + "integrity": "sha512-9P1Lmc7tsA/5padTdq4UWC16ZUN51diWw6fZaTSFcx1adWSxSM/UAOE/x3vStYDNOzPG7+xce/a/y8t4ysYLyw==", "dev": true, "requires": { - "@buttercup/channel-queue": "^0.3.0", - "@buttercup/credentials": "^1.2.0", - "@buttercup/datasources": "^3.2.4", + "@buttercup/channel-queue": "^0.5.0", + "@buttercup/credentials": "^2.0.0", + "@buttercup/datasources": "^4.0.1", "@buttercup/signing": "^0.1.0", + "cowl": "^0.8.0", "eventemitter3": "^3.1.0", "fuse.js": "^2.7.4", - "iocane": "^1.0.2", + "hash.js": "^1.1.7", "is-promise": "^2.1.0", - "node-fetch": "^2.6.0", - "node-rsa": "^1.0.5", - "string-hash": "^1.1.3", + "node-rsa": "^1.0.7", "url-join": "^4.0.1", "uuid": "^3.3.2", "verror": "^1.10.0" @@ -5683,12 +5688,6 @@ "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-2.7.4.tgz", "integrity": "sha1-luQg/efvARrEnCWKYhMU/ldlNvk=", "dev": true - }, - "node-fetch": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", - "dev": true } } }, @@ -6514,9 +6513,9 @@ } }, "cowl": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cowl/-/cowl-0.5.0.tgz", - "integrity": "sha512-atPoI4NTUh6KhsXp75fqKm/iIcBvI7ICAB4yiLtpgbMBaYeAJQrGMy9GOqkttiNFSw/Y6cPESf6Q7LZjQTv3Pg==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/cowl/-/cowl-0.8.0.tgz", + "integrity": "sha512-fGgBkCPaZycwbbFAhbwa5W1iIQSzcyiCTYNtKdMmTVAy2al2bITCfQc4sWvAS6H0LzRFimcK20J3MthyFiossg==", "dev": true, "requires": { "arraybuffer-to-buffer": "0.0.6", @@ -9456,6 +9455,12 @@ "safe-buffer": "^5.0.1" } }, + "hash-sum": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz", + "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", + "dev": true + }, "hash.js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", @@ -9980,9 +9985,9 @@ "dev": true }, "iocane": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/iocane/-/iocane-1.0.2.tgz", - "integrity": "sha512-9sTea4NOODPZAfWOpztzwdPZQ4KUHyVDspE6heKD0FsRwYVOYElDdf+Nzhirj3fOtfX6WmitsX2JovtdbV9PqQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/iocane/-/iocane-3.0.0.tgz", + "integrity": "sha512-hNjxbuYR/jE5ZefROFugpEwcSsKQTwRZy3jXeEZ1MqBVlGiD9RRhcj2KSd6xLbTK8zXTfaf6gwjLjJgVCtkjqA==", "dev": true, "requires": { "pbkdf2": "~3.0.17" @@ -12744,9 +12749,9 @@ } }, "node-rsa": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.0.5.tgz", - "integrity": "sha512-9o51yfV167CtQANnuAf+5owNs7aIMsAKVLhNaKuRxihsUUnfoBMN5OTVOK/2mHSOWaWq9zZBiRM3bHORbTZqrg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.0.7.tgz", + "integrity": "sha512-idwRXma6scFufZmbaKkHpJoLL93yynRefP6yur13wZ5i9FR35ex451KCoF2OORDeJanyRVahmjjiwmUlCnTqJA==", "dev": true, "requires": { "asn1": "^0.2.4" @@ -14582,9 +14587,9 @@ "dev": true }, "query-string": { - "version": "6.8.3", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.8.3.tgz", - "integrity": "sha512-llcxWccnyaWlODe7A9hRjkvdCKamEKTh+wH8ITdTc3OhchaqUZteiSCX/2ablWHVrkVIe04dntnaZJ7BdyW0lQ==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.10.0.tgz", + "integrity": "sha512-hUUAps/SKS+uXzSGHUXirzGF+ymxrtxb+F1C3fgfwmvKSVsIop5GTYNkbgi12Md2laVz4LUjrCyrGZ1y5ct5eA==", "dev": true, "requires": { "decode-uri-component": "^0.2.0", @@ -16544,12 +16549,6 @@ "integrity": "sha1-2sMECGkMIfPDYwo/86BYd73L1zY=", "dev": true }, - "string-hash": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", - "integrity": "sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=", - "dev": true - }, "string-length": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", @@ -17861,9 +17860,9 @@ "dev": true }, "webdav": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/webdav/-/webdav-2.9.1.tgz", - "integrity": "sha512-YgI6IvbbKPv1Dn+n5Vu1Swtxmtfdj/1kmcC8cOgPGmuW+/TYsMB6hJlgy+EPTlhX0LEhUud3fsqCaT/7u3/Y5w==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/webdav/-/webdav-2.10.1.tgz", + "integrity": "sha512-3UfnjGTAqSM9MW3Rpt1KrY1KneYK0wPCFryHTncqw1OP1pyiniT3uYhVpgmH6za/TkWOfnTnKCDKhwrLJFdzow==", "dev": true, "requires": { "axios": "^0.19.0", @@ -18421,19 +18420,19 @@ "dev": true }, "xml2js": { - "version": "0.4.20", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.20.tgz", - "integrity": "sha512-5+6o7cEzGZerpxm1WspA2C5ylzcvmNpyGyt8Q5GJtaMzXOmKgxNhsatXGXozHFinzT1fQVZoQIq+zgJd1QsjZA==", + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", "dev": true, "requires": { "sax": ">=0.6.0", - "xmlbuilder": "~10.0.0" + "xmlbuilder": "~11.0.0" } }, "xmlbuilder": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.0.0.tgz", - "integrity": "sha512-7RWHlmF1yU/E++BZkRQTEv8ZFAhZ+YHINUAxiZ5LQTKRQq//igpiY8rh7dJqPzgb/IzeC5jH9P7OaCERfM9DwA==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", "dev": true }, "xtend": { diff --git a/package.json b/package.json index e6b6fbd..7a3eb58 100644 --- a/package.json +++ b/package.json @@ -51,14 +51,15 @@ "@babel/plugin-transform-spread": "^7.2.2", "@babel/preset-env": "^7.5.5", "@babel/preset-react": "^7.0.0", - "@buttercup/facades": "^1.1.0", + "@buttercup/app-env": "^0.1.1", + "@buttercup/facades": "^1.3.4", "@storybook/addon-actions": "^4.1.11", "@storybook/addons": "^4.1.11", "@storybook/react": "^4.1.11", "babel-loader": "^8.0.6", "babel-plugin-jsx-control-statements": "^4.0.0", "babel-plugin-ramda": "^2.0.0", - "buttercup": "^2.15.4", + "buttercup": "^3.0.0-rc3.0", "css-loader": "^2.1.1", "enzyme": "^3.10.0", "enzyme-adapter-react-16": "^1.14.0", @@ -90,7 +91,7 @@ "webpack-cli": "^3.3.7" }, "peerDependencies": { - "@buttercup/facades": ">= 1.1.0", + "@buttercup/facades": ">= 1.3.4", "react": "^16.8.1", "react-dom": "^16.8.1", "styled-components": "^4.1.3" diff --git a/src/components/vault/EntryDetails.js b/src/components/vault/EntryDetails.js index c441eb3..f662095 100644 --- a/src/components/vault/EntryDetails.js +++ b/src/components/vault/EntryDetails.js @@ -1,4 +1,4 @@ -import React, { useState, useRef } from 'react'; +import React, { useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; import cx from 'classnames'; import TextArea from 'react-textarea-autosize'; @@ -7,6 +7,7 @@ import { ButtonGroup, Classes, ControlGroup, + Dialog, EditableText, HTMLSelect, Icon, @@ -133,11 +134,33 @@ const FieldTextWrapper = styled.span` } } `; +const HistoryTable = styled.table` + width: 100%; +`; +const HistoryScrollContainer = styled.div` + width: 100%; + max-height: 300px; + overflow-x: hidden; + overflow-y: scroll; +`; -const FieldText = ({ field }) => { +const FieldText = ({ entryFacade, field }) => { const [visible, toggleVisibility] = useState(false); + const [historyDialogVisible, setHistoryDialogVisible] = useState(false); const otpRef = useRef(field.value); + const { onFieldUpdateInPlace } = useCurrentEntry(); const Element = field.valueType === FIELD_VALUE_TYPE_PASSWORD ? 'code' : 'span'; + const { _history: history = [] } = entryFacade; + const historyItems = useMemo(() => { + const items = history.filter( + item => item.property === field.property && item.propertyType === field.propertyType + ); + if (items.length > 0 && items[items.length - 1].newValue === field.value) { + // Remove last item as it just shows the current value + items.pop(); + } + return items; + }, [history]); return ( @@ -184,7 +207,60 @@ const FieldText = ({ field }) => { /> copyToClipboard(otpRef.current)} /> + setHistoryDialogVisible(true)} + /> + setHistoryDialogVisible(false)} + title="Entry field history" + isOpen={historyDialogVisible} + > + + + + + + Previous Value + + + + + + + + {change.newValue} + + + + copyToClipboard(change.newValue)} /> + { + setHistoryDialogVisible(false); + onFieldUpdateInPlace(entryFacade.id, field, change.newValue); + }} + /> + + + + + + + + + + + setHistoryDialogVisible(false)}> + Close + + + + ); }; @@ -192,6 +268,7 @@ const FieldText = ({ field }) => { const FieldRow = ({ field, editing, + entryFacade, onFieldNameUpdate, onFieldUpdate, onFieldSetValueType, @@ -302,7 +379,7 @@ const FieldRow = ({ - + @@ -340,6 +417,7 @@ const EntryDetailsContent = () => { { { const renderGroupsMenu = (items, parentNode, selectedGroupID) => ( <> + + {/* Only show on first level */} + moveGroupToGroup(selectedGroupID, '0')} + disabled={groupsRaw.find(groupRaw => groupRaw.id === selectedGroupID).parentID === '0'} + /> + + { + const vaultSourceHash = hashVaultFacade(vaultSource); const [vault, dispatch] = useReducer(vaultReducer, clone(vaultSource)); + const [lastVaultSourceHash, setLastVaultSourceHash] = useState(vaultSourceHash); const [selectedGroupID, setSelectedGroupID] = useState(vault.groups[0].id); const [selectedEntryID, setSelectedEntryID] = useState(null); const [editingEntry, dispatchEditing] = useReducer(entryReducer, null); const [groupFilters, dispatchGroupFilters] = useReducer(filterReducer, defaultFilter); const [entriesFilters, dispatchEntriesFilters] = useReducer(filterReducer, defaultFilter); const [expandedGroups, setExpandedGroups] = useState([]); - const initRef = useRef(false); const selectedEntry = vault.entries.find(entry => entry.id === selectedEntryID); const currentEntries = vault.entries.filter(entry => entry.parentID === selectedGroupID); - useEffect(() => { - if (initRef.current === false) { - initRef.current = true; - return; + useDeepEffect(() => { + if (vaultSourceHash !== lastVaultSourceHash) { + // External updated, update internal state + dispatch({ + type: 'reset', + payload: clone(vaultSource) + }); + setLastVaultSourceHash(vaultSourceHash); + } else if (hashVaultFacade(vault) !== hashVaultFacade(vaultSource)) { + // Internal updated, fire update event for external save + onUpdate(vault); } - onUpdate(vault); - }, [vault]); + }, [vault, vaultSourceHash]); const context = { vault, @@ -188,6 +197,14 @@ export const VaultProvider = ({ onUpdate, vault: vaultSource, children }) => { value }); }, + onFieldUpdateInPlace: (entryID, field, value) => { + dispatch({ + type: 'set-entry-field', + entryID, + field, + value + }); + }, onFieldSetValueType: (changedField, valueType) => { dispatchEditing({ type: 'set-field-value-type', diff --git a/src/components/vault/hooks/compare.js b/src/components/vault/hooks/compare.js new file mode 100644 index 0000000..89b5415 --- /dev/null +++ b/src/components/vault/hooks/compare.js @@ -0,0 +1,6 @@ +import { useEffect } from 'react'; +import { hashVaultFacade, isVaultFacade } from '@buttercup/facades'; + +export function useDeepEffect(callback, dependencies = []) { + useEffect(callback, dependencies.map(dep => (isVaultFacade(dep) ? hashVaultFacade(dep) : dep))); +} diff --git a/src/components/vault/hooks/vault.js b/src/components/vault/hooks/vault.js index ae96005..5cea06d 100644 --- a/src/components/vault/hooks/vault.js +++ b/src/components/vault/hooks/vault.js @@ -20,6 +20,7 @@ export function useCurrentEntry() { onEdit, onFieldNameUpdate, onFieldUpdate, + onFieldUpdateInPlace, onFieldSetValueType, onRemoveField, onSaveEdit @@ -34,6 +35,7 @@ export function useCurrentEntry() { onEdit, onFieldNameUpdate, onFieldUpdate, + onFieldUpdateInPlace, onFieldSetValueType, onRemoveField, onSaveEdit diff --git a/src/components/vault/reducers/vault.js b/src/components/vault/reducers/vault.js index 6d63345..c78a1c7 100644 --- a/src/components/vault/reducers/vault.js +++ b/src/components/vault/reducers/vault.js @@ -1,5 +1,9 @@ export function vaultReducer(state, action) { switch (action.type) { + case 'reset': + return { + ...action.payload + }; case 'save-entry': { const { entry: baseEntry } = action; const { isNew, ...entry } = baseEntry; @@ -43,6 +47,32 @@ export function vaultReducer(state, action) { ...state, groups: [...state.groups, action.payload] }; + case 'set-entry-field': { + const { entryID, field, value } = action; + return { + ...state, + entries: state.entries.map(entry => { + if (entry.id === entryID) { + return { + ...entry, + fields: entry.fields.map(entryField => { + if ( + entryField.property === field.property && + entryField.propertyType === field.propertyType + ) { + return { + ...entryField, + value: value + }; + } + return entryField; + }) + }; + } + return entry; + }) + }; + } case 'rename-group': return { ...state, diff --git a/stories/vault.js b/stories/vault.js index 98ee595..a996833 100644 --- a/stories/vault.js +++ b/stories/vault.js @@ -1,10 +1,14 @@ import React, { Component } from 'react'; import styled from 'styled-components'; import uuid from 'uuid/v4'; -import { Archive, Entry } from 'buttercup'; -import { FIELD_VALUE_TYPE_OTP } from '@buttercup/facades'; +import { Archive, Entry } from 'buttercup/dist/buttercup-web.min.js'; +import '@buttercup/app-env/web'; import { ThemeProvider } from 'styled-components'; -import { createArchiveFacade } from '@buttercup/facades'; +import { + FIELD_VALUE_TYPE_OTP, + consumeArchiveFacade, + createArchiveFacade +} from '@buttercup/facades'; import { VaultProvider, VaultUI, themes } from '../src/index'; function createArchive() { @@ -13,10 +17,19 @@ function createArchive() { general .createEntry('Home wi-fi') .setProperty('username', 'somehow') + .setProperty('password', 'idsfio49v-1') + .setProperty('password', 'h78.dI2m;110') + .setProperty('password', '[5LC-j_"C7b;"nbn') + .setProperty('password', '8rE7=XkmZSh-,!Ly7Hrd&:Cv^@~,d') + .setProperty('password', '7#eELw%GS^)/"') + .setProperty('password', 'K3"J8JSKHk|5hwks_^') .setProperty('password', 'x8v@mId01') .setProperty('url', 'https://google.com'); general - .createEntry('Social website') + .createEntry('Social') + .setProperty('title', 'Social website') .setProperty('username', 'user@test.com') .setProperty('password', 'vdfs867sd5') .setProperty( @@ -28,12 +41,15 @@ function createArchive() { 'otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30' ) .setAttribute(`${Entry.Attributes.FieldTypePrefix}otpURI`, FIELD_VALUE_TYPE_OTP) + .setProperty('url', 'https://site.com/setup/create-account.php?token=123') + .setProperty('url', 'https://site.com/login.php') .setProperty('url', 'https://site.com') .setProperty('Recovery pin', '1234'); general .createEntry('Gate lock combination') .setProperty('username', 'test') - .setProperty('password', '4812'); + .setProperty('password', '4812') + .setProperty('password', 'passw0rd'); const notes = archive.createGroup('Notes'); notes .createEntry('Meeting notes 2019-02-01') @@ -48,14 +64,10 @@ function createArchive() { return archive; } -function normaliseFacade(vault) { - // Set UUIDs for new groups, or else we get collisions - vault.groups.forEach(group => { - if (!group.id) { - group.id = uuid(); - } - }); - return vault; +function processVaultUpdate(archive, facade) { + consumeArchiveFacade(archive, facade); + const out = createArchiveFacade(archive); + return out; } const View = styled.div` @@ -66,8 +78,10 @@ const View = styled.div` export default class VaultStory extends Component { constructor(...args) { super(...args); + const archive = createArchive(); this.state = { - facade: createArchiveFacade(createArchive()) + archive, + facade: createArchiveFacade(archive) }; } @@ -77,7 +91,10 @@ export default class VaultStory extends Component { this.setState({ facade: normaliseFacade(vault) })} + onUpdate={vault => { + console.log('Saving vault...'); + this.setState({ facade: processVaultUpdate(this.state.archive, vault) }); + }} >
{change.newValue}