Rules.vue 46 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334
  1. <template>
  2. <div>
  3. <Head :title="$page.component" />
  4. <h1 id="primary-heading" class="sr-only">
  5. {{ $page.component }}
  6. </h1>
  7. <div class="sm:flex sm:items-center mb-6">
  8. <div class="sm:flex-auto">
  9. <h1 class="text-2xl font-semibold text-grey-900">Rules</h1>
  10. <p class="mt-2 text-sm text-grey-700">
  11. A list of all the rules {{ search ? 'found for your search' : 'in your account' }}
  12. <button @click="moreInfoOpen = !moreInfoOpen">
  13. <InformationCircleIcon
  14. class="h-6 w-6 inline-block cursor-pointer text-grey-500"
  15. title="Click for more information"
  16. />
  17. </button>
  18. </p>
  19. </div>
  20. <div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
  21. <button
  22. type="button"
  23. @click="openCreateModal"
  24. class="inline-flex items-center justify-center rounded-md border border-transparent bg-cyan-400 hover:bg-cyan-300 text-cyan-900 px-4 py-2 font-bold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 sm:w-auto"
  25. >
  26. Create Rule
  27. </button>
  28. </div>
  29. </div>
  30. <div v-if="rows.length" class="bg-white shadow">
  31. <div class="vgt-responsive">
  32. <table class="table-auto w-full">
  33. <thead class="border-b border-grey-100 text-grey-400">
  34. <tr>
  35. <th scope="col" class="p-3"></th>
  36. <th scope="col" class="p-3 text-left">Created</th>
  37. <th scope="col" class="p-3 text-left">Name</th>
  38. <th scope="col" class="p-3 text-left">Active</th>
  39. <th scope="col" class="p-3 text-left">
  40. Applied
  41. <span
  42. class="tooltip outline-none"
  43. data-tippy-content="This is the number of times that the rule has been applied. Hover over the count to see when it was last applied."
  44. >
  45. <icon name="info" class="inline-block w-4 h-4 text-grey-300 fill-current" />
  46. </span>
  47. </th>
  48. <th scope="col" class="p-3"></th>
  49. </tr>
  50. </thead>
  51. <draggable
  52. :component-data="{ type: 'transition', name: 'flip-list' }"
  53. v-model="rows"
  54. item-key="id"
  55. tag="tbody"
  56. handle=".handle"
  57. :group="{ name: 'description' }"
  58. ghost-class="ghost"
  59. @change="reorderRules"
  60. @update="debounceToolips"
  61. >
  62. <template #item="{ element }">
  63. <tr class="border-b border-grey-100 h-20">
  64. <td scope="row" class="p-3">
  65. <icon
  66. name="menu"
  67. class="handle block w-6 h-6 text-grey-300 fill-current cursor-pointer"
  68. />
  69. </td>
  70. <td scope="row" class="p-3">
  71. <span
  72. class="tooltip outline-none cursor-default text-sm text-grey-500"
  73. :data-tippy-content="$filters.formatDate(element.created_at)"
  74. >{{ $filters.timeAgo(element.created_at) }}
  75. </span>
  76. </td>
  77. <td scope="row" class="p-3">
  78. <span class="font-medium text-grey-700">{{ element.name }}</span>
  79. </td>
  80. <td scope="row" class="p-3">
  81. <Toggle
  82. v-model="element.active"
  83. @on="activateRule(element.id)"
  84. @off="deactivateRule(element.id)"
  85. />
  86. </td>
  87. <td scope="row" class="p-3">
  88. <span
  89. v-if="element.last_applied"
  90. class="tooltip outline-none cursor-default font-semibold text-indigo-800"
  91. :data-tippy-content="
  92. $filters.timeAgo(element.last_applied) +
  93. ' (' +
  94. $filters.formatDate(element.last_applied) +
  95. ')'
  96. "
  97. >{{ element.applied.toLocaleString() }}
  98. </span>
  99. <span v-else>{{ element.applied.toLocaleString() }} </span>
  100. </td>
  101. <td scope="row" class="p-3 text-right w-0 min-w-fit whitespace-nowrap">
  102. <button
  103. @click="openEditModal(element)"
  104. as="button"
  105. type="button"
  106. class="text-indigo-500 hover:text-indigo-800 font-medium"
  107. >
  108. Edit
  109. </button>
  110. <button
  111. @click="openDeleteModal(element.id)"
  112. as="button"
  113. type="button"
  114. class="text-indigo-500 hover:text-indigo-800 font-medium ml-4"
  115. >
  116. Delete
  117. </button>
  118. </td>
  119. </tr>
  120. </template>
  121. </draggable>
  122. </table>
  123. </div>
  124. </div>
  125. <div v-else-if="search" class="text-center">
  126. <FunnelIcon class="mx-auto h-16 w-16 text-grey-400" />
  127. <h3 class="mt-2 text-lg font-medium text-grey-900">No Rules found for that search</h3>
  128. <p class="mt-1 text-md text-grey-500">Try entering a different search term.</p>
  129. <div class="mt-6">
  130. <Link
  131. :href="route('rules.index')"
  132. type="button"
  133. class="inline-flex items-center rounded-md border border-transparent bg-cyan-400 hover:bg-cyan-300 text-cyan-900 px-4 py-2 text-sm font-medium shadow-sm focus:outline-none"
  134. >
  135. View All Rules
  136. </Link>
  137. </div>
  138. </div>
  139. <div v-else class="text-center">
  140. <FunnelIcon class="mx-auto h-16 w-16 text-grey-400" />
  141. <h3 class="mt-2 text-lg font-medium text-grey-900">No Rules</h3>
  142. <p class="mt-1 text-md text-grey-500">Get started by creating a new rule.</p>
  143. <div class="mt-6">
  144. <button
  145. @click="openCreateModal"
  146. type="button"
  147. class="inline-flex items-center rounded-md border border-transparent bg-cyan-400 hover:bg-cyan-300 text-cyan-900 px-4 py-2 text-sm font-medium shadow-sm focus:outline-none"
  148. >
  149. <PlusIcon class="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
  150. Create a Rule
  151. </button>
  152. </div>
  153. </div>
  154. <Modal
  155. :open="createRuleModalOpen"
  156. @close="createRuleModalOpen = false"
  157. max-width="md:max-w-2xl"
  158. >
  159. <template v-slot:title> Create new rule </template>
  160. <template v-slot:content>
  161. <p class="mt-4 text-grey-700">
  162. Rules work on all emails, including replies and also send froms. New conditions and
  163. actions will be added over time.
  164. </p>
  165. <label for="rule_name" class="block font-medium leading-6 text-grey-600 text-sm my-2">
  166. Name
  167. </label>
  168. <p v-show="errors.ruleName" class="mb-3 text-red-500 text-sm">
  169. {{ errors.ruleName }}
  170. </p>
  171. <input
  172. v-model="createRuleObject.name"
  173. id="rule_name"
  174. type="text"
  175. class="block w-full rounded-md border-0 py-2 pr-10 ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-base sm:leading-6"
  176. :class="errors.ruleName ? 'ring-red-500' : ''"
  177. placeholder="Enter name"
  178. autofocus
  179. />
  180. <fieldset class="border border-cyan-400 p-4 my-4 rounded-sm">
  181. <legend class="px-2 leading-none text-sm">Conditions</legend>
  182. <!-- Loop for conditions -->
  183. <div v-for="(condition, key) in createRuleObject.conditions" :key="key">
  184. <!-- AND/OR operator -->
  185. <div v-if="key !== 0" class="flex justify-center my-2">
  186. <div class="relative">
  187. <select
  188. v-model="createRuleObject.operator"
  189. :id="`create_rule_operator_${key}`"
  190. class="block appearance-none w-full text-grey-700 bg-white p-2 pr-8 rounded shadow focus:ring"
  191. required
  192. >
  193. <option value="AND">AND</option>
  194. <option value="OR">OR</option>
  195. </select>
  196. </div>
  197. </div>
  198. <div class="p-2 w-full bg-grey-100">
  199. <div class="flex">
  200. <div
  201. class="w-full flex flex-col sm:flex-row sm:items-center space-y-2 sm:space-y-0"
  202. >
  203. <span>If</span>
  204. <span class="sm:ml-2">
  205. <div class="relative">
  206. <select
  207. v-model="createRuleObject.conditions[key].type"
  208. :id="`create_rule_condition_types_${key}`"
  209. class="block appearance-none w-full sm:w-32 text-grey-700 bg-white p-2 pr-8 rounded shadow focus:ring"
  210. required
  211. >
  212. <option
  213. v-for="option in conditionTypeOptions"
  214. :key="option.value"
  215. :value="option.value"
  216. >
  217. {{ option.label }}
  218. </option>
  219. </select>
  220. </div>
  221. </span>
  222. <span
  223. v-if="conditionMatchOptions(createRuleObject, key).length"
  224. class="sm:ml-4 flex flex-col sm:flex-row space-y-2 sm:space-y-0"
  225. >
  226. <div class="relative sm:mr-4">
  227. <select
  228. v-model="createRuleObject.conditions[key].match"
  229. :id="`create_rule_condition_matches_${key}`"
  230. class="block appearance-none w-full sm:w-40 text-grey-700 bg-white p-2 pr-8 rounded shadow focus:ring"
  231. required
  232. >
  233. <option
  234. v-for="option in conditionMatchOptions(createRuleObject, key)"
  235. :key="option"
  236. :value="option"
  237. >
  238. {{ option }}
  239. </option>
  240. </select>
  241. </div>
  242. <div class="flex">
  243. <input
  244. v-model="createRuleObject.conditions[key].currentConditionValue"
  245. @keyup.enter="addValueToCondition(createRuleObect, key)"
  246. type="text"
  247. class="w-full appearance-none bg-white border border-transparent rounded-l text-grey-700 focus:outline-none p-2"
  248. :class="errors.ruleConditions ? 'border-red-500' : ''"
  249. placeholder="Enter value"
  250. autofocus
  251. />
  252. <button
  253. @click="addValueToCondition(createRuleObject, key)"
  254. class="p-2 bg-grey-200 rounded-r text-grey-600"
  255. >
  256. Insert
  257. </button>
  258. </div>
  259. </span>
  260. </div>
  261. <div class="flex items-center">
  262. <!-- delete button -->
  263. <icon
  264. v-if="createRuleObject.conditions.length > 1"
  265. name="trash"
  266. class="block ml-4 w-6 h-6 text-grey-300 fill-current cursor-pointer"
  267. @click="deleteCondition(createRuleObject, key)"
  268. />
  269. </div>
  270. </div>
  271. <div class="mt-2 text-left">
  272. <span
  273. v-for="(value, index) in createRuleObject.conditions[key].values"
  274. :key="index"
  275. >
  276. <span class="bg-green-200 text-sm font-semibold rounded-sm pl-1 text-nowrap">
  277. {{ value }}
  278. <icon
  279. name="close"
  280. class="inline-block w-4 h-4 text-grey-900 fill-current cursor-pointer"
  281. @click="createRuleObject.conditions[key].values.splice(index, 1)"
  282. />
  283. </span>
  284. <span
  285. class="mx-1"
  286. v-if="index + 1 !== createRuleObject.conditions[key].values.length"
  287. >
  288. or
  289. </span>
  290. </span>
  291. </div>
  292. </div>
  293. </div>
  294. <!-- add condition button -->
  295. <button
  296. @click="addCondition(createRuleObject)"
  297. class="mt-4 p-2 text-grey-800 bg-white hover:bg-grey-50 border border-grey-100 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
  298. >
  299. Add condition
  300. </button>
  301. <p v-show="errors.ruleConditions" class="mt-2 text-red-500 text-sm">
  302. {{ errors.ruleConditions }}
  303. </p>
  304. </fieldset>
  305. <fieldset class="border border-cyan-400 p-4 my-4 rounded-sm">
  306. <legend class="px-2 leading-none text-sm">Actions</legend>
  307. <!-- Loop for actions -->
  308. <div v-for="(action, key) in createRuleObject.actions" :key="key">
  309. <!-- AND/OR operator -->
  310. <div v-if="key !== 0" class="flex justify-center my-2">
  311. <div class="relative">AND</div>
  312. </div>
  313. <div class="p-2 w-full bg-grey-100">
  314. <div class="flex">
  315. <div
  316. class="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:items-center w-full"
  317. >
  318. <span>Then</span>
  319. <span class="sm:ml-2">
  320. <div class="relative">
  321. <select
  322. v-model="createRuleObject.actions[key].type"
  323. @change="ruleActionChange(createRuleObject.actions[key])"
  324. :id="`rule_action_types_${key}`"
  325. class="w-full block appearance-none text-grey-700 bg-white p-2 pr-8 rounded shadow focus:ring"
  326. required
  327. >
  328. <option
  329. v-for="option in actionTypeOptions"
  330. :key="option.value"
  331. :value="option.value"
  332. >
  333. {{ option.label }}
  334. </option>
  335. </select>
  336. </div>
  337. </span>
  338. <span
  339. v-if="
  340. createRuleObject.actions[key].type === 'subject' ||
  341. createRuleObject.actions[key].type === 'displayFrom'
  342. "
  343. class="sm:ml-4 flex"
  344. >
  345. <div class="flex w-full">
  346. <input
  347. v-model="createRuleObject.actions[key].value"
  348. type="text"
  349. class="w-full appearance-none bg-white border border-transparent rounded text-grey-700 focus:outline-none p-2"
  350. :class="errors.ruleActions ? 'border-red-500' : ''"
  351. placeholder="Enter value"
  352. autofocus
  353. />
  354. </div>
  355. </span>
  356. <span
  357. v-else-if="createRuleObject.actions[key].type === 'banner'"
  358. class="sm:ml-4 flex"
  359. >
  360. <div class="relative sm:mr-4 w-full">
  361. <select
  362. v-model="createRuleObject.actions[key].value"
  363. :id="`create_rule_action_banner_${key}`"
  364. class="w-full block appearance-none sm:w-40 text-grey-700 bg-white p-2 pr-8 rounded shadow focus:ring"
  365. required
  366. >
  367. <option value="top">Top</option>
  368. <option value="bottom">Bottom</option>
  369. <option value="off">Off</option>
  370. </select>
  371. </div>
  372. </span>
  373. </div>
  374. <div class="flex items-center">
  375. <!-- delete button -->
  376. <icon
  377. v-if="createRuleObject.actions.length > 1"
  378. name="trash"
  379. class="block ml-4 w-6 h-6 text-grey-300 fill-current cursor-pointer"
  380. @click="deleteAction(createRuleObject, key)"
  381. />
  382. </div>
  383. </div>
  384. </div>
  385. </div>
  386. <!-- add action button -->
  387. <button
  388. @click="addAction(createRuleObject)"
  389. class="mt-4 p-2 text-grey-800 bg-white hover:bg-grey-50 border border-grey-100 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
  390. >
  391. Add action
  392. </button>
  393. <p v-show="errors.ruleActions" class="mt-2 text-red-500 text-sm">
  394. {{ errors.ruleActions }}
  395. </p>
  396. </fieldset>
  397. <fieldset class="border border-cyan-400 p-4 my-4 rounded-sm">
  398. <legend class="px-2 leading-none text-sm">Apply rule on</legend>
  399. <div class="w-full flex">
  400. <div class="relative flex items-center">
  401. <input
  402. v-model="createRuleObject.forwards"
  403. id="forwards"
  404. name="forwards"
  405. type="checkbox"
  406. class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-grey-300 rounded"
  407. />
  408. <label for="forwards" class="ml-2 text-sm text-grey-700">Forwards</label>
  409. </div>
  410. <div class="relative flex items-center mx-4">
  411. <input
  412. v-model="createRuleObject.replies"
  413. id="replies"
  414. name="replies"
  415. type="checkbox"
  416. class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-grey-300 rounded"
  417. />
  418. <label for="replies" class="ml-2 text-sm text-grey-700">Replies</label>
  419. </div>
  420. <div class="relative flex items-center">
  421. <input
  422. v-model="createRuleObject.sends"
  423. id="sends"
  424. name="sends"
  425. type="checkbox"
  426. class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-grey-300 rounded"
  427. />
  428. <label for="sends" class="ml-2 text-sm text-grey-700">Sends</label>
  429. </div>
  430. </div>
  431. </fieldset>
  432. <div class="mt-6 flex flex-col sm:flex-row space-y-4 sm:space-y-0 sm:space-x-4">
  433. <button
  434. @click="createNewRule"
  435. class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:cursor-not-allowed"
  436. :disabled="createRuleLoading"
  437. >
  438. Create Rule
  439. <loader v-if="createRuleLoading" />
  440. </button>
  441. <button
  442. @click="createRuleModalOpen = false"
  443. class="px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
  444. >
  445. Cancel
  446. </button>
  447. </div>
  448. </template>
  449. </Modal>
  450. <Modal :open="editRuleModalOpen" @close="closeEditModal" max-width="md:max-w-2xl">
  451. <template v-slot:title> Edit rule </template>
  452. <template v-slot:content>
  453. <p class="mt-4 text-grey-700">
  454. Rules work on all emails, including replies and also send froms. New conditions and
  455. actions will be added over time.
  456. </p>
  457. <label for="edit_rule_name" class="block font-medium leading-6 text-grey-600 text-sm my-2">
  458. Name
  459. </label>
  460. <p v-show="errors.ruleName" class="mb-3 text-red-500 text-sm">
  461. {{ errors.ruleName }}
  462. </p>
  463. <input
  464. v-model="editRuleObject.name"
  465. id="edit_rule_name"
  466. type="text"
  467. class="block w-full rounded-md border-0 py-2 pr-10 ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-base sm:leading-6"
  468. :class="errors.ruleName ? 'ring-red-500' : ''"
  469. placeholder="Enter name"
  470. autofocus
  471. />
  472. <fieldset class="border border-cyan-400 p-4 my-4 rounded-sm">
  473. <legend class="px-2 leading-none text-sm">Conditions</legend>
  474. <!-- Loop for conditions -->
  475. <div v-for="(condition, key) in editRuleObject.conditions" :key="key">
  476. <!-- AND/OR operator -->
  477. <div v-if="key !== 0" class="flex justify-center my-2">
  478. <div class="relative">
  479. <select
  480. v-model="editRuleObject.operator"
  481. :id="`edit_rule_operator_${key}`"
  482. class="block appearance-none w-full text-grey-700 bg-white p-2 pr-8 rounded shadow focus:ring"
  483. required
  484. >
  485. <option value="AND">AND</option>
  486. <option value="OR">OR</option>
  487. </select>
  488. </div>
  489. </div>
  490. <div class="p-2 w-full bg-grey-100">
  491. <div class="flex">
  492. <div
  493. class="w-full flex flex-col sm:flex-row sm:items-center space-y-2 sm:space-y-0"
  494. >
  495. <span>If</span>
  496. <span class="sm:ml-2">
  497. <div class="relative">
  498. <select
  499. v-model="editRuleObject.conditions[key].type"
  500. :id="`edit_rule_condition_types_${key}`"
  501. class="block appearance-none w-full sm:w-32 text-grey-700 bg-white p-2 pr-8 rounded shadow focus:ring"
  502. required
  503. >
  504. <option
  505. v-for="option in conditionTypeOptions"
  506. :key="option.value"
  507. :value="option.value"
  508. >
  509. {{ option.label }}
  510. </option>
  511. </select>
  512. </div>
  513. </span>
  514. <span
  515. v-if="conditionMatchOptions(editRuleObject, key).length"
  516. class="sm:ml-4 flex flex-col sm:flex-row space-y-2 sm:space-y-0"
  517. >
  518. <div class="relative sm:mr-4">
  519. <select
  520. v-model="editRuleObject.conditions[key].match"
  521. :id="`edit_rule_condition_matches_${key}`"
  522. class="block appearance-none w-full sm:w-40 text-grey-700 bg-white p-2 pr-8 rounded shadow focus:ring"
  523. required
  524. >
  525. <option
  526. v-for="option in conditionMatchOptions(editRuleObject, key)"
  527. :key="option"
  528. :value="option"
  529. >
  530. {{ option }}
  531. </option>
  532. </select>
  533. </div>
  534. <div class="flex">
  535. <input
  536. v-model="editRuleObject.conditions[key].currentConditionValue"
  537. @keyup.enter="addValueToCondition(editRuleObect, key)"
  538. type="text"
  539. class="w-full appearance-none bg-white border border-transparent rounded-l text-grey-700 focus:outline-none p-2"
  540. :class="errors.ruleConditions ? 'border-red-500' : ''"
  541. placeholder="Enter value"
  542. autofocus
  543. />
  544. <button
  545. @click="addValueToCondition(editRuleObject, key)"
  546. class="p-2 bg-grey-200 rounded-r text-grey-600"
  547. >
  548. Insert
  549. </button>
  550. </div>
  551. </span>
  552. </div>
  553. <div class="flex items-center">
  554. <!-- delete button -->
  555. <icon
  556. v-if="editRuleObject.conditions.length > 1"
  557. name="trash"
  558. class="block ml-4 w-6 h-6 text-grey-300 fill-current cursor-pointer"
  559. @click="deleteCondition(editRuleObject, key)"
  560. />
  561. </div>
  562. </div>
  563. <div class="mt-2 text-left">
  564. <span v-for="(value, index) in editRuleObject.conditions[key].values" :key="index">
  565. <span class="bg-green-200 text-sm font-semibold rounded-sm pl-1 text-nowrap">
  566. {{ value }}
  567. <icon
  568. name="close"
  569. class="inline-block w-4 h-4 text-grey-900 fill-current cursor-pointer"
  570. @click="editRuleObject.conditions[key].values.splice(index, 1)"
  571. />
  572. </span>
  573. <span
  574. class="mx-1"
  575. v-if="index + 1 !== editRuleObject.conditions[key].values.length"
  576. >
  577. or
  578. </span>
  579. </span>
  580. </div>
  581. </div>
  582. </div>
  583. <!-- add condition button -->
  584. <button
  585. @click="addCondition(editRuleObject)"
  586. class="mt-4 p-2 text-grey-800 bg-white hover:bg-grey-50 border border-grey-100 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
  587. >
  588. Add condition
  589. </button>
  590. <p v-show="errors.ruleConditions" class="mt-2 text-red-500 text-sm">
  591. {{ errors.ruleConditions }}
  592. </p>
  593. </fieldset>
  594. <fieldset class="border border-cyan-400 p-4 my-4 rounded-sm">
  595. <legend class="px-2 leading-none text-sm">Actions</legend>
  596. <!-- Loop for actions -->
  597. <div v-for="(action, key) in editRuleObject.actions" :key="key">
  598. <!-- AND/OR operator -->
  599. <div v-if="key !== 0" class="flex justify-center my-2">
  600. <div class="relative">AND</div>
  601. </div>
  602. <div class="p-2 w-full bg-grey-100">
  603. <div class="flex">
  604. <div
  605. class="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:items-center w-full"
  606. >
  607. <span>Then</span>
  608. <span class="sm:ml-2">
  609. <div class="relative">
  610. <select
  611. v-model="editRuleObject.actions[key].type"
  612. @change="ruleActionChange(editRuleObject.actions[key])"
  613. :id="`rule_action_types_${key}`"
  614. class="w-full block appearance-none text-grey-700 bg-white p-2 pr-8 rounded shadow focus:ring"
  615. required
  616. >
  617. <option
  618. v-for="option in actionTypeOptions"
  619. :key="option.value"
  620. :value="option.value"
  621. >
  622. {{ option.label }}
  623. </option>
  624. </select>
  625. </div>
  626. </span>
  627. <span
  628. v-if="
  629. editRuleObject.actions[key].type === 'subject' ||
  630. editRuleObject.actions[key].type === 'displayFrom'
  631. "
  632. class="sm:ml-4 flex"
  633. >
  634. <div class="flex w-full">
  635. <input
  636. v-model="editRuleObject.actions[key].value"
  637. type="text"
  638. class="w-full appearance-none bg-white border border-transparent rounded text-grey-700 focus:outline-none p-2"
  639. :class="errors.ruleActions ? 'border-red-500' : ''"
  640. placeholder="Enter value"
  641. autofocus
  642. />
  643. </div>
  644. </span>
  645. <span
  646. v-else-if="editRuleObject.actions[key].type === 'banner'"
  647. class="sm:ml-4 flex"
  648. >
  649. <div class="relative sm:mr-4 w-full">
  650. <select
  651. v-model="editRuleObject.actions[key].value"
  652. :id="`edit_rule_action_banner_${key}`"
  653. class="w-full block appearance-none sm:w-40 text-grey-700 bg-white p-2 pr-8 rounded shadow focus:ring"
  654. required
  655. >
  656. <option value="top">Top</option>
  657. <option value="bottom">Bottom</option>
  658. <option value="off">Off</option>
  659. </select>
  660. </div>
  661. </span>
  662. </div>
  663. <div class="flex items-center">
  664. <!-- delete button -->
  665. <icon
  666. v-if="editRuleObject.actions.length > 1"
  667. name="trash"
  668. class="block ml-4 w-6 h-6 text-grey-300 fill-current cursor-pointer"
  669. @click="deleteAction(editRuleObject, key)"
  670. />
  671. </div>
  672. </div>
  673. </div>
  674. </div>
  675. <!-- add action button -->
  676. <button
  677. @click="addAction(editRuleObject)"
  678. class="mt-4 p-2 text-grey-800 bg-white hover:bg-grey-50 border border-grey-100 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
  679. >
  680. Add action
  681. </button>
  682. <p v-show="errors.ruleActions" class="mt-2 text-red-500 text-sm">
  683. {{ errors.ruleActions }}
  684. </p>
  685. </fieldset>
  686. <fieldset class="border border-cyan-400 p-4 my-4 rounded-sm">
  687. <legend class="px-2 leading-none text-sm">Apply rule on</legend>
  688. <div class="w-full flex">
  689. <div class="relative flex items-center">
  690. <input
  691. v-model="editRuleObject.forwards"
  692. id="forwards"
  693. name="forwards"
  694. type="checkbox"
  695. class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-grey-300 rounded"
  696. />
  697. <label for="forwards" class="ml-2 text-sm text-grey-700">Forwards</label>
  698. </div>
  699. <div class="relative flex items-center mx-4">
  700. <input
  701. v-model="editRuleObject.replies"
  702. id="replies"
  703. name="replies"
  704. type="checkbox"
  705. class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-grey-300 rounded"
  706. />
  707. <label for="replies" class="ml-2 text-sm text-grey-700">Replies</label>
  708. </div>
  709. <div class="relative flex items-center">
  710. <input
  711. v-model="editRuleObject.sends"
  712. id="sends"
  713. name="sends"
  714. type="checkbox"
  715. class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-grey-300 rounded"
  716. />
  717. <label for="sends" class="ml-2 text-sm text-grey-700">Sends</label>
  718. </div>
  719. </div>
  720. </fieldset>
  721. <div class="mt-6 flex flex-col sm:flex-row space-y-4 sm:space-y-0 sm:space-x-4">
  722. <button
  723. @click="editRule"
  724. class="bg-cyan-400 hover:bg-cyan-300 text-cyan-900 font-bold py-3 px-4 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:cursor-not-allowed"
  725. :disabled="editRuleLoading"
  726. >
  727. Save
  728. <loader v-if="editRuleLoading" />
  729. </button>
  730. <button
  731. @click="closeEditModal"
  732. class="px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
  733. >
  734. Cancel
  735. </button>
  736. </div>
  737. </template>
  738. </Modal>
  739. <Modal :open="deleteRuleModalOpen" @close="closeDeleteModal">
  740. <template v-slot:title> Delete rule </template>
  741. <template v-slot:content>
  742. <p class="mt-4 text-grey-700">Are you sure you want to delete this rule?</p>
  743. <div class="mt-6 flex flex-col sm:flex-row space-y-4 sm:space-y-0 sm:space-x-4">
  744. <button
  745. type="button"
  746. @click="deleteRule(ruleIdToDelete)"
  747. class="px-4 py-3 text-white font-semibold bg-red-500 hover:bg-red-600 border border-transparent rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:cursor-not-allowed"
  748. :disabled="deleteRuleLoading"
  749. >
  750. Delete rule
  751. <loader v-if="deleteRuleLoading" />
  752. </button>
  753. <button
  754. @click="closeDeleteModal"
  755. class="px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
  756. >
  757. Cancel
  758. </button>
  759. </div>
  760. </template>
  761. </Modal>
  762. <Modal :open="moreInfoOpen" @close="moreInfoOpen = false">
  763. <template v-slot:title> More information </template>
  764. <template v-slot:content>
  765. <p class="mt-4 text-grey-700">
  766. Rules can be used to perform different actions if certain conditions are met.
  767. </p>
  768. <p class="mt-4 text-grey-700">
  769. For example you could create a rule that checks if the alias is for your custom domain and
  770. if so then to replace the email subject.
  771. </p>
  772. <p class="mt-4 text-grey-700">
  773. You can choose to apply rules on forwards, replies and/or sends.
  774. </p>
  775. <p class="mt-4 text-grey-700">
  776. Rules are applied in the order displayed on this page from top to bottom. You can re-order
  777. your rules by dragging them using the icon on the left of each row.
  778. </p>
  779. <div class="mt-6 flex flex-col">
  780. <button
  781. @click="moreInfoOpen = false"
  782. class="px-4 py-3 text-grey-800 font-semibold bg-white hover:bg-grey-50 border border-grey-100 rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
  783. >
  784. Close
  785. </button>
  786. </div>
  787. </template>
  788. </Modal>
  789. </div>
  790. </template>
  791. <script setup>
  792. import { onMounted, ref, computed } from 'vue'
  793. import { Head, Link } from '@inertiajs/vue3'
  794. import Modal from '../Components/Modal.vue'
  795. import Toggle from '../Components/Toggle.vue'
  796. import { roundArrow } from 'tippy.js'
  797. import tippy from 'tippy.js'
  798. import draggable from 'vuedraggable'
  799. import { notify } from '@kyvg/vue3-notification'
  800. import { InformationCircleIcon, FunnelIcon } from '@heroicons/vue/24/outline'
  801. import { PlusIcon } from '@heroicons/vue/20/solid'
  802. const props = defineProps({
  803. initialRows: {
  804. type: Array,
  805. required: true,
  806. },
  807. search: {
  808. type: String,
  809. },
  810. })
  811. const rows = ref(props.initialRows)
  812. const editRuleObject = ref({})
  813. const ruleIdToDelete = ref('')
  814. const deleteRuleLoading = ref(false)
  815. const deleteRuleModalOpen = ref(false)
  816. const createRuleModalOpen = ref(false)
  817. const editRuleModalOpen = ref(false)
  818. const moreInfoOpen = ref(false)
  819. const createRuleLoading = ref(false)
  820. const editRuleLoading = ref(false)
  821. const createRuleObject = ref({
  822. name: '',
  823. conditions: [
  824. {
  825. type: 'select',
  826. match: 'contains',
  827. values: [],
  828. },
  829. ],
  830. actions: [
  831. {
  832. type: 'select',
  833. value: '',
  834. },
  835. ],
  836. operator: 'AND',
  837. forwards: false,
  838. replies: false,
  839. sends: false,
  840. })
  841. const tippyInstance = ref(null)
  842. const errors = ref({})
  843. const conditionTypeOptions = [
  844. {
  845. value: 'select',
  846. label: 'Select',
  847. },
  848. {
  849. value: 'sender',
  850. label: 'the sender',
  851. },
  852. {
  853. value: 'subject',
  854. label: 'the subject',
  855. },
  856. {
  857. value: 'alias',
  858. label: 'the alias',
  859. },
  860. ]
  861. const actionTypeOptions = [
  862. {
  863. value: 'select',
  864. label: 'Select',
  865. },
  866. {
  867. value: 'subject',
  868. label: 'replace the subject with',
  869. },
  870. {
  871. value: 'displayFrom',
  872. label: 'replace the "from name" with',
  873. },
  874. {
  875. value: 'encryption',
  876. label: 'turn PGP encryption off',
  877. },
  878. {
  879. value: 'banner',
  880. label: 'set the banner information location to',
  881. },
  882. {
  883. value: 'block',
  884. label: 'block the email',
  885. },
  886. ]
  887. const indexToHuman = {
  888. 0: 'first',
  889. 1: 'second',
  890. 2: 'third',
  891. 3: 'forth',
  892. 4: 'fifth',
  893. }
  894. onMounted(() => {
  895. addTooltips()
  896. })
  897. const activeRules = () => {
  898. return _.filter(rows.value, rule => rule.active)
  899. }
  900. const rowsIds = computed(() => {
  901. return _.map(rows.value, row => row.id)
  902. })
  903. const addTooltips = () => {
  904. if (tippyInstance.value) {
  905. _.each(tippyInstance.value, instance => instance.destroy())
  906. }
  907. tippyInstance.value = tippy('.tooltip', {
  908. arrow: roundArrow,
  909. allowHTML: true,
  910. })
  911. }
  912. const debounceToolips = _.debounce(function () {
  913. addTooltips()
  914. }, 50)
  915. const createNewRule = () => {
  916. errors.value = {}
  917. if (!createRuleObject.value.name.length) {
  918. return (errors.value.ruleName = 'Please enter a rule name')
  919. }
  920. if (createRuleObject.value.name.length > 50) {
  921. return (errors.value.ruleName = 'Rule name cannot exceed 50 characters')
  922. }
  923. Object.entries(createRuleObject.value.conditions).forEach(([key, condition]) => {
  924. if (!condition.values.length) {
  925. return (errors.value.ruleConditions = `You must add some values for the ${indexToHuman[key]} condition, make sure to click "Insert"`)
  926. }
  927. })
  928. if (errors.value.ruleConditions) {
  929. return
  930. }
  931. Object.entries(createRuleObject.value.actions).forEach(([key, action]) => {
  932. if (!action.value && action.value !== false) {
  933. return (errors.value.ruleActions = `You must add a value for the ${indexToHuman[key]} action`)
  934. }
  935. })
  936. if (errors.value.ruleActions) {
  937. return
  938. }
  939. createRuleLoading.value = true
  940. axios
  941. .post(
  942. '/api/v1/rules',
  943. JSON.stringify({
  944. name: createRuleObject.value.name,
  945. conditions: createRuleObject.value.conditions,
  946. actions: createRuleObject.value.actions,
  947. operator: createRuleObject.value.operator,
  948. forwards: createRuleObject.value.forwards,
  949. replies: createRuleObject.value.replies,
  950. sends: createRuleObject.value.sends,
  951. }),
  952. {
  953. headers: { 'Content-Type': 'application/json' },
  954. },
  955. )
  956. .then(({ data }) => {
  957. createRuleLoading.value = false
  958. resetCreateRuleObject()
  959. rows.value.push(data.data)
  960. createRuleModalOpen.value = false
  961. debounceToolips()
  962. reorderRules(false)
  963. successMessage('New rule created successfully')
  964. })
  965. .catch(error => {
  966. createRuleLoading.value = false
  967. if (error.response.status === 403) {
  968. errorMessage(error.response.data)
  969. } else if (error.response.data) {
  970. errorMessage(Object.entries(error.response.data.errors)[0][1][0])
  971. } else {
  972. errorMessage()
  973. }
  974. })
  975. }
  976. const editRule = () => {
  977. errors.value = {}
  978. if (!editRuleObject.value.name.length) {
  979. return (errors.value.ruleName = 'Please enter a rule name')
  980. }
  981. if (editRuleObject.value.name.length > 50) {
  982. return (errors.value.ruleName = 'Rule name cannot exceed 50 characters')
  983. }
  984. Object.entries(editRuleObject.value.conditions).forEach(([key, condition]) => {
  985. if (!condition.values.length) {
  986. return (errors.value.ruleConditions = `You must add some values for the ${indexToHuman[key]} condition, make sure to click "Insert"`)
  987. }
  988. })
  989. if (errors.value.ruleConditions) {
  990. return
  991. }
  992. Object.entries(editRuleObject.value.actions).forEach(([key, action]) => {
  993. if (!action.value && action.value !== false) {
  994. return (errors.value.ruleActions = `You must add a value for the ${indexToHuman[key]} action`)
  995. }
  996. })
  997. if (errors.value.ruleActions) {
  998. return
  999. }
  1000. editRuleLoading.value = true
  1001. axios
  1002. .patch(
  1003. `/api/v1/rules/${editRuleObject.value.id}`,
  1004. JSON.stringify({
  1005. name: editRuleObject.value.name,
  1006. conditions: editRuleObject.value.conditions,
  1007. actions: editRuleObject.value.actions,
  1008. operator: editRuleObject.value.operator,
  1009. forwards: editRuleObject.value.forwards,
  1010. replies: editRuleObject.value.replies,
  1011. sends: editRuleObject.value.sends,
  1012. }),
  1013. {
  1014. headers: { 'Content-Type': 'application/json' },
  1015. },
  1016. )
  1017. .then(response => {
  1018. let rule = _.find(rows.value, ['id', editRuleObject.value.id])
  1019. editRuleLoading.value = false
  1020. rule.name = editRuleObject.value.name
  1021. rule.conditions = editRuleObject.value.conditions
  1022. rule.actions = editRuleObject.value.actions
  1023. rule.operator = editRuleObject.value.operator
  1024. rule.forwards = editRuleObject.value.forwards
  1025. rule.replies = editRuleObject.value.replies
  1026. rule.sends = editRuleObject.value.sends
  1027. closeEditModal()
  1028. successMessage('Rule successfully updated')
  1029. })
  1030. .catch(error => {
  1031. editRuleLoading.value = false
  1032. if (error.response.data) {
  1033. errorMessage(Object.entries(error.response.data.errors)[0][1][0])
  1034. } else {
  1035. errorMessage()
  1036. }
  1037. })
  1038. }
  1039. const deleteRule = id => {
  1040. deleteRuleLoading.value = true
  1041. axios
  1042. .delete(`/api/v1/rules/${id}`)
  1043. .then(response => {
  1044. rows.value = _.reject(rows.value, rule => rule.id === id)
  1045. deleteRuleModalOpen.value = false
  1046. deleteRuleLoading.value = false
  1047. })
  1048. .catch(error => {
  1049. errorMessage()
  1050. deleteRuleModalOpen.value = false
  1051. deleteRuleLoading.value = false
  1052. })
  1053. }
  1054. const activateRule = id => {
  1055. axios
  1056. .post(
  1057. `/api/v1/active-rules`,
  1058. JSON.stringify({
  1059. id: id,
  1060. }),
  1061. {
  1062. headers: { 'Content-Type': 'application/json' },
  1063. },
  1064. )
  1065. .then(response => {
  1066. //
  1067. })
  1068. .catch(error => {
  1069. if (error.response !== undefined) {
  1070. errorMessage(error.response.data)
  1071. } else {
  1072. errorMessage()
  1073. }
  1074. })
  1075. }
  1076. const deactivateRule = id => {
  1077. axios
  1078. .delete(`/api/v1/active-rules/${id}`)
  1079. .then(response => {
  1080. //
  1081. })
  1082. .catch(error => {
  1083. if (error.response !== undefined) {
  1084. errorMessage(error.response.data)
  1085. } else {
  1086. errorMessage()
  1087. }
  1088. })
  1089. }
  1090. const reorderRules = (displaySuccess = true) => {
  1091. axios
  1092. .post(
  1093. `/api/v1/reorder-rules`,
  1094. JSON.stringify({
  1095. ids: rowsIds.value,
  1096. }),
  1097. {
  1098. headers: { 'Content-Type': 'application/json' },
  1099. },
  1100. )
  1101. .then(response => {
  1102. if (displaySuccess) {
  1103. successMessage('Rule order successfully updated')
  1104. }
  1105. })
  1106. .catch(error => {
  1107. if (error.response !== undefined) {
  1108. errorMessage(error.response.data)
  1109. } else {
  1110. errorMessage()
  1111. }
  1112. })
  1113. }
  1114. const conditionMatchOptions = (object, key) => {
  1115. if (_.includes(['sender', 'subject', 'alias'], object.conditions[key].type)) {
  1116. return [
  1117. 'contains',
  1118. 'does not contain',
  1119. 'is exactly',
  1120. 'is not',
  1121. 'starts with',
  1122. 'does not start with',
  1123. 'ends with',
  1124. 'does not end with',
  1125. ]
  1126. }
  1127. return []
  1128. }
  1129. const addCondition = object => {
  1130. if (object.conditions.length >= 5) {
  1131. return (errors.value.ruleConditions = `You cannot add more than 5 conditions per rule`)
  1132. }
  1133. object.conditions.push({
  1134. type: 'select',
  1135. match: 'contains',
  1136. values: [],
  1137. })
  1138. }
  1139. const deleteCondition = (object, key) => {
  1140. object.conditions.splice(key, 1)
  1141. }
  1142. const addValueToCondition = (object, key) => {
  1143. if (object.conditions[key].values.length >= 10) {
  1144. return (errors.value.ruleConditions = `You cannot add more than 10 values per condition`)
  1145. }
  1146. if (object.conditions[key].currentConditionValue) {
  1147. object.conditions[key].values.push(object.conditions[key].currentConditionValue)
  1148. }
  1149. // Reset current conditon value input
  1150. object.conditions[key].currentConditionValue = ''
  1151. }
  1152. const addAction = object => {
  1153. if (object.actions.length >= 5) {
  1154. return (errors.value.ruleActions = `You cannot add more than 5 actions per rule`)
  1155. }
  1156. object.actions.push({
  1157. type: 'select',
  1158. value: '',
  1159. })
  1160. }
  1161. const deleteAction = (object, key) => {
  1162. object.actions.splice(key, 1)
  1163. }
  1164. const resetCreateRuleObject = () => {
  1165. createRuleObject.value = {
  1166. name: '',
  1167. conditions: [
  1168. {
  1169. type: 'select',
  1170. match: 'contains',
  1171. values: [],
  1172. },
  1173. ],
  1174. actions: [
  1175. {
  1176. type: 'select',
  1177. value: '',
  1178. },
  1179. ],
  1180. operator: 'AND',
  1181. forwards: false,
  1182. replies: false,
  1183. sends: false,
  1184. }
  1185. }
  1186. const ruleActionChange = action => {
  1187. if (action.type === 'subject' || action.type === 'displayFrom' || action.type === 'select') {
  1188. action.value = ''
  1189. } else if (action.type === 'encryption') {
  1190. action.value = false
  1191. } else if (action.type === 'banner') {
  1192. action.value = 'top'
  1193. } else if (action.type === 'block') {
  1194. action.value = true
  1195. }
  1196. }
  1197. const openCreateModal = () => {
  1198. errors.value = {}
  1199. createRuleModalOpen.value = true
  1200. }
  1201. const openDeleteModal = id => {
  1202. deleteRuleModalOpen.value = true
  1203. ruleIdToDelete.value = id
  1204. }
  1205. const closeDeleteModal = () => {
  1206. deleteRuleModalOpen.value = false
  1207. _.delay(() => (ruleIdToDelete.value = ''), 300)
  1208. }
  1209. const openEditModal = rule => {
  1210. errors.value = {}
  1211. editRuleModalOpen.value = true
  1212. editRuleObject.value = _.cloneDeep(rule)
  1213. }
  1214. const closeEditModal = () => {
  1215. editRuleModalOpen.value = false
  1216. _.delay(() => (editRuleObject.value = {}), 300)
  1217. }
  1218. const successMessage = (text = '') => {
  1219. notify({
  1220. title: 'Success',
  1221. text: text,
  1222. type: 'success',
  1223. })
  1224. }
  1225. const errorMessage = (text = 'An error has occurred, please try again later') => {
  1226. notify({
  1227. title: 'Error',
  1228. text: text,
  1229. type: 'error',
  1230. })
  1231. }
  1232. </script>
  1233. <style>
  1234. .ghost {
  1235. opacity: 0.5;
  1236. background: #c8ebfb;
  1237. }
  1238. </style>