feature: add multiselect component and improve adding users to project

This commit is contained in:
Scott Wilson
2024-10-09 09:58:00 -07:00
parent 56798f09bf
commit 4aad36601c
8 changed files with 396 additions and 214 deletions
@@ -21,7 +21,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
secretVersioning: true, secretVersioning: true,
pitRecovery: false, pitRecovery: false,
ipAllowlisting: false, ipAllowlisting: false,
rbac: false, rbac: true,
customRateLimits: false, customRateLimits: false,
customAlerts: false, customAlerts: false,
auditLogs: false, auditLogs: false,
+3 -1
View File
@@ -108,7 +108,9 @@ export const orgDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.Users).as("userId"), db.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey) db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
) )
.where({ isGhost: false }); // MAKE SURE USER IS NOT A GHOST USER .where({ isGhost: false }) // MAKE SURE USER IS NOT A GHOST USER
.orderBy("firstName")
.orderBy("lastName");
return members.map(({ email, isEmailVerified, username, firstName, lastName, userId, publicKey, ...data }) => ({ return members.map(({ email, isEmailVerified, username, firstName, lastName, userId, publicKey, ...data }) => ({
...data, ...data,
+168 -41
View File
@@ -1,5 +1,5 @@
{ {
"name": "relock-npm-lock-v2-SvMQeF", "name": "frontend",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
@@ -88,6 +88,7 @@
"react-mailchimp-subscribe": "^2.1.3", "react-mailchimp-subscribe": "^2.1.3",
"react-markdown": "^8.0.3", "react-markdown": "^8.0.3",
"react-redux": "^8.0.2", "react-redux": "^8.0.2",
"react-select": "^5.8.1",
"react-table": "^7.8.0", "react-table": "^7.8.0",
"react-toastify": "^9.1.3", "react-toastify": "^9.1.3",
"sanitize-html": "^2.12.1", "sanitize-html": "^2.12.1",
@@ -2505,15 +2506,16 @@
} }
}, },
"node_modules/@emotion/babel-plugin": { "node_modules/@emotion/babel-plugin": {
"version": "11.11.0", "version": "11.12.0",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz",
"integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", "integrity": "sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-module-imports": "^7.16.7", "@babel/helper-module-imports": "^7.16.7",
"@babel/runtime": "^7.18.3", "@babel/runtime": "^7.18.3",
"@emotion/hash": "^0.9.1", "@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.8.1", "@emotion/memoize": "^0.9.0",
"@emotion/serialize": "^1.1.2", "@emotion/serialize": "^1.2.0",
"babel-plugin-macros": "^3.1.0", "babel-plugin-macros": "^3.1.0",
"convert-source-map": "^1.5.0", "convert-source-map": "^1.5.0",
"escape-string-regexp": "^4.0.0", "escape-string-regexp": "^4.0.0",
@@ -2522,18 +2524,31 @@
"stylis": "4.2.0" "stylis": "4.2.0"
} }
}, },
"node_modules/@emotion/babel-plugin/node_modules/@emotion/memoize": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
"license": "MIT"
},
"node_modules/@emotion/cache": { "node_modules/@emotion/cache": {
"version": "11.11.0", "version": "11.13.1",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz",
"integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", "integrity": "sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@emotion/memoize": "^0.8.1", "@emotion/memoize": "^0.9.0",
"@emotion/sheet": "^1.2.2", "@emotion/sheet": "^1.4.0",
"@emotion/utils": "^1.2.1", "@emotion/utils": "^1.4.0",
"@emotion/weak-memoize": "^0.3.1", "@emotion/weak-memoize": "^0.4.0",
"stylis": "4.2.0" "stylis": "4.2.0"
} }
}, },
"node_modules/@emotion/cache/node_modules/@emotion/memoize": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
"license": "MIT"
},
"node_modules/@emotion/css": { "node_modules/@emotion/css": {
"version": "11.11.2", "version": "11.11.2",
"resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.11.2.tgz", "resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.11.2.tgz",
@@ -2547,9 +2562,10 @@
} }
}, },
"node_modules/@emotion/hash": { "node_modules/@emotion/hash": {
"version": "0.9.1", "version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
"integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
"license": "MIT"
}, },
"node_modules/@emotion/is-prop-valid": { "node_modules/@emotion/is-prop-valid": {
"version": "0.8.8", "version": "0.8.8",
@@ -2571,18 +2587,49 @@
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
"integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="
}, },
"node_modules/@emotion/serialize": { "node_modules/@emotion/react": {
"version": "1.1.3", "version": "11.13.3",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.3.tgz", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.3.tgz",
"integrity": "sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA==", "integrity": "sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@emotion/hash": "^0.9.1", "@babel/runtime": "^7.18.3",
"@emotion/memoize": "^0.8.1", "@emotion/babel-plugin": "^11.12.0",
"@emotion/unitless": "^0.8.1", "@emotion/cache": "^11.13.0",
"@emotion/utils": "^1.2.1", "@emotion/serialize": "^1.3.1",
"@emotion/use-insertion-effect-with-fallbacks": "^1.1.0",
"@emotion/utils": "^1.4.0",
"@emotion/weak-memoize": "^0.4.0",
"hoist-non-react-statics": "^3.3.1"
},
"peerDependencies": {
"react": ">=16.8.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@emotion/serialize": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.2.tgz",
"integrity": "sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA==",
"license": "MIT",
"dependencies": {
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/unitless": "^0.10.0",
"@emotion/utils": "^1.4.1",
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
}, },
"node_modules/@emotion/serialize/node_modules/@emotion/memoize": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
"license": "MIT"
},
"node_modules/@emotion/server": { "node_modules/@emotion/server": {
"version": "11.11.0", "version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/server/-/server-11.11.0.tgz", "resolved": "https://registry.npmjs.org/@emotion/server/-/server-11.11.0.tgz",
@@ -2603,9 +2650,10 @@
} }
}, },
"node_modules/@emotion/sheet": { "node_modules/@emotion/sheet": {
"version": "1.2.2", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
"integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
"license": "MIT"
}, },
"node_modules/@emotion/stylis": { "node_modules/@emotion/stylis": {
"version": "0.8.5", "version": "0.8.5",
@@ -2613,28 +2661,31 @@
"integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ=="
}, },
"node_modules/@emotion/unitless": { "node_modules/@emotion/unitless": {
"version": "0.8.1", "version": "0.10.0",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
"integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==",
"license": "MIT"
}, },
"node_modules/@emotion/use-insertion-effect-with-fallbacks": { "node_modules/@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.0.1", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz",
"integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", "integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==",
"dev": true, "license": "MIT",
"peerDependencies": { "peerDependencies": {
"react": ">=16.8.0" "react": ">=16.8.0"
} }
}, },
"node_modules/@emotion/utils": { "node_modules/@emotion/utils": {
"version": "1.2.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.1.tgz",
"integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" "integrity": "sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA==",
"license": "MIT"
}, },
"node_modules/@emotion/weak-memoize": { "node_modules/@emotion/weak-memoize": {
"version": "0.3.1", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
"integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
"license": "MIT"
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.18.20", "version": "0.18.20",
@@ -8854,6 +8905,15 @@
"redux": "^4.0.0" "redux": "^4.0.0"
} }
}, },
"node_modules/@types/react-transition-group": {
"version": "4.4.11",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz",
"integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==",
"license": "MIT",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/resolve": { "node_modules/@types/resolve": {
"version": "1.20.6", "version": "1.20.6",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz",
@@ -12678,6 +12738,16 @@
"utila": "~0.4" "utila": "~0.4"
} }
}, },
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dom-serializer": { "node_modules/dom-serializer": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
@@ -21117,6 +21187,33 @@
"react": ">= 16.3" "react": ">= 16.3"
} }
}, },
"node_modules/react-select": {
"version": "5.8.1",
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.1.tgz",
"integrity": "sha512-RT1CJmuc+ejqm5MPgzyZujqDskdvB9a9ZqrdnVLsvAHjJ3Tj0hELnLeVPQlmYdVKCdCpxanepl6z7R5KhXhWzg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.0",
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.8.1",
"@floating-ui/dom": "^1.0.1",
"@types/react-transition-group": "^4.4.0",
"memoize-one": "^6.0.0",
"prop-types": "^15.6.0",
"react-transition-group": "^4.3.0",
"use-isomorphic-layout-effect": "^1.1.2"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-select/node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/react-style-singleton": { "node_modules/react-style-singleton": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
@@ -21171,6 +21268,22 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"license": "BSD-3-Clause",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/read-cache": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -24246,6 +24359,20 @@
} }
} }
}, },
"node_modules/use-isomorphic-layout-effect": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
"integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/use-memo-one": { "node_modules/use-memo-one": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz",
+1
View File
@@ -96,6 +96,7 @@
"react-mailchimp-subscribe": "^2.1.3", "react-mailchimp-subscribe": "^2.1.3",
"react-markdown": "^8.0.3", "react-markdown": "^8.0.3",
"react-redux": "^8.0.2", "react-redux": "^8.0.2",
"react-select": "^5.8.1",
"react-table": "^7.8.0", "react-table": "^7.8.0",
"react-toastify": "^9.1.3", "react-toastify": "^9.1.3",
"sanitize-html": "^2.12.1", "sanitize-html": "^2.12.1",
@@ -0,0 +1,104 @@
import Select, {
ClearIndicatorProps,
components,
DropdownIndicatorProps,
MultiValueRemoveProps,
OptionProps,
Props
} from "react-select";
import { faCheckCircle, faCircleXmark } from "@fortawesome/free-regular-svg-icons";
import { faChevronDown, faX } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
const DropdownIndicator = (props: DropdownIndicatorProps) => {
return (
<components.DropdownIndicator {...props}>
<FontAwesomeIcon icon={faChevronDown} size="xs" />
</components.DropdownIndicator>
);
};
const ClearIndicator = (props: ClearIndicatorProps) => {
return (
<components.ClearIndicator {...props}>
<FontAwesomeIcon icon={faCircleXmark} />
</components.ClearIndicator>
);
};
const MultiValueRemove = (props: MultiValueRemoveProps) => {
return (
<components.MultiValueRemove {...props}>
<FontAwesomeIcon icon={faX} size="xs" />
</components.MultiValueRemove>
);
};
const Option = ({ isSelected, children, ...props }: OptionProps) => {
return (
<components.Option isSelected={isSelected} {...props}>
{children}
{isSelected && (
<FontAwesomeIcon className="ml-2 text-primary" icon={faCheckCircle} size="sm" />
)}
</components.Option>
);
};
export const MultiSelect = (props: Props) => (
<Select
isMulti
closeMenuOnSelect={false}
hideSelectedOptions={false}
unstyled
styles={{
input: (base) => ({
...base,
"input:focus": {
boxShadow: "none"
}
}),
multiValueLabel: (base) => ({
...base,
whiteSpace: "normal",
overflow: "visible"
}),
control: (base) => ({
...base,
transition: "none"
})
}}
components={{ DropdownIndicator, ClearIndicator, MultiValueRemove, Option }}
classNames={{
container: () => "w-full font-inter",
control: ({ isFocused }) =>
twMerge(
isFocused ? "border-primary-400/50" : "border-mineshaft-600 hover:border-gray-400",
"border w-full p-0.5 rounded-md font-inter bg-mineshaft-900 hover:cursor-pointer"
),
placeholder: () => "text-mineshaft-400 text-sm pl-1 py-0.5",
input: () => "pl-1 py-0.5",
valueContainer: () => "p-1 max-h-[14rem] !overflow-y-scroll gap-1",
singleValue: () => "leading-7 ml-1",
multiValue: () => "bg-mineshaft-600 rounded items-center py-0.5 px-2 gap-1.5",
multiValueLabel: () => "leading-6 text-sm",
multiValueRemove: () => "hover:text-red text-bunker-400",
indicatorsContainer: () => "p-1 gap-1",
clearIndicator: () => "p-1 hover:text-red text-bunker-400",
indicatorSeparator: () => "bg-bunker-400",
dropdownIndicator: () => "text-bunker-200 p-1",
menu: () =>
"mt-2 border text-sm text-mineshaft-200 bg-mineshaft-900 border-mineshaft-600 rounded-md",
groupHeading: () => "ml-3 mt-2 mb-1 text-mineshaft-400 text-sm",
option: ({ isFocused, isSelected }) =>
twMerge(
isFocused && "bg-mineshaft-700 active:bg-mineshaft-600",
isSelected && "text-mineshaft-400",
"hover:cursor-pointer text-xs px-3 py-2"
),
noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md"
}}
{...props}
/>
);
@@ -0,0 +1 @@
export * from "./MultiSelect";
+1
View File
@@ -18,6 +18,7 @@ export * from "./IconButton";
export * from "./Input"; export * from "./Input";
export * from "./Menu"; export * from "./Menu";
export * from "./Modal"; export * from "./Modal";
export * from "./MultiSelect";
export * from "./NoticeBanner"; export * from "./NoticeBanner";
export * from "./Pagination"; export * from "./Pagination";
export * from "./Popoverv2"; export * from "./Popoverv2";
@@ -17,7 +17,8 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
FormControl, FormControl,
Modal, Modal,
ModalContent ModalContent,
MultiSelect
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization, useWorkspace } from "@app/context"; import { useOrganization, useWorkspace } from "@app/context";
import { import {
@@ -31,7 +32,7 @@ import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const addMemberFormSchema = z.object({ const addMemberFormSchema = z.object({
orgMembershipIds: z.array(z.string().trim()).min(1), orgMemberships: z.array(z.object({ label: z.string().trim(), value: z.string().trim() })).min(1),
projectRoleSlugs: z.array(z.string().trim().min(1)).min(1) projectRoleSlugs: z.array(z.string().trim().min(1)).min(1)
}); });
@@ -60,20 +61,20 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
handleSubmit, handleSubmit,
reset, reset,
watch, watch,
formState: { isSubmitting } formState: { isSubmitting, errors }
} = useForm<TAddMemberForm>({ } = useForm<TAddMemberForm>({
resolver: zodResolver(addMemberFormSchema), resolver: zodResolver(addMemberFormSchema),
defaultValues: { orgMembershipIds: [], projectRoleSlugs: [ProjectMembershipRole.Member] } defaultValues: { orgMemberships: [], projectRoleSlugs: [ProjectMembershipRole.Member] }
}); });
const { mutateAsync: addMembersToProject } = useAddUsersToOrg(); const { mutateAsync: addMembersToProject } = useAddUsersToOrg();
const onAddMember = async ({ orgMembershipIds, projectRoleSlugs }: TAddMemberForm) => { const onAddMembers = async ({ orgMemberships, projectRoleSlugs }: TAddMemberForm) => {
if (!currentWorkspace) return; if (!currentWorkspace) return;
if (!currentOrg?.id) return; if (!currentOrg?.id) return;
const selectedMembers = orgMembershipIds.map((orgMembershipId) => const selectedMembers = orgMemberships.map((orgMembership) =>
orgUsers?.find((orgUser) => orgUser.id === orgMembershipId) orgUsers?.find((orgUser) => orgUser.id === orgMembership.value)
); );
if (!selectedMembers) return; if (!selectedMembers) return;
@@ -118,10 +119,15 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
members?.forEach((member) => { members?.forEach((member) => {
wsUserUsernames.set(member.user.username, true); wsUserUsernames.set(member.user.username, true);
}); });
return (orgUsers || []).filter(({ user: u }) => !wsUserUsernames.has(u.username)); return (orgUsers || [])
.filter(({ user: u }) => !wsUserUsernames.has(u.username))
.map((member) => ({
value: member.id,
label: `${member.user.firstName} ${member.user.lastName}`
}));
}, [orgUsers, members]); }, [orgUsers, members]);
const selectedOrgMembershipIds = watch("orgMembershipIds"); const selectedOrgMemberships = watch("orgMemberships");
const selectedRoleSlugs = watch("projectRoleSlugs"); const selectedRoleSlugs = watch("projectRoleSlugs");
return ( return (
@@ -130,175 +136,115 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
onOpenChange={(isOpen) => handlePopUpToggle("addMember", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("addMember", isOpen)}
> >
<ModalContent <ModalContent
bodyClassName="overflow-visible"
title={t("section.members.add-dialog.add-member-to-project") as string} title={t("section.members.add-dialog.add-member-to-project") as string}
subTitle={t("section.members.add-dialog.user-will-email")} subTitle={t("section.members.add-dialog.user-will-email")}
> >
{filteredOrgUsers.length ? ( {filteredOrgUsers.length ? (
<form onSubmit={handleSubmit(onAddMember)}> <form onSubmit={handleSubmit(onAddMembers)}>
<div className="flex w-full items-center gap-2"> <div className="flex w-full flex-col items-start gap-2">
<div className="w-full"> <Controller
<Controller control={control}
control={control} name="orgMemberships"
name="orgMembershipIds" render={({ field }) => (
render={({ field }) => ( <FormControl
<FormControl label="Invite users to project"> className="w-full"
<DropdownMenu> isError={!!errors.orgMemberships?.length}
<DropdownMenuTrigger asChild> errorText={errors.orgMemberships?.[0]?.message}
{filteredOrgUsers && filteredOrgUsers.length > 0 ? ( label="Invite users to project"
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200"> >
{/* eslint-disable-next-line no-nested-ternary */} <MultiSelect
{selectedOrgMembershipIds.length === 1 className="w-full"
? filteredOrgUsers.find( placeholder="Add one or more users..."
(orgUser) => orgUser.id === selectedOrgMembershipIds[0] isMulti
)?.user.username name="members"
: selectedOrgMembershipIds.length === 0 options={filteredOrgUsers}
? "No users selected" value={field.value}
: `${selectedOrgMembershipIds.length} users selected`} onChange={field.onChange}
<FontAwesomeIcon icon={faChevronDown} className="text-xs" /> />
</div> </FormControl>
) : ( )}
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200"> />
No users found
</div>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="thin-scrollbar z-[100] max-h-80"
>
{filteredOrgUsers && filteredOrgUsers.length > 0 ? (
filteredOrgUsers.map((member) => {
const isSelected = selectedOrgMembershipIds.includes(member.id);
return ( <Controller
<DropdownMenuItem control={control}
onSelect={(event) => name="projectRoleSlugs"
filteredOrgUsers.length > 1 && event.preventDefault() render={({ field }) => (
} <FormControl
onClick={() => { className="w-full"
if (selectedOrgMembershipIds.includes(String(member.id))) { label="Select roles"
field.onChange( tooltipText="Select the roles that you wish to assign to the users"
selectedOrgMembershipIds.filter( >
(membershipId: string) => <DropdownMenu>
membershipId !== String(member.id) <DropdownMenuTrigger asChild>
) {roles && roles.length > 0 ? (
); <div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
} else { {/* eslint-disable-next-line no-nested-ternary */}
field.onChange([ {selectedRoleSlugs.length === 1
...selectedOrgMembershipIds, ? roles.find((role) => role.slug === selectedRoleSlugs[0])?.name
String(member.id) : selectedRoleSlugs.length === 0
]); ? "Select at least one role"
} : `${selectedRoleSlugs.length} roles selected`}
}} <FontAwesomeIcon
key={`membership-id-${member.id}`} icon={faChevronDown}
icon={ className={twMerge("ml-2 text-xs")}
isSelected ? ( />
<FontAwesomeIcon </div>
icon={faCheckCircle} ) : (
className="pr-0.5 text-primary" <div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
/> No roles found
) : ( </div>
<div className="pl-[1.01rem]" /> )}
) </DropdownMenuTrigger>
} <DropdownMenuContent
iconPos="left" align="start"
className="w-[28.4rem] text-sm" className="thin-scrollbar z-[100] max-h-80"
> >
{member.user.username} {roles && roles.length > 0 ? (
</DropdownMenuItem> roles.map((role) => {
); const isSelected = selectedRoleSlugs.includes(role.slug);
})
) : (
<div />
)}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
</div>
<div className="flex min-w-fit justify-end"> return (
<Controller <DropdownMenuItem
control={control} onSelect={(event) => roles.length > 1 && event.preventDefault()}
name="projectRoleSlugs" onClick={() => {
render={({ field }) => ( if (selectedRoleSlugs.includes(String(role.slug))) {
<FormControl field.onChange(
label="Select roles" selectedRoleSlugs.filter(
tooltipText="Select the roles that you wish to assign to the users" (roleSlug: string) => roleSlug !== String(role.slug)
> )
<DropdownMenu> );
<DropdownMenuTrigger asChild> } else {
{roles && roles.length > 0 ? ( field.onChange([...selectedRoleSlugs, role.slug]);
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
{/* eslint-disable-next-line no-nested-ternary */}
{selectedRoleSlugs.length === 1
? roles.find((role) => role.slug === selectedRoleSlugs[0])?.name
: selectedRoleSlugs.length === 0
? "Select at least one role"
: `${selectedRoleSlugs.length} roles selected`}
<FontAwesomeIcon
icon={faChevronDown}
className={twMerge("ml-2 text-xs")}
/>
</div>
) : (
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
No roles found
</div>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="thin-scrollbar z-[100] max-h-80"
>
{roles && roles.length > 0 ? (
roles.map((role) => {
const isSelected = selectedRoleSlugs.includes(role.slug);
return (
<DropdownMenuItem
onSelect={(event) => roles.length > 1 && event.preventDefault()}
onClick={() => {
if (selectedRoleSlugs.includes(String(role.slug))) {
field.onChange(
selectedRoleSlugs.filter(
(roleSlug: string) => roleSlug !== String(role.slug)
)
);
} else {
field.onChange([...selectedRoleSlugs, role.slug]);
}
}}
key={`role-slug-${role.slug}`}
icon={
isSelected ? (
<FontAwesomeIcon
icon={faCheckCircle}
className="pr-0.5 text-primary"
/>
) : (
<div className="pl-[1.01rem]" />
)
} }
iconPos="left" }}
className="w-[28.4rem] text-sm" key={`role-slug-${role.slug}`}
> icon={
{role.name} isSelected ? (
</DropdownMenuItem> <FontAwesomeIcon
); icon={faCheckCircle}
}) className="pr-0.5 text-primary"
) : ( />
<div /> ) : (
)} <div className="pl-[1.01rem]" />
</DropdownMenuContent> )
</DropdownMenu> }
</FormControl> iconPos="left"
)} className="w-[28.4rem] text-sm"
/> >
</div> {role.name}
</DropdownMenuItem>
);
})
) : (
<div />
)}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
</div> </div>
<div className="mt-8 flex items-center"> <div className="mt-8 flex items-center">
<Button <Button
className="mr-4" className="mr-4"
@@ -307,7 +253,7 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
isLoading={isSubmitting} isLoading={isSubmitting}
isDisabled={ isDisabled={
isSubmitting || isSubmitting ||
selectedOrgMembershipIds.length === 0 || selectedOrgMemberships.length === 0 ||
selectedRoleSlugs.length === 0 selectedRoleSlugs.length === 0
} }
> >