Aliases.vue 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960
  1. <template>
  2. <div>
  3. <div class="flex flex-wrap flex-row items-center justify-between mb-8 md:px-2 lg:px-6">
  4. <div
  5. class="w-full md:w-1/2 lg:w-1/3 xl:w-1/6 md:-mx-2 lg:-mx-6 rounded overflow-hidden shadow-md bg-white mb-4 lg:mb-4 xl:mb-0"
  6. >
  7. <div class="p-4 flex items-center justify-between relative">
  8. <icon
  9. name="check-circle"
  10. class="inline-block w-16 h-16 text-indigo-50 stroke-current absolute top-0 right-0"
  11. />
  12. <div class="font-bold text-xl md:text-3xl text-indigo-800">
  13. {{ totalActive }}
  14. <p class="text-grey-200 text-sm tracking-wide uppercase">
  15. Active
  16. </p>
  17. </div>
  18. </div>
  19. </div>
  20. <div
  21. class="w-full md:w-1/2 lg:w-1/3 xl:w-1/6 md:-mx-2 lg:-mx-6 rounded overflow-hidden shadow-md bg-white mb-4 lg:mb-4 xl:mb-0"
  22. >
  23. <div class="p-4 flex items-center justify-between relative">
  24. <icon
  25. name="cross-circle"
  26. class="inline-block w-16 h-16 text-indigo-50 stroke-current absolute top-0 right-0"
  27. />
  28. <div class="font-bold text-xl md:text-3xl text-indigo-800">
  29. {{ totalInactive }}
  30. <p class="text-grey-200 text-sm tracking-wide uppercase">
  31. Inactive
  32. </p>
  33. </div>
  34. </div>
  35. </div>
  36. <div
  37. class="w-full md:w-1/2 lg:w-1/3 xl:w-1/6 md:-mx-2 lg:-mx-6 rounded overflow-hidden shadow-md bg-white mb-4 lg:mb-4 xl:mb-0"
  38. >
  39. <div class="p-4 flex items-center justify-between relative">
  40. <icon
  41. name="send"
  42. class="inline-block w-16 h-16 text-indigo-50 stroke-current absolute top-0 right-0"
  43. />
  44. <div class="font-bold text-xl md:text-3xl text-indigo-800">
  45. {{ totalForwarded }}
  46. <p class="text-grey-200 text-sm tracking-wide uppercase">
  47. Emails Forwarded
  48. </p>
  49. </div>
  50. </div>
  51. </div>
  52. <div
  53. class="w-full md:w-1/2 lg:w-1/3 xl:w-1/6 md:-mx-2 lg:-mx-6 rounded overflow-hidden shadow-md bg-white mb-4 lg:mb-0"
  54. >
  55. <div class="p-4 flex items-center justify-between relative">
  56. <icon
  57. name="blocked"
  58. class="inline-block w-16 h-16 text-indigo-50 stroke-current absolute top-0 right-0"
  59. />
  60. <div class="font-bold text-xl md:text-3xl text-indigo-800">
  61. {{ totalBlocked }}
  62. <p class="text-grey-200 text-sm tracking-wide uppercase">
  63. Emails Blocked
  64. </p>
  65. </div>
  66. </div>
  67. </div>
  68. <div
  69. class="w-full md:w-1/2 lg:w-1/3 xl:w-1/6 md:-mx-2 lg:-mx-6 rounded overflow-hidden shadow-md bg-white mb-4 md:mb-0"
  70. >
  71. <div class="p-4 flex items-center justify-between relative">
  72. <icon
  73. name="corner-up-left"
  74. class="inline-block w-16 h-16 text-indigo-50 stroke-current absolute top-0 right-0"
  75. />
  76. <div class="font-bold text-xl md:text-3xl text-indigo-800">
  77. {{ totalReplies }}
  78. <p class="text-grey-200 text-sm tracking-wide uppercase">
  79. Email Replies
  80. </p>
  81. </div>
  82. </div>
  83. </div>
  84. <div
  85. class="w-full md:w-1/2 lg:w-1/3 xl:w-1/6 md:-mx-2 lg:-mx-6 rounded overflow-hidden shadow-md bg-white"
  86. >
  87. <div class="p-4 flex items-center justify-between relative">
  88. <icon
  89. name="inbox"
  90. class="inline-block w-16 h-16 text-indigo-50 stroke-current absolute top-0 right-0"
  91. />
  92. <div class="font-bold text-xl md:text-3xl text-indigo-800">
  93. {{ bandwidthMb }}<span class="text-sm tracking-wide uppercase">MB</span>
  94. <p class="text-grey-200 text-sm tracking-wide uppercase">Bandwidth ({{ month }})</p>
  95. </div>
  96. </div>
  97. </div>
  98. </div>
  99. <div class="mb-6 flex flex-col md:flex-row justify-between md:items-center">
  100. <div class="relative">
  101. <input
  102. v-model="search"
  103. @keyup.esc="search = ''"
  104. tabindex="0"
  105. type="text"
  106. class="w-full md:w-64 appearance-none shadow bg-white text-grey-700 focus:outline-none rounded py-3 pl-3 pr-8"
  107. placeholder="Search Aliases"
  108. />
  109. <icon
  110. v-if="search"
  111. @click.native="search = ''"
  112. name="close-circle"
  113. class="absolute right-0 inset-y-0 w-5 h-full text-grey-300 fill-current mr-2 flex items-center cursor-pointer"
  114. />
  115. <icon
  116. v-else
  117. name="search"
  118. class="absolute right-0 inset-y-0 w-5 h-full text-grey-300 fill-current pointer-events-none mr-2 flex items-center"
  119. />
  120. </div>
  121. <div class="mt-4 md:mt-0">
  122. <button
  123. @click="generateAliasModalOpen = true"
  124. class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none ml-auto"
  125. >
  126. Generate New Alias
  127. </button>
  128. </div>
  129. </div>
  130. <div class="bg-white rounded shadow overflow-x-auto">
  131. <table v-if="initialAliases.length" class="w-full whitespace-no-wrap">
  132. <tr class="text-left font-semibold text-grey-500 text-sm tracking-wider">
  133. <th class="pl-4 pr-2 py-4">
  134. <div class="flex items-center">
  135. Created
  136. <div class="inline-flex flex-col">
  137. <icon
  138. name="chevron-up"
  139. @click.native="sort('created_at', 'asc')"
  140. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  141. :class="{ 'text-grey-800': isCurrentSort('created_at', 'asc') }"
  142. />
  143. <icon
  144. name="chevron-down"
  145. @click.native="sort('created_at', 'desc')"
  146. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  147. :class="{
  148. 'text-grey-800': isCurrentSort('created_at', 'desc'),
  149. }"
  150. />
  151. </div>
  152. </div>
  153. </th>
  154. <th class="px-2 py-4">
  155. <div class="flex items-center">
  156. Alias
  157. <div class="inline-flex flex-col">
  158. <icon
  159. name="chevron-up"
  160. @click.native="sort('email', 'asc')"
  161. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  162. :class="{ 'text-grey-800': isCurrentSort('email', 'asc') }"
  163. />
  164. <icon
  165. name="chevron-down"
  166. @click.native="sort('email', 'desc')"
  167. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  168. :class="{ 'text-grey-800': isCurrentSort('email', 'desc') }"
  169. />
  170. </div>
  171. </div>
  172. </th>
  173. <th class="px-2 py-4">
  174. <div class="flex items-center">
  175. Recipients
  176. <div class="inline-flex flex-col">
  177. <icon
  178. name="chevron-up"
  179. @click.native="sort('recipients', 'asc')"
  180. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  181. :class="{ 'text-grey-800': isCurrentSort('recipients', 'asc') }"
  182. />
  183. <icon
  184. name="chevron-down"
  185. @click.native="sort('recipients', 'desc')"
  186. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  187. :class="{ 'text-grey-800': isCurrentSort('recipients', 'desc') }"
  188. />
  189. </div>
  190. </div>
  191. </th>
  192. <th class="px-2 py-4">
  193. <div class="flex items-center">
  194. Description
  195. </div>
  196. </th>
  197. <th class="px-2 py-4">
  198. <div class="flex items-center">
  199. Forwarded
  200. <div class="inline-flex flex-col">
  201. <icon
  202. name="chevron-up"
  203. @click.native="sort('emails_forwarded', 'asc')"
  204. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  205. :class="{
  206. 'text-grey-800': isCurrentSort('emails_forwarded', 'asc'),
  207. }"
  208. />
  209. <icon
  210. name="chevron-down"
  211. @click.native="sort('emails_forwarded', 'desc')"
  212. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  213. :class="{
  214. 'text-grey-800': isCurrentSort('emails_forwarded', 'desc'),
  215. }"
  216. />
  217. </div>
  218. </div>
  219. </th>
  220. <th class="px-2 py-4 items-center">
  221. <div class="flex items-center">
  222. Blocked
  223. <div class="inline-flex flex-col">
  224. <icon
  225. name="chevron-up"
  226. @click.native="sort('emails_blocked', 'asc')"
  227. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  228. :class="{
  229. 'text-grey-800': isCurrentSort('emails_blocked', 'asc'),
  230. }"
  231. />
  232. <icon
  233. name="chevron-down"
  234. @click.native="sort('emails_blocked', 'desc')"
  235. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  236. :class="{
  237. 'text-grey-800': isCurrentSort('emails_blocked', 'desc'),
  238. }"
  239. />
  240. </div>
  241. </div>
  242. </th>
  243. <th class="px-2 py-4 items-center">
  244. <div class="flex items-center">
  245. Replies
  246. <div class="inline-flex flex-col">
  247. <icon
  248. name="chevron-up"
  249. @click.native="sort('emails_replied', 'asc')"
  250. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  251. :class="{
  252. 'text-grey-800': isCurrentSort('emails_replied', 'asc'),
  253. }"
  254. />
  255. <icon
  256. name="chevron-down"
  257. @click.native="sort('emails_replied', 'desc')"
  258. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  259. :class="{
  260. 'text-grey-800': isCurrentSort('emails_replied', 'desc'),
  261. }"
  262. />
  263. </div>
  264. </div>
  265. </th>
  266. <th class="px-2 py-4 items-center" colspan="2">
  267. <div class="flex items-center">
  268. Active
  269. <div class="inline-flex flex-col">
  270. <icon
  271. name="chevron-up"
  272. @click.native="sort('active', 'asc')"
  273. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  274. :class="{ 'text-grey-800': isCurrentSort('active', 'asc') }"
  275. />
  276. <icon
  277. name="chevron-down"
  278. @click.native="sort('active', 'desc')"
  279. class="w-4 h-4 text-grey-300 fill-current cursor-pointer"
  280. :class="{ 'text-grey-800': isCurrentSort('active', 'desc') }"
  281. />
  282. </div>
  283. </div>
  284. </th>
  285. </tr>
  286. <tr
  287. v-for="alias in queriedAliases"
  288. :key="alias.id"
  289. class="hover:bg-grey-50 focus-within:bg-grey-50 h-20"
  290. >
  291. <td class="border-grey-200 border-t">
  292. <div class="pl-4 pr-2 py-4 flex items-center">
  293. <span
  294. class="tooltip outline-none text-sm"
  295. :data-tippy-content="alias.created_at | formatDate"
  296. >{{ alias.created_at | timeAgo }}</span
  297. >
  298. </div>
  299. </td>
  300. <td class="border-grey-200 border-t">
  301. <div class="px-2 py-4 flex items-center">
  302. <span
  303. class="tooltip cursor-pointer outline-none"
  304. data-tippy-content="Click to copy"
  305. v-clipboard="() => getAliasEmail(alias)"
  306. v-clipboard:success="clipboardSuccess"
  307. v-clipboard:error="clipboardError"
  308. >
  309. <span class="font-semibold text-indigo-800">{{
  310. alias.local_part | truncate(36)
  311. }}</span>
  312. <span class="block text-grey-400 text-sm">{{
  313. getAliasEmail(alias) | truncate(40)
  314. }}</span>
  315. </span>
  316. </div>
  317. </td>
  318. <td class="border-grey-200 border-t">
  319. <div class="px-2 flex items-center">
  320. <span
  321. v-if="alias.recipients.length && alias.id !== recipientsAliasToEdit.id"
  322. class="tooltip outline-none"
  323. :data-tippy-content="recipientsTooltip(alias.recipients)"
  324. >{{ alias.recipients[0].email | truncate(25) }}
  325. <span
  326. v-if="alias.recipients.length > 1"
  327. class="block text-center text-grey-500 text-sm"
  328. >
  329. + {{ alias.recipients.length - 1 }}</span
  330. >
  331. </span>
  332. <span v-else-if="alias.id === recipientsAliasToEdit.id">{{
  333. aliasRecipientsToEdit.length ? aliasRecipientsToEdit.length : '1'
  334. }}</span>
  335. <span
  336. v-else
  337. class="py-1 px-2 text-sm bg-yellow-200 text-yellow-900 rounded-full tooltip outline-none"
  338. :data-tippy-content="defaultRecipient.email"
  339. >default</span
  340. >
  341. <icon
  342. name="edit"
  343. class="ml-2 block w-6 h-6 text-grey-200 fill-current cursor-pointer"
  344. @click.native="openAliasRecipientsModal(alias)"
  345. />
  346. </div>
  347. </td>
  348. <td class="border-grey-200 border-t">
  349. <div class="px-2 py-4 text-sm">
  350. <div
  351. v-if="aliasIdToEdit === alias.id"
  352. class="w-full flex items-center justify-between"
  353. >
  354. <input
  355. @keyup.enter="editAlias(alias)"
  356. @keyup.esc="aliasIdToEdit = aliasDescriptionToEdit = ''"
  357. v-model="aliasDescriptionToEdit"
  358. type="text"
  359. class="appearance-none bg-grey-100 border text-grey-700 focus:outline-none rounded px-2 py-1"
  360. :class="
  361. aliasDescriptionToEdit.length > 100 ? 'border-red-500' : 'border-transparent'
  362. "
  363. placeholder="Add description"
  364. tabindex="0"
  365. autofocus
  366. />
  367. <icon
  368. name="close"
  369. class="inline-block w-6 h-6 text-red-300 fill-current cursor-pointer"
  370. @click.native="aliasIdToEdit = aliasDescriptionToEdit = ''"
  371. />
  372. <icon
  373. name="save"
  374. class="inline-block w-6 h-6 text-cyan-500 fill-current cursor-pointer"
  375. @click.native="editAlias(alias)"
  376. />
  377. </div>
  378. <div v-else-if="alias.description" class="flex items-center justify-around">
  379. <span
  380. class="tooltip outline-none"
  381. :data-tippy-content="alias.description"
  382. v-clipboard="() => alias.description"
  383. v-clipboard:success="clipboardSuccess"
  384. v-clipboard:error="clipboardError"
  385. >
  386. <icon
  387. name="desc"
  388. class="inline-block w-6 h-6 text-grey-200 fill-current cursor-pointer"
  389. />
  390. </span>
  391. <icon
  392. name="edit"
  393. class="inline-block w-6 h-6 text-grey-200 fill-current cursor-pointer"
  394. @click.native="
  395. ;(aliasIdToEdit = alias.id), (aliasDescriptionToEdit = alias.description)
  396. "
  397. />
  398. </div>
  399. <div v-else class="w-full flex justify-center">
  400. <icon
  401. name="plus"
  402. class="block w-6 h-6 text-grey-200 fill-current cursor-pointer"
  403. @click.native="aliasIdToEdit = alias.id"
  404. />
  405. </div>
  406. </div>
  407. </td>
  408. <td class="border-grey-200 border-t">
  409. <div class="px-2 py-4 flex items-center justify-center font-semibold text-indigo-800">
  410. {{ alias.emails_forwarded }}
  411. </div>
  412. </td>
  413. <td class="border-grey-200 border-t">
  414. <div class="px-2 py-4 flex items-center justify-center font-semibold text-indigo-800">
  415. {{ alias.emails_blocked }}
  416. </div>
  417. </td>
  418. <td class="border-grey-200 border-t">
  419. <div class="px-2 py-4 flex items-center justify-center font-semibold text-indigo-800">
  420. {{ alias.emails_replied }}
  421. </div>
  422. </td>
  423. <td class="border-grey-200 border-t">
  424. <div class="px-2 py-4 flex items-center justify-center">
  425. <Toggle
  426. v-model="alias.active"
  427. @on="activateAlias(alias)"
  428. @off="deactivateAlias(alias)"
  429. />
  430. </div>
  431. </td>
  432. <td class="border-grey-200 border-t w-px">
  433. <div class="px-4 flex items-center justify-center outline-none" tabindex="-1">
  434. <icon
  435. name="trash"
  436. class="block w-6 h-6 text-grey-200 fill-current cursor-pointer"
  437. @click.native="openDeleteModal(alias.id)"
  438. />
  439. </div>
  440. </td>
  441. </tr>
  442. <tr v-if="queriedAliases.length === 0">
  443. <td
  444. class="border-grey-200 border-t px-6 py-4 text-center h-24 text-lg text-grey-700"
  445. colspan="9"
  446. >
  447. No aliases found for that search!
  448. </td>
  449. </tr>
  450. </table>
  451. <div v-else class="p-8 text-center text-lg text-grey-700">
  452. <h1 class="mb-6 text-2xl text-indigo-800 font-semibold">
  453. It doesn't look like you have any aliases yet!
  454. </h1>
  455. <div class="mx-auto mb-6 w-24 border-b-2 border-grey-200"></div>
  456. <p class="mb-4">
  457. There are two ways to create new aliases.
  458. </p>
  459. <h3 class="mb-4 text-xl text-indigo-800 font-semibold">
  460. Option 1: Create aliases on the fly
  461. </h3>
  462. <p class="mb-4">
  463. To create aliases on the fly all you have to do is make up any new alias and give that out
  464. instead of your real email address.
  465. </p>
  466. <p class="mb-4">
  467. Let's say you're signing up to <b>example.com</b> you could enter
  468. <b>example@{{ subdomain }}</b> as your email address.
  469. </p>
  470. <p class="mb-4">
  471. The alias will show up here automatically as soon as it has forwarded its first email.
  472. </p>
  473. <p class="mb-4">
  474. If you start receiving spam to the alias you can simply deactivate it or delete it all
  475. together!
  476. </p>
  477. <p class="mb-4">
  478. Try it out now by sending an email to <b>first@{{ subdomain }}</b> and then refresh this
  479. page.
  480. </p>
  481. <h3 class="mb-4 text-xl text-indigo-800 font-semibold">
  482. Option 2: Generate a unique random alias
  483. </h3>
  484. <p class="mb-4">
  485. You can click the button above to generate a random UUID alias that will look something
  486. like this:
  487. </p>
  488. <p class="mb-4">
  489. <b>86064c92-da41-443e-a2bf-5a7b0247842f@{{ domain }}</b>
  490. </p>
  491. <p>
  492. Useful if you do not wish to include your username in the email as a potential link
  493. between aliases.
  494. </p>
  495. </div>
  496. </div>
  497. <Modal :open="generateAliasModalOpen" @close="generateAliasModalOpen = false">
  498. <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
  499. <h2
  500. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  501. >
  502. Generate new UUID alias
  503. </h2>
  504. <p class="mt-4 text-grey-700">
  505. This will generate a new unique alias in the form of
  506. <span class="text-sm block mt-2 font-semibold"
  507. >86064c92-da41-443e-a2bf-5a7b0247842f@{{ domain }}</span
  508. >
  509. </p>
  510. <p class="mt-2 text-grey-700">
  511. Other aliases e.g. alias@{{ subdomain }} are created automatically when they receive their
  512. first email.
  513. </p>
  514. <label for="banner_location" class="block text-grey-700 text-sm my-2">
  515. Alias Domain:
  516. </label>
  517. <div class="block relative w-full">
  518. <select
  519. v-model="generateAliasDomain"
  520. class="block appearance-none w-full text-grey-700 bg-grey-100 p-3 pr-8 rounded shadow focus:shadow-outline"
  521. required
  522. >
  523. <option v-for="domainOption in allDomains" :key="domainOption" :value="domainOption">{{
  524. domainOption
  525. }}</option>
  526. </select>
  527. <div
  528. class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"
  529. >
  530. <svg
  531. class="fill-current h-4 w-4"
  532. xmlns="http://www.w3.org/2000/svg"
  533. viewBox="0 0 20 20"
  534. >
  535. <path
  536. d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"
  537. />
  538. </svg>
  539. </div>
  540. </div>
  541. <div class="mt-6">
  542. <button
  543. @click="generateNewAlias"
  544. class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none"
  545. :class="generateAliasLoading ? 'cursor-not-allowed' : ''"
  546. :disabled="generateAliasLoading"
  547. >
  548. Generate Alias
  549. <loader v-if="generateAliasLoading" />
  550. </button>
  551. <button
  552. @click="generateAliasModalOpen = false"
  553. 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"
  554. >
  555. Cancel
  556. </button>
  557. </div>
  558. </div>
  559. </Modal>
  560. <Modal :open="editAliasRecipientsModalOpen" @close="closeAliasRecipientsModal">
  561. <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
  562. <h2
  563. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  564. >
  565. Update Alias Recipients
  566. </h2>
  567. <p class="my-4 text-grey-700">
  568. Select the recipients for this alias. You can choose multiple recipients. Leave it empty
  569. if you would like to use the default recipient.
  570. </p>
  571. <multiselect
  572. v-model="aliasRecipientsToEdit"
  573. :options="recipientOptions"
  574. :multiple="true"
  575. :close-on-select="true"
  576. :clear-on-select="false"
  577. :searchable="true"
  578. :max="10"
  579. placeholder="Select recipients"
  580. label="email"
  581. track-by="email"
  582. :preselect-first="false"
  583. :show-labels="false"
  584. >
  585. </multiselect>
  586. <div class="mt-6">
  587. <button
  588. type="button"
  589. @click="editAliasRecipients()"
  590. class="px-4 py-3 text-cyan-900 font-semibold bg-cyan-400 hover:bg-cyan-300 border border-transparent rounded focus:outline-none"
  591. :class="editAliasRecipientsLoading ? 'cursor-not-allowed' : ''"
  592. :disabled="editAliasRecipientsLoading"
  593. >
  594. Update Recipients
  595. <loader v-if="editAliasRecipientsLoading" />
  596. </button>
  597. <button
  598. @click="closeAliasRecipientsModal()"
  599. 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"
  600. >
  601. Cancel
  602. </button>
  603. </div>
  604. </div>
  605. </Modal>
  606. <Modal :open="deleteAliasModalOpen" @close="closeDeleteModal">
  607. <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
  608. <h2
  609. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  610. >
  611. Delete alias
  612. </h2>
  613. <p class="mt-4 text-grey-700">
  614. Are you sure you want to delete this alias? This action cannot be undone.
  615. </p>
  616. <div class="mt-6">
  617. <button
  618. type="button"
  619. @click="deleteAlias(aliasIdToDelete)"
  620. class="px-4 py-3 text-white font-semibold bg-red-500 hover:bg-red-600 border border-transparent rounded focus:outline-none"
  621. :class="deleteAliasLoading ? 'cursor-not-allowed' : ''"
  622. :disabled="deleteAliasLoading"
  623. >
  624. Delete alias
  625. <loader v-if="deleteAliasLoading" />
  626. </button>
  627. <button
  628. @click="closeDeleteModal"
  629. 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"
  630. >
  631. Cancel
  632. </button>
  633. </div>
  634. </div>
  635. </Modal>
  636. </div>
  637. </template>
  638. <script>
  639. import Modal from './../components/Modal.vue'
  640. import Toggle from './../components/Toggle.vue'
  641. import tippy from 'tippy.js'
  642. import Multiselect from 'vue-multiselect'
  643. export default {
  644. props: {
  645. defaultRecipient: {
  646. type: Object,
  647. required: true,
  648. },
  649. initialAliases: {
  650. type: Array,
  651. required: true,
  652. },
  653. recipientOptions: {
  654. type: Array,
  655. required: true,
  656. },
  657. totalForwarded: {
  658. type: Number,
  659. required: true,
  660. },
  661. totalBlocked: {
  662. type: Number,
  663. required: true,
  664. },
  665. totalReplies: {
  666. type: Number,
  667. required: true,
  668. },
  669. domain: {
  670. type: String,
  671. required: true,
  672. },
  673. subdomain: {
  674. type: String,
  675. required: true,
  676. },
  677. bandwidthMb: {
  678. type: Number,
  679. required: true,
  680. },
  681. month: {
  682. type: String,
  683. required: true,
  684. },
  685. allDomains: {
  686. type: Array,
  687. required: true,
  688. },
  689. },
  690. components: {
  691. Modal,
  692. Toggle,
  693. Multiselect,
  694. },
  695. mounted() {
  696. this.addTooltips()
  697. },
  698. data() {
  699. return {
  700. aliases: this.initialAliases,
  701. search: '',
  702. aliasIdToEdit: '',
  703. aliasDescriptionToEdit: '',
  704. aliasIdToDelete: '',
  705. deleteAliasLoading: false,
  706. deleteAliasModalOpen: false,
  707. currentSort: 'created_at',
  708. currentSortDir: 'desc',
  709. editAliasRecipientsLoading: false,
  710. editAliasRecipientsModalOpen: false,
  711. generateAliasModalOpen: false,
  712. generateAliasLoading: false,
  713. generateAliasDomain: this.domain,
  714. recipientsAliasToEdit: {},
  715. aliasRecipientsToEdit: [],
  716. }
  717. },
  718. watch: {
  719. queriedAliases: _.debounce(function() {
  720. this.addTooltips()
  721. }, 50),
  722. aliasIdToEdit: _.debounce(function() {
  723. this.addTooltips()
  724. }, 50),
  725. editAliasRecipientsModalOpen: _.debounce(function() {
  726. this.addTooltips()
  727. }, 50),
  728. },
  729. computed: {
  730. queriedAliases() {
  731. return _.filter(this.aliases, alias => alias.email.includes(this.search))
  732. },
  733. totalActive() {
  734. return _.filter(this.aliases, 'active').length
  735. },
  736. totalInactive() {
  737. return _.reject(this.aliases, 'active').length
  738. },
  739. },
  740. methods: {
  741. addTooltips() {
  742. tippy('.tooltip', {
  743. arrow: true,
  744. arrowType: 'round',
  745. })
  746. },
  747. recipientsTooltip(recipients) {
  748. return _.reduce(recipients, (list, recipient) => list + `${recipient.email}<br>`, '')
  749. },
  750. isCurrentSort(col, dir) {
  751. return this.currentSort === col && this.currentSortDir === dir
  752. },
  753. openDeleteModal(id) {
  754. this.deleteAliasModalOpen = true
  755. this.aliasIdToDelete = id
  756. },
  757. closeDeleteModal() {
  758. this.deleteAliasModalOpen = false
  759. this.aliasIdToDelete = ''
  760. },
  761. deleteAlias(id) {
  762. this.deleteAliasLoading = true
  763. axios
  764. .delete(`/aliases/${id}`)
  765. .then(response => {
  766. this.aliases = _.filter(this.aliases, aliases => aliases.id !== id)
  767. this.deleteAliasModalOpen = false
  768. this.deleteAliasLoading = false
  769. })
  770. .catch(error => {
  771. this.error()
  772. this.deleteAliasModalOpen = false
  773. this.deleteAliasLoading = false
  774. })
  775. },
  776. sort(col, dir) {
  777. if (this.currentSort === col && this.currentSortDir === dir) {
  778. this.currentSort = 'created_at'
  779. this.currentSortDir = 'desc'
  780. } else {
  781. this.currentSort = col
  782. this.currentSortDir = dir
  783. }
  784. this.aliases = _.orderBy(this.aliases, [this.currentSort], [this.currentSortDir])
  785. },
  786. reSort() {
  787. this.aliases = _.orderBy(this.aliases, [this.currentSort], [this.currentSortDir])
  788. },
  789. openAliasRecipientsModal(alias) {
  790. this.editAliasRecipientsModalOpen = true
  791. this.recipientsAliasToEdit = alias
  792. this.aliasRecipientsToEdit = alias.recipients
  793. },
  794. closeAliasRecipientsModal() {
  795. this.editAliasRecipientsModalOpen = false
  796. this.recipientsAliasToEdit = {}
  797. this.aliasRecipientsToEdit = []
  798. },
  799. editAliasRecipients() {
  800. this.editAliasRecipientsLoading = true
  801. axios
  802. .post(
  803. '/alias-recipients',
  804. JSON.stringify({
  805. alias_id: this.recipientsAliasToEdit.id,
  806. recipient_ids: _.map(this.aliasRecipientsToEdit, recipient => recipient.id),
  807. }),
  808. {
  809. headers: { 'Content-Type': 'application/json' },
  810. }
  811. )
  812. .then(response => {
  813. let alias = _.find(this.aliases, ['id', this.recipientsAliasToEdit.id])
  814. alias.recipients = this.aliasRecipientsToEdit
  815. this.editAliasRecipientsModalOpen = false
  816. this.editAliasRecipientsLoading = false
  817. this.recipientsAliasToEdit = {}
  818. this.aliasRecipientsToEdit = []
  819. this.success('Alias recipients updated')
  820. })
  821. .catch(error => {
  822. this.editAliasRecipientsModalOpen = false
  823. this.editAliasRecipientsLoading = false
  824. this.recipientsAliasToEdit = {}
  825. this.aliasRecipientsToEdit = []
  826. this.error()
  827. })
  828. },
  829. generateNewAlias() {
  830. this.generateAliasLoading = true
  831. axios
  832. .post(
  833. '/aliases',
  834. JSON.stringify({
  835. domain: this.generateAliasDomain,
  836. }),
  837. {
  838. headers: { 'Content-Type': 'application/json' },
  839. }
  840. )
  841. .then(({ data }) => {
  842. this.generateAliasLoading = false
  843. this.aliases.push(data.data)
  844. this.reSort()
  845. this.generateAliasModalOpen = false
  846. this.success('New alias generated successfully')
  847. })
  848. .catch(error => {
  849. this.generateAliasLoading = false
  850. if (error.response.status === 429) {
  851. this.error('You have reached your hourly limit for creating new aliases')
  852. } else {
  853. this.error()
  854. }
  855. })
  856. },
  857. editAlias(alias) {
  858. if (this.aliasDescriptionToEdit.length > 100) {
  859. return this.error('Description cannot be more than 100 characters')
  860. }
  861. axios
  862. .patch(
  863. `/aliases/${alias.id}`,
  864. JSON.stringify({
  865. description: this.aliasDescriptionToEdit,
  866. }),
  867. {
  868. headers: { 'Content-Type': 'application/json' },
  869. }
  870. )
  871. .then(response => {
  872. alias.description = this.aliasDescriptionToEdit
  873. this.aliasIdToEdit = ''
  874. this.aliasDescriptionToEdit = ''
  875. this.success('Alias description updated')
  876. })
  877. .catch(error => {
  878. this.aliasIdToEdit = ''
  879. this.aliasDescriptionToEdit = ''
  880. this.error()
  881. })
  882. },
  883. activateAlias(alias) {
  884. axios
  885. .post(
  886. `/active-aliases`,
  887. JSON.stringify({
  888. id: alias.id,
  889. }),
  890. {
  891. headers: { 'Content-Type': 'application/json' },
  892. }
  893. )
  894. .then(response => {
  895. //
  896. })
  897. .catch(error => {
  898. this.error()
  899. })
  900. },
  901. deactivateAlias(alias) {
  902. axios
  903. .delete(`/active-aliases/${alias.id}`)
  904. .then(response => {
  905. //
  906. })
  907. .catch(error => {
  908. this.error()
  909. })
  910. },
  911. getAliasEmail(alias) {
  912. return alias.extension
  913. ? `${alias.local_part}+${alias.extension}@${alias.domain}`
  914. : alias.email
  915. },
  916. clipboardSuccess() {
  917. this.success('Copied to clipboard')
  918. },
  919. clipboardError() {
  920. this.error('Could not copy to clipboard')
  921. },
  922. success(text = '') {
  923. this.$notify({
  924. title: 'Success',
  925. text: text,
  926. type: 'success',
  927. })
  928. },
  929. error(text = 'An error has occurred, please try again later') {
  930. this.$notify({
  931. title: 'Error',
  932. text: text,
  933. type: 'error',
  934. })
  935. },
  936. },
  937. }
  938. </script>
  939. <style src="vue-multiselect/dist/vue-multiselect.min.css"></style>