diff --git a/README.md b/README.md index 594b73d..b7ff429 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Idea Hub is a place to share ideas and innovations, with feedbacks from other us `git clone https://github.com/1-Platform/idea-hub.git && cd idea-hub` 2. Install Dependencies `npm install` +3. Copy `.env.example` to `.env.local` and fill necessary fields ## Start SPA @@ -32,11 +33,11 @@ run `npm run lint:fix` ## Load remote couchdb with design documents and default tag -run `npm run generate:couchdb-documents`: To save both design documents and default tags to couchdb +run `npm run migrate:all`: To save both design documents and default tags to couchdb -run `nom run generate:design-doc`: To save design documents to couchdb +run `npm run migrate:design-doc`: To save design documents to couchdb -run `nom run generate:default-tags`: To save default tags to couchdb +run `npm run migrate:default-tags`: To save default tags to couchdb ## 🤝 Contributors diff --git a/package-lock.json b/package-lock.json index 20b8fc4..a0e8b07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2338,8 +2338,16 @@ "@types/debug": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.6.tgz", - "integrity": "sha512-7fDOJFA/x8B+sO1901BmHlf5dE1cxBU8mRXj8QOEDnn16hhGJv/IHxJtZhvsabZsIMn0eLIyeOKAeqSNJJYTpA==", - "dev": true + "integrity": "sha512-7fDOJFA/x8B+sO1901BmHlf5dE1cxBU8mRXj8QOEDnn16hhGJv/IHxJtZhvsabZsIMn0eLIyeOKAeqSNJJYTpA==" + }, + "@types/dotenv-safe": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@types/dotenv-safe/-/dotenv-safe-8.1.2.tgz", + "integrity": "sha512-R/B/wIMda6lRE2P1H0vwSoJsV78IOkhccE4vIvmKZQNXOIjiU0QyJsUXwSotBxOPZFZ/oOnjCa3+kK5kVJwGyw==", + "dev": true, + "requires": { + "dotenv": "^8.2.0" + } }, "@types/eslint": { "version": "7.2.13", @@ -2441,7 +2449,6 @@ "version": "2.5.11", "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.11.tgz", "integrity": "sha512-2upCKaqVZETDRb8A2VTaRymqFBEgH8u6yr96b/u3+1uQEPDRo3mJLEiPk7vdXBHRtjwkjqzFYMJXrt0Z9QsYjQ==", - "dev": true, "requires": { "@types/node": "*", "form-data": "^3.0.0" @@ -2457,52 +2464,10 @@ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" }, - "@types/pouchdb": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@types/pouchdb/-/pouchdb-6.4.0.tgz", - "integrity": "sha512-eGCpX+NXhd5VLJuJMzwe3L79fa9+IDTrAG3CPaf4s/31PD56hOrhDJTSmRELSXuiqXr6+OHzzP0PldSaWsFt7w==", - "dev": true, - "requires": { - "@types/pouchdb-adapter-cordova-sqlite": "*", - "@types/pouchdb-adapter-fruitdown": "*", - "@types/pouchdb-adapter-http": "*", - "@types/pouchdb-adapter-idb": "*", - "@types/pouchdb-adapter-leveldb": "*", - "@types/pouchdb-adapter-localstorage": "*", - "@types/pouchdb-adapter-memory": "*", - "@types/pouchdb-adapter-node-websql": "*", - "@types/pouchdb-adapter-websql": "*", - "@types/pouchdb-browser": "*", - "@types/pouchdb-core": "*", - "@types/pouchdb-http": "*", - "@types/pouchdb-mapreduce": "*", - "@types/pouchdb-node": "*", - "@types/pouchdb-replication": "*" - } - }, - "@types/pouchdb-adapter-cordova-sqlite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-cordova-sqlite/-/pouchdb-adapter-cordova-sqlite-1.0.0.tgz", - "integrity": "sha512-NsqpEYKunBS/BPvttlOVQ5Me6LdU6UYZB0Qak3XS/AvLeIRdF61MZ/czSuL/ozydYr6bikewt6dvlpCK1HWG9Q==", - "dev": true, - "requires": { - "@types/pouchdb-core": "*" - } - }, - "@types/pouchdb-adapter-fruitdown": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-fruitdown/-/pouchdb-adapter-fruitdown-6.1.3.tgz", - "integrity": "sha512-Wz1Z1JLOW1hgmFQjqnSkmyyfH7by/iWb4abKn684WMvQfmxx6BxKJpJ4+eulkVPQzzgMMSgU1MpnQOm9FgRkbw==", - "dev": true, - "requires": { - "@types/pouchdb-core": "*" - } - }, "@types/pouchdb-adapter-http": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-http/-/pouchdb-adapter-http-6.1.3.tgz", "integrity": "sha512-9Z4TLbF/KJWy/D2sWRPBA+RNU0odQimfdvlDX+EY7rGcd3aVoH8qjD/X0Xcd/0dfBH5pKrNIMFFQgW/TylRCmA==", - "dev": true, "requires": { "@types/pouchdb-core": "*" } @@ -2511,7 +2476,6 @@ "version": "6.1.3", "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-idb/-/pouchdb-adapter-idb-6.1.3.tgz", "integrity": "sha512-K4G9pmHkR2JyL8d6cllIEix2dtQFVIJyDcgoT7ctrbIyyhT4kRjieGc3O7tzIhm1bv7W2qz1aResO9lq7qjKVQ==", - "dev": true, "requires": { "@types/pouchdb-core": "*" } @@ -2525,39 +2489,10 @@ "@types/pouchdb-core": "*" } }, - "@types/pouchdb-adapter-localstorage": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-localstorage/-/pouchdb-adapter-localstorage-6.1.3.tgz", - "integrity": "sha512-oor040tye1KKiGLWYtIy7rRT7C2yoyX3Tf6elEJRpjOA7Ja/H8lKc4LaSh9ATbptIcES6MRqZDxtp7ly9hsW3Q==", - "dev": true, - "requires": { - "@types/pouchdb-core": "*" - } - }, - "@types/pouchdb-adapter-memory": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-memory/-/pouchdb-adapter-memory-6.1.3.tgz", - "integrity": "sha512-gVbsIMzDzgZYThFVT4eVNsmuZwVm/4jDxP1sjlgc3qtDIxbtBhGgyNfcskwwz9Zu5Lv1avkDsIWvcxQhnvRlHg==", - "dev": true, - "requires": { - "@types/pouchdb-core": "*" - } - }, - "@types/pouchdb-adapter-node-websql": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-node-websql/-/pouchdb-adapter-node-websql-6.1.3.tgz", - "integrity": "sha512-F/P+os6Jsa7CgHtH64+Z0HfwIcj0hIRB5z8gNhF7L7dxPWoAfkopK5H2gydrP3sQrlGyN4WInF+UJW/Zu1+FKg==", - "dev": true, - "requires": { - "@types/pouchdb-adapter-websql": "*", - "@types/pouchdb-core": "*" - } - }, "@types/pouchdb-adapter-websql": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/@types/pouchdb-adapter-websql/-/pouchdb-adapter-websql-6.1.3.tgz", "integrity": "sha512-0AsCWnliwg/3PKj5NAoFuzpnMQKXGBOl+6q8aNxK3N9Tq3SbV91QhgW/mdJsOdqSOw0EBudkGdE6/CZlrgeLpw==", - "dev": true, "requires": { "@types/pouchdb-core": "*" } @@ -2566,7 +2501,6 @@ "version": "6.1.3", "resolved": "https://registry.npmjs.org/@types/pouchdb-browser/-/pouchdb-browser-6.1.3.tgz", "integrity": "sha512-EdYowrWxW9SWBMX/rux2eq7dbHi5Zeyzz+FF/IAsgQKnUxgeCO5VO2j4zTzos0SDyJvAQU+EYRc11r7xGn5tvA==", - "dev": true, "requires": { "@types/pouchdb-adapter-http": "*", "@types/pouchdb-adapter-idb": "*", @@ -2580,7 +2514,6 @@ "version": "7.0.7", "resolved": "https://registry.npmjs.org/@types/pouchdb-core/-/pouchdb-core-7.0.7.tgz", "integrity": "sha512-QP1Cjx8yq6FNpwbrS/6OD5OyT0Oo4ZNmE27wQ1tWVjC6W8NPi6hjtf25r0VcyYEXg+pmMrz8dRRvP10wjZJQaw==", - "dev": true, "requires": { "@types/debug": "*", "@types/node-fetch": "*", @@ -2591,34 +2524,22 @@ "version": "6.3.7", "resolved": "https://registry.npmjs.org/@types/pouchdb-find/-/pouchdb-find-6.3.7.tgz", "integrity": "sha512-b2dr9xoZRK5Mwl8UiRA9l5j9mmCxNfqXuu63H1KZHwJLILjoIIz7BntCvM0hnlnl7Q8P8wORq0IskuaMq5Nnnw==", - "dev": true, "requires": { "@types/pouchdb-core": "*" } }, - "@types/pouchdb-http": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/@types/pouchdb-http/-/pouchdb-http-6.1.3.tgz", - "integrity": "sha512-0e9E5SqNOyPl/3FnEIbENssB4FlJsNYuOy131nxrZk36S+y1R/6qO7ZVRypWpGTqBWSuVd7gCsq2UDwO/285+w==", - "dev": true, - "requires": { - "@types/pouchdb-adapter-http": "*", - "@types/pouchdb-core": "*" - } - }, "@types/pouchdb-mapreduce": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/@types/pouchdb-mapreduce/-/pouchdb-mapreduce-6.1.4.tgz", "integrity": "sha512-c8cZ8E9zEl7ZMcrE5jaF2qOm+cxQ01+B0NG9oGHVX3Mj5G8m4oIBAUELJmJw9LvEq3r9nXXn6g9TvvN8NiRalg==", - "dev": true, "requires": { "@types/pouchdb-core": "*" } }, "@types/pouchdb-node": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/@types/pouchdb-node/-/pouchdb-node-6.1.3.tgz", - "integrity": "sha512-rZb3eWGLKJo4LBhUIJLofyoric/5wKYI6HVUld8CfDWeVZgYVUJ/GEw/vXuqXsbgbsaNyk6XeU9461ULBt+0qg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/pouchdb-node/-/pouchdb-node-6.1.4.tgz", + "integrity": "sha512-wnTCH8X1JOPpNOfVhz8HW0AvmdHh6pt40MuRj0jQnK7QEHsHS79WujsKTKSOF8QXtPwpvCNSsI7ut7H7tfxxJQ==", "dev": true, "requires": { "@types/pouchdb-adapter-http": "*", @@ -2632,7 +2553,6 @@ "version": "6.4.3", "resolved": "https://registry.npmjs.org/@types/pouchdb-replication/-/pouchdb-replication-6.4.3.tgz", "integrity": "sha512-Dsyy8EhKGBIQAECKelfvufDjRO5Rsy8RlujVllbjHzu3OGu8cNP92/bykeXXC3hTgvrp5Kviqhh9VON1Iw+M3Q==", - "dev": true, "requires": { "@types/pouchdb-core": "*", "@types/pouchdb-find": "*" @@ -3066,6 +2986,7 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz", "integrity": "sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==", + "dev": true, "requires": { "buffer": "^5.5.0", "immediate": "^3.2.3", @@ -3078,6 +2999,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, "requires": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -5436,6 +5358,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-5.3.0.tgz", "integrity": "sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==", + "dev": true, "requires": { "abstract-leveldown": "~6.2.1", "inherits": "^2.0.3" @@ -5774,19 +5697,28 @@ } }, "dotenv": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", - "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==" + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==" }, "dotenv-expand": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==" }, + "dotenv-safe": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv-safe/-/dotenv-safe-8.2.0.tgz", + "integrity": "sha512-uWwWWdUQkSs5a3mySDB22UtNwyEYi0JtEQu+vDzIqr9OjbDdC2Ip13PnSpi/fctqlYmzkxCeabiyCAOROuAIaA==", + "requires": { + "dotenv": "^8.2.0" + } + }, "double-ended-queue": { "version": "2.1.0-0", "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz", - "integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=" + "integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=", + "dev": true }, "duplexer": { "version": "0.1.2", @@ -5873,6 +5805,7 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/encoding-down/-/encoding-down-6.3.0.tgz", "integrity": "sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw==", + "dev": true, "requires": { "abstract-leveldown": "^6.2.1", "inherits": "^2.0.3", @@ -5892,6 +5825,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/end-stream/-/end-stream-0.1.0.tgz", "integrity": "sha1-MgA/P0OKKwFDFoE3+PpumGbIHtU=", + "dev": true, "requires": { "write-stream": "~0.4.3" } @@ -10451,6 +10385,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/level/-/level-6.0.1.tgz", "integrity": "sha512-psRSqJZCsC/irNhfHzrVZbmPYXDcEYhA5TVNwr+V92jF44rbf86hqGp8fiT702FyiArScYIlPSBTDUASCVNSpw==", + "dev": true, "requires": { "level-js": "^5.0.0", "level-packager": "^5.1.0", @@ -10461,6 +10396,7 @@ "version": "9.0.2", "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-9.0.2.tgz", "integrity": "sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==", + "dev": true, "requires": { "buffer": "^5.6.0" }, @@ -10469,6 +10405,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, "requires": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -10479,12 +10416,14 @@ "level-concat-iterator": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz", - "integrity": "sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==" + "integrity": "sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==", + "dev": true }, "level-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/level-errors/-/level-errors-2.0.1.tgz", "integrity": "sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==", + "dev": true, "requires": { "errno": "~0.1.1" } @@ -10493,6 +10432,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-4.0.2.tgz", "integrity": "sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==", + "dev": true, "requires": { "inherits": "^2.0.4", "readable-stream": "^3.4.0", @@ -10503,6 +10443,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -10515,6 +10456,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/level-js/-/level-js-5.0.2.tgz", "integrity": "sha512-SnBIDo2pdO5VXh02ZmtAyPP6/+6YTJg2ibLtl9C34pWvmtMEmRTWpra+qO/hifkUtBTOtfx6S9vLDjBsBK4gRg==", + "dev": true, "requires": { "abstract-leveldown": "~6.2.3", "buffer": "^5.5.0", @@ -10526,6 +10468,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, "requires": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -10537,6 +10480,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/level-packager/-/level-packager-5.1.1.tgz", "integrity": "sha512-HMwMaQPlTC1IlcwT3+swhqf/NUO+ZhXVz6TY1zZIIZlIR0YSn8GtAAWmIvKjNY16ZkEg/JcpAuQskxsXqC0yOQ==", + "dev": true, "requires": { "encoding-down": "^6.3.0", "levelup": "^4.3.2" @@ -10546,6 +10490,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-1.0.1.tgz", "integrity": "sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==", + "dev": true, "requires": { "xtend": "^4.0.2" } @@ -10554,6 +10499,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/level-write-stream/-/level-write-stream-1.0.0.tgz", "integrity": "sha1-P3+7Z5pVE3wP6zA97nZuEu4Twdw=", + "dev": true, "requires": { "end-stream": "~0.1.0" } @@ -10562,6 +10508,7 @@ "version": "5.6.0", "resolved": "https://registry.npmjs.org/leveldown/-/leveldown-5.6.0.tgz", "integrity": "sha512-iB8O/7Db9lPaITU1aA2txU/cBEXAt4vWwKQRrrWuS6XDgbP4QZGj9BL2aNbwb002atoQ/lIotJkfyzz+ygQnUQ==", + "dev": true, "requires": { "abstract-leveldown": "~6.2.1", "napi-macros": "~2.0.0", @@ -10572,6 +10519,7 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/levelup/-/levelup-4.4.0.tgz", "integrity": "sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==", + "dev": true, "requires": { "deferred-leveldown": "~5.3.0", "level-errors": "~2.0.0", @@ -11040,7 +10988,8 @@ "ltgt": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz", - "integrity": "sha1-81ypHEk/e3PaDgdJUwTxezH4fuU=" + "integrity": "sha1-81ypHEk/e3PaDgdJUwTxezH4fuU=", + "dev": true }, "lz-string": { "version": "1.4.4", @@ -11577,7 +11526,8 @@ "napi-macros": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz", - "integrity": "sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==" + "integrity": "sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==", + "dev": true }, "native-url": { "version": "0.2.6", @@ -11668,7 +11618,8 @@ "node-gyp-build": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.1.1.tgz", - "integrity": "sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ==" + "integrity": "sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ==", + "dev": true }, "node-int64": { "version": "0.4.0", @@ -13510,95 +13461,6 @@ "uniq": "^1.0.1" } }, - "pouchdb": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/pouchdb/-/pouchdb-7.2.2.tgz", - "integrity": "sha512-5gf5nw5XH/2H/DJj8b0YkvG9fhA/4Jt6kL0Y8QjtztVjb1y4J19Rg4rG+fUbXu96gsUrlyIvZ3XfM0b4mogGmw==", - "requires": { - "abort-controller": "3.0.0", - "argsarray": "0.0.1", - "buffer-from": "1.1.1", - "clone-buffer": "1.0.0", - "double-ended-queue": "2.1.0-0", - "fetch-cookie": "0.10.1", - "immediate": "3.3.0", - "inherits": "2.0.4", - "level": "6.0.1", - "level-codec": "9.0.2", - "level-write-stream": "1.0.0", - "leveldown": "5.6.0", - "levelup": "4.4.0", - "ltgt": "2.2.1", - "node-fetch": "2.6.0", - "readable-stream": "1.1.14", - "spark-md5": "3.0.1", - "through2": "3.0.2", - "uuid": "8.1.0", - "vuvuzela": "1.0.3" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "through2": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", - "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", - "requires": { - "inherits": "^2.0.4", - "readable-stream": "2 || 3" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - } - } - }, - "uuid": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.1.0.tgz", - "integrity": "sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==" - } - } - }, "pouchdb-abstract-mapreduce": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/pouchdb-abstract-mapreduce/-/pouchdb-abstract-mapreduce-7.2.2.tgz", @@ -13634,6 +13496,26 @@ "buffer-from": "1.1.1" } }, + "pouchdb-browser": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/pouchdb-browser/-/pouchdb-browser-7.2.2.tgz", + "integrity": "sha512-90pkngk9/F95knUwOhGQ0xuNzG51uIecF5kD5AfNdhqqrV9wbngz+I3QA7u/9eVyZRVyOrTrT3oaKcNqAB7Brg==", + "requires": { + "argsarray": "0.0.1", + "immediate": "3.3.0", + "inherits": "2.0.4", + "spark-md5": "3.0.1", + "uuid": "8.1.0", + "vuvuzela": "1.0.3" + }, + "dependencies": { + "uuid": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.1.0.tgz", + "integrity": "sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==" + } + } + }, "pouchdb-collate": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/pouchdb-collate/-/pouchdb-collate-7.2.2.tgz", @@ -13719,6 +13601,102 @@ "spark-md5": "3.0.1" } }, + "pouchdb-node": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/pouchdb-node/-/pouchdb-node-7.2.2.tgz", + "integrity": "sha512-r1JenKn+HWXAK8EMcoWp8rXzeBVGcnPd/IOiOHwac36rUAZkKBFrWnS1jkJcbJcghIFxTn1w9tL0HBdaR1PFSg==", + "dev": true, + "requires": { + "abort-controller": "3.0.0", + "argsarray": "0.0.1", + "buffer-from": "1.1.1", + "clone-buffer": "1.0.0", + "double-ended-queue": "2.1.0-0", + "fetch-cookie": "0.10.1", + "inherits": "2.0.4", + "level": "6.0.1", + "level-codec": "9.0.2", + "level-write-stream": "1.0.0", + "leveldown": "5.6.0", + "levelup": "4.4.0", + "ltgt": "2.2.1", + "node-fetch": "2.6.0", + "readable-stream": "1.1.14", + "through2": "3.0.2", + "uuid": "8.1.0", + "vuvuzela": "1.0.3" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, + "uuid": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.1.0.tgz", + "integrity": "sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg==", + "dev": true + } + } + }, "pouchdb-selector-core": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/pouchdb-selector-core/-/pouchdb-selector-core-7.2.2.tgz", @@ -18560,6 +18538,7 @@ "version": "0.4.3", "resolved": "https://registry.npmjs.org/write-stream/-/write-stream-0.4.3.tgz", "integrity": "sha1-g8yMA0fQr2BXqThitOOuAd5cgcE=", + "dev": true, "requires": { "readable-stream": "~0.0.2" }, @@ -18567,7 +18546,8 @@ "readable-stream": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-0.0.4.tgz", - "integrity": "sha1-8y124/uGM0SlSNeZIwBxc2ZbO40=" + "integrity": "sha1-8y124/uGM0SlSNeZIwBxc2ZbO40=", + "dev": true } } }, diff --git a/package.json b/package.json index 3686fca..a99695e 100644 --- a/package.json +++ b/package.json @@ -18,14 +18,13 @@ "@testing-library/user-event": "^13.1.9", "@types/jest": "^26.0.23", "@types/node": "^15.12.2", + "@types/pouchdb-browser": "^6.1.3", "@types/react": "^17.0.11", "@types/react-dom": "^17.0.7", - "dotenv": "^10.0.0", "history": "^5.0.0", "nanoid": "^3.1.23", "node-sass": "^6.0.1", - "pouchdb": "^7.2.2", - "pouchdb-adapter-http": "^7.2.2", + "pouchdb-browser": "^7.2.2", "pouchdb-debug": "^7.2.1", "pouchdb-find": "^7.2.2", "react": "^17.0.2", @@ -45,9 +44,9 @@ "lint": "eslint --ext .ts,.tsx src/", "lint:fix": "eslint --ext .ts,.tsx src/ --fix", "prepare": "husky install", - "generate:design-doc": "ts-node -P scripts/tsconfig.json scripts/generate-couchdb-design-documents.ts", - "generate:default-tags": "ts-node -P scripts/tsconfig.json scripts/generate-couchdb-default-tags.ts", - "generate:couchdb-documents": "npm run generate:design-doc && npm run generate:default-tags" + "migrate:design-doc": "ts-node -P scripts/tsconfig.json scripts/migrate-couchdb-design-documents.ts", + "migrate:default-tags": "ts-node -P scripts/tsconfig.json scripts/migrate-couchdb-default-tags.ts", + "migrate:all": "npm run migrate:design-doc && npm run migrate:default-tags" }, "lint-staged": { "*.{ts,tsx}": "eslint --cache --fix" @@ -71,7 +70,8 @@ ] }, "devDependencies": { - "@types/pouchdb": "^6.4.0", + "@types/dotenv-safe": "^8.1.2", + "@types/pouchdb-node": "^6.1.4", "@types/react-test-renderer": "^17.0.1", "@typescript-eslint/eslint-plugin": "^4.26.1", "@typescript-eslint/parser": "^4.26.1", @@ -82,8 +82,10 @@ "eslint-plugin-react-hooks": "^4.2.0", "husky": "^6.0.0", "lint-staged": "^11.0.0", + "pouchdb-node": "^7.2.2", "prettier": "^2.3.1", "react-test-renderer": "^17.0.2", - "ts-node": "^10.2.1" + "ts-node": "^10.2.1", + "dotenv-safe": "^8.2.0" } } diff --git a/scripts/generate-couchdb-default-tags.ts b/scripts/migrate-couchdb-default-tags.ts similarity index 100% rename from scripts/generate-couchdb-default-tags.ts rename to scripts/migrate-couchdb-default-tags.ts diff --git a/scripts/generate-couchdb-design-documents.ts b/scripts/migrate-couchdb-design-documents.ts similarity index 100% rename from scripts/generate-couchdb-design-documents.ts rename to scripts/migrate-couchdb-design-documents.ts diff --git a/scripts/remote-pouch-db-instance.ts b/scripts/remote-pouch-db-instance.ts index 1a964a6..30bf5b2 100644 --- a/scripts/remote-pouch-db-instance.ts +++ b/scripts/remote-pouch-db-instance.ts @@ -1,13 +1,11 @@ -import PouchDB from 'pouchdb'; +import PouchDB from 'pouchdb-node'; import PouchDBFind from 'pouchdb-find'; -import pouchdbHTTPAdapter from 'pouchdb-adapter-http'; -import dotenv from 'dotenv'; -dotenv.config(); +import dotenv from 'dotenv-safe'; +dotenv.config({ path: '.env.local' }); import { POUCHDB_DB_URL } from '../src/pouchDB/config'; PouchDB.plugin(PouchDBFind); -PouchDB.plugin(pouchdbHTTPAdapter); export const db = new PouchDB(POUCHDB_DB_URL, { skip_setup: true, diff --git a/src/App.scss b/src/App.scss index 01396bf..292ca96 100644 --- a/src/App.scss +++ b/src/App.scss @@ -77,6 +77,11 @@ a { --pf-c-backdrop--ZIndex: 9999; } +.pf-c-select__menu { + overflow: auto; + max-height: 200px; +} + .rounded { border-radius: 8px; } diff --git a/src/containers/CommentsContainer/CommentsContainer.tsx b/src/containers/CommentsContainer/CommentsContainer.tsx index b520456..0553f31 100644 --- a/src/containers/CommentsContainer/CommentsContainer.tsx +++ b/src/containers/CommentsContainer/CommentsContainer.tsx @@ -2,7 +2,6 @@ import { ReactNode, useRef, useCallback, useEffect, useState } from 'react'; import { Avatar, - Button, Bullseye, Dropdown, DropdownItem, @@ -19,12 +18,13 @@ import { EmptyState, EmptyStateIcon, EmptyStateBody, + DropdownToggle, } from '@patternfly/react-core'; import { SortAmountDownIcon, CubesIcon } from '@patternfly/react-icons'; import { Controller, useForm } from 'react-hook-form'; import { CommentField } from 'components'; -import { useInfiniteScroll, useToggle } from 'hooks'; +import { useInfiniteScroll, useStateRef, useToggle } from 'hooks'; import { usePouchDB } from 'context'; import { CommentDoc, DesignDoc, GetList, IdeaDoc } from 'pouchDB/types'; import { onCommentChange } from './commentsContainer.helper'; @@ -45,7 +45,7 @@ export const CommentsContainer = ({ ideaDetails }: Props): JSX.Element => { const { isOpen, handleToggle } = useToggle(false); const [sortOrder, setSortOrder] = useState<0 | 1>(0); const { control, handleSubmit, reset } = useForm<FormData>(); - const [commentDoc, setCommentDoc] = useState<GetList<CommentDoc>>({ + const [commentDoc, setCommentDoc, commentDocRef] = useStateRef<GetList<CommentDoc>>({ hasNextPage: false, docs: [], }); @@ -75,13 +75,7 @@ export const CommentsContainer = ({ ideaDetails }: Props): JSX.Element => { ideaId, }, }) - .on('change', async function ({ doc }) { - // change.id contains the doc id, change.doc contains the doc - if (doc && doc?.type === 'comment') { - const newCommentList = await onCommentChange(doc, commentDoc.docs, comment); - setCommentDoc((comments) => ({ ...comments, docs: newCommentList })); - } - }) + .on('change', handleCommentFeedChange) .on('error', function (err) { console.error(err); window.OpNotification.warning({ @@ -90,7 +84,19 @@ export const CommentsContainer = ({ ideaDetails }: Props): JSX.Element => { }); }); return () => dbChanges.cancel(); - }, [comment, commentDoc.docs, ideaId, db]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ideaId]); + + const handleCommentFeedChange = useCallback( + async ({ doc }: PouchDB.Core.ChangesResponseChange<IdeaDoc | CommentDoc>) => { + if (doc && doc?.type === 'comment') { + const newCommentList = await onCommentChange(doc, commentDocRef.current.docs, comment); + setCommentDoc((comments) => ({ ...comments, docs: newCommentList })); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [commentDoc.docs] + ); useEffect(() => { window.OpAuthHelper.onLogin(() => handleFetchComments(sortOrder)); @@ -121,6 +127,7 @@ export const CommentsContainer = ({ ideaDetails }: Props): JSX.Element => { }); } }, + // eslint-disable-next-line react-hooks/exhaustive-deps [comment, ideaId] ); @@ -204,15 +211,14 @@ export const CommentsContainer = ({ ideaDetails }: Props): JSX.Element => { <Bullseye> <Dropdown toggle={ - <Button - variant="link" + <DropdownToggle className="pf-u-p-0 pf-u-color-400" - onClick={handleToggle} - icon={<SortAmountDownIcon />} - iconPosition="right" + onToggle={handleToggle} + toggleIndicator={SortAmountDownIcon} + isPlain > Sort by - </Button> + </DropdownToggle> } isOpen={isOpen} onSelect={handleToggle} diff --git a/src/context/PouchDB/PouchDBContext.tsx b/src/context/PouchDB/PouchDBContext.tsx index b7b13d1..2f1bcd3 100644 --- a/src/context/PouchDB/PouchDBContext.tsx +++ b/src/context/PouchDB/PouchDBContext.tsx @@ -1,20 +1,23 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { createContext, ReactNode, useContext, useEffect, useMemo, useRef } from 'react'; -import PouchDB from 'pouchdb'; +import PouchDB from 'pouchdb-browser'; import PouchDBFind from 'pouchdb-find'; import pouchdbDebug from 'pouchdb-debug'; +import pouchdbHTTPAdapter from 'pouchdb-adapter-http'; -import { pouchDBIndexCreator, POUCHDB_DB_NAME } from 'pouchDB/config'; +import { pouchDBIndexCreator, POUCHDB_DB_NAME, POUCHDB_DB_URL } from 'pouchDB/config'; import { IdeaModel } from 'pouchDB/api/idea'; import { TagModel } from 'pouchDB/api/tag'; import { PouchDBConsumer } from './types'; import { VoteModel } from 'pouchDB/api/vote'; import { pouchDBDesignDocCreator } from 'pouchDB/design'; import { CommentModel } from 'pouchDB/api/comments'; +import { DesignDoc } from 'pouchDB/types'; PouchDB.plugin(PouchDBFind); PouchDB.plugin(pouchdbDebug); +PouchDB.plugin(pouchdbHTTPAdapter); PouchDB.debug.enable('pouchdb:find'); const PouchDBContext = createContext<PouchDBConsumer | null>(null); @@ -23,16 +26,30 @@ interface Props { } export const PouchDBProvider = ({ children }: Props): JSX.Element => { - const db = useRef(new PouchDB(POUCHDB_DB_NAME)); + const db = useRef(new PouchDB(POUCHDB_DB_NAME, { auto_compaction: true })); + const remoteDb = useRef( + new PouchDB(POUCHDB_DB_URL, { + skip_setup: true, + auto_compaction: true, + fetch: function (url, opts) { + if (opts) { + (opts?.headers as any).set('Authorization', `Bearer ${window?.OpAuthHelper?.jwtToken}`); + opts.credentials = 'omit'; + } + return PouchDB.fetch(url, opts); + }, + } as PouchDB.Configuration.RemoteDatabaseConfiguration) + ); const idea = useRef(new IdeaModel(db.current)); const tag = useRef(new TagModel(db.current)); const vote = useRef(new VoteModel(db.current)); const comment = useRef(new CommentModel(db.current)); // to sync with couch database onMount useEffect(() => { - db.current.sync(process.env.COUCH_DB || '', { + db.current.sync(remoteDb.current, { retry: true, live: true, + filter: DesignDoc.ReplicationFilter, }); pouchDBIndexCreator(db.current); pouchDBDesignDocCreator(db.current); diff --git a/src/hooks/index.tsx b/src/hooks/index.tsx index e886b91..18a2968 100644 --- a/src/hooks/index.tsx +++ b/src/hooks/index.tsx @@ -4,3 +4,4 @@ export { useInfiniteScroll } from './useInfiniteScroll'; export { useFormSelect } from './useFormSelect'; export { useQuery } from './useQuery'; export { useDebounce } from './useDebounce'; +export { useStateRef } from './useStateRef'; diff --git a/src/hooks/useFormSelect/useFormSelect.tsx b/src/hooks/useFormSelect/useFormSelect.tsx index 3c8818e..792533d 100644 --- a/src/hooks/useFormSelect/useFormSelect.tsx +++ b/src/hooks/useFormSelect/useFormSelect.tsx @@ -36,8 +36,8 @@ export const useFormSelect = <T extends FieldValues>( // eslint-disable-next-line @typescript-eslint/no-explicit-any const selections = useMemo(() => data.fields.map((field: any) => field.name), [data.fields]); - const onToggle = useCallback((): void => { - setSelectIsOpen((state) => !state); + const onToggle = useCallback((isExpanded?: boolean): void => { + setSelectIsOpen(typeof isExpanded === 'boolean' ? isExpanded : (state) => !state); }, []); /** diff --git a/src/hooks/useStateRef/index.tsx b/src/hooks/useStateRef/index.tsx new file mode 100644 index 0000000..ff4e294 --- /dev/null +++ b/src/hooks/useStateRef/index.tsx @@ -0,0 +1 @@ +export { useStateRef } from './useStateRef'; diff --git a/src/hooks/useStateRef/useStateRef.tsx b/src/hooks/useStateRef/useStateRef.tsx new file mode 100644 index 0000000..f85698e --- /dev/null +++ b/src/hooks/useStateRef/useStateRef.tsx @@ -0,0 +1,15 @@ +import { useEffect, useRef, useState } from 'react'; + +export const useStateRef = <T extends unknown>( + initialValue: T +): [T, React.Dispatch<React.SetStateAction<T>>, React.MutableRefObject<T>] => { + const [value, setValue] = useState<T>(initialValue); + + const ref = useRef<T>(value); + + useEffect(() => { + ref.current = value; + }, [value]); + + return [value, setValue, ref]; +}; diff --git a/src/pages/HomePage/HomePage.tsx b/src/pages/HomePage/HomePage.tsx index 5a3b59a..81fbf55 100644 --- a/src/pages/HomePage/HomePage.tsx +++ b/src/pages/HomePage/HomePage.tsx @@ -18,7 +18,7 @@ import { } from '@patternfly/react-core'; import { CubesIcon } from '@patternfly/react-icons'; -import { useInfiniteScroll, usePopUp, useQuery } from 'hooks'; +import { useInfiniteScroll, usePopUp, useQuery, useStateRef } from 'hooks'; import { CreateIdeaDoc, DesignDoc, GetList, IdeaDoc, VoteDoc } from 'pouchDB/types'; import { usePouchDB } from 'context'; @@ -28,13 +28,17 @@ import { IdeaItem } from './components/IdeaItem'; import { IdeaCreateUpdateContainer } from './components/IdeaCreateUpdateContainer'; import styles from './homePage.module.scss'; import { onIdeaChange } from './homePage.helper'; -import { Filter, TabType } from './types'; +import { Filter, TabType, TagCount } from './types'; const DOCS_ON_EACH_LOAD = 20; export const HomePage = (): JSX.Element => { const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(['newIdea'] as const); - const [ideas, setIdeas] = useState<GetList<IdeaDoc>>({ hasNextPage: false, docs: [] }); + const [ideas, setIdeas, ideasRef] = useStateRef<GetList<IdeaDoc>>({ + hasNextPage: false, + docs: [], + }); + const [tagCount, setTagCount] = useState<TagCount>({ isLoading: true, data: [] }); const { idea, tag, vote, db } = usePouchDB(); const query = useQuery(); const useInfo = window?.OpAuthHelper?.getUserInfo(); @@ -72,15 +76,35 @@ export const HomePage = (): JSX.Element => { console.error(error); } }, + // eslint-disable-next-line react-hooks/exhaustive-deps [idea] ); + const handleFetchTags = useCallback(async () => { + try { + const { rows } = await tag.getTagCounts(); + rows.sort((a, b) => b.value - a.value).splice(10); + setTagCount({ isLoading: false, data: rows }); + } catch (error) { + setTagCount({ isLoading: false, data: [] }); + console.error(error); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + useEffect(() => { window.OpAuthHelper.onLogin(() => { handleFetchIdeaList(tab.tabName, { author: authorQuery, category: categoryQuery }); + handleFetchTags(); }); - }, [tab.tabName, handleFetchIdeaList, authorQuery, categoryQuery]); + }, [tab.tabName, handleFetchIdeaList, authorQuery, categoryQuery, handleFetchTags]); + /** + * Couchdb changes feed + * Why ref state -> To avoid registering back and forth event each time on state change + * change event is getting reflected on ideaList state which causes re-registration each time + * To avoid this and making it one time registration, I have used reference to avoid stale state + */ useEffect(() => { const dbChanges = db .changes<IdeaDoc | VoteDoc>({ @@ -92,13 +116,7 @@ export const HomePage = (): JSX.Element => { user: useInfo?.rhatUUID, }, }) - .on('change', async function ({ doc }) { - // change.id contains the doc id, change.doc contains the doc - if (doc && doc?.type === 'idea') { - const newIdeaList = await onIdeaChange(doc, ideas.docs, idea); - setIdeas((ideas) => ({ ...ideas, docs: newIdeaList })); - } - }) + .on('change', onIdeaChangeCB) .on('error', function (err) { console.error(err); window.OpNotification.warning({ @@ -107,7 +125,20 @@ export const HomePage = (): JSX.Element => { }); }); return () => dbChanges.cancel(); - }, [ideas.docs, idea, db, useInfo?.rhatUUID]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const onIdeaChangeCB = useCallback( + async ({ doc }: PouchDB.Core.ChangesResponseChange<IdeaDoc | VoteDoc>) => { + if (doc && doc?.type === 'idea') { + await handleFetchTags(); + const newIdeaList = await onIdeaChange(doc, ideasRef.current.docs, idea); + setIdeas((ideas) => ({ ...ideas, docs: newIdeaList })); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [ideas.docs] + ); const handleTabChange = useCallback((tabIndex: number) => { setTab(tabIndex === 1 ? { tabIndex, tabName: 'popular' } : { tabIndex, tabName: 'recent' }); @@ -189,7 +220,7 @@ export const HomePage = (): JSX.Element => { </Button> </FlexItem> <FlexItem> - <Categories /> + <Categories tags={tagCount} /> </FlexItem> </Flex> </GridItem> diff --git a/src/pages/HomePage/components/Categories/Categories.tsx b/src/pages/HomePage/components/Categories/Categories.tsx index e83a938..c12e7dd 100644 --- a/src/pages/HomePage/components/Categories/Categories.tsx +++ b/src/pages/HomePage/components/Categories/Categories.tsx @@ -1,26 +1,16 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { CSSProperties, useEffect, useState, useCallback } from 'react'; +import { CSSProperties, useCallback } from 'react'; import { createSearchParams, useNavigate } from 'react-router-dom'; import { Menu, MenuList, MenuGroup, MenuItem, Split, SplitItem } from '@patternfly/react-core'; import { css } from '@patternfly/react-styles'; import { useQuery } from 'hooks'; -import { usePouchDB } from 'context'; -import { TagDoc } from 'pouchDB/types'; +import { TagCount } from 'pages/HomePage/types'; -interface TagCount { - isLoading: boolean; - data: Array<{ - id: any; - key: any; - value: any; - doc?: PouchDB.Core.ExistingDocument<TagDoc & PouchDB.Core.AllDocsMeta>; - }>; +interface Props { + tags: TagCount; } -export const Categories = (): JSX.Element => { - const { tag } = usePouchDB(); - const [tagCount, setTagCount] = useState<TagCount>({ isLoading: true, data: [] }); +export const Categories = ({ tags }: Props): JSX.Element => { const userInfo = window?.OpAuthHelper?.getUserInfo(); // url manipulation hooks @@ -29,23 +19,6 @@ export const Categories = (): JSX.Element => { const author = query.get('author'); const category = query.get('category'); - useEffect(() => { - handleFetchTags(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const handleFetchTags = useCallback(async () => { - try { - const { rows } = await tag.getTagCounts(); - rows.sort((a, b) => b.value - a.value).splice(10); - setTagCount({ isLoading: false, data: rows }); - } catch (error) { - setTagCount({ isLoading: false, data: [] }); - console.error(error); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - const handleMenuClick = useCallback( (key: 'author' | 'category', value: string | null) => { const params: Record<string, string> = {}; @@ -85,7 +58,7 @@ export const Categories = (): JSX.Element => { </MenuGroup> <MenuGroup label="Categories"> <MenuList> - {tagCount.data.map(({ key, value }) => ( + {tags.data.map(({ key, value }) => ( <MenuItem key={key} onClick={() => handleMenuClick('category', category === key ? null : key)} diff --git a/src/pages/HomePage/components/IdeaCreateUpdateContainer/IdeaCreateUpdateContainer.tsx b/src/pages/HomePage/components/IdeaCreateUpdateContainer/IdeaCreateUpdateContainer.tsx index b1b2c42..cecc8a8 100644 --- a/src/pages/HomePage/components/IdeaCreateUpdateContainer/IdeaCreateUpdateContainer.tsx +++ b/src/pages/HomePage/components/IdeaCreateUpdateContainer/IdeaCreateUpdateContainer.tsx @@ -1,11 +1,10 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { Button, FormGroup, Stack, StackItem, TextInput, - Text, TextArea, Split, SplitItem, @@ -16,7 +15,7 @@ import { } from '@patternfly/react-core'; import { Controller, useForm } from 'react-hook-form'; -import { useFormSelect, useDebounce } from 'hooks'; +import { useFormSelect } from 'hooks'; import { usePouchDB } from 'context'; import { CreateNewIdea } from 'pages/HomePage/types'; import { CreateIdeaDoc, IdeaDoc } from 'pouchDB/types'; @@ -31,13 +30,15 @@ interface Props { updateDefaultValue?: IdeaDoc; } +type SearchTag = { isLoading: boolean; data: string[] }; + export const IdeaCreateUpdateContainer = ({ handleModalClose, handleCreateOrUpdateIdeaDoc, updateDefaultValue, }: Props): JSX.Element => { const { tag } = usePouchDB(); - const [searchedTags, setSearchTags] = useState<string[]>([]); + const [tagList, setTagList] = useState<SearchTag>({ isLoading: true, data: [] }); // form handling hooks const { handleSubmit, @@ -66,24 +67,24 @@ export const IdeaCreateUpdateContainer = ({ } }, [updateDefaultValue, reset]); - const handleSearchTag = useCallback( - async (value: string | null) => { - if (value) { - const tags = await tag.getTagList(value); - setSearchTags(tags.docs.map((el) => el._id)); - } else { - setSearchTags([]); - } - }, - [tag] - ); + useEffect(() => { + handleFetchTagList(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - const { setUnDebouncedState, isDebouncing } = useDebounce<string | null>(null, handleSearchTag); + const handleFetchTagList = async () => { + try { + const tags = await tag.getTagList(1000); + setTagList({ isLoading: false, data: tags.docs.map((el) => el._id) }); + } catch (error) { + setTagList({ isLoading: false, data: [] }); + window.OpNotification.danger({ subject: 'Fetching tag failed' }); + } + }; const onFormSubmit = async (data: CreateNewIdea) => { const tags = data.tags.map(({ name }) => name); await handleCreateOrUpdateIdeaDoc({ ...data, tags }, createdSelect, isUpdate); - return; }; @@ -91,19 +92,13 @@ export const IdeaCreateUpdateContainer = ({ <Form onSubmit={handleSubmit(onFormSubmit)}> <Stack hasGutter> <StackItem> - <FormGroup - fieldId="title" - label={ - <Text style={{ color: '#2121218A' }} className="pf-u-pb-sm"> - Give a title for your idea: - </Text> - } - > - <Controller - name="title" - control={control} - defaultValue="" - render={({ field }) => ( + <Controller + name="title" + control={control} + defaultValue="" + rules={{ required: true }} + render={({ field }) => ( + <FormGroup fieldId="title" isRequired label="Give a title for your idea:"> <TextInput id="title" aria-label="title" @@ -111,24 +106,18 @@ export const IdeaCreateUpdateContainer = ({ placeholder="What is your idea about?" {...field} /> - )} - /> - </FormGroup> + </FormGroup> + )} + /> </StackItem> <StackItem> - <FormGroup - fieldId="desc" - label={ - <Text style={{ color: '#2121218A' }} className="pf-u-pb-sm"> - Brief description - </Text> - } - > - <Controller - name="description" - control={control} - defaultValue="" - render={({ field }) => ( + <Controller + name="description" + control={control} + defaultValue="" + rules={{ required: true }} + render={({ field }) => ( + <FormGroup fieldId="desc" isRequired label=" Brief description"> <TextArea isRequired id="description" @@ -137,19 +126,12 @@ export const IdeaCreateUpdateContainer = ({ rows={10} {...field} /> - )} - /> - </FormGroup> + </FormGroup> + )} + /> </StackItem> <StackItem> - <FormGroup - fieldId="title" - label={ - <Text style={{ color: '#2121218A' }} className="pf-u-pb-sm"> - Add some tags to your idea so others can find it: - </Text> - } - > + <FormGroup fieldId="title" label=" Add some tags to your idea so others can find it:"> <Select chipGroupProps={{ numChips: 3, @@ -163,16 +145,13 @@ export const IdeaCreateUpdateContainer = ({ isCreatable onCreateOption={(newOptionValue) => onCreate({ name: newOptionValue }, 'name')} onClear={onClear} - onTypeaheadInputChanged={(value) => { - setUnDebouncedState(value); - }} selections={selections} isOpen={selectIsOpen} - loadingVariant={isDebouncing ? 'spinner' : undefined} + loadingVariant={tagList.isLoading ? 'spinner' : undefined} aria-labelledby="tags for an idea" placeholderText="Select a tag" > - {searchedTags.map((tag) => ( + {tagList.data.map((tag) => ( <SelectOption value={tag} key={tag} /> ))} </Select> diff --git a/src/pages/HomePage/components/MenuTab/MenuTab.tsx b/src/pages/HomePage/components/MenuTab/MenuTab.tsx index a9b8504..f0dab1c 100644 --- a/src/pages/HomePage/components/MenuTab/MenuTab.tsx +++ b/src/pages/HomePage/components/MenuTab/MenuTab.tsx @@ -24,6 +24,8 @@ export const MenuTab = ({ handleTabChange, tab }: Props): JSX.Element => { <TextInput placeholder="Looking for an idea!" type="search" + aria-label="Search ideas" + id="search-bar" iconVariant="search" style={{ borderColor: '#EEEEEE' }} /> diff --git a/src/pages/HomePage/types.ts b/src/pages/HomePage/types.ts index 47286e7..bad47b0 100644 --- a/src/pages/HomePage/types.ts +++ b/src/pages/HomePage/types.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { TagDoc } from 'pouchDB/types'; + export interface CreateNewIdea { title: string; description: string; @@ -15,3 +18,13 @@ export interface Filter { author: string | null; category: string; } + +export interface TagCount { + isLoading: boolean; + data: Array<{ + id: any; + key: any; + value: any; + doc?: PouchDB.Core.ExistingDocument<TagDoc & PouchDB.Core.AllDocsMeta>; + }>; +} diff --git a/src/pages/IdeaDetailPage/IdeaDetailPage.tsx b/src/pages/IdeaDetailPage/IdeaDetailPage.tsx index 07e9d99..f3ee4ce 100644 --- a/src/pages/IdeaDetailPage/IdeaDetailPage.tsx +++ b/src/pages/IdeaDetailPage/IdeaDetailPage.tsx @@ -62,7 +62,8 @@ export const IdeaDetailPage = (): JSX.Element => { }); }); return () => dbChanges.cancel(); - }, [id, db, idea]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [id]); const handleIdeaDetailsFetch = useCallback(async () => { try { @@ -89,8 +90,7 @@ export const IdeaDetailPage = (): JSX.Element => { const handleVoteClick = useCallback( async (hasVoted: boolean, ideaId: string) => { try { - const res = hasVoted ? await vote.deleteVote(ideaId) : await vote.createVote(ideaId); - console.log({ res }); + hasVoted ? await vote.deleteVote(ideaId) : await vote.createVote(ideaId); } catch (error) { console.error(error); window.OpNotification.danger({ diff --git a/src/pouchDB/api/idea.ts b/src/pouchDB/api/idea.ts index 7683e9b..bebb348 100644 --- a/src/pouchDB/api/idea.ts +++ b/src/pouchDB/api/idea.ts @@ -151,7 +151,6 @@ export class IdeaModel { if (ideaDetails.docs.length !== 1) { return; } - console.log({ ideaDetails }); return this.formatIdea(ideaDetails.docs[0]); } diff --git a/src/pouchDB/api/tag.ts b/src/pouchDB/api/tag.ts index d7462b6..219892f 100644 --- a/src/pouchDB/api/tag.ts +++ b/src/pouchDB/api/tag.ts @@ -1,4 +1,4 @@ -import { DesignDoc, TagDoc } from 'pouchDB/types'; +import { DesignDoc, TagDoc } from '../types'; export class TagModel { _db: PouchDB.Database; @@ -6,10 +6,7 @@ export class TagModel { this._db = db; } - async getTagList( - search: string, - limit = 10 - ): Promise<PouchDB.Find.FindResponse<Record<string, unknown>>> { + async getTagList(limit = 10): Promise<PouchDB.Find.FindResponse<Record<string, unknown>>> { return this._db.find({ selector: { type: 'tag', @@ -25,7 +22,7 @@ export class TagModel { async createNewTags(tags: string[]): Promise<(PouchDB.Core.Response | PouchDB.Core.Error)[]> { const timestamp = new Date().getTime(); const tagsToBeCreated = tags.map((tagName) => ({ - _id: tagName, + _id: tagName.toLowerCase(), createdAt: timestamp, type: 'tag', })); diff --git a/src/pouchDB/config.tsx b/src/pouchDB/config.tsx index eb49ad1..68aef76 100644 --- a/src/pouchDB/config.tsx +++ b/src/pouchDB/config.tsx @@ -1,4 +1,5 @@ -export const POUCHDB_DB_NAME = process.env.POUCHDB_DB_NAME || 'idea-hub'; +export const POUCHDB_DB_NAME = process.env.REACT_APP_POUCH_DB_NAME || 'ideahub'; +export const POUCHDB_DB_URL = `${process.env.REACT_APP_POUCH_DB_URL}/${POUCHDB_DB_NAME}`; const indexDetails = [ { @@ -26,9 +27,11 @@ const indexDetails = [ export const pouchDBIndexCreator = async (db: PouchDB.Database): Promise<void> => { try { - indexDetails.map(async (el) => { - await db.createIndex(el); - }); + await Promise.all( + indexDetails.map(async (el) => { + await db.createIndex(el); + }) + ); } catch (error) { console.error(error); } diff --git a/src/pouchDB/design.ts b/src/pouchDB/design.ts index 2c53938..920a585 100644 --- a/src/pouchDB/design.ts +++ b/src/pouchDB/design.ts @@ -1,4 +1,6 @@ -const designDocuments: PouchDB.Core.PutDocument<{ version: number }>[] = [ +import { DesignDocument } from './types'; + +const designDocuments: PouchDB.Core.PutDocument<DesignDocument>[] = [ { _id: '_design/votes', views: { @@ -11,6 +13,33 @@ const designDocuments: PouchDB.Core.PutDocument<{ version: number }>[] = [ reduce: `_count`, }, }, + validate_doc_update: `function (newDoc, oldDoc, userCtx, secObj) { + if (newDoc.type === 'idea' || newDoc.type === 'comment') { + if (userCtx.roles.indexOf(newDoc.authorId) !== -1) { + return; + } else { + throw { forbidden: 'Unauthorized access' }; + } + } + if (newDoc.type === 'like' || newDoc.type === 'vote') { + var idSplittedByColor = newDoc._id.split(':'); + var rhatUUID = idSplittedByColor[idSplittedByColor.length - 1]; + + if (userCtx.roles.indexOf(rhatUUID) !== -1) { + return; + } else { + throw { forbidden: 'Unauthorized access' }; + } + } + + if(newDoc.type === 'tag' && newDoc._deleted === true){ + if (userCtx.roles.indexOf('_admin') !== -1) { + return; + } else { + throw { forbidden: 'Unauthorized access' }; + } + } + }`, version: 0.1, }, { @@ -74,28 +103,31 @@ const designDocuments: PouchDB.Core.PutDocument<{ version: number }>[] = [ } return false; }`, + replication: `function (doc, req) { + return doc._id.indexOf('_design') !== 0; + }`, }, version: 0.1, }, ]; export const pouchDBDesignDocCreator = async (db: PouchDB.Database): Promise<void> => { - designDocuments.map(async (designDocument) => { - try { - const res = await db.put(designDocument); - console.log(res); - } catch (error) { - if (error.status === 409) { - const existingOneId = error.id; - const exisitingDesignDoc = await db.get<{ version: number }>(existingOneId); - if (exisitingDesignDoc?.version < designDocument.version) { - designDocument._rev = exisitingDesignDoc._rev; - const res = await db.put(designDocument); - console.log(res); + await Promise.all( + designDocuments.map(async (designDocument) => { + try { + await db.put(designDocument); + } catch (error) { + if (error.status === 409) { + const existingOneId = error.docId; + const exisitingDesignDoc = await db.get<{ version: number }>(existingOneId); + if (exisitingDesignDoc?.version < designDocument.version) { + designDocument._rev = exisitingDesignDoc._rev; + await db.put(designDocument); + } + } else { + console.error(error); } - } else { - throw new Error(error); } - } - }); + }) + ); }; diff --git a/src/pouchDB/types.ts b/src/pouchDB/types.ts index 3c77b1c..cffa025 100644 --- a/src/pouchDB/types.ts +++ b/src/pouchDB/types.ts @@ -5,6 +5,7 @@ export enum DesignDoc { CountOfTagsUsed = 'tags/tags-count', HomePageFilter = 'filters/homepage', IdeaDetailPageFilter = 'filters/ideaDetailPage', + ReplicationFilter = 'filters/replication', } export enum IndexDoc { @@ -13,6 +14,11 @@ export enum IndexDoc { SortedByTypeIdeaidVotes = 'type-ideaId-votes-index', } +export interface DesignDocument { + version: number; + validate_doc_update?: string; +} + interface Updation { _rev: string; } @@ -40,7 +46,7 @@ export interface IdeaDoc extends CreateIdeaDoc, Updation { ideaId: string; } -// _id: comments/<idea_id>/<timestamp> +// _id: comments:<idea_id>:<timestamp> export interface CreateIdeaCommentDoc { _id: string; type: 'comment'; @@ -61,7 +67,7 @@ export interface TagDoc extends CreateTagDoc, Updation { createdAt: number; } -//_id likes/<idea_id>/comments/timestamp/<user-id> +//_id likes:<idea_id>:comments:timestamp:<user-id> export interface CreateLikeDoc { type: 'like'; commentId: string; @@ -71,7 +77,7 @@ export interface LikeDoc extends CreateLikeDoc, Updation { createdAt: number; } -//_id votes/<idea_id>/<user-id> +//_id votes:<idea_id>:<user-id> export interface CreateVoteDoc { type: 'vote'; ideaId: string;