Aliases.vue 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866
  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. <vue-good-table
  131. v-if="initialAliases.length"
  132. @on-search="debounceToolips"
  133. @on-page-change="debounceToolips"
  134. :columns="columns"
  135. :rows="rows"
  136. :search-options="{
  137. enabled: true,
  138. skipDiacritics: true,
  139. externalQuery: search,
  140. }"
  141. :sort-options="{
  142. enabled: true,
  143. initialSortBy: { field: 'created_at', type: 'desc' },
  144. }"
  145. :pagination-options="{
  146. enabled: true,
  147. mode: 'pages',
  148. perPage: 25,
  149. perPageDropdown: [25, 50, 100],
  150. rowsPerPageLabel: 'Aliases per page',
  151. }"
  152. styleClass="vgt-table"
  153. >
  154. <div slot="emptystate" class="flex items-center justify-center h-24 text-lg text-grey-700">
  155. No aliases found for that search!
  156. </div>
  157. <template slot="table-row" slot-scope="props">
  158. <span
  159. v-if="props.column.field == 'created_at'"
  160. class="tooltip outline-none text-sm"
  161. :data-tippy-content="props.row.created_at | formatDate"
  162. >{{ props.row.created_at | timeAgo }}
  163. </span>
  164. <span v-else-if="props.column.field == 'email'" class="block">
  165. <span
  166. class="text-grey-400 tooltip cursor-pointer outline-none"
  167. data-tippy-content="Click to copy"
  168. v-clipboard="() => getAliasEmail(props.row)"
  169. v-clipboard:success="clipboardSuccess"
  170. v-clipboard:error="clipboardError"
  171. ><span class="font-semibold text-indigo-800">{{
  172. getAliasLocalPart(props.row) | truncate(60)
  173. }}</span
  174. ><span v-if="getAliasLocalPart(props.row).length <= 60">{{
  175. ('@' + props.row.domain) | truncate(60 - getAliasLocalPart(props.row).length)
  176. }}</span>
  177. </span>
  178. <div v-if="aliasIdToEdit === props.row.id" class="flex items-center">
  179. <input
  180. @keyup.enter="editAlias(rows[props.row.originalIndex])"
  181. @keyup.esc="aliasIdToEdit = aliasDescriptionToEdit = ''"
  182. v-model="aliasDescriptionToEdit"
  183. type="text"
  184. class="flex-grow text-sm appearance-none bg-grey-100 border text-grey-700 focus:outline-none rounded px-2 py-1"
  185. :class="aliasDescriptionToEdit.length > 100 ? 'border-red-500' : 'border-transparent'"
  186. placeholder="Add description"
  187. tabindex="0"
  188. autofocus
  189. />
  190. <icon
  191. name="close"
  192. class="inline-block w-6 h-6 text-red-300 fill-current cursor-pointer"
  193. @click.native="aliasIdToEdit = aliasDescriptionToEdit = ''"
  194. />
  195. <icon
  196. name="save"
  197. class="inline-block w-6 h-6 text-cyan-500 fill-current cursor-pointer"
  198. @click.native="editAlias(rows[props.row.originalIndex])"
  199. />
  200. </div>
  201. <div v-else-if="props.row.description" class="flex items-center">
  202. <span class="inline-block text-grey-400 text-sm py-1 border border-transparent">
  203. {{ props.row.description | truncate(60) }}
  204. </span>
  205. <icon
  206. name="edit"
  207. class="inline-block w-6 h-6 ml-2 text-grey-200 fill-current cursor-pointer"
  208. @click.native="
  209. ;(aliasIdToEdit = props.row.id), (aliasDescriptionToEdit = props.row.description)
  210. "
  211. />
  212. </div>
  213. <div v-else>
  214. <span
  215. class="inline-block text-grey-200 text-sm cursor-pointer py-1 border border-transparent"
  216. @click=";(aliasIdToEdit = props.row.id), (aliasDescriptionToEdit = '')"
  217. >Add description</span
  218. >
  219. </div>
  220. </span>
  221. <span
  222. v-else-if="props.column.field == 'recipients'"
  223. class="flex items-center justify-center"
  224. >
  225. <span
  226. v-if="props.row.recipients.length && props.row.id !== recipientsAliasToEdit.id"
  227. class="inline-block tooltip outline-none font-semibold text-indigo-800"
  228. :data-tippy-content="recipientsTooltip(props.row.recipients)"
  229. >
  230. {{ props.row.recipients.length }}
  231. </span>
  232. <span v-else-if="props.row.id === recipientsAliasToEdit.id">{{
  233. aliasRecipientsToEdit.length ? aliasRecipientsToEdit.length : '1'
  234. }}</span>
  235. <span
  236. v-else-if="has(props.row.aliasable, 'default_recipient.email')"
  237. class="py-1 px-2 text-sm bg-yellow-200 text-yellow-900 rounded-full tooltip outline-none"
  238. :data-tippy-content="props.row.aliasable.default_recipient.email"
  239. >{{ props.row.aliasable_type === 'App\\Domain' ? 'domain' : 'username' }}'s
  240. default</span
  241. >
  242. <span
  243. v-else
  244. class="py-1 px-2 text-sm bg-yellow-200 text-yellow-900 rounded-full tooltip outline-none"
  245. :data-tippy-content="defaultRecipient.email"
  246. >default</span
  247. >
  248. <icon
  249. name="edit"
  250. class="ml-2 inline-block w-6 h-6 text-grey-200 fill-current cursor-pointer"
  251. @click.native="openAliasRecipientsModal(props.row)"
  252. />
  253. </span>
  254. <span
  255. v-else-if="props.column.field == 'emails_forwarded'"
  256. class="font-semibold text-indigo-800"
  257. >
  258. {{ props.row.emails_forwarded }}
  259. </span>
  260. <span
  261. v-else-if="props.column.field == 'emails_blocked'"
  262. class="font-semibold text-indigo-800"
  263. >
  264. {{ props.row.emails_blocked }}
  265. </span>
  266. <span
  267. v-else-if="props.column.field == 'emails_replied'"
  268. class="font-semibold text-indigo-800"
  269. >
  270. {{ props.row.emails_replied }}
  271. </span>
  272. <span v-else-if="props.column.field === 'active'" class="flex items-center">
  273. <Toggle
  274. v-model="rows[props.row.originalIndex].active"
  275. @on="activateAlias(props.row.id)"
  276. @off="deactivateAlias(props.row.id)"
  277. />
  278. </span>
  279. <span v-else class="flex items-center justify-center outline-none" tabindex="-1">
  280. <icon
  281. name="trash"
  282. class="block w-6 h-6 text-grey-200 fill-current cursor-pointer"
  283. @click.native="openDeleteModal(props.row.id)"
  284. />
  285. </span>
  286. </template>
  287. </vue-good-table>
  288. <div v-else class="bg-white rounded shadow overflow-x-auto">
  289. <div class="p-8 text-center text-lg text-grey-700">
  290. <h1 class="mb-6 text-2xl text-indigo-800 font-semibold">
  291. It doesn't look like you have any aliases yet!
  292. </h1>
  293. <div class="mx-auto mb-6 w-24 border-b-2 border-grey-200"></div>
  294. <p class="mb-4">
  295. There are two ways to create new aliases.
  296. </p>
  297. <h3 class="mb-4 text-xl text-indigo-800 font-semibold">
  298. Option 1: Create aliases on the fly
  299. </h3>
  300. <p class="mb-4">
  301. To create aliases on the fly all you have to do is make up any new alias and give that out
  302. instead of your real email address.
  303. </p>
  304. <p class="mb-4">
  305. Let's say you're signing up to <b>example.com</b> you could enter
  306. <b>example@{{ subdomain }}</b> as your email address.
  307. </p>
  308. <p class="mb-4">
  309. The alias will show up here automatically as soon as it has forwarded its first email.
  310. </p>
  311. <p class="mb-4">
  312. If you start receiving spam to the alias you can simply deactivate it or delete it all
  313. together!
  314. </p>
  315. <p class="mb-4">
  316. Try it out now by sending an email to <b>first@{{ subdomain }}</b> and then refresh this
  317. page.
  318. </p>
  319. <h3 class="mb-4 text-xl text-indigo-800 font-semibold">
  320. Option 2: Generate a unique random alias
  321. </h3>
  322. <p class="mb-4">
  323. You can click the button above to generate a random UUID alias that will look something
  324. like this:
  325. </p>
  326. <p class="mb-4">
  327. <b>86064c92-da41-443e-a2bf-5a7b0247842f@{{ domain }}</b>
  328. </p>
  329. <p>
  330. Useful if you do not wish to include your username in the email as a potential link
  331. between aliases.
  332. </p>
  333. </div>
  334. </div>
  335. <Modal :open="generateAliasModalOpen" @close="generateAliasModalOpen = false">
  336. <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl p-6">
  337. <h2
  338. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  339. >
  340. Generate new UUID alias
  341. </h2>
  342. <p class="mt-4 text-grey-700">
  343. This will generate a new unique alias in the form of
  344. <span class="text-sm block mt-2 font-semibold"
  345. >86064c92-da41-443e-a2bf-5a7b0247842f@{{ domain }}</span
  346. >
  347. </p>
  348. <p class="mt-2 text-grey-700">
  349. Other aliases e.g. alias@{{ subdomain }} are created automatically when they receive their
  350. first email.
  351. </p>
  352. <label for="alias_domain" class="block text-grey-700 text-sm my-2">
  353. Alias Domain:
  354. </label>
  355. <div class="block relative w-full mb-4">
  356. <select
  357. v-model="generateAliasDomain"
  358. id="alias_domain"
  359. class="block appearance-none w-full text-grey-700 bg-grey-100 p-3 pr-8 rounded shadow focus:shadow-outline"
  360. required
  361. >
  362. <option v-for="domainOption in allDomains" :key="domainOption" :value="domainOption">{{
  363. domainOption
  364. }}</option>
  365. </select>
  366. <div
  367. class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"
  368. >
  369. <svg
  370. class="fill-current h-4 w-4"
  371. xmlns="http://www.w3.org/2000/svg"
  372. viewBox="0 0 20 20"
  373. >
  374. <path
  375. d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"
  376. />
  377. </svg>
  378. </div>
  379. </div>
  380. <label for="alias_description" class="block text-grey-700 text-sm my-2">
  381. Description:
  382. </label>
  383. <p v-show="errors.generateAliasDescription" class="mb-3 text-red-500 text-sm">
  384. {{ errors.generateAliasDescription }}
  385. </p>
  386. <input
  387. v-model="generateAliasDescription"
  388. id="alias_description"
  389. type="text"
  390. class="w-full appearance-none bg-grey-100 border border-transparent text-grey-700 focus:outline-none rounded p-3"
  391. :class="errors.generateAliasDescription ? 'border-red-500' : ''"
  392. placeholder="Enter description (optional)..."
  393. autofocus
  394. />
  395. <div class="mt-6">
  396. <button
  397. @click="generateNewAlias"
  398. class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus:outline-none"
  399. :class="generateAliasLoading ? 'cursor-not-allowed' : ''"
  400. :disabled="generateAliasLoading"
  401. >
  402. Generate Alias
  403. <loader v-if="generateAliasLoading" />
  404. </button>
  405. <button
  406. @click="generateAliasModalOpen = false"
  407. 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"
  408. >
  409. Cancel
  410. </button>
  411. </div>
  412. </div>
  413. </Modal>
  414. <Modal :open="editAliasRecipientsModalOpen" @close="closeAliasRecipientsModal">
  415. <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
  416. <h2
  417. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  418. >
  419. Update Alias Recipients
  420. </h2>
  421. <p class="my-4 text-grey-700">
  422. Select the recipients for this alias. You can choose multiple recipients. Leave it empty
  423. if you would like to use the default recipient.
  424. </p>
  425. <multiselect
  426. v-model="aliasRecipientsToEdit"
  427. :options="recipientOptions"
  428. :multiple="true"
  429. :close-on-select="true"
  430. :clear-on-select="false"
  431. :searchable="true"
  432. :max="10"
  433. placeholder="Select recipients"
  434. label="email"
  435. track-by="email"
  436. :preselect-first="false"
  437. :show-labels="false"
  438. >
  439. </multiselect>
  440. <div class="mt-6">
  441. <button
  442. type="button"
  443. @click="editAliasRecipients()"
  444. class="px-4 py-3 text-cyan-900 font-semibold bg-cyan-400 hover:bg-cyan-300 border border-transparent rounded focus:outline-none"
  445. :class="editAliasRecipientsLoading ? 'cursor-not-allowed' : ''"
  446. :disabled="editAliasRecipientsLoading"
  447. >
  448. Update Recipients
  449. <loader v-if="editAliasRecipientsLoading" />
  450. </button>
  451. <button
  452. @click="closeAliasRecipientsModal()"
  453. 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"
  454. >
  455. Cancel
  456. </button>
  457. </div>
  458. </div>
  459. </Modal>
  460. <Modal :open="deleteAliasModalOpen" @close="closeDeleteModal">
  461. <div class="max-w-lg w-full bg-white rounded-lg shadow-2xl px-6 py-6">
  462. <h2
  463. class="font-semibold text-grey-900 text-2xl leading-tight border-b-2 border-grey-100 pb-4"
  464. >
  465. Delete alias
  466. </h2>
  467. <p class="mt-4 text-grey-700">
  468. Are you sure you want to delete this alias? This action cannot be undone. Once deleted,
  469. this alias will <b>not be able to be used again</b> and will bounce any emails sent to it.
  470. </p>
  471. <div class="mt-6">
  472. <button
  473. type="button"
  474. @click="deleteAlias(aliasIdToDelete)"
  475. class="px-4 py-3 text-white font-semibold bg-red-500 hover:bg-red-600 border border-transparent rounded focus:outline-none"
  476. :class="deleteAliasLoading ? 'cursor-not-allowed' : ''"
  477. :disabled="deleteAliasLoading"
  478. >
  479. Delete alias
  480. <loader v-if="deleteAliasLoading" />
  481. </button>
  482. <button
  483. @click="closeDeleteModal"
  484. 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"
  485. >
  486. Cancel
  487. </button>
  488. </div>
  489. </div>
  490. </Modal>
  491. </div>
  492. </template>
  493. <script>
  494. import Modal from './../components/Modal.vue'
  495. import Toggle from './../components/Toggle.vue'
  496. import tippy from 'tippy.js'
  497. import Multiselect from 'vue-multiselect'
  498. export default {
  499. props: {
  500. defaultRecipient: {
  501. type: Object,
  502. required: true,
  503. },
  504. initialAliases: {
  505. type: Array,
  506. required: true,
  507. },
  508. recipientOptions: {
  509. type: Array,
  510. required: true,
  511. },
  512. totalForwarded: {
  513. type: Number,
  514. required: true,
  515. },
  516. totalBlocked: {
  517. type: Number,
  518. required: true,
  519. },
  520. totalReplies: {
  521. type: Number,
  522. required: true,
  523. },
  524. domain: {
  525. type: String,
  526. required: true,
  527. },
  528. subdomain: {
  529. type: String,
  530. required: true,
  531. },
  532. bandwidthMb: {
  533. type: Number,
  534. required: true,
  535. },
  536. month: {
  537. type: String,
  538. required: true,
  539. },
  540. allDomains: {
  541. type: Array,
  542. required: true,
  543. },
  544. },
  545. components: {
  546. Modal,
  547. Toggle,
  548. Multiselect,
  549. },
  550. mounted() {
  551. this.addTooltips()
  552. },
  553. data() {
  554. return {
  555. search: '',
  556. aliasIdToEdit: '',
  557. aliasDescriptionToEdit: '',
  558. aliasIdToDelete: '',
  559. deleteAliasLoading: false,
  560. deleteAliasModalOpen: false,
  561. editAliasRecipientsLoading: false,
  562. editAliasRecipientsModalOpen: false,
  563. generateAliasModalOpen: false,
  564. generateAliasLoading: false,
  565. generateAliasDomain: this.domain,
  566. generateAliasDescription: '',
  567. recipientsAliasToEdit: {},
  568. aliasRecipientsToEdit: [],
  569. columns: [
  570. {
  571. label: 'Created',
  572. field: 'created_at',
  573. globalSearchDisabled: true,
  574. },
  575. {
  576. label: 'Alias',
  577. field: 'email',
  578. },
  579. {
  580. label: 'Recipients',
  581. field: 'recipients',
  582. tdClass: 'text-center',
  583. sortable: true,
  584. sortFn: this.sortRecipients,
  585. globalSearchDisabled: true,
  586. },
  587. {
  588. label: 'Description',
  589. field: 'description',
  590. sortable: false,
  591. hidden: true,
  592. },
  593. {
  594. label: 'Forwarded',
  595. field: 'emails_forwarded',
  596. type: 'number',
  597. tdClass: 'text-center',
  598. globalSearchDisabled: true,
  599. },
  600. {
  601. label: 'Blocked',
  602. field: 'emails_blocked',
  603. type: 'number',
  604. tdClass: 'text-center',
  605. globalSearchDisabled: true,
  606. },
  607. {
  608. label: 'Replies',
  609. field: 'emails_replied',
  610. type: 'number',
  611. tdClass: 'text-center',
  612. globalSearchDisabled: true,
  613. },
  614. {
  615. label: 'Active',
  616. field: 'active',
  617. type: 'boolean',
  618. globalSearchDisabled: true,
  619. },
  620. {
  621. label: '',
  622. field: 'actions',
  623. sortable: false,
  624. globalSearchDisabled: true,
  625. },
  626. ],
  627. rows: this.initialAliases,
  628. errors: {},
  629. }
  630. },
  631. watch: {
  632. aliasIdToEdit: _.debounce(function() {
  633. this.addTooltips()
  634. }, 50),
  635. editAliasRecipientsModalOpen: _.debounce(function() {
  636. this.addTooltips()
  637. }, 50),
  638. },
  639. computed: {
  640. activeUuidAliases() {
  641. return _.filter(this.rows, alias => alias.id === alias.local_part && alias.active)
  642. },
  643. totalActive() {
  644. return _.filter(this.rows, 'active').length
  645. },
  646. totalInactive() {
  647. return _.reject(this.rows, 'active').length
  648. },
  649. },
  650. methods: {
  651. addTooltips() {
  652. tippy('.tooltip', {
  653. arrow: true,
  654. arrowType: 'round',
  655. })
  656. },
  657. debounceToolips: _.debounce(function() {
  658. this.addTooltips()
  659. }, 50),
  660. recipientsTooltip(recipients) {
  661. return _.reduce(recipients, (list, recipient) => list + `${recipient.email}<br>`, '')
  662. },
  663. openDeleteModal(id) {
  664. this.deleteAliasModalOpen = true
  665. this.aliasIdToDelete = id
  666. },
  667. closeDeleteModal() {
  668. this.deleteAliasModalOpen = false
  669. this.aliasIdToDelete = ''
  670. },
  671. deleteAlias(id) {
  672. this.deleteAliasLoading = true
  673. axios
  674. .delete(`/api/v1/aliases/${id}`)
  675. .then(response => {
  676. this.rows = _.reject(this.rows, alias => alias.id === id)
  677. this.deleteAliasModalOpen = false
  678. this.deleteAliasLoading = false
  679. })
  680. .catch(error => {
  681. this.error()
  682. this.deleteAliasModalOpen = false
  683. this.deleteAliasLoading = false
  684. })
  685. },
  686. openAliasRecipientsModal(alias) {
  687. this.editAliasRecipientsModalOpen = true
  688. this.recipientsAliasToEdit = alias
  689. this.aliasRecipientsToEdit = alias.recipients
  690. },
  691. closeAliasRecipientsModal() {
  692. this.editAliasRecipientsModalOpen = false
  693. this.recipientsAliasToEdit = {}
  694. this.aliasRecipientsToEdit = []
  695. },
  696. editAliasRecipients() {
  697. this.editAliasRecipientsLoading = true
  698. axios
  699. .post(
  700. '/api/v1/alias-recipients',
  701. JSON.stringify({
  702. alias_id: this.recipientsAliasToEdit.id,
  703. recipient_ids: _.map(this.aliasRecipientsToEdit, recipient => recipient.id),
  704. }),
  705. {
  706. headers: { 'Content-Type': 'application/json' },
  707. }
  708. )
  709. .then(response => {
  710. let alias = _.find(this.rows, ['id', this.recipientsAliasToEdit.id])
  711. alias.recipients = this.aliasRecipientsToEdit
  712. this.editAliasRecipientsModalOpen = false
  713. this.editAliasRecipientsLoading = false
  714. this.recipientsAliasToEdit = {}
  715. this.aliasRecipientsToEdit = []
  716. this.success('Alias recipients updated')
  717. })
  718. .catch(error => {
  719. this.editAliasRecipientsModalOpen = false
  720. this.editAliasRecipientsLoading = false
  721. this.recipientsAliasToEdit = {}
  722. this.aliasRecipientsToEdit = []
  723. this.error()
  724. })
  725. },
  726. generateNewAlias() {
  727. this.errors = {}
  728. if (this.generateAliasDescription.length > 100) {
  729. return (this.errors.generateAliasDescription = 'Description cannot exceed 100 characters')
  730. }
  731. this.generateAliasLoading = true
  732. axios
  733. .post(
  734. '/api/v1/aliases',
  735. JSON.stringify({
  736. domain: this.generateAliasDomain,
  737. description: this.generateAliasDescription,
  738. }),
  739. {
  740. headers: { 'Content-Type': 'application/json' },
  741. }
  742. )
  743. .then(({ data }) => {
  744. this.generateAliasLoading = false
  745. this.generateAliasDescription = ''
  746. this.rows.push(data.data)
  747. this.generateAliasModalOpen = false
  748. this.success('New alias generated successfully')
  749. })
  750. .catch(error => {
  751. this.generateAliasLoading = false
  752. if (error.response.status === 429) {
  753. this.error('You have reached your hourly limit for creating new aliases')
  754. } else {
  755. this.error()
  756. }
  757. })
  758. },
  759. editAlias(alias) {
  760. if (this.aliasDescriptionToEdit.length > 100) {
  761. return this.error('Description cannot be more than 100 characters')
  762. }
  763. axios
  764. .patch(
  765. `/api/v1/aliases/${alias.id}`,
  766. JSON.stringify({
  767. description: this.aliasDescriptionToEdit,
  768. }),
  769. {
  770. headers: { 'Content-Type': 'application/json' },
  771. }
  772. )
  773. .then(response => {
  774. alias.description = this.aliasDescriptionToEdit
  775. this.aliasIdToEdit = ''
  776. this.aliasDescriptionToEdit = ''
  777. this.success('Alias description updated')
  778. })
  779. .catch(error => {
  780. this.aliasIdToEdit = ''
  781. this.aliasDescriptionToEdit = ''
  782. this.error()
  783. })
  784. },
  785. activateAlias(id) {
  786. axios
  787. .post(
  788. `/api/v1/active-aliases`,
  789. JSON.stringify({
  790. id: id,
  791. }),
  792. {
  793. headers: { 'Content-Type': 'application/json' },
  794. }
  795. )
  796. .then(response => {
  797. //
  798. })
  799. .catch(error => {
  800. this.error()
  801. })
  802. },
  803. deactivateAlias(id) {
  804. axios
  805. .delete(`/api/v1/active-aliases/${id}`)
  806. .then(response => {
  807. //
  808. })
  809. .catch(error => {
  810. this.error()
  811. })
  812. },
  813. getAliasEmail(alias) {
  814. return alias.extension
  815. ? `${alias.local_part}+${alias.extension}@${alias.domain}`
  816. : alias.email
  817. },
  818. getAliasLocalPart(alias) {
  819. return alias.extension ? `${alias.local_part}+${alias.extension}` : alias.local_part
  820. },
  821. sortRecipients(x, y) {
  822. return x.length < y.length ? -1 : x.length > y.length ? 1 : 0
  823. },
  824. has(object, path) {
  825. return _.has(object, path)
  826. },
  827. clipboardSuccess() {
  828. this.success('Copied to clipboard')
  829. },
  830. clipboardError() {
  831. this.error('Could not copy to clipboard')
  832. },
  833. success(text = '') {
  834. this.$notify({
  835. title: 'Success',
  836. text: text,
  837. type: 'success',
  838. })
  839. },
  840. error(text = 'An error has occurred, please try again later') {
  841. this.$notify({
  842. title: 'Error',
  843. text: text,
  844. type: 'error',
  845. })
  846. },
  847. },
  848. }
  849. </script>
  850. <style src="vue-multiselect/dist/vue-multiselect.min.css"></style>