AdvancedTable.vue 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508
  1. <template>
  2. <div>
  3. <div
  4. class="table-outer-container"
  5. @mousemove="columnResizingMouseMove($event)"
  6. >
  7. <div class="table-header">
  8. <div>
  9. <tippy
  10. v-if="filters.length > 0"
  11. :touch="true"
  12. :interactive="true"
  13. placement="bottom"
  14. theme="search"
  15. ref="search"
  16. trigger="click"
  17. >
  18. <button class="button is-info">
  19. <i class="material-icons icon-with-button"
  20. >filter_list</i
  21. >
  22. Filters
  23. </button>
  24. <template #content>
  25. <div class="control is-grouped input-with-button">
  26. <p class="control select is-expanded">
  27. <select v-model="addFilterValue">
  28. <option
  29. v-for="type in filters"
  30. :key="type.name"
  31. :value="type"
  32. >
  33. {{ type.displayName }}
  34. </option>
  35. </select>
  36. </p>
  37. <p class="control">
  38. <button
  39. :disabled="!addFilterValue"
  40. class="button material-icons is-success"
  41. @click="addFilterItem()"
  42. >
  43. control_point
  44. </button>
  45. </p>
  46. </div>
  47. <div
  48. v-for="(filter, index) in editingFilters"
  49. :key="`filter-${index}`"
  50. class="
  51. advanced-filter
  52. control
  53. is-grouped is-expanded
  54. "
  55. >
  56. <div class="control select">
  57. <select
  58. v-model="filter.filter"
  59. @change="changeFilterType(index)"
  60. >
  61. <option
  62. v-for="type in filters"
  63. :key="type.name"
  64. :value="type"
  65. >
  66. {{ type.displayName }}
  67. </option>
  68. </select>
  69. </div>
  70. <div class="control select">
  71. <select
  72. v-model="filter.filterType"
  73. :disabled="!filter.filterType"
  74. >
  75. <option
  76. v-for="filterType in filterTypes(
  77. filter.filter
  78. )"
  79. :key="filterType.name"
  80. :value="filterType.name"
  81. :selected="
  82. filter.filter
  83. .defaultFilterType ===
  84. filterType.name
  85. "
  86. >
  87. {{ filterType.displayName }}
  88. </option>
  89. </select>
  90. </div>
  91. <p class="control is-expanded">
  92. <input
  93. v-model="filter.data"
  94. class="input"
  95. type="text"
  96. placeholder="Search value"
  97. :disabled="!filter.filterType"
  98. @keydown.enter="applyFilterAndGetData()"
  99. />
  100. </p>
  101. <div class="control">
  102. <button
  103. class="button material-icons is-danger"
  104. @click="removeFilterItem(index)"
  105. >
  106. remove_circle_outline
  107. </button>
  108. </div>
  109. </div>
  110. <div
  111. v-if="editingFilters.length > 0"
  112. class="control is-expanded is-grouped"
  113. >
  114. <label class="control label"
  115. >Filter operator</label
  116. >
  117. <div class="control select is-expanded">
  118. <select v-model="filterOperator">
  119. <option
  120. v-for="operator in filterOperators"
  121. :key="operator.name"
  122. :value="operator.name"
  123. >
  124. {{ operator.displayName }}
  125. </option>
  126. </select>
  127. </div>
  128. </div>
  129. <div
  130. class="advanced-filter-bottom"
  131. v-if="editingFilters.length > 0"
  132. >
  133. <div class="control is-expanded">
  134. <button
  135. class="button is-info"
  136. @click="applyFilterAndGetData()"
  137. >
  138. <i
  139. class="
  140. material-icons
  141. icon-with-button
  142. "
  143. >filter_list</i
  144. >
  145. Apply filters
  146. </button>
  147. </div>
  148. </div>
  149. <div
  150. class="advanced-filter-bottom"
  151. v-else-if="editingFilters.length === 0"
  152. >
  153. <div class="control is-expanded">
  154. <button
  155. class="button is-info"
  156. @click="applyFilterAndGetData()"
  157. >
  158. <i
  159. class="
  160. material-icons
  161. icon-with-button
  162. "
  163. >filter_list</i
  164. >
  165. Apply filters
  166. </button>
  167. </div>
  168. </div>
  169. </template>
  170. </tippy>
  171. <tippy
  172. v-if="appliedFilters.length > 0"
  173. :touch="true"
  174. :interactive="true"
  175. theme="info"
  176. ref="activeFilters"
  177. >
  178. <div class="filters-indicator">
  179. {{ appliedFilters.length }}
  180. <i class="material-icons" @click.prevent="true"
  181. >filter_list</i
  182. >
  183. </div>
  184. <template #content>
  185. <p
  186. v-for="(filter, index) in appliedFilters"
  187. :key="`filter-${index}`"
  188. >
  189. {{ filter.filter.displayName }}
  190. {{
  191. appliedFilters.length === 1 &&
  192. filterOperator === "nor"
  193. ? "not"
  194. : ""
  195. }}
  196. {{ filter.filterType }} "{{ filter.data }}"
  197. {{
  198. appliedFilters.length === index + 1
  199. ? ""
  200. : filterOperator
  201. }}
  202. </p>
  203. </template>
  204. </tippy>
  205. <i
  206. v-else
  207. class="filters-indicator material-icons"
  208. content="No active filters"
  209. v-tippy="{ theme: 'info' }"
  210. >
  211. filter_list_off
  212. </i>
  213. </div>
  214. <div>
  215. <tippy
  216. v-if="hidableSortedColumns.length > 0"
  217. :touch="true"
  218. :interactive="true"
  219. placement="bottom"
  220. theme="dropdown"
  221. ref="editColumns"
  222. trigger="click"
  223. >
  224. <a class="button is-info" @click.prevent="true">
  225. <i class="material-icons icon-with-button">tune</i>
  226. Columns
  227. </a>
  228. <template #content>
  229. <draggable
  230. item-key="name"
  231. v-model="orderedColumns"
  232. v-bind="columnDragOptions"
  233. tag="div"
  234. draggable=".item-draggable"
  235. class="nav-dropdown-items"
  236. @change="columnOrderChanged"
  237. >
  238. <template #item="{ element: column }">
  239. <button
  240. v-if="
  241. column.name !== 'select' &&
  242. column.name !== 'placeholder'
  243. "
  244. :class="{
  245. sortable: column.sortable,
  246. 'item-draggable': column.draggable,
  247. 'nav-item': true
  248. }"
  249. @click.prevent="
  250. toggleColumnVisibility(column)
  251. "
  252. >
  253. <p
  254. class="
  255. control
  256. is-expanded
  257. checkbox-control
  258. "
  259. >
  260. <label class="switch">
  261. <input
  262. v-if="column.hidable"
  263. type="checkbox"
  264. :id="index"
  265. :checked="
  266. shownColumns.indexOf(
  267. column.name
  268. ) !== -1
  269. "
  270. @click="
  271. toggleColumnVisibility(
  272. column
  273. )
  274. "
  275. />
  276. <span
  277. :class="{
  278. slider: true,
  279. round: true,
  280. disabled:
  281. !column.hidable
  282. }"
  283. ></span>
  284. </label>
  285. <label :for="index">
  286. <span></span>
  287. <p>{{ column.displayName }}</p>
  288. </label>
  289. </p>
  290. </button>
  291. </template>
  292. </draggable>
  293. </template>
  294. </tippy>
  295. </div>
  296. </div>
  297. <div class="table-container">
  298. <table class="table">
  299. <thead>
  300. <draggable
  301. item-key="name"
  302. v-model="orderedColumns"
  303. v-bind="columnDragOptions"
  304. tag="tr"
  305. draggable=".item-draggable"
  306. @change="columnOrderChanged"
  307. >
  308. <template #item="{ element: column }">
  309. <th
  310. v-if="
  311. !(
  312. column.name === 'select' &&
  313. data.length === 0
  314. ) &&
  315. shownColumns.indexOf(column.name) !== -1
  316. "
  317. :class="{
  318. sortable: column.sortable,
  319. 'item-draggable': column.draggable
  320. }"
  321. :style="{
  322. minWidth: Number.isNaN(column.minWidth)
  323. ? column.minWidth
  324. : `${column.minWidth}px`,
  325. width: Number.isNaN(column.width)
  326. ? column.width
  327. : `${column.width}px`,
  328. maxWidth: Number.isNaN(column.maxWidth)
  329. ? column.maxWidth
  330. : `${column.maxWidth}px`
  331. }"
  332. >
  333. <p
  334. v-if="column.name === 'select'"
  335. class="checkbox"
  336. >
  337. <input
  338. type="checkbox"
  339. :checked="
  340. data.length ===
  341. selectedRows.length
  342. "
  343. @click="toggleAllRows()"
  344. />
  345. </p>
  346. <div v-else>
  347. <span>
  348. {{ column.displayName }}
  349. </span>
  350. <span
  351. v-if="column.sortable"
  352. :content="`Sort by ${column.displayName}`"
  353. v-tippy
  354. >
  355. <span
  356. v-if="
  357. !sort[column.sortProperty]
  358. "
  359. class="material-icons"
  360. @click="changeSort(column)"
  361. >
  362. unfold_more
  363. </span>
  364. <span
  365. v-if="
  366. sort[
  367. column.sortProperty
  368. ] === 'ascending'
  369. "
  370. class="material-icons active"
  371. @click="changeSort(column)"
  372. >
  373. expand_more
  374. </span>
  375. <span
  376. v-if="
  377. sort[
  378. column.sortProperty
  379. ] === 'descending'
  380. "
  381. class="material-icons active"
  382. @click="changeSort(column)"
  383. >
  384. expand_less
  385. </span>
  386. </span>
  387. </div>
  388. <div
  389. class="resizer"
  390. v-if="column.resizable"
  391. @mousedown.prevent.stop="
  392. columnResizingMouseDown(
  393. column,
  394. $event
  395. )
  396. "
  397. @mouseup="columnResizingMouseUp()"
  398. @dblclick="columnResetWidth(column)"
  399. ></div>
  400. </th>
  401. </template>
  402. </draggable>
  403. </thead>
  404. <tbody>
  405. <tr
  406. v-for="(item, itemIndex) in data"
  407. :key="item._id"
  408. :class="{
  409. selected: item.selected,
  410. highlighted: item.highlighted
  411. }"
  412. >
  413. <td
  414. v-for="column in sortedFilteredColumns"
  415. :key="`${item._id}-${column.name}`"
  416. >
  417. <slot
  418. :name="`column-${column.name}`"
  419. :item="item"
  420. v-if="
  421. column.properties.length === 0 ||
  422. column.properties.every(
  423. property =>
  424. item[property] !== undefined
  425. )
  426. "
  427. ></slot>
  428. <p
  429. class="checkbox"
  430. v-if="column.name === 'select'"
  431. >
  432. <input
  433. type="checkbox"
  434. :checked="item.selected"
  435. @click="
  436. toggleSelectedRow(itemIndex, $event)
  437. "
  438. />
  439. </p>
  440. <div
  441. class="resizer"
  442. v-if="column.resizable"
  443. @mousedown.prevent.stop="
  444. columnResizingMouseDown(column, $event)
  445. "
  446. @mouseup="columnResizingMouseUp()"
  447. @dblclick="columnResetWidth(column)"
  448. ></div>
  449. </td>
  450. </tr>
  451. </tbody>
  452. </table>
  453. </div>
  454. <div class="table-footer">
  455. <div class="page-controls">
  456. <button
  457. :class="{ disabled: page === 1 }"
  458. class="button is-primary material-icons"
  459. :disabled="page === 1"
  460. @click="changePage(1)"
  461. content="First Page"
  462. v-tippy
  463. >
  464. skip_previous
  465. </button>
  466. <button
  467. :class="{ disabled: page === 1 }"
  468. class="button is-primary material-icons"
  469. :disabled="page === 1"
  470. @click="changePage(page - 1)"
  471. content="Previous Page"
  472. v-tippy
  473. >
  474. fast_rewind
  475. </button>
  476. <p>Page {{ page }} / {{ lastPage }}</p>
  477. <button
  478. :class="{ disabled: page === lastPage }"
  479. class="button is-primary material-icons"
  480. :disabled="page === lastPage"
  481. @click="changePage(page + 1)"
  482. content="Next Page"
  483. v-tippy
  484. >
  485. fast_forward
  486. </button>
  487. <button
  488. :class="{ disabled: page === lastPage }"
  489. class="button is-primary material-icons"
  490. :disabled="page === lastPage"
  491. @click="changePage(lastPage)"
  492. content="Last Page"
  493. v-tippy
  494. >
  495. skip_next
  496. </button>
  497. </div>
  498. <div class="page-size">
  499. <div class="control">
  500. <label class="label">Items per page</label>
  501. <p class="control select">
  502. <select
  503. v-model.number="pageSize"
  504. @change="changePageSize()"
  505. >
  506. <option value="10">10</option>
  507. <option value="25">25</option>
  508. <option value="50">50</option>
  509. <option value="100">100</option>
  510. <option value="250">250</option>
  511. <option value="500">500</option>
  512. <option value="1000">1000</option>
  513. </select>
  514. </p>
  515. </div>
  516. </div>
  517. </div>
  518. </div>
  519. <div
  520. v-if="selectedRows.length > 0"
  521. class="bulk-popup"
  522. :style="{
  523. top: bulkPopup.top + 'px',
  524. left: bulkPopup.left + 'px'
  525. }"
  526. >
  527. <button
  528. class="button is-primary"
  529. :content="
  530. selectedRows.length === 1
  531. ? `${selectedRows.length} row selected`
  532. : `${selectedRows.length} rows selected`
  533. "
  534. v-tippy="{ theme: 'info' }"
  535. >
  536. {{ selectedRows.length }}
  537. </button>
  538. <slot name="bulk-actions" :item="selectedRows" />
  539. <div class="right">
  540. <slot name="bulk-actions-right" :item="selectedRows" />
  541. <span
  542. class="material-icons drag-icon"
  543. @mousedown.left="onDragBox"
  544. @dblclick="resetBulkActionsPosition()"
  545. >
  546. drag_indicator
  547. </span>
  548. </div>
  549. </div>
  550. </div>
  551. </template>
  552. <script>
  553. import { mapGetters } from "vuex";
  554. import draggable from "vuedraggable";
  555. import Toast from "toasters";
  556. import ws from "@/ws";
  557. export default {
  558. components: {
  559. draggable
  560. },
  561. props: {
  562. /*
  563. Column properties:
  564. name: Unique lowercase name
  565. displayName: Nice name for the column header
  566. properties: The properties this column needs to show data
  567. sortable: Boolean for whether the order of a particular column can be changed
  568. sortProperty: The property the backend will sort on if this column gets sorted, e.g. title
  569. hidable: Boolean for whether a column can be hidden
  570. defaultVisibility: Default visibility for a column, either "shown" or "hidden"
  571. draggable: Boolean for whether a column can be dragged/reordered,
  572. resizable: Boolean for whether a column can be resized
  573. minWidth: Minimum width of column, e.g. 50px
  574. width: Width of column, e.g. 100px
  575. maxWidth: Maximum width of column, e.g. 150px
  576. */
  577. columnDefault: { type: Object, default: () => {} },
  578. columns: { type: Array, default: null },
  579. filters: { type: Array, default: null },
  580. dataAction: { type: String, default: null },
  581. name: { type: String, default: null }
  582. },
  583. data() {
  584. return {
  585. page: 1,
  586. pageSize: 10,
  587. data: [],
  588. count: 0, // TODO Rename
  589. sort: {},
  590. orderedColumns: [],
  591. shownColumns: [],
  592. columnDragOptions() {
  593. return {
  594. animation: 200,
  595. group: "columns",
  596. disabled: false,
  597. ghostClass: "draggable-list-ghost",
  598. filter: ".ignore-elements",
  599. fallbackTolerance: 50
  600. };
  601. },
  602. editingFilters: [],
  603. appliedFilters: [],
  604. filterOperator: "or",
  605. appliedFilterOperator: "or",
  606. filterOperators: [
  607. {
  608. name: "or",
  609. displayName: "OR"
  610. },
  611. {
  612. name: "and",
  613. displayName: "AND"
  614. },
  615. {
  616. name: "nor",
  617. displayName: "NOR"
  618. }
  619. ],
  620. resizing: {},
  621. allFilterTypes: {
  622. contains: {
  623. name: "contains",
  624. displayName: "Contains"
  625. },
  626. exact: {
  627. name: "exact",
  628. displayName: "Exact"
  629. },
  630. regex: {
  631. name: "regex",
  632. displayName: "Regex"
  633. }
  634. },
  635. bulkPopup: {
  636. top: 0,
  637. left: 0,
  638. pos1: 0,
  639. pos2: 0,
  640. pos3: 0,
  641. pos4: 0
  642. },
  643. addFilterValue: null
  644. };
  645. },
  646. computed: {
  647. properties() {
  648. return Array.from(
  649. new Set(
  650. this.sortedFilteredColumns.flatMap(
  651. column => column.properties
  652. )
  653. )
  654. );
  655. },
  656. lastPage() {
  657. return Math.ceil(this.count / this.pageSize);
  658. },
  659. sortedFilteredColumns() {
  660. return this.orderedColumns.filter(
  661. column => this.shownColumns.indexOf(column.name) !== -1
  662. );
  663. },
  664. hidableSortedColumns() {
  665. return this.orderedColumns.filter(column => column.hidable);
  666. },
  667. lastSelectedItemIndex() {
  668. return this.data.findIndex(item => item.highlighted);
  669. },
  670. selectedRows() {
  671. return this.data.filter(data => data.selected);
  672. },
  673. ...mapGetters({
  674. socket: "websockets/getSocket"
  675. })
  676. },
  677. mounted() {
  678. const tableSettings = this.getTableSettings();
  679. this.orderedColumns = [
  680. {
  681. name: "select",
  682. displayName: "",
  683. properties: [],
  684. sortable: false,
  685. hidable: false,
  686. draggable: false,
  687. resizable: false,
  688. minWidth: 47,
  689. defaultWidth: 47,
  690. maxWidth: 47
  691. },
  692. ...this.columns.map(column => ({
  693. ...this.columnDefault,
  694. ...column
  695. })),
  696. {
  697. name: "placeholder",
  698. displayName: "",
  699. properties: [],
  700. sortable: false,
  701. hidable: false,
  702. draggable: false,
  703. resizable: false,
  704. minWidth: "auto",
  705. width: "auto",
  706. maxWidth: "auto"
  707. }
  708. ].sort((columnA, columnB) => {
  709. // Always places select column in the first position
  710. if (columnA.name === "select") return -1;
  711. // Always places placeholder column in the last position
  712. if (columnB.name === "placeholder") return -1;
  713. // If there are no table settings stored, use default ordering
  714. if (!tableSettings || !tableSettings.columnOrder) return 0;
  715. const indexA = tableSettings.columnOrder.indexOf(columnA.name);
  716. const indexB = tableSettings.columnOrder.indexOf(columnB.name);
  717. // If either of the columns is not stored in the table settings, use default ordering
  718. if (indexA === -1 || indexB === -1) return 0;
  719. return indexA - indexB;
  720. });
  721. this.shownColumns = this.orderedColumns
  722. .filter(column => {
  723. // If table settings exist, use shownColumns from settings to determine which columns to show
  724. if (tableSettings && tableSettings.shownColumns)
  725. return (
  726. tableSettings.shownColumns.indexOf(column.name) !== -1
  727. );
  728. // Table settings don't exist, only show if the default visibility isn't hidden
  729. return column.defaultVisibility !== "hidden";
  730. })
  731. .map(column => column.name);
  732. this.recalculateWidths();
  733. if (tableSettings) {
  734. // If table settings' pageSize is an integer, use it for the pageSize
  735. if (Number.isInteger(tableSettings?.pageSize))
  736. this.pageSize = tableSettings.pageSize;
  737. // If table settings' columnSort exists, sort all still existing columns based on table settings' columnSort object
  738. if (tableSettings.columnSort) {
  739. Object.entries(tableSettings.columnSort).forEach(
  740. ([columnName, sortDirection]) => {
  741. if (
  742. this.columns.find(
  743. column => column.name === columnName
  744. )
  745. )
  746. this.sort[columnName] = sortDirection;
  747. }
  748. );
  749. }
  750. // If table settings' columnWidths exists, load the stored widths into the columns
  751. if (tableSettings.columnWidths) {
  752. this.orderedColumns = this.orderedColumns.map(orderedColumn => {
  753. const columnWidth = tableSettings.columnWidths.find(
  754. column => column.name === orderedColumn.name
  755. )?.width;
  756. if (columnWidth)
  757. return { ...orderedColumn, width: columnWidth };
  758. return orderedColumn;
  759. });
  760. }
  761. if (
  762. tableSettings.filter &&
  763. tableSettings.filter.appliedFilters &&
  764. tableSettings.filter.appliedFilterOperator
  765. ) {
  766. const { appliedFilters, appliedFilterOperator } =
  767. tableSettings.filter;
  768. // Set the applied filter operator and filter operator to the value stored in table settings
  769. this.appliedFilterOperator = this.filterOperator =
  770. appliedFilterOperator;
  771. // Set the applied filters and editing filters to the value stored in table settings, for all filters that are allowed
  772. this.appliedFilters = this.editingFilters =
  773. appliedFilters.filter(appliedFilter =>
  774. this.filters.find(
  775. filter => appliedFilter.filter.name === filter.name
  776. )
  777. );
  778. }
  779. }
  780. this.resetBulkActionsPosition();
  781. ws.onConnect(this.init);
  782. },
  783. methods: {
  784. init() {
  785. this.getData();
  786. },
  787. getData() {
  788. this.socket.dispatch(
  789. this.dataAction,
  790. this.page,
  791. this.pageSize,
  792. this.properties,
  793. this.sort,
  794. this.appliedFilters,
  795. this.appliedFilterOperator,
  796. res => {
  797. console.log(111, res);
  798. if (res.status === "success") {
  799. const { data, count } = res.data;
  800. this.data = data.map(row => ({
  801. ...row,
  802. selected: false
  803. }));
  804. this.count = count;
  805. } else {
  806. new Toast(res.message);
  807. }
  808. }
  809. );
  810. },
  811. changePageSize() {
  812. this.getData();
  813. this.storeTableSettings();
  814. },
  815. changePage(page) {
  816. if (page < 1) return;
  817. if (page > this.lastPage) return;
  818. if (page === this.page) return;
  819. this.page = page;
  820. this.getData();
  821. },
  822. changeSort(column) {
  823. if (column.sortable) {
  824. const { sortProperty } = column;
  825. if (this.sort[sortProperty] === undefined)
  826. this.sort[sortProperty] = "ascending";
  827. else if (this.sort[sortProperty] === "ascending")
  828. this.sort[sortProperty] = "descending";
  829. else if (this.sort[sortProperty] === "descending")
  830. delete this.sort[sortProperty];
  831. this.getData();
  832. this.storeTableSettings();
  833. }
  834. },
  835. toggleColumnVisibility(column) {
  836. if (this.shownColumns.indexOf(column.name) !== -1) {
  837. if (this.shownColumns.length <= 3)
  838. return new Toast(
  839. `Unable to hide column ${column.displayName}, there must be at least 1 visibile column`
  840. );
  841. this.shownColumns.splice(
  842. this.shownColumns.indexOf(column.name),
  843. 1
  844. );
  845. } else {
  846. this.shownColumns.push(column.name);
  847. }
  848. this.recalculateWidths();
  849. this.getData();
  850. return this.storeTableSettings();
  851. },
  852. toggleSelectedRow(itemIndex, event) {
  853. const { shiftKey, ctrlKey } = event;
  854. // Shift was pressed, so attempt to select all items between the clicked item and last clicked item
  855. if (shiftKey) {
  856. // If the clicked item is already selected, prevent default, otherwise the checkbox will be unchecked
  857. if (this.data[itemIndex].selected) event.preventDefault();
  858. // If there is a last clicked item
  859. if (this.lastSelectedItemIndex >= 0) {
  860. // Clicked item is lower than last item, so select upwards until it reaches the last selected item
  861. if (itemIndex > this.lastSelectedItemIndex) {
  862. for (
  863. let itemIndexUp = itemIndex;
  864. itemIndexUp > this.lastSelectedItemIndex;
  865. itemIndexUp -= 1
  866. ) {
  867. this.data[itemIndexUp].selected = true;
  868. }
  869. }
  870. // Clicked item is higher than last item, so select downwards until it reaches the last selected item
  871. else if (itemIndex < this.lastSelectedItemIndex) {
  872. for (
  873. let itemIndexDown = itemIndex;
  874. itemIndexDown < this.lastSelectedItemIndex;
  875. itemIndexDown += 1
  876. ) {
  877. this.data[itemIndexDown].selected = true;
  878. }
  879. }
  880. }
  881. }
  882. // Ctrl was pressed, so attempt to unselect all items between the clicked item and last clicked item
  883. else if (ctrlKey) {
  884. // If the clicked item is already unselected, prevent default, otherwise the checkbox will be checked
  885. if (!this.data[itemIndex].selected) event.preventDefault();
  886. // If there is a last clicked item
  887. if (this.lastSelectedItemIndex >= 0) {
  888. // Clicked item is lower than last item, so unselect upwards until it reaches the last selected item
  889. if (itemIndex > this.lastSelectedItemIndex) {
  890. for (
  891. let itemIndexUp = itemIndex;
  892. itemIndexUp >= this.lastSelectedItemIndex;
  893. itemIndexUp -= 1
  894. ) {
  895. this.data[itemIndexUp].selected = false;
  896. }
  897. }
  898. // Clicked item is higher than last item, so unselect downwards until it reaches the last selected item
  899. else if (itemIndex < this.lastSelectedItemIndex) {
  900. for (
  901. let itemIndexDown = itemIndex;
  902. itemIndexDown <= this.lastSelectedItemIndex;
  903. itemIndexDown += 1
  904. ) {
  905. this.data[itemIndexDown].selected = false;
  906. }
  907. }
  908. }
  909. }
  910. // Neither ctrl nor shift were pressed, so toggle clicked item
  911. else {
  912. this.data[itemIndex].selected = !this.data[itemIndex].selected;
  913. }
  914. // Set the last clicked item to no longer be highlighted, if it exists
  915. if (this.lastSelectedItemIndex >= 0)
  916. this.data[this.lastSelectedItemIndex].highlighted = false;
  917. // Set the clicked item to be highlighted
  918. this.data[itemIndex].highlighted = true;
  919. },
  920. toggleAllRows() {
  921. if (this.data.length > this.selectedRows.length) {
  922. this.data = this.data.map(row => ({ ...row, selected: true }));
  923. } else {
  924. this.data = this.data.map(row => ({ ...row, selected: false }));
  925. }
  926. },
  927. addFilterItem() {
  928. this.editingFilters.push({
  929. data: "",
  930. filter: this.addFilterValue,
  931. filterType: this.addFilterValue.defaultFilterType
  932. });
  933. },
  934. removeFilterItem(index) {
  935. this.editingFilters.splice(index, 1);
  936. },
  937. columnResizingMouseDown(column, event) {
  938. this.resizing.resizing = true;
  939. this.resizing.resizingColumn = column;
  940. this.resizing.width = event.target.parentElement.offsetWidth;
  941. this.resizing.lastX = event.x;
  942. },
  943. columnResizingMouseMove(event) {
  944. if (this.resizing.resizing) {
  945. if (event.buttons !== 1) {
  946. this.resizing.resizing = false;
  947. this.storeTableSettings();
  948. }
  949. this.resizing.width -= this.resizing.lastX - event.x;
  950. this.resizing.lastX = event.x;
  951. if (
  952. this.resizing.resizingColumn.minWidth &&
  953. this.resizing.resizingColumn.maxWidth
  954. ) {
  955. this.resizing.resizingColumn.width = Math.max(
  956. Math.min(
  957. this.resizing.resizingColumn.maxWidth,
  958. this.resizing.width
  959. ),
  960. this.resizing.resizingColumn.minWidth
  961. );
  962. } else if (this.resizing.resizingColumn.minWidth) {
  963. this.resizing.resizingColumn.width = Math.max(
  964. this.resizing.width,
  965. this.resizing.resizingColumn.minWidth
  966. );
  967. } else if (this.resizing.resizingColumn.maxWidth) {
  968. this.resizing.resizingColumn.width = Math.min(
  969. this.resizing.resizingColumn.maxWidth,
  970. this.resizing.width
  971. );
  972. } else {
  973. this.resizing.resizingColumn.width = this.resizing.width;
  974. }
  975. this.resizing.width = this.resizing.resizingColumn.width;
  976. console.log(`New width: ${this.resizing.width}px`);
  977. this.storeTableSettings();
  978. }
  979. },
  980. columnResizingMouseUp() {
  981. this.resizing.resizing = false;
  982. this.storeTableSettings();
  983. },
  984. columnResetWidth(column) {
  985. const index = this.orderedColumns.indexOf(column);
  986. if (column.defaultWidth && !Number.isNaN(column.defaultWidth))
  987. this.orderedColumns[index].width = column.defaultWidth;
  988. else if (
  989. column.calculatedWidth &&
  990. !Number.isNaN(column.calculatedWidth)
  991. )
  992. this.orderedColumns[index].width = column.calculatedWidth;
  993. this.storeTableSettings();
  994. },
  995. filterTypes(filter) {
  996. if (!filter || !filter.filterTypes) return [];
  997. return filter.filterTypes.map(
  998. filterType => this.allFilterTypes[filterType]
  999. );
  1000. },
  1001. changeFilterType(index) {
  1002. this.editingFilters[index].filterType =
  1003. this.editingFilters[index].filter.defaultFilterType;
  1004. },
  1005. onDragBox(e) {
  1006. const e1 = e || window.event;
  1007. e1.preventDefault();
  1008. this.bulkPopup.pos3 = e1.clientX;
  1009. this.bulkPopup.pos4 = e1.clientY;
  1010. document.onmousemove = e => {
  1011. const e2 = e || window.event;
  1012. e2.preventDefault();
  1013. // calculate the new cursor position:
  1014. this.bulkPopup.pos1 = this.bulkPopup.pos3 - e.clientX;
  1015. this.bulkPopup.pos2 = this.bulkPopup.pos4 - e.clientY;
  1016. this.bulkPopup.pos3 = e.clientX;
  1017. this.bulkPopup.pos4 = e.clientY;
  1018. // set the element's new position:
  1019. this.bulkPopup.top -= this.bulkPopup.pos2;
  1020. this.bulkPopup.left -= this.bulkPopup.pos1;
  1021. if (this.bulkPopup.top < 0) this.bulkPopup.top = 0;
  1022. if (this.bulkPopup.top > document.body.clientHeight - 50)
  1023. this.bulkPopup.top = document.body.clientHeight - 50;
  1024. if (this.bulkPopup.left < 0) this.bulkPopup.left = 0;
  1025. if (this.bulkPopup.left > document.body.clientWidth - 400)
  1026. this.bulkPopup.left = document.body.clientWidth - 400;
  1027. };
  1028. document.onmouseup = () => {
  1029. document.onmouseup = null;
  1030. document.onmousemove = null;
  1031. };
  1032. },
  1033. resetBulkActionsPosition() {
  1034. this.bulkPopup.top = document.body.clientHeight - 56;
  1035. this.bulkPopup.left = document.body.clientWidth / 2 - 200;
  1036. },
  1037. applyFilterAndGetData() {
  1038. this.appliedFilters = JSON.parse(
  1039. JSON.stringify(this.editingFilters)
  1040. );
  1041. this.appliedFilterOperator = this.filterOperator;
  1042. this.getData();
  1043. this.storeTableSettings();
  1044. },
  1045. recalculateWidths() {
  1046. let noWidthCount = 0;
  1047. let calculatedWidth = 0;
  1048. this.orderedColumns.forEach(column => {
  1049. if (this.shownColumns.indexOf(column.name) !== -1)
  1050. if (Number.isFinite(column.width)) {
  1051. calculatedWidth += column.width;
  1052. } else if (Number.isFinite(column.defaultWidth)) {
  1053. calculatedWidth += column.defaultWidth;
  1054. } else {
  1055. noWidthCount += 1;
  1056. }
  1057. });
  1058. calculatedWidth = Math.floor(
  1059. // max-width of table is 1880px
  1060. (Math.min(1880, document.body.clientWidth) - calculatedWidth) /
  1061. (noWidthCount - 1)
  1062. );
  1063. this.orderedColumns = this.orderedColumns.map(column => {
  1064. const orderedColumn = column;
  1065. if (
  1066. this.shownColumns.indexOf(orderedColumn.name) !== -1 &&
  1067. !Number.isFinite(orderedColumn.width)
  1068. ) {
  1069. if (Number.isFinite(orderedColumn.defaultWidth)) {
  1070. orderedColumn.width = orderedColumn.defaultWidth;
  1071. } else {
  1072. // eslint-disable-next-line no-param-reassign
  1073. orderedColumn.width = orderedColumn.calculatedWidth =
  1074. Math.min(
  1075. Math.max(
  1076. orderedColumn.minWidth || 100, // fallback 100px min width
  1077. calculatedWidth
  1078. ),
  1079. orderedColumn.maxWidth || 1000 // fallback 1000px max width
  1080. );
  1081. }
  1082. }
  1083. return orderedColumn;
  1084. });
  1085. },
  1086. columnOrderChanged() {
  1087. this.storeTableSettings();
  1088. },
  1089. getTableSettings() {
  1090. return JSON.parse(
  1091. localStorage.getItem(`advancedTableSettings:${this.name}`)
  1092. );
  1093. },
  1094. storeTableSettings() {
  1095. // Clear debounce timeout
  1096. if (this.storeTableSettingsDebounceTimeout)
  1097. clearTimeout(this.storeTableSettingsDebounceTimeout);
  1098. // Resizing calls this function a lot, so rather than saving dozens of times a second, use debouncing
  1099. this.storeTableSettingsDebounceTimeout = setTimeout(() => {
  1100. localStorage.setItem(
  1101. `advancedTableSettings:${this.name}`,
  1102. JSON.stringify({
  1103. pageSize: this.pageSize,
  1104. filter: {
  1105. appliedFilters: this.appliedFilters,
  1106. appliedFilterOperator: this.appliedFilterOperator
  1107. },
  1108. columnSort: this.sort,
  1109. columnOrder: this.orderedColumns.map(
  1110. column => column.name
  1111. ),
  1112. columnWidths: this.orderedColumns.map(column => ({
  1113. name: column.name,
  1114. width: column.width
  1115. })),
  1116. shownColumns: this.shownColumns
  1117. })
  1118. );
  1119. }, 250);
  1120. }
  1121. }
  1122. };
  1123. </script>
  1124. <style lang="scss" scoped>
  1125. .night-mode {
  1126. .table-outer-container {
  1127. .table-container .table {
  1128. &,
  1129. thead th {
  1130. background-color: var(--dark-grey-3);
  1131. color: var(--light-grey-2);
  1132. }
  1133. tr {
  1134. th,
  1135. td {
  1136. border-color: var(--dark-grey) !important;
  1137. &:first-child {
  1138. background-color: var(--dark-grey-3) !important;
  1139. }
  1140. }
  1141. &:nth-child(even) {
  1142. &,
  1143. td:first-child {
  1144. background-color: var(--dark-grey-2) !important;
  1145. }
  1146. }
  1147. &:hover,
  1148. &:focus,
  1149. &.highlighted {
  1150. th,
  1151. td {
  1152. &,
  1153. &:first-child {
  1154. background-color: var(--dark-grey-4) !important;
  1155. }
  1156. }
  1157. }
  1158. }
  1159. }
  1160. .table-header,
  1161. .table-footer {
  1162. background-color: var(--dark-grey-3);
  1163. color: var(--light-grey-2);
  1164. }
  1165. .label.control {
  1166. background-color: var(--dark-grey) !important;
  1167. border-color: var(--grey-3) !important;
  1168. color: var(--white) !important;
  1169. }
  1170. }
  1171. .bulk-popup {
  1172. border: 0;
  1173. background-color: var(--dark-grey-2);
  1174. color: var(--white);
  1175. .material-icons {
  1176. color: var(--white);
  1177. }
  1178. }
  1179. }
  1180. .table-outer-container {
  1181. border-radius: 5px;
  1182. box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1);
  1183. margin: 10px 0;
  1184. overflow: hidden;
  1185. .table-container {
  1186. overflow-x: auto;
  1187. table {
  1188. border-collapse: separate;
  1189. table-layout: fixed;
  1190. thead {
  1191. tr {
  1192. th {
  1193. height: 40px;
  1194. line-height: 40px;
  1195. border: 1px solid var(--light-grey-2);
  1196. border-width: 1px 1px 1px 0;
  1197. &:last-child {
  1198. border-width: 1px 0 1px;
  1199. }
  1200. &.sortable {
  1201. cursor: pointer;
  1202. }
  1203. & > div {
  1204. display: flex;
  1205. white-space: nowrap;
  1206. & > span {
  1207. margin-left: 5px;
  1208. &:first-child {
  1209. margin-left: 0;
  1210. margin-right: auto;
  1211. }
  1212. & > .material-icons {
  1213. font-size: 22px;
  1214. position: relative;
  1215. top: 6px;
  1216. cursor: pointer;
  1217. &.active {
  1218. color: var(--primary-color);
  1219. }
  1220. &:hover,
  1221. &:focus {
  1222. filter: brightness(90%);
  1223. }
  1224. }
  1225. }
  1226. }
  1227. }
  1228. }
  1229. }
  1230. tbody {
  1231. tr {
  1232. &.highlighted {
  1233. background-color: var(--light-grey);
  1234. }
  1235. td {
  1236. border: 1px solid var(--light-grey-2);
  1237. border-width: 0 1px 1px 0;
  1238. &:last-child {
  1239. border-width: 0 0 1px;
  1240. }
  1241. }
  1242. }
  1243. }
  1244. }
  1245. table thead tr,
  1246. table tbody tr {
  1247. th,
  1248. td {
  1249. position: relative;
  1250. white-space: nowrap;
  1251. text-overflow: ellipsis;
  1252. overflow: hidden;
  1253. &:first-child {
  1254. position: sticky;
  1255. left: 0;
  1256. background-color: var(--white);
  1257. z-index: 2;
  1258. }
  1259. .resizer {
  1260. height: 100%;
  1261. width: 5px;
  1262. background-color: transparent;
  1263. cursor: col-resize;
  1264. position: absolute;
  1265. right: 0;
  1266. top: 0;
  1267. }
  1268. }
  1269. &:nth-child(even) td:first-child {
  1270. background-color: #fafafa;
  1271. }
  1272. &:hover,
  1273. &:focus,
  1274. &.highlighted {
  1275. th,
  1276. td {
  1277. &,
  1278. &:first-child {
  1279. background-color: var(--light-grey);
  1280. }
  1281. }
  1282. }
  1283. }
  1284. }
  1285. .table-header,
  1286. .table-footer {
  1287. display: flex;
  1288. flex-direction: row;
  1289. flex-wrap: wrap;
  1290. justify-content: space-between;
  1291. line-height: 36px;
  1292. background-color: var(--white);
  1293. }
  1294. .table-header > div {
  1295. display: flex;
  1296. flex-direction: row;
  1297. > span > .button {
  1298. margin: 5px;
  1299. }
  1300. .filters-indicator {
  1301. line-height: 46px;
  1302. display: flex;
  1303. align-items: center;
  1304. column-gap: 4px;
  1305. }
  1306. }
  1307. .table-footer {
  1308. .page-controls,
  1309. .page-size > .control {
  1310. display: flex;
  1311. flex-direction: row;
  1312. margin-bottom: 0 !important;
  1313. button {
  1314. margin: 5px;
  1315. font-size: 20px;
  1316. }
  1317. p,
  1318. label {
  1319. margin: 5px;
  1320. font-size: 14px;
  1321. font-weight: 600;
  1322. }
  1323. &.select::after {
  1324. top: 18px;
  1325. }
  1326. }
  1327. }
  1328. }
  1329. .control.is-grouped {
  1330. display: flex;
  1331. & > .control {
  1332. &.label {
  1333. height: 36px;
  1334. background-color: var(--white);
  1335. border: 1px solid var(--light-grey-2);
  1336. color: var(--dark-grey-2);
  1337. appearance: none;
  1338. border-radius: 3px;
  1339. font-size: 14px;
  1340. line-height: 34px;
  1341. padding-left: 8px;
  1342. padding-right: 8px;
  1343. }
  1344. &.select.is-expanded > select {
  1345. width: 100%;
  1346. }
  1347. & > input,
  1348. & > select,
  1349. & > .button,
  1350. &.label {
  1351. border-radius: 0;
  1352. }
  1353. &:first-child {
  1354. & > input,
  1355. & > select,
  1356. & > .button,
  1357. &.label {
  1358. border-radius: 5px 0 0 5px;
  1359. }
  1360. }
  1361. &:last-child {
  1362. & > input,
  1363. & > select,
  1364. & > .button,
  1365. &.label {
  1366. border-radius: 0 5px 5px 0;
  1367. }
  1368. }
  1369. & > .button {
  1370. font-size: 22px;
  1371. }
  1372. }
  1373. @media screen and (max-width: 500px) {
  1374. &.advanced-filter {
  1375. flex-wrap: wrap;
  1376. .control.select {
  1377. width: 50%;
  1378. select {
  1379. width: 100%;
  1380. }
  1381. }
  1382. .control {
  1383. margin-bottom: 0 !important;
  1384. &:nth-child(1) select {
  1385. border-radius: 5px 0 0 0;
  1386. }
  1387. &:nth-child(2) select {
  1388. border-radius: 0 5px 0 0;
  1389. }
  1390. &:nth-child(3) input {
  1391. border-radius: 0 0 0 5px;
  1392. }
  1393. &:nth-child(4) button {
  1394. border-radius: 0 0 5px 0;
  1395. }
  1396. }
  1397. }
  1398. }
  1399. }
  1400. .advanced-filter-bottom {
  1401. display: flex;
  1402. .button {
  1403. font-size: 16px !important;
  1404. width: 100%;
  1405. }
  1406. .control {
  1407. margin: 0 !important;
  1408. }
  1409. }
  1410. .bulk-popup {
  1411. display: flex;
  1412. position: fixed;
  1413. flex-direction: row;
  1414. width: 100%;
  1415. max-width: 400px;
  1416. line-height: 36px;
  1417. z-index: 5;
  1418. border: 1px solid var(--light-grey-3);
  1419. border-radius: 5px;
  1420. box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
  1421. background-color: var(--white);
  1422. color: var(--dark-grey);
  1423. padding: 5px;
  1424. .right {
  1425. display: flex;
  1426. flex-direction: row;
  1427. margin-left: auto;
  1428. }
  1429. .drag-icon {
  1430. position: relative;
  1431. top: 6px;
  1432. color: var(--dark-grey);
  1433. cursor: move;
  1434. }
  1435. }
  1436. </style>