diff --git a/package-lock.json b/package-lock.json index 3bcb0dab87..8f203659fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1498,6 +1498,22 @@ "resolved": "https://registry.npmjs.org/@mdi/react/-/react-1.2.1.tgz", "integrity": "sha512-1IRIVCT07vlLmaZjVtGfyfwCMivg/tCtPj0+r1BKrkoh9z4xLf+M1TD0LhjJPO+4+O0ibW+xrNRvf+boRRtX9A==" }, + "@mrmlnc/readdir-enhanced": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", + "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", + "dev": true, + "requires": { + "call-me-maybe": "^1.0.1", + "glob-to-regexp": "^0.3.0" + } + }, + "@nodelib/fs.stat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", + "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", + "dev": true + }, "@testing-library/react-hooks": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-2.0.1.tgz", @@ -2389,6 +2405,12 @@ "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", "dev": true }, + "address": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/address/-/address-1.1.2.tgz", + "integrity": "sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA==", + "dev": true + }, "ajv": { "version": "6.10.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", @@ -2542,6 +2564,12 @@ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "dev": true }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -2649,6 +2677,59 @@ "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", "dev": true }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, "babel-jest": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-24.9.0.tgz", @@ -3223,6 +3304,12 @@ "unset-value": "^1.0.0" } }, + "call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", + "dev": true + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3410,6 +3497,11 @@ } } }, + "classcat": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-4.0.2.tgz", + "integrity": "sha512-RlMPOPp8VDu3CJOUVorPumhz/CI+t9ft6f0uexxxCguk28/M+Kf27eQXjNWeDTisEQWei/30oDfITOQqr1TNpQ==" + }, "clean-css": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.1.tgz", @@ -4238,6 +4330,33 @@ "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", "dev": true }, + "detect-port-alt": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", + "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", + "dev": true, + "requires": { + "address": "^1.0.1", + "debug": "^2.6.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, "diff": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", @@ -4261,6 +4380,16 @@ "randombytes": "^2.0.0" } }, + "dir-glob": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz", + "integrity": "sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "path-type": "^3.0.0" + } + }, "dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", @@ -4373,6 +4502,12 @@ "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz", "integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=" }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "dev": true + }, "duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -4500,6 +4635,18 @@ "is-arrayish": "^0.2.1" } }, + "error-overlay-webpack-plugin": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/error-overlay-webpack-plugin/-/error-overlay-webpack-plugin-0.4.1.tgz", + "integrity": "sha512-1SyHaKmggmEN9yt8aQeh/hKV3moXpqpOK1p5EEP/u9U0KngBdRHoiVRQLV8ZunUYRA/mSA0HB00KY+eeM9YKxw==", + "dev": true, + "requires": { + "react-dev-utils": "^9.0.3", + "react-error-overlay": "^6.0.1", + "sockjs-client": "^1.4.0", + "url": "^0.11.0" + } + }, "es-abstract": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", @@ -5137,6 +5284,43 @@ "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", "dev": true }, + "fast-glob": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", + "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", + "dev": true, + "requires": { + "@mrmlnc/readdir-enhanced": "^2.2.1", + "@nodelib/fs.stat": "^1.1.2", + "glob-parent": "^3.1.0", + "is-glob": "^4.0.0", + "merge2": "^1.2.3", + "micromatch": "^3.1.10" + }, + "dependencies": { + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + } + } + }, "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", @@ -5223,6 +5407,12 @@ "trim-repeated": "^1.0.0" } }, + "filesize": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz", + "integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==", + "dev": true + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -5397,6 +5587,22 @@ "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", "dev": true }, + "fork-ts-checker-webpack-plugin": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-1.5.0.tgz", + "integrity": "sha512-zEhg7Hz+KhZlBhILYpXy+Beu96gwvkROWJiTXOCyOOMMrdBIRPvsBpBqgTI4jfJGrJXcqGwJR8zsBGDmzY0jsA==", + "dev": true, + "requires": { + "babel-code-frame": "^6.22.0", + "chalk": "^2.4.1", + "chokidar": "^2.0.4", + "micromatch": "^3.1.10", + "minimatch": "^3.0.4", + "semver": "^5.6.0", + "tapable": "^1.0.0", + "worker-rpc": "^0.1.0" + } + }, "form-data": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", @@ -6105,6 +6311,12 @@ "is-glob": "^4.0.1" } }, + "glob-to-regexp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", + "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=", + "dev": true + }, "global": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", @@ -6188,6 +6400,16 @@ "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", "dev": true }, + "gzip-size": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", + "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", + "dev": true, + "requires": { + "duplexer": "^0.1.1", + "pify": "^4.0.1" + } + }, "handle-thing": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz", @@ -6231,6 +6453,15 @@ "function-bind": "^1.1.1" } }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -7146,6 +7377,12 @@ "has": "^1.0.1" } }, + "is-root": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", + "integrity": "sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==", + "dev": true + }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -8943,12 +9180,24 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "merge2": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz", + "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==", + "dev": true + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", "dev": true }, + "microevent.ts": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz", + "integrity": "sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==", + "dev": true + }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", @@ -12596,6 +12845,15 @@ "mimic-fn": "^1.0.0" } }, + "open": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", + "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", + "dev": true, + "requires": { + "is-wsl": "^1.1.0" + } + }, "open-color": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/open-color/-/open-color-1.7.0.tgz", @@ -12961,6 +13219,12 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", "dev": true }, + "picomatch": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz", + "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==", + "dev": true + }, "pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", @@ -13000,6 +13264,60 @@ "find-up": "^3.0.0" } }, + "pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz", + "integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + } + } + }, "pn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", @@ -13807,6 +14125,159 @@ "prop-types": "^15.6.2" } }, + "react-dev-utils": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-9.1.0.tgz", + "integrity": "sha512-X2KYF/lIGyGwP/F/oXgGDF24nxDA2KC4b7AFto+eqzc/t838gpSGiaU8trTqHXOohuLxxc5qi1eDzsl9ucPDpg==", + "dev": true, + "requires": { + "@babel/code-frame": "7.5.5", + "address": "1.1.2", + "browserslist": "4.7.0", + "chalk": "2.4.2", + "cross-spawn": "6.0.5", + "detect-port-alt": "1.1.6", + "escape-string-regexp": "1.0.5", + "filesize": "3.6.1", + "find-up": "3.0.0", + "fork-ts-checker-webpack-plugin": "1.5.0", + "global-modules": "2.0.0", + "globby": "8.0.2", + "gzip-size": "5.1.1", + "immer": "1.10.0", + "inquirer": "6.5.0", + "is-root": "2.1.0", + "loader-utils": "1.2.3", + "open": "^6.3.0", + "pkg-up": "2.0.0", + "react-error-overlay": "^6.0.3", + "recursive-readdir": "2.2.2", + "shell-quote": "1.7.2", + "sockjs-client": "1.4.0", + "strip-ansi": "5.2.0", + "text-table": "0.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "globby": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-8.0.2.tgz", + "integrity": "sha512-yTzMmKygLp8RUpG1Ymu2VXPSJQZjNAZPD4ywgYEaG7e4tBJeUQBO8OpXrf1RCNcEs5alsoJYPAMiIHP0cmeC7w==", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "dir-glob": "2.0.0", + "fast-glob": "^2.0.2", + "glob": "^7.1.2", + "ignore": "^3.3.5", + "pify": "^3.0.0", + "slash": "^1.0.0" + } + }, + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "immer": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-1.10.0.tgz", + "integrity": "sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==", + "dev": true + }, + "inquirer": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.0.tgz", + "integrity": "sha512-scfHejeG/lVZSpvCXpsB4j/wQNPM5JC8kiElOI0OUTwmc1RTpXr4H32/HOlQHcZiYl2z2VElwuCVDRG8vFmbnA==", + "dev": true, + "requires": { + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^2.0.0", + "lodash": "^4.17.12", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^2.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + } + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", + "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, "react-dom": { "version": "16.9.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.9.0.tgz", @@ -13818,6 +14289,12 @@ "scheduler": "^0.15.0" } }, + "react-error-overlay": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.3.tgz", + "integrity": "sha512-bOUvMWFQVk5oz8Ded9Xb7WVdEi3QGLC8tH7HmYP0Fdp4Bn3qw0tRFmr5TW6mvahzvmrK4a6bqWGfCevBflP+Xw==", + "dev": true + }, "react-hot-loader": { "version": "4.12.13", "resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.12.13.tgz", @@ -14018,6 +14495,15 @@ "util.promisify": "^1.0.0" } }, + "recursive-readdir": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz", + "integrity": "sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg==", + "dev": true, + "requires": { + "minimatch": "3.0.4" + } + }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", @@ -14691,6 +15177,12 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, + "shell-quote": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", + "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", + "dev": true + }, "shellwords": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", @@ -15686,16 +16178,16 @@ "integrity": "sha512-tdzBRDGWcI1OpPVmChbdSKhvSVurznZ8X36AYURAcl+0o2ldlCY2XPzyXNNxwJwwyIU+rIglTCG4kxtNKBQH7Q==" }, "ts-loader": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-5.4.5.tgz", - "integrity": "sha512-XYsjfnRQCBum9AMRZpk2rTYSVpdZBpZK+kDh0TeT3kxmQNBDVIeUjdPjY5RZry4eIAb8XHc4gYSUiUWPYvzSRw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-6.2.0.tgz", + "integrity": "sha512-Da8h3fD+HiZ9GvZJydqzk3mTC9nuOKYlJcpuk+Zv6Y1DPaMvBL+56GRzZFypx2cWrZFMsQr869+Ua2slGoLxvQ==", "dev": true, "requires": { "chalk": "^2.3.0", "enhanced-resolve": "^4.0.0", "loader-utils": "^1.0.2", - "micromatch": "^3.1.4", - "semver": "^5.0.1" + "micromatch": "^4.0.0", + "semver": "^6.0.0" }, "dependencies": { "big.js": { @@ -15704,6 +16196,30 @@ "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", "dev": true }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, "json5": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", @@ -15724,11 +16240,36 @@ "json5": "^1.0.1" } }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, "minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } } } }, @@ -16908,6 +17449,15 @@ "errno": "~0.1.7" } }, + "worker-rpc": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/worker-rpc/-/worker-rpc-0.1.1.tgz", + "integrity": "sha512-P1WjMrUB3qgJNI9jfmpZ/htmBEjFh//6l/5y8SD9hg1Ef5zTTVVoRjTrTEzPrNBQvmhMxkoTsjOXN10GWU7aCg==", + "dev": true, + "requires": { + "microevent.ts": "~0.1.1" + } + }, "wrap-ansi": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", diff --git a/package.json b/package.json index 72caf149c4..41e9f07faf 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "url": "https://github.com/BoostIO/boost.git" }, "scripts": { - "start": "cross-env TS_NODE_PROJECT=\"tsconfig-webpack.json\" webpack-dev-server --mode development --open", + "start": "cross-env TS_NODE_PROJECT=\"tsconfig-webpack.json\" webpack-dev-server --mode development --open-page \"app\"", "start-dev": "NODE_ENV=development electron app/index.js", "compile": "NODE_ENV=production webpack --config webpack.config.js --progress", "analyze": "NODE_ENV=production webpack --config webpack.config.js --json | webpack-bundle-size-analyzer", @@ -60,6 +60,7 @@ "babel-jest": "^24.9.0", "cross-env": "^6.0.0", "css-loader": "^3.2.0", + "error-overlay-webpack-plugin": "^0.4.1", "eslint": "^6.4.0", "eslint-config-prettier": "^6.3.0", "eslint-plugin-prettier": "^3.1.1", @@ -72,7 +73,7 @@ "react-hot-loader": "^4.12.13", "react-test-renderer": "^16.9.0", "style-loader": "^1.0.0", - "ts-loader": "^5.4.5", + "ts-loader": "^6.2.0", "ts-node": "^8.4.1", "type-fest": "^0.7.1", "url-loader": "^2.1.0", @@ -89,6 +90,7 @@ "@types/pouchdb-core": "^7.0.3", "@types/pouchdb-mapreduce": "^6.1.4", "@types/pouchdb-replication": "^6.4.2", + "classcat": "^4.0.2", "codemirror": "^5.49.0", "filenamify": "^2.1.0", "hast-util-sanitize": "^2.0.1", diff --git a/src/components/App.tsx b/src/components/App.tsx index 4bce708edf..ef0ec93604 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -8,6 +8,7 @@ import { StyledAppContainer } from './styled' import ContextMenu from './ContextMenu' import Dialog from './Dialog/Dialog' import { useDb } from '../lib/db' +import TwoPaneLayout from './atoms/TwoPaneLayout' const App = () => { const { initialize, initialized } = useDb() @@ -19,10 +20,11 @@ const App = () => { {initialized ? ( - <> - - - + } + right={} + /> ) : (
Loading data
)} diff --git a/src/components/NotePage/NoteDetail/NoteDetail.tsx b/src/components/NotePage/NoteDetail/NoteDetail.tsx index 9ced5cde83..0af00748e3 100644 --- a/src/components/NotePage/NoteDetail/NoteDetail.tsx +++ b/src/components/NotePage/NoteDetail/NoteDetail.tsx @@ -6,6 +6,7 @@ import styled from '../../../lib/styled' import CodeEditor from '../../atoms/CodeEditor' import MarkdownPreviewer from '../../atoms/MarkdownPreviewer' import NoteDetailToolbar from './NoteDetailToolbar' +import TwoPaneLayout from '../../atoms/TwoPaneLayout' const StyledNoteDetailContainer = styled.div` display: flex; @@ -41,31 +42,28 @@ const StyledNoteDetailContainer = styled.div` margin: 2px; position: relative; border-top: solid 1px ${({ theme }) => theme.colors.border}; - & > textarea { + .CodeMirror { position: absolute; top: 0; bottom: 0; - resize: none; - padding: 4px; width: 100%; - border: none; - box-sizing: border-box; + height: 100%; } - .CodeMirror { + .MarkdownPreviewer { position: absolute; top: 0; bottom: 0; width: 100%; height: 100%; + overflow: auto; + padding: 0 10px; } - .MarkdownPreviewer { + .split { position: absolute; top: 0; bottom: 0; width: 100%; height: 100%; - overflow: auto; - padding: 0 10px; } } ` @@ -106,7 +104,10 @@ export default class NoteDetail extends React.Component< } titleInputRef = React.createRef() newTagNameInputRef = React.createRef() - contentTextareaRef = React.createRef() + codeMirror?: CodeMirror.EditorFromTextArea + codeMirrorRef = (codeMirror: CodeMirror.EditorFromTextArea) => { + this.codeMirror = codeMirror + } static getDerivedStateFromProps( props: NoteDetailProps, @@ -254,6 +255,12 @@ export default class NoteDetail extends React.Component< this.setState({ mode }) } + refreshCodeEditor = () => { + if (this.codeMirror != null) { + this.codeMirror.refresh() + } + } + render() { const { note } = this.props @@ -293,9 +300,26 @@ export default class NoteDetail extends React.Component< {this.state.mode === 'edit' ? ( + ) : this.state.mode === 'split' ? ( + + } + right={} + onResizeEnd={this.refreshCodeEditor} + /> ) : ( )} diff --git a/src/components/NotePage/NoteList/NoteItem.tsx b/src/components/NotePage/NoteList/NoteItem.tsx index 0f977b372c..aa1e8ae5b3 100644 --- a/src/components/NotePage/NoteList/NoteItem.tsx +++ b/src/components/NotePage/NoteList/NoteItem.tsx @@ -13,6 +13,7 @@ const StyledNoteListItem = styled.div<{ active: boolean }>` color: ${theme.colors.inverseText};`} border-bottom: solid 1px ${({ theme }) => theme.colors.border}; padding: 8px; + user-select: none; a { text-decoration: none; diff --git a/src/components/NotePage/NoteList/NoteList.tsx b/src/components/NotePage/NoteList/NoteList.tsx index 738315b1ff..7ad57623f3 100644 --- a/src/components/NotePage/NoteList/NoteList.tsx +++ b/src/components/NotePage/NoteList/NoteList.tsx @@ -4,7 +4,8 @@ import { NoteDoc } from '../../../lib/db/types' import styled from '../../../lib/styled' import Toolbar from '../../atoms/Toolbar' import ToolbarIconButton from '../../atoms/ToolbarIconButton' -import { mdiSquareEditOutline } from '@mdi/js' +import { mdiMagnify, mdiSquareEditOutline } from '@mdi/js' +import ToolbarIconInput from '../../atoms/ToolbarIconInput' const StyledContainer = styled.div` display: flex; @@ -18,6 +19,9 @@ const StyledContainer = styled.div` list-style: none; overflow-y: auto; } + .searchInput { + flex: 1; + } ` type NoteListProps = { @@ -36,6 +40,12 @@ const NoteList = ({ return ( + {}} + />
    diff --git a/src/components/NotePage/NotePage.tsx b/src/components/NotePage/NotePage.tsx index ce04a57920..609c201812 100644 --- a/src/components/NotePage/NotePage.tsx +++ b/src/components/NotePage/NotePage.tsx @@ -3,7 +3,7 @@ import NoteList from './NoteList' import NoteDetail from './NoteDetail' import { useRouter, tagRegexp, useNotesPathname } from '../../lib/router' import { useDb } from '../../lib/db' -import TwoPaneLayout from './TwoPaneLayout' +import TwoPaneLayout from '../atoms/TwoPaneLayout' export default () => { const db = useDb() @@ -57,6 +57,7 @@ export default () => { return currentStorageId != null ? ( theme.colors.border}; + height: 100%; + + .storageList { + list-style: none; + padding: 0; + margin: 0; + flex: 1; + overflow: auto; + } + .empty { + padding: 4px; + user-select: none; + } + + .bottomControl { + height: 30px; + display: flex; + border-top: 1px solid ${({ theme }) => theme.colors.border}; + button { + height: 30px; + border: none; + background-color: transparent; + display: flex; + align-items: center; + } + .addFolderButton { + flex: 1; + border-right: 1px solid ${({ theme }) => theme.colors.border}; + } + .addFolderButtonIcon { + margin-right: 4px; + } + .moreButton { + width: 30px; + display: flex; + justify-content: center; + } + } ` export default () => { @@ -33,39 +64,110 @@ export default () => { removeFolder, storageMap } = useDb() - const router = useRouter() + const [storageId, folderPathname] = useNotesPathname() + const { popup } = useContextMenu() + const { prompt } = useDialog() const storageEntries = useMemo(() => { return entries(storageMap) }, [storageMap]) + const currentPathnameWithoutNoteId = useMemo(() => { + if (storageId == null) return `/` + if (folderPathname == null) return `/storages/${storageId}` + if (folderPathname === '/') return `/app/storages/${storageId}/notes` + return `/app/storages/${storageId}/notes${folderPathname}` + }, [storageId, folderPathname]) + + const currentStorage = useMemo(() => { + if (storageId == null) return null + return storageMap[storageId] + }, [storageMap, storageId]) + + const addFolder = useCallback(() => { + if (currentStorage == null) { + return + } + const defaultValue = folderPathname == null ? '/' : folderPathname + prompt({ + title: 'Create a Folder', + message: 'Enter the path where do you want to create a folder', + iconType: DialogIconTypes.Question, + defaultValue, + submitButtonLabel: 'Create Folder', + onClose: (value: string | null) => { + if (value == null) return + createFolder(storageId as string, value) + } + }) + }, [prompt, createFolder, storageId, folderPathname, currentStorage]) + + const openSideNavContextMenu = useCallback( + (event: React.MouseEvent) => { + event.preventDefault() + popup(event, [ + { + type: MenuTypes.Normal, + label: 'New Storage', + onClick: async () => { + prompt({ + title: 'Create a Storage', + message: 'Enter name of a storage to create', + iconType: DialogIconTypes.Question, + submitButtonLabel: 'Create Storage', + onClose: async (value: string | null) => { + if (value == null) return + await createStorage(value) + } + }) + } + } + ]) + }, + [popup, prompt, createStorage] + ) + return ( - + {}} /> - +
      {storageEntries.map(([id, storage]) => { - const pathname = router.pathname - const active = `/app/storages/${storage.name}` === pathname return ( - ) })} - - {storageEntries.length === 0 &&

      No storages

      } - + {storageEntries.length === 0 && ( +
      No storages
      + )} +
    +
    + + +
    ) } diff --git a/src/components/SideNavigator/SideNavigatorItem.tsx b/src/components/SideNavigator/SideNavigatorItem.tsx new file mode 100644 index 0000000000..6dda300695 --- /dev/null +++ b/src/components/SideNavigator/SideNavigatorItem.tsx @@ -0,0 +1,100 @@ +import React, { useState, MouseEventHandler } from 'react' +import styled from '../../lib/styled' +import { Link } from '../../lib/router' +import Icon from '../atoms/Icon' +import { mdiChevronDown, mdiChevronRight } from '@mdi/js' +import cc from 'classcat' + +const StyledContainer = styled.div` + user-select: none; + .header { + position: relative; + height: 22px; + display: flex; + align-items: center; + } + .headerLink { + width: 100%; + height: 22px; + display: flex; + align-items: center; + text-decoration: none; + &:hover { + background-color: ${({ theme }) => theme.colors.alternativeBackground}; + } + &.active { + background-color: ${({ theme }) => theme.colors.active}; + color: ${({ theme }) => theme.colors.inverseText}; + } + } + .toggleButton { + position: absolute; + width: 18px; + height: 18px; + padding: 0; + border: none; + background-color: transparent; + margin-right: 3px; + border-radius: 2px; + top: 2px; + } + .storageIcon { + margin-right: 4px; + } +` + +export interface NavigatorNode { + iconPath?: string + name: string + href?: string + children?: NavigatorNode[] + onContextMenu?: MouseEventHandler + active?: boolean +} + +interface SideNavigatorItemProps { + node: NavigatorNode + openAlways?: boolean + depth?: number +} + +const SideNavigatorItem = ({ + node: item, + openAlways = false, + depth = 0 +}: SideNavigatorItemProps) => { + const { iconPath, name, href, children, onContextMenu, active } = item + const [open, setOpen] = useState(true) + const childrenExists = children != null && children.length > 0 + return ( + +
    + {childrenExists && !openAlways && ( + + )} + + {iconPath != null && } + {name} + +
    + {open && + childrenExists && + children!.map(child => ( + + ))} +
    + ) +} + +export default SideNavigatorItem diff --git a/src/components/SideNavigator/StorageItem/FolderItem.tsx b/src/components/SideNavigator/StorageItem/FolderItem.tsx deleted file mode 100644 index f966933060..0000000000 --- a/src/components/SideNavigator/StorageItem/FolderItem.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React, { useCallback } from 'react' -import { StyledStorageItemFolderItem, StyledNavLink } from './styled' -import { useContextMenu } from '../../../lib/contextMenu' -import { MenuTypes } from '../../../lib/contextMenu/types' -import { useDialog } from '../../../lib/dialog' -import { DialogIconTypes } from '../../../lib/dialog/types' -import { PopulatedFolderDoc } from '../../../lib/db/types' - -type FolderItemProps = { - storageId: string - folder: PopulatedFolderDoc - createFolder: (storageId: string, folderPath: string) => Promise - removeFolder: (storageId: string, folderPath: string) => Promise - active: boolean -} - -export default (props: FolderItemProps) => { - const { prompt, messageBox } = useDialog() - const contextMenu = useContextMenu() - const { storageId, folder, active, removeFolder, createFolder } = props - const openContextMenu = useCallback( - (event: React.MouseEvent) => { - const folderIsRootFolder = folder.pathname === '/' - - event.preventDefault() - contextMenu.popup(event, [ - { - type: MenuTypes.Normal, - label: 'New Folder', - onClick: async () => { - prompt({ - title: 'Create a Folder', - message: 'Enter the path where do you want to create a folder', - iconType: DialogIconTypes.Question, - defaultValue: folderIsRootFolder ? '/' : `${folder.pathname}/`, - submitButtonLabel: 'Create Folder', - onClose: (value: string | null) => { - if (value == null) return - createFolder(storageId, value) - } - }) - } - }, - { - type: MenuTypes.Normal, - label: 'Remove Folder', - enabled: !folderIsRootFolder, - onClick: () => { - messageBox({ - title: `Remove "${folder.pathname}" folder`, - message: 'All notes and subfolders will be deleted.', - iconType: DialogIconTypes.Warning, - buttons: ['Remove Folder', 'Cancel'], - defaultButtonIndex: 0, - cancelButtonIndex: 1, - onClose: (value: number | null) => { - if (value === 0) { - removeFolder(storageId, folder.pathname) - } - } - }) - } - } - ]) - }, - [ - folder.pathname, - prompt, - messageBox, - contextMenu, - createFolder, - storageId, - removeFolder - ] - ) - - return ( - - - {folder.pathname} - - - ) -} diff --git a/src/components/SideNavigator/StorageItem/StorageItem.tsx b/src/components/SideNavigator/StorageItem/StorageItem.tsx deleted file mode 100644 index fa2ee7426c..0000000000 --- a/src/components/SideNavigator/StorageItem/StorageItem.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import React, { useMemo, useCallback } from 'react' -import { useContextMenu } from '../../../lib/contextMenu' -import { MenuTypes } from '../../../lib/contextMenu/types' -import { useDialog } from '../../../lib/dialog' -import { DialogIconTypes } from '../../../lib/dialog/types' -import FolderItem from './FolderItem' -import { - StyledStorageItem, - StyledStorageItemHeader, - StyledNavLink, - StyledStorageItemFolderList -} from './styled' -import { NoteStorage } from '../../../lib/db/types' -import { useRouter, useNotesPathname } from '../../../lib/router' -import { values } from '../../../lib/db/utils' - -type StorageItemProps = { - id: string - storage: NoteStorage - renameStorage: (storageId: string, name: string) => Promise - removeStorage: (storageId: string) => Promise - createFolder: (storageId: string, folderPath: string) => Promise - removeFolder: (storageId: string, folderPath: string) => Promise - pathname: string - active: boolean -} - -export default (props: StorageItemProps) => { - const { prompt, messageBox } = useDialog() - const contextMenu = useContextMenu() - const { - id, - storage, - createFolder, - renameStorage, - removeStorage, - removeFolder, - active - } = props - const storageName = storage.name - const { pathname } = useRouter() - const { folderMap, tagMap } = storage - - const tags = useMemo(() => { - return Object.keys(tagMap) - }, [tagMap]) - const folders = useMemo(() => { - return values(folderMap) - }, [folderMap]) - - const openContextMenu = useCallback( - (event: React.MouseEvent) => { - event.preventDefault() - - contextMenu.popup(event, [ - { - type: MenuTypes.Normal, - label: 'New Folder', - onClick: async () => { - prompt({ - title: 'Create a Folder', - message: 'Enter the path where do you want to create a folder', - iconType: DialogIconTypes.Question, - defaultValue: '/', - submitButtonLabel: 'Create Folder', - onClose: (value: string | null) => { - if (value == null) return - createFolder(id, value) - } - }) - } - }, - { - type: MenuTypes.Normal, - label: 'Rename Storage', - onClick: async () => { - prompt({ - title: `Rename "${storageName}" storage`, - message: 'Enter new name for the storage', - iconType: DialogIconTypes.Question, - defaultValue: storageName, - submitButtonLabel: 'Rename Folder', - onClose: (value: string | null) => { - if (value == null) return - renameStorage(id, value) - } - }) - } - }, - { - type: MenuTypes.Normal, - label: 'Remove Storage', - onClick: async () => { - messageBox({ - title: `Remove "${storageName}" storage`, - message: 'All notes and folders will be deleted.', - iconType: DialogIconTypes.Warning, - buttons: ['Remove Storage', 'Cancel'], - defaultButtonIndex: 0, - cancelButtonIndex: 1, - onClose: (value: number | null) => { - if (value === 0) { - removeStorage(id) - } - } - }) - } - } - ]) - }, - [ - contextMenu, - prompt, - messageBox, - createFolder, - id, - storageName, - renameStorage, - removeStorage - ] - ) - - const [currentStorageId, currentFolderPathname] = useNotesPathname() - - return ( - - - - {storageName} - - - - {folders.map(folder => { - const folderIsActive = - id === currentStorageId && folder.pathname === currentFolderPathname - - return ( - - ) - })} - -
      - {tags.map(tag => { - const tagIsActive = - pathname === `/app/storages/${storageName}/tags/${tag}` - return ( -
    • - - {tag} - -
    • - ) - })} -
    -
    - ) -} diff --git a/src/components/SideNavigator/StorageItem/styled.tsx b/src/components/SideNavigator/StorageItem/styled.tsx deleted file mode 100644 index acd337aa99..0000000000 --- a/src/components/SideNavigator/StorageItem/styled.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react' -import styled from '../../../lib/styled' -import { LinkProps, Link } from '../../../lib/router' - -export const StyledStorageItem = styled.li` - margin: 0; - padding: 0; -` - -export const StyledStorageItemHeader = styled.header` - height: 26px; - display: flex; -` - -export const StyledStorageItemFolderList = styled.ul` - padding: 0; - list-style: none; -` - -export const StyledStorageItemFolderItem = styled.li` - display: flex; - height: 26px; -` - -export const StyledNavLink = styled( - ({ - active, - ...props - }: LinkProps & { - active: boolean - onContextMenu?: React.MouseEventHandler - }) => -)<{ active: boolean }>` - line-height: 26px; - padding: 0 5px; - text-decoration: none; - width: 100%; - user-select: none; - ${({ active, theme }) => - active && - `background-color: ${theme.colors.active}; - color: ${theme.colors.inverseText};`} -` diff --git a/src/components/SideNavigator/StorageNavigatorItem.tsx b/src/components/SideNavigator/StorageNavigatorItem.tsx new file mode 100644 index 0000000000..85a7f9e05d --- /dev/null +++ b/src/components/SideNavigator/StorageNavigatorItem.tsx @@ -0,0 +1,265 @@ +import React, { useMemo, useCallback, MouseEventHandler } from 'react' +import SideNavigatorItem, { NavigatorNode } from './SideNavigatorItem' +import { NoteStorage } from '../../lib/db/types' +import { mdiTagMultiple, mdiTrashCan } from '@mdi/js' +import { useContextMenu, MenuTypes } from '../../lib/contextMenu' +import { useDialog, DialogIconTypes } from '../../lib/dialog' + +interface StorageNaviagtorItemProps { + storage: NoteStorage + currentPathname: string + renameStorage: (storageId: string, name: string) => Promise + removeStorage: (storageId: string) => Promise + createFolder: (storageId: string, folderPath: string) => Promise + removeFolder: (storageId: string, folderPath: string) => Promise +} + +type FolderTree = { + [key: string]: FolderTree +} + +const StorageNavigatorItem = ({ + storage, + currentPathname, + renameStorage, + removeStorage, + createFolder, + removeFolder +}: StorageNaviagtorItemProps) => { + const { prompt, messageBox } = useDialog() + const contextMenu = useContextMenu() + const { id: storageId, name: storageName } = storage + const openContextMenu = useCallback( + (event: React.MouseEvent) => { + event.preventDefault() + + contextMenu.popup(event, [ + { + type: MenuTypes.Normal, + label: 'New Folder', + onClick: async () => { + prompt({ + title: 'Create a Folder', + message: 'Enter the path where do you want to create a folder', + iconType: DialogIconTypes.Question, + defaultValue: '/', + submitButtonLabel: 'Create Folder', + onClose: (value: string | null) => { + if (value == null) return + createFolder(storageId, value) + } + }) + } + }, + { + type: MenuTypes.Normal, + label: 'Rename Storage', + onClick: async () => { + prompt({ + title: `Rename "${storageName}" storage`, + message: 'Enter new name for the storage', + iconType: DialogIconTypes.Question, + defaultValue: storageName, + submitButtonLabel: 'Rename Folder', + onClose: (value: string | null) => { + if (value == null) return + renameStorage(storageId, value) + } + }) + } + }, + { + type: MenuTypes.Normal, + label: 'Remove Storage', + onClick: async () => { + messageBox({ + title: `Remove "${storageName}" storage`, + message: 'All notes and folders will be deleted.', + iconType: DialogIconTypes.Warning, + buttons: ['Remove Storage', 'Cancel'], + defaultButtonIndex: 0, + cancelButtonIndex: 1, + onClose: (value: number | null) => { + if (value === 0) { + removeStorage(storageId) + } + } + }) + } + } + ]) + }, + [ + contextMenu, + prompt, + messageBox, + createFolder, + storageId, + storageName, + renameStorage, + removeStorage + ] + ) + + const createFolderContextMenuHandler = useCallback( + (pathname: string) => { + return (event: React.MouseEvent) => { + const folderIsRootFolder = pathname === '/' + + event.preventDefault() + contextMenu.popup(event, [ + { + type: MenuTypes.Normal, + label: 'New Folder', + onClick: async () => { + prompt({ + title: 'Create a Folder', + message: 'Enter the path where do you want to create a folder', + iconType: DialogIconTypes.Question, + defaultValue: folderIsRootFolder ? '/' : `${pathname}/`, + submitButtonLabel: 'Create Folder', + onClose: (value: string | null) => { + if (value == null) return + createFolder(storageId, value) + } + }) + } + }, + { + type: MenuTypes.Normal, + label: 'Remove Folder', + enabled: !folderIsRootFolder, + onClick: () => { + messageBox({ + title: `Remove "${pathname}" folder`, + message: 'All notes and subfolders will be deleted.', + iconType: DialogIconTypes.Warning, + buttons: ['Remove Folder', 'Cancel'], + defaultButtonIndex: 0, + cancelButtonIndex: 1, + onClose: (value: number | null) => { + if (value === 0) { + removeFolder(storageId, pathname) + } + } + }) + } + } + ]) + } + }, + [contextMenu, storageId, messageBox, prompt, createFolder, removeFolder] + ) + + const folderNodes = useMemo(() => { + const folderTree = getFolderTree(Object.keys(storage.folderMap)) + + return getNavigatorNodeFromPathnameTree( + folderTree, + storageId, + '/', + currentPathname, + createFolderContextMenuHandler + ) + }, [ + storageId, + currentPathname, + storage.folderMap, + createFolderContextMenuHandler + ]) + const tagNodes = Object.keys(storage.tagMap).map(tagName => ({ + name: tagName, + href: `/app/storages/${storage.id}/tags/${tagName}` + })) + + const node = useMemo(() => { + const storagePathname = `/app/storages/${storage.id}` + const notesPathname = `/app/storages/${storage.id}/notes` + + return { + name: storage.name, + href: storagePathname, + active: currentPathname === storagePathname, + onContextMenu: openContextMenu, + children: [ + { + name: 'Notes', + href: notesPathname, + active: currentPathname === notesPathname, + onContextMenu: createFolderContextMenuHandler('/') + }, + ...folderNodes, + { + iconPath: mdiTagMultiple, + name: 'Tags', + href: `${storagePathname}/tags`, + children: tagNodes + }, + { + iconPath: mdiTrashCan, + href: `${storagePathname}/trash-can`, + name: 'Trash Can' + } + ] + } + }, [ + storage, + folderNodes, + tagNodes, + openContextMenu, + createFolderContextMenuHandler, + currentPathname + ]) + + return +} + +export default StorageNavigatorItem + +function getFolderTree(pathnames: string[]) { + const tree = {} + for (const pathname of pathnames) { + if (pathname === '/') continue + const [, ...folderNames] = pathname.split('/') + let currentNode = tree + for (let index = 0; index < folderNames.length; index++) { + const currentPathname = folderNames[index] + if (currentNode[currentPathname] == null) { + currentNode[currentPathname] = {} + } + currentNode = currentNode[currentPathname] + } + } + + return tree +} + +function getNavigatorNodeFromPathnameTree( + tree: FolderTree, + storageId: string, + parentFolderPathname: string, + currentPathname: string, + contextMenuHandlerCreator: (pathname: string) => MouseEventHandler +): NavigatorNode[] { + return Object.entries(tree).map(([folderName, tree]) => { + const folderPathname = + parentFolderPathname === '/' + ? `/${folderName}` + : `${parentFolderPathname}/${folderName}` + const pathname = `/app/storages/${storageId}/notes${folderPathname}` + + return { + name: folderName, + href: pathname, + active: new RegExp(`${pathname}(/note:.+)?$`).test(currentPathname), + onContextMenu: contextMenuHandlerCreator(folderPathname), + children: getNavigatorNodeFromPathnameTree( + tree, + storageId, + folderPathname, + currentPathname, + contextMenuHandlerCreator + ) + } + }) +} diff --git a/src/components/atoms/CodeEditor.tsx b/src/components/atoms/CodeEditor.tsx index 6f40237f08..38ca0a1efa 100644 --- a/src/components/atoms/CodeEditor.tsx +++ b/src/components/atoms/CodeEditor.tsx @@ -13,6 +13,7 @@ interface CodeEditorProps { newValue: string, change: CodeMirror.EditorChangeLinkedList ) => void + codeMirrorRef?: (codeMirror: CodeMirror.EditorFromTextArea) => void } class CodeEditor extends React.Component { @@ -26,6 +27,9 @@ class CodeEditor extends React.Component { ) this.codeMirror.on('change', this.handleCodeMirrorChange) window.addEventListener('codemirror-mode-load', this.reloadOptions) + if (this.props.codeMirrorRef != null) { + this.props.codeMirrorRef(this.codeMirror) + } } reloadOptions = () => { diff --git a/src/components/atoms/Icon.tsx b/src/components/atoms/Icon.tsx index 75d35dd0cb..ee360d344f 100644 --- a/src/components/atoms/Icon.tsx +++ b/src/components/atoms/Icon.tsx @@ -3,10 +3,11 @@ import MdiIcon from '@mdi/react' interface IconProps { path: string + className?: string } -const Icon = ({ path }: IconProps) => ( - +const Icon = ({ path, className }: IconProps) => ( + ) export default Icon diff --git a/src/components/atoms/ToolbarButtonGroup.tsx b/src/components/atoms/ToolbarButtonGroup.tsx index 8df8939456..0b02747e4a 100644 --- a/src/components/atoms/ToolbarButtonGroup.tsx +++ b/src/components/atoms/ToolbarButtonGroup.tsx @@ -8,8 +8,14 @@ export default styled.div` border: 0; border-right: 1px solid ${({ theme }) => theme.colors.border}; border-radius: 0; + &:first-child { + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; + } &:last-child { border-right: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; } } ` diff --git a/src/components/atoms/ToolbarIconInput.tsx b/src/components/atoms/ToolbarIconInput.tsx new file mode 100644 index 0000000000..67a5c84fa9 --- /dev/null +++ b/src/components/atoms/ToolbarIconInput.tsx @@ -0,0 +1,48 @@ +import React from 'react' +import styled from '../../lib/styled' +import Icon from './Icon' +const StyledContainer = styled.div` + position: relative; + height: 22px; + .icon { + position: absolute; + top: 4px; + left: 4px; + height: 14px; + width: 14px; + z-index: 0; + } + .input { + position: relative; + width: 100%; + background-color: transparent; + border: solid 1px ${({ theme }) => theme.colors.border}; + height: 22px; + padding-left: 18px; + border-radius: 2px; + box-sizing: border-box; + } +` + +interface ToolbarIconInputProps { + className?: string + iconPath: string + value: string + onChange: React.ChangeEventHandler +} + +const ToolbarIconInput = ({ + className, + iconPath, + value, + onChange +}: ToolbarIconInputProps) => { + return ( + + + + + ) +} + +export default ToolbarIconInput diff --git a/src/components/NotePage/TwoPaneLayout.tsx b/src/components/atoms/TwoPaneLayout.tsx similarity index 84% rename from src/components/NotePage/TwoPaneLayout.tsx rename to src/components/atoms/TwoPaneLayout.tsx index 3fc018406e..2f905f5018 100644 --- a/src/components/NotePage/TwoPaneLayout.tsx +++ b/src/components/atoms/TwoPaneLayout.tsx @@ -1,4 +1,10 @@ -import React, { useState, useCallback, useEffect, useRef } from 'react' +import React, { + useState, + useCallback, + useEffect, + useRef, + CSSProperties +} from 'react' import styled, { defaultTheme } from '../../lib/styled' import throttle from 'lodash/throttle' import { clamp } from 'ramda' @@ -6,11 +12,14 @@ import { clamp } from 'ramda' interface TwoPaneLayoutProps { left: React.ReactNode right: React.ReactNode + className?: string + style?: CSSProperties + defaultLeftWidth?: number + maxLeftWidth?: number + onResizeEnd?: () => void } -const defaultLeftWidth = 250 const minLeftWidth = 100 -const maxLeftWidth = 500 const Container = styled.div` flex: 1px; @@ -61,7 +70,15 @@ const Divider = ({ onMouseDown, dragging, leftWidth }: DividerProps) => ( ) -const TwoPaneLayout = ({ left, right }: TwoPaneLayoutProps) => { +const TwoPaneLayout = ({ + left, + right, + className, + style, + defaultLeftWidth = 250, + maxLeftWidth = 500, + onResizeEnd +}: TwoPaneLayoutProps) => { const [leftWidth, setLeftWidth] = useState(defaultLeftWidth) const [dragging, setDragging] = useState(false) const mouseupListenerIsSetRef = useRef(false) @@ -117,8 +134,14 @@ const TwoPaneLayout = ({ left, right }: TwoPaneLayoutProps) => { } }, [dragging, endDragging, moveDragging]) + useEffect(() => { + if (onResizeEnd != null && !dragging) { + onResizeEnd() + } + }, [onResizeEnd, leftWidth, dragging]) + return ( - + {left} ): Promise + trashNote(storageId: string, noteId: string): Promise } export function createDbStoreCreator( @@ -307,6 +308,28 @@ export function createDbStoreCreator( [storageMap] ) + const trashNote = useCallback( + async (storageId: string, noteId: string) => { + const storage = storageMap[storageId] + if (storage == null) { + return + } + const noteDoc = await storage.db.trashNote(noteId) + if (noteDoc == null) { + return + } + + setStorageMap( + produce((draft: ObjectMap) => { + draft[storageId]!.noteMap[noteDoc._id] = noteDoc + }) + ) + + return noteDoc + }, + [storageMap] + ) + return { initialized, storageMap, @@ -317,7 +340,8 @@ export function createDbStoreCreator( createFolder, removeFolder, createNote, - updateNote + updateNote, + trashNote } } } diff --git a/src/lib/router/Link.tsx b/src/lib/router/Link.tsx index 1907a81265..3ed88b02ab 100644 --- a/src/lib/router/Link.tsx +++ b/src/lib/router/Link.tsx @@ -1,25 +1,41 @@ -import React, { useCallback, FC } from 'react' +import React, { useCallback, FC, CSSProperties, MouseEventHandler } from 'react' import { useRouter } from './store' export interface LinkProps { - href: string + href?: string children: React.ReactNode className?: string + style?: CSSProperties + onContextMenu?: MouseEventHandler } -export const Link: FC = ({ children, href, className }) => { +export const Link: FC = ({ + children, + href, + className, + style, + onContextMenu +}) => { const router = useRouter() const push = useCallback( (e: React.MouseEvent) => { e.preventDefault() - router.push(href) + if (href != null) { + router.push(href) + } }, [href, router] ) return ( - + {children} ) diff --git a/src/lib/router/utils.ts b/src/lib/router/utils.ts index 068c96c906..1192937f0a 100644 --- a/src/lib/router/utils.ts +++ b/src/lib/router/utils.ts @@ -41,7 +41,10 @@ export function parseUrl(urlStr: string): Location { export const useNotesPathname = () => { const { pathname } = useRouter() return useMemo((): [null | string, null | string, null | string] => { - const names = pathname.slice(1).split('/').slice(1) + const names = pathname + .slice(4) + .split('/') + .slice(1) if (names[0] !== 'storages' || names[1] == null) { return [null, null, null] } diff --git a/webpack.config.ts b/webpack.config.ts index a95cddced4..9df5375f5f 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -2,6 +2,7 @@ import path from 'path' import webpack from 'webpack' import HtmlWebpackPlugin from 'html-webpack-plugin' import express from 'express' +import ErrorOverlayPlugin from 'error-overlay-webpack-plugin' module.exports = { entry: [ @@ -26,7 +27,7 @@ module.exports = { path: path.resolve(__dirname, 'dist'), - publicPath: '/app/' + publicPath: '/app' // necessary for HMR to know where to load the hot update chunks }, @@ -47,7 +48,7 @@ module.exports = { }, { test: /\.tsx?$/, - use: [{ loader: 'ts-loader', options: { transpileOnly: true } }], + use: [{ loader: 'ts-loader' }], exclude: /node_modules/ } ] @@ -62,20 +63,23 @@ module.exports = { new webpack.NoEmitOnErrorsPlugin(), // do not emit compiled assets that include errors - new HtmlWebpackPlugin() + new HtmlWebpackPlugin(), + new ErrorOverlayPlugin() ], devServer: { host: 'localhost', port: 3000, - historyApiFallback: true, + historyApiFallback: { + index: '/app' + }, // respond to 404s with index.html hot: true, // enable HMR on the server - before: function (app, server) { + before: function(app, server) { app.use( '/codemirror/mode', express.static(path.join(__dirname, 'node_modules/codemirror/mode'))