Edit.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. <template>
  2. <SmartModal v-if="show" :title="$t('team.edit')" @close="hideModal">
  3. <template #body>
  4. <div class="flex flex-col px-2">
  5. <div class="flex relative">
  6. <input
  7. id="selectLabelTeamEdit"
  8. v-model="name"
  9. v-focus
  10. class="input floating-input"
  11. placeholder=" "
  12. type="text"
  13. autocomplete="off"
  14. @keyup.enter="saveTeam"
  15. />
  16. <label for="selectLabelTeamEdit">
  17. {{ $t("action.label") }}
  18. </label>
  19. </div>
  20. <div class="flex flex-1 justify-between items-center">
  21. <label for="memberList" class="p-4">
  22. {{ $t("team.members") }}
  23. </label>
  24. <div class="flex">
  25. <ButtonSecondary
  26. svg="plus"
  27. :label="$t('add.new')"
  28. @click.native="addTeamMember"
  29. />
  30. </div>
  31. </div>
  32. <div class="divide-y divide-dividerLight border-divider border rounded">
  33. <div
  34. v-for="(member, index) in members"
  35. :key="`member-${index}`"
  36. class="divide-x divide-dividerLight flex"
  37. >
  38. <input
  39. class="bg-transparent flex flex-1 py-2 px-4"
  40. :placeholder="$t('team.email')"
  41. :name="'param' + index"
  42. :value="member.user.email"
  43. readonly
  44. />
  45. <span>
  46. <tippy
  47. :ref="`memberOptions-${index}`"
  48. interactive
  49. trigger="click"
  50. theme="popover"
  51. arrow
  52. >
  53. <template #trigger>
  54. <span class="select-wrapper">
  55. <input
  56. class="
  57. bg-transparent
  58. cursor-pointer
  59. flex flex-1
  60. py-2
  61. px-4
  62. "
  63. :placeholder="$t('team.permissions')"
  64. :name="'value' + index"
  65. :value="
  66. typeof member.role === 'string'
  67. ? member.role
  68. : JSON.stringify(member.role)
  69. "
  70. readonly
  71. />
  72. </span>
  73. </template>
  74. <SmartItem
  75. label="OWNER"
  76. @click.native="updateMemberRole(index, 'OWNER')"
  77. />
  78. <SmartItem
  79. label="EDITOR"
  80. @click.native="updateMemberRole(index, 'EDITOR')"
  81. />
  82. <SmartItem
  83. label="VIEWER"
  84. @click.native="updateMemberRole(index, 'VIEWER')"
  85. />
  86. </tippy>
  87. </span>
  88. <div class="flex">
  89. <ButtonSecondary
  90. id="member"
  91. v-tippy="{ theme: 'tooltip' }"
  92. :title="$t('action.remove')"
  93. svg="trash"
  94. color="red"
  95. @click.native="removeExistingTeamMember(member.user.uid)"
  96. />
  97. </div>
  98. </div>
  99. <div
  100. v-for="(member, index) in newMembers"
  101. :key="`new-member-${index}`"
  102. class="divide-x divide-dividerLight flex"
  103. >
  104. <input
  105. v-model="member.key"
  106. class="bg-transparent flex flex-1 py-2 px-4"
  107. :placeholder="$t('team.email')"
  108. :name="'member' + index"
  109. autofocus
  110. />
  111. <span>
  112. <tippy
  113. :ref="`newMemberOptions-${index}`"
  114. interactive
  115. trigger="click"
  116. theme="popover"
  117. arrow
  118. >
  119. <template #trigger>
  120. <span class="select-wrapper">
  121. <input
  122. class="
  123. bg-transparent
  124. cursor-pointer
  125. flex flex-1
  126. py-2
  127. px-4
  128. "
  129. :placeholder="$t('team.permissions')"
  130. :name="'value' + index"
  131. :value="
  132. typeof member.value === 'string'
  133. ? member.value
  134. : JSON.stringify(member.value)
  135. "
  136. readonly
  137. />
  138. </span>
  139. </template>
  140. <SmartItem
  141. label="OWNER"
  142. @click.native="updateNewMemberRole(index, 'OWNER')"
  143. />
  144. <SmartItem
  145. label="EDITOR"
  146. @click.native="updateNewMemberRole(index, 'EDITOR')"
  147. />
  148. <SmartItem
  149. label="VIEWER"
  150. @click.native="updateNewMemberRole(index, 'VIEWER')"
  151. />
  152. </tippy>
  153. </span>
  154. <div class="flex">
  155. <ButtonSecondary
  156. id="member"
  157. v-tippy="{ theme: 'tooltip' }"
  158. :title="$t('action.remove')"
  159. svg="trash"
  160. color="red"
  161. @click.native="removeTeamMember(index)"
  162. />
  163. </div>
  164. </div>
  165. <div
  166. v-if="members.length === 0 && newMembers.length === 0"
  167. class="
  168. flex flex-col
  169. text-secondaryLight
  170. p-4
  171. items-center
  172. justify-center
  173. "
  174. >
  175. <SmartIcon class="opacity-75 pb-2" name="users" />
  176. <span class="text-center pb-4">
  177. {{ $t("empty.members") }}
  178. </span>
  179. <ButtonSecondary
  180. :label="$t('add.new')"
  181. filled
  182. @click.native="addTeamMember"
  183. />
  184. </div>
  185. </div>
  186. </div>
  187. </template>
  188. <template #footer>
  189. <span>
  190. <ButtonPrimary :label="$t('action.save')" @click.native="saveTeam" />
  191. <ButtonSecondary
  192. :label="$t('action.cancel')"
  193. @click.native="hideModal"
  194. />
  195. </span>
  196. </template>
  197. </SmartModal>
  198. </template>
  199. <script>
  200. import cloneDeep from "lodash/cloneDeep"
  201. import { defineComponent } from "@nuxtjs/composition-api"
  202. import * as teamUtils from "~/helpers/teams/utils"
  203. import TeamMemberAdapter from "~/helpers/teams/TeamMemberAdapter"
  204. export default defineComponent({
  205. props: {
  206. show: Boolean,
  207. editingTeam: { type: Object, default: () => {} },
  208. editingteamID: { type: String, default: null },
  209. },
  210. data() {
  211. return {
  212. rename: null,
  213. members: [],
  214. newMembers: [],
  215. membersAdapter: new TeamMemberAdapter(null),
  216. }
  217. },
  218. computed: {
  219. editingTeamCopy() {
  220. return this.editingTeam
  221. },
  222. name: {
  223. get() {
  224. return this.editingTeam.name
  225. },
  226. set(name) {
  227. this.rename = name
  228. },
  229. },
  230. },
  231. watch: {
  232. editingteamID(teamID) {
  233. this.membersAdapter.changeTeamID(teamID)
  234. },
  235. },
  236. mounted() {
  237. this.membersAdapter.members$.subscribe((list) => {
  238. this.members = cloneDeep(list)
  239. })
  240. },
  241. methods: {
  242. updateMemberRole(id, role) {
  243. this.members[id].role = role
  244. this.$refs[`memberOptions-${id}`][0].tippy().hide()
  245. },
  246. updateNewMemberRole(id, role) {
  247. this.newMembers[id].value = role
  248. this.$refs[`newMemberOptions-${id}`][0].tippy().hide()
  249. },
  250. addTeamMember() {
  251. const member = { key: "", value: "" }
  252. this.newMembers.push(member)
  253. },
  254. removeExistingTeamMember(userID) {
  255. teamUtils
  256. .removeTeamMember(this.$apollo, userID, this.editingteamID)
  257. .then(() => {
  258. this.$toast.success(this.$t("team.member_removed"), {
  259. icon: "done",
  260. })
  261. this.hideModal()
  262. })
  263. .catch((e) => {
  264. this.$toast.error(this.$t("error.something_went_wrong"), {
  265. icon: "error_outline",
  266. })
  267. console.error(e)
  268. })
  269. },
  270. removeTeamMember(index) {
  271. this.newMembers.splice(index, 1)
  272. },
  273. validateEmail(emailID) {
  274. if (
  275. /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/.test(
  276. emailID
  277. )
  278. ) {
  279. return true
  280. }
  281. return false
  282. },
  283. saveTeam() {
  284. if (
  285. this.$data.rename !== null &&
  286. this.$data.rename.replace(/\s/g, "").length < 6
  287. ) {
  288. this.$toast.error(this.$t("team.name_length_insufficient"), {
  289. icon: "error_outline",
  290. })
  291. return
  292. }
  293. let invalidEmail = false
  294. this.$data.newMembers.forEach((element) => {
  295. if (!this.validateEmail(element.key)) {
  296. this.$toast.error(this.$t("team.invalid_email_format"), {
  297. icon: "error_outline",
  298. })
  299. invalidEmail = true
  300. }
  301. })
  302. if (invalidEmail) return
  303. let invalidPermission = false
  304. this.$data.newMembers.forEach((element) => {
  305. if (!element.value) {
  306. this.$toast.error(this.$t("invalid_member_permission"), {
  307. icon: "error_outline",
  308. })
  309. invalidPermission = true
  310. }
  311. })
  312. if (invalidPermission) return
  313. this.$data.newMembers.forEach((element) => {
  314. // Call to the graphql mutation
  315. teamUtils
  316. .addTeamMemberByEmail(
  317. this.$apollo,
  318. element.value,
  319. element.key,
  320. this.editingteamID
  321. )
  322. .then(() => {
  323. this.$toast.success(this.$t("team.saved"), {
  324. icon: "done",
  325. })
  326. })
  327. .catch((e) => {
  328. this.$toast.error(e, {
  329. icon: "error_outline",
  330. })
  331. console.error(e)
  332. })
  333. })
  334. this.members.forEach((element) => {
  335. teamUtils
  336. .updateTeamMemberRole(
  337. this.$apollo,
  338. element.user.uid,
  339. element.role,
  340. this.editingteamID
  341. )
  342. .then(() => {
  343. this.$toast.success(this.$t("team.member_role_updated"), {
  344. icon: "done",
  345. })
  346. })
  347. .catch((e) => {
  348. this.$toast.error(e, {
  349. icon: "error_outline",
  350. })
  351. console.error(e)
  352. })
  353. })
  354. if (this.$data.rename !== null) {
  355. const newName =
  356. this.name === this.$data.rename ? this.name : this.$data.rename
  357. if (!/\S/.test(newName))
  358. return this.$toast.error(this.$t("empty.team_name"), {
  359. icon: "error_outline",
  360. })
  361. // Call to the graphql mutation
  362. if (this.name !== this.rename)
  363. teamUtils
  364. .renameTeam(this.$apollo, newName, this.editingteamID)
  365. .then(() => {
  366. this.$toast.success(this.$t("team.saved"), {
  367. icon: "done",
  368. })
  369. })
  370. .catch((e) => {
  371. this.$toast.error(this.$t("error.something_went_wrong"), {
  372. icon: "error_outline",
  373. })
  374. console.error(e)
  375. })
  376. }
  377. this.hideModal()
  378. },
  379. hideModal() {
  380. this.rename = null
  381. this.newMembers = []
  382. this.$emit("hide-modal")
  383. },
  384. },
  385. })
  386. </script>