Recipients.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. <template>
  2. <div>
  3. <div class="mb-6 flex flex-col md:flex-row justify-between md:items-center">
  4. <div class="relative">
  5. <input
  6. v-model="search"
  7. @keyup.esc="search = ''"
  8. tabindex="0"
  9. type="text"
  10. class="w-full md:w-64 appearance-none shadow bg-white text-grey-700 focus:outline-none rounded py-3 pl-3 pr-8"
  11. placeholder="Search Recipients"
  12. />
  13. <icon
  14. v-if="search"
  15. @click.native="search = ''"
  16. name="close-circle"
  17. class="absolute right-0 inset-y-0 w-5 h-full text-grey-300 fill-current mr-2 flex items-center cursor-pointer"
  18. />
  19. <icon
  20. v-else
  21. name="search"
  22. class="absolute right-0 inset-y-0 w-5 h-full text-grey-300 fill-current pointer-events-none mr-2 flex items-center"
  23. />
  24. </div>
  25. <div class="mt-4 md:mt-0">
  26. <button
  27. @click="addRecipientModalOpen = true"
  28. class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none ml-auto"
  29. >
  30. Add Recipient
  31. </button>
  32. </div>
  33. </div>
  34. <div class="bg-white rounded shadow overflow-x-auto">
  35. <table class="w-full whitespace-no-wrap">
  36. <tr class="text-left font-semibold text-grey-500 text-sm tracking-wider">
  37. <th class="p-4">
  38. <div class="flex items-center">
  39. Created
  40. <div class="inline-flex flex-col">
  41. <icon
  42. name="chevron-up"
  43. @click.native="sort('created_at', 'asc')"
  44. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  45. :class="{ 'text-grey-800': isCurrentSort('created_at', 'asc') }"
  46. />
  47. <icon
  48. name="chevron-down"
  49. @click.native="sort('created_at', 'desc')"
  50. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  51. :class="{
  52. 'text-grey-800': isCurrentSort('created_at', 'desc'),
  53. }"
  54. />
  55. </div>
  56. </div>
  57. </th>
  58. <th class="p-4">
  59. <div class="flex items-center">
  60. Email
  61. <div class="inline-flex flex-col">
  62. <icon
  63. name="chevron-up"
  64. @click.native="sort('email', 'asc')"
  65. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  66. :class="{ 'text-grey-800': isCurrentSort('email', 'asc') }"
  67. />
  68. <icon
  69. name="chevron-down"
  70. @click.native="sort('email', 'desc')"
  71. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  72. :class="{ 'text-grey-800': isCurrentSort('email', 'desc') }"
  73. />
  74. </div>
  75. </div>
  76. </th>
  77. <th class="p-4">
  78. <div class="flex items-center">
  79. Recipient Aliases
  80. <div class="inline-flex flex-col">
  81. <icon
  82. name="chevron-up"
  83. @click.native="sort('aliases', 'asc')"
  84. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  85. :class="{
  86. 'text-grey-800': isCurrentSort('aliases', 'asc'),
  87. }"
  88. />
  89. <icon
  90. name="chevron-down"
  91. @click.native="sort('aliases', 'desc')"
  92. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  93. :class="{
  94. 'text-grey-800': isCurrentSort('aliases', 'desc'),
  95. }"
  96. />
  97. </div>
  98. </div>
  99. </th>
  100. <th class="p-4" colspan="2">
  101. <div class="flex items-center">
  102. Verified
  103. <div class="inline-flex flex-col">
  104. <icon
  105. name="chevron-up"
  106. @click.native="sort('email_verified_at', 'asc')"
  107. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  108. :class="{
  109. 'text-grey-800': isCurrentSort('email_verified_at', 'asc'),
  110. }"
  111. />
  112. <icon
  113. name="chevron-down"
  114. @click.native="sort('email_verified_at', 'desc')"
  115. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  116. :class="{
  117. 'text-grey-800': isCurrentSort('email_verified_at', 'desc'),
  118. }"
  119. />
  120. </div>
  121. </div>
  122. </th>
  123. </tr>
  124. <tr
  125. v-for="recipient in queriedRecipients"
  126. :key="recipient.email"
  127. class="hover:bg-grey-50 focus-within:bg-grey-50 h-20"
  128. >
  129. <td class="border-grey-200 border-t">
  130. <div class="p-4 flex items-center">
  131. <span
  132. class="tooltip outline-none text-sm"
  133. :data-tippy-content="recipient.created_at | formatDate"
  134. >{{ recipient.created_at | timeAgo }}</span
  135. >
  136. </div>
  137. </td>
  138. <td class="border-grey-200 border-t">
  139. <div class="p-4 flex items-center focus:text-indigo-500">
  140. <span
  141. class="tooltip cursor-pointer outline-none"
  142. data-tippy-content="Click to copy"
  143. v-clipboard="() => recipient.email"
  144. v-clipboard:success="clipboardSuccess"
  145. v-clipboard:error="clipboardError"
  146. >{{ recipient.email | truncate(30) }}</span
  147. >
  148. <span
  149. v-if="isDefault(recipient.id)"
  150. class="ml-3 py-1 px-2 text-sm bg-yellow-200 text-yellow-900 rounded-full tooltip"
  151. data-tippy-content="The default recipient will be used for all aliases with no other recipients assigned"
  152. >
  153. default
  154. </span>
  155. </div>
  156. </td>
  157. <td class="border-grey-200 border-t">
  158. <div class="p-4 flex items-center focus:text-indigo-500">
  159. <span
  160. v-if="recipient.aliases.length"
  161. class="tooltip outline-none"
  162. :data-tippy-content="aliasesTooltip(recipient.aliases)"
  163. >{{ recipient.aliases[0].email | truncate(40) }}
  164. <span
  165. v-if="recipient.aliases.length > 1"
  166. class="block text-center text-grey-500 text-sm"
  167. >
  168. + {{ recipient.aliases.length - 1 }}</span
  169. >
  170. </span>
  171. <span v-else class="block text-center text-grey-500 text-sm">{{
  172. recipient.aliases.length
  173. }}</span>
  174. </div>
  175. </td>
  176. <td class="border-grey-200 border-t">
  177. <div class="p-4 flex items-center focus:text-indigo-500 text-sm">
  178. <span
  179. name="check"
  180. v-if="recipient.email_verified_at"
  181. class="py-1 px-2 bg-green-200 text-green-900 rounded-full"
  182. >
  183. verified
  184. </span>
  185. <button
  186. v-else
  187. @click="resendVerification(recipient.id)"
  188. class="focus:outline-none"
  189. :class="resendVerificationLoading ? 'cursor-not-allowed' : ''"
  190. :disabled="resendVerificationLoading"
  191. >
  192. Resend email
  193. </button>
  194. </div>
  195. </td>
  196. <td class="border-grey-200 border-t w-px">
  197. <div
  198. v-if="!isDefault(recipient.id)"
  199. class="px-4 flex items-center cursor-pointer outline-none focus:text-indigo-500"
  200. @click="openDeleteModal(recipient.id)"
  201. tabindex="-1"
  202. >
  203. <icon name="trash" class="block w-6 h-6 text-grey-200 fill-current" />
  204. </div>
  205. </td>
  206. </tr>
  207. <tr v-if="queriedRecipients.length === 0">
  208. <td
  209. class="border-grey-200 border-t p-4 text-center h-24 text-lg text-grey-700"
  210. colspan="4"
  211. >
  212. No recipients found for that search!
  213. </td>
  214. </tr>
  215. </table>
  216. </div>
  217. <Modal :open="addRecipientModalOpen" @close="addRecipientModalOpen = false">
  218. <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
  219. <h2
  220. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  221. >
  222. Add new recipient
  223. </h2>
  224. <p class="mt-4 text-grey-700">
  225. Enter the individual email of the new recipient you'd like to add.
  226. </p>
  227. <div class="mt-6">
  228. <p v-show="errors.newRecipient" class="mb-3 text-red-500">
  229. {{ errors.newRecipient }}
  230. </p>
  231. <input
  232. v-model="newRecipient"
  233. type="email"
  234. class="w-full appearance-none bg-grey-100 border border-transparent text-grey-700 focus:outline-none rounded p-3 mb-6"
  235. :class="errors.newRecipient ? 'border-red-500' : ''"
  236. placeholder="johndoe@example.com"
  237. autofocus
  238. />
  239. <button
  240. @click="validateNewRecipient"
  241. class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none"
  242. :class="addRecipientLoading ? 'cursor-not-allowed' : ''"
  243. :disabled="addRecipientLoading"
  244. >
  245. Add Recipient
  246. </button>
  247. <button
  248. @click="addRecipientModalOpen = false"
  249. class="ml-4 px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus:outline-none"
  250. >
  251. Cancel
  252. </button>
  253. </div>
  254. </div>
  255. </Modal>
  256. <Modal :open="deleteRecipientModalOpen" @close="closeDeleteModal">
  257. <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
  258. <h2
  259. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  260. >
  261. Delete recipient
  262. </h2>
  263. <p class="mt-4 text-grey-700">Are you sure you want to delete this recipient?</p>
  264. <div class="mt-6">
  265. <button
  266. type="button"
  267. @click="deleteRecipient(recipientIdToDelete)"
  268. class="px-4 py-3 text-white font-semibold bg-red-500 hover:bg-red-600 border border-transparent rounded focus:outline-none"
  269. :class="deleteRecipientLoading ? 'cursor-not-allowed' : ''"
  270. :disabled="deleteRecipientLoading"
  271. >
  272. Delete recipient
  273. </button>
  274. <button
  275. @click="closeDeleteModal"
  276. class="ml-4 px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus:outline-none"
  277. >
  278. Cancel
  279. </button>
  280. </div>
  281. </div>
  282. </Modal>
  283. </div>
  284. </template>
  285. <script>
  286. import Modal from './../components/Modal.vue'
  287. import tippy from 'tippy.js'
  288. export default {
  289. props: {
  290. user: {
  291. type: Object,
  292. required: true,
  293. },
  294. initialRecipients: {
  295. type: Array,
  296. required: true,
  297. },
  298. aliasesUsingDefault: {
  299. type: Array,
  300. required: true,
  301. },
  302. },
  303. components: {
  304. Modal,
  305. },
  306. created() {
  307. this.defaultRecipient = _.find(this.recipients, ['id', this.user.default_recipient_id])
  308. this.defaultRecipient.aliases = this.defaultRecipient.aliases.concat(this.aliasesUsingDefault)
  309. },
  310. mounted() {
  311. this.addTooltips()
  312. },
  313. data() {
  314. return {
  315. recipients: this.initialRecipients,
  316. defaultRecipient: {},
  317. newRecipient: '',
  318. search: '',
  319. addRecipientLoading: false,
  320. addRecipientModalOpen: false,
  321. recipientIdToDelete: null,
  322. deleteRecipientLoading: false,
  323. deleteRecipientModalOpen: false,
  324. resendVerificationLoading: false,
  325. currentSort: 'created_at',
  326. currentSortDir: 'desc',
  327. errors: {},
  328. }
  329. },
  330. watch: {
  331. queriedRecipients: _.debounce(function() {
  332. this.addTooltips()
  333. }, 50),
  334. },
  335. computed: {
  336. queriedRecipients() {
  337. return _.filter(this.recipients, recipient => recipient.email.includes(this.search))
  338. },
  339. },
  340. methods: {
  341. addTooltips() {
  342. tippy('.tooltip', {
  343. arrow: true,
  344. arrowType: 'round',
  345. })
  346. },
  347. aliasesTooltip(aliases) {
  348. return _.reduce(aliases, (list, alias) => list + `${alias.email}<br>`, '')
  349. },
  350. isDefault(id) {
  351. return this.user.default_recipient_id === id
  352. },
  353. isCurrentSort(col, dir) {
  354. return this.currentSort === col && this.currentSortDir === dir
  355. },
  356. validateNewRecipient(e) {
  357. this.errors = {}
  358. if (!this.newRecipient) {
  359. this.errors.newRecipient = 'Email required'
  360. } else if (!this.validEmail(this.newRecipient)) {
  361. this.errors.newRecipient = 'Valid Email required'
  362. }
  363. if (!this.errors.newRecipient) {
  364. this.addNewRecipient()
  365. }
  366. e.preventDefault()
  367. },
  368. addNewRecipient() {
  369. this.addRecipientLoading = true
  370. axios
  371. .post(
  372. '/recipients',
  373. JSON.stringify({
  374. email: this.newRecipient,
  375. }),
  376. {
  377. headers: { 'Content-Type': 'application/json' },
  378. }
  379. )
  380. .then(({ data }) => {
  381. this.addRecipientLoading = false
  382. this.recipients.push(data.data)
  383. this.reSort()
  384. this.newRecipient = ''
  385. this.addRecipientModalOpen = false
  386. this.success('Recipient created and verification email sent')
  387. })
  388. .catch(error => {
  389. this.addRecipientLoading = false
  390. this.error()
  391. })
  392. },
  393. resendVerification(id) {
  394. this.resendVerificationLoading = true
  395. axios
  396. .get(`/recipients/${id}/email/resend`)
  397. .then(({ data }) => {
  398. this.resendVerificationLoading = false
  399. this.success('Verification email resent')
  400. })
  401. .catch(error => {
  402. this.resendVerificationLoading = false
  403. this.error()
  404. })
  405. },
  406. openDeleteModal(id) {
  407. this.deleteRecipientModalOpen = true
  408. this.recipientIdToDelete = id
  409. },
  410. closeDeleteModal() {
  411. this.deleteRecipientModalOpen = false
  412. this.recipientIdToDelete = null
  413. },
  414. deleteRecipient(id) {
  415. this.deleteRecipientLoading = true
  416. axios
  417. .delete(`/recipients/${id}`)
  418. .then(response => {
  419. this.recipients = _.filter(this.recipients, recipient => recipient.id !== id)
  420. this.deleteRecipientModalOpen = false
  421. this.deleteRecipientLoading = false
  422. })
  423. .catch(error => {
  424. this.error()
  425. this.deleteRecipientLoading = false
  426. this.deleteRecipientModalOpen = false
  427. })
  428. },
  429. sort(col, dir) {
  430. if (this.currentSort === col && this.currentSortDir === dir) {
  431. this.currentSort = 'created_at'
  432. this.currentSortDir = 'desc'
  433. } else {
  434. this.currentSort = col
  435. this.currentSortDir = dir
  436. }
  437. this.reSort()
  438. },
  439. reSort() {
  440. this.recipients = _.orderBy(this.recipients, [this.currentSort], [this.currentSortDir])
  441. },
  442. validEmail(email) {
  443. let re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
  444. return re.test(email)
  445. },
  446. clipboardSuccess() {
  447. this.success('Copied to clipboard')
  448. },
  449. clipboardError() {
  450. this.error('Could not copy to clipboard')
  451. },
  452. success(text = '') {
  453. this.$notify({
  454. title: 'Success',
  455. text: text,
  456. type: 'success',
  457. })
  458. },
  459. error(text = 'An error has occurred, please try again later') {
  460. this.$notify({
  461. title: 'Error',
  462. text: text,
  463. type: 'error',
  464. })
  465. },
  466. },
  467. }
  468. </script>