1
0

AdvancedTable.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727
  1. <template>
  2. <div>
  3. <div>
  4. <button
  5. v-for="column in hidableSortedColumns"
  6. :key="column.name"
  7. class="button"
  8. @click="toggleColumnVisibility(column)"
  9. >
  10. {{
  11. `${
  12. this.shownColumns.indexOf(column.name) !== -1
  13. ? "Hide"
  14. : "Show"
  15. } ${column.name} column`
  16. }}
  17. </button>
  18. </div>
  19. <div class="table-outer-container">
  20. <div class="table-container">
  21. <div class="table-header">
  22. <div class="table-buttons">
  23. <tippy
  24. :touch="true"
  25. :interactive="true"
  26. placement="bottom"
  27. theme="search"
  28. ref="search"
  29. trigger="click"
  30. >
  31. <a class="button is-info" @click.prevent="true">
  32. <i class="material-icons icon-with-button"
  33. >search</i
  34. >
  35. Search
  36. </a>
  37. <template #content>
  38. <div
  39. v-for="(query, index) in advancedQuery"
  40. :key="`query-${index}`"
  41. class="advanced-query"
  42. >
  43. <div class="control select">
  44. <select v-model="query.filter">
  45. <option
  46. v-for="f in filters"
  47. :key="f.name"
  48. :value="f"
  49. >
  50. {{ f.displayName }}
  51. </option>
  52. </select>
  53. </div>
  54. <div class="control select">
  55. <select
  56. v-if="query.filter.type"
  57. v-model="query.type"
  58. >
  59. <option
  60. v-for="type in filterTypes"
  61. :key="type"
  62. :value="type"
  63. >
  64. {{ type }}
  65. </option>
  66. </select>
  67. </div>
  68. <p class="control is-expanded">
  69. <input
  70. v-if="query.type === 'regex'"
  71. v-model="query.regex"
  72. class="input"
  73. type="text"
  74. placeholder="Search value"
  75. @keyup.enter="changeFilter()"
  76. />
  77. </p>
  78. <div class="control">
  79. <button
  80. class="
  81. button
  82. material-icons
  83. is-success
  84. "
  85. @click="addQueryItem()"
  86. >
  87. control_point
  88. </button>
  89. </div>
  90. <div
  91. v-if="advancedQuery.length > 1"
  92. class="control"
  93. >
  94. <button
  95. class="
  96. button
  97. material-icons
  98. is-danger
  99. "
  100. @click="removeQueryItem(index)"
  101. >
  102. remove_circle_outline
  103. </button>
  104. </div>
  105. </div>
  106. <a
  107. class="button is-info"
  108. @click="changeFilter()"
  109. >
  110. <i class="material-icons icon-with-button"
  111. >search</i
  112. >
  113. Search
  114. </a>
  115. </template>
  116. </tippy>
  117. </div>
  118. </div>
  119. <table class="table">
  120. <thead>
  121. <draggable
  122. item-key="name"
  123. v-model="orderedColumns"
  124. v-bind="columnDragOptions"
  125. tag="tr"
  126. draggable=".item-draggable"
  127. >
  128. <template #item="{ element: column }">
  129. <th
  130. :class="{
  131. sortable: column.sortable,
  132. 'item-draggable': column.draggable
  133. }"
  134. :style="{
  135. minWidth: column.minWidth,
  136. width: column.width,
  137. maxWidth: column.maxWidth
  138. }"
  139. v-if="
  140. shownColumns.indexOf(column.name) !== -1
  141. "
  142. >
  143. <div>
  144. <span>
  145. {{ column.displayName }}
  146. </span>
  147. <span
  148. v-if="column.sortable"
  149. :content="`Sort by ${column.displayName}`"
  150. v-tippy
  151. >
  152. <span
  153. v-if="
  154. !sort[column.sortProperty]
  155. "
  156. class="material-icons"
  157. @click="changeSort(column)"
  158. >
  159. unfold_more
  160. </span>
  161. <span
  162. v-if="
  163. sort[
  164. column.sortProperty
  165. ] === 'ascending'
  166. "
  167. class="material-icons"
  168. @click="changeSort(column)"
  169. >
  170. expand_less
  171. </span>
  172. <span
  173. v-if="
  174. sort[
  175. column.sortProperty
  176. ] === 'descending'
  177. "
  178. class="material-icons"
  179. @click="changeSort(column)"
  180. >
  181. expand_more
  182. </span>
  183. </span>
  184. </div>
  185. </th>
  186. </template>
  187. </draggable>
  188. </thead>
  189. <tbody>
  190. <tr
  191. v-for="(item, itemIndex) in data"
  192. :key="item._id"
  193. :class="{
  194. selected: item.selected,
  195. highlighted: item.highlighted
  196. }"
  197. @click="clickItem(itemIndex, $event)"
  198. >
  199. <td
  200. v-for="column in sortedFilteredColumns"
  201. :key="`${item._id}-${column.name}`"
  202. >
  203. <slot
  204. :name="`column-${column.name}`"
  205. :item="item"
  206. v-if="
  207. column.properties.every(
  208. property =>
  209. item[property] !== undefined
  210. )
  211. "
  212. ></slot>
  213. </td>
  214. </tr>
  215. </tbody>
  216. </table>
  217. <div class="table-footer">
  218. <div class="page-controls">
  219. <button
  220. :class="{ disabled: page === 1 }"
  221. class="button is-primary material-icons"
  222. :disabled="page === 1"
  223. @click="changePage(1)"
  224. content="First Page"
  225. v-tippy
  226. >
  227. skip_previous
  228. </button>
  229. <button
  230. :class="{ disabled: page === 1 }"
  231. class="button is-primary material-icons"
  232. :disabled="page === 1"
  233. @click="changePage(page - 1)"
  234. content="Previous Page"
  235. v-tippy
  236. >
  237. fast_rewind
  238. </button>
  239. <p>Page {{ page }} / {{ lastPage }}</p>
  240. <button
  241. :class="{ disabled: page === lastPage }"
  242. class="button is-primary material-icons"
  243. :disabled="page === lastPage"
  244. @click="changePage(page + 1)"
  245. content="Next Page"
  246. v-tippy
  247. >
  248. fast_forward
  249. </button>
  250. <button
  251. :class="{ disabled: page === lastPage }"
  252. class="button is-primary material-icons"
  253. :disabled="page === lastPage"
  254. @click="changePage(lastPage)"
  255. content="Last Page"
  256. v-tippy
  257. >
  258. skip_next
  259. </button>
  260. </div>
  261. <div class="page-size">
  262. <div class="control">
  263. <label class="label">Items per page</label>
  264. <p class="control select">
  265. <select
  266. v-model.number="pageSize"
  267. @change="changePageSize()"
  268. >
  269. <option value="10">10</option>
  270. <option value="25">25</option>
  271. <option value="50">50</option>
  272. <option value="100">100</option>
  273. <option value="250">250</option>
  274. <option value="500">500</option>
  275. <option value="1000">1000</option>
  276. </select>
  277. </p>
  278. </div>
  279. </div>
  280. </div>
  281. </div>
  282. </div>
  283. </div>
  284. </template>
  285. <script>
  286. import { mapGetters } from "vuex";
  287. import draggable from "vuedraggable";
  288. import Toast from "toasters";
  289. import ws from "@/ws";
  290. export default {
  291. components: {
  292. draggable
  293. },
  294. props: {
  295. /*
  296. Column properties:
  297. name: Unique lowercase name
  298. displayName: Nice name for the column header
  299. properties: The properties this column needs to show data
  300. sortable: Boolean for whether the order of a particular column can be changed
  301. sortProperty: The property the backend will sort on if this column gets sorted, e.g. title
  302. hidable: Boolean for whether a column can be hidden
  303. defaultVisibility: Default visibility for a column, either "shown" or "hidden"
  304. draggable: Boolean for whether a column can be dragged/reordered
  305. minWidth: Minimum width of column, e.g. 50px
  306. width: Width of column, e.g. 100px
  307. maxWidth: Maximum width of column, e.g. 150px
  308. */
  309. columns: { type: Array, default: null },
  310. filters: { type: Array, default: null },
  311. dataAction: { type: String, default: null }
  312. },
  313. data() {
  314. return {
  315. page: 1,
  316. pageSize: 10,
  317. data: [],
  318. count: 0, // TODO Rename
  319. sort: {},
  320. filter: {},
  321. orderedColumns: [],
  322. shownColumns: [],
  323. columnDragOptions() {
  324. return {
  325. animation: 200,
  326. group: "columns",
  327. disabled: false,
  328. ghostClass: "draggable-list-ghost",
  329. filter: ".ignore-elements",
  330. fallbackTolerance: 50
  331. };
  332. },
  333. advancedQuery: [
  334. {
  335. regex: "",
  336. filter: {},
  337. type: ""
  338. }
  339. ]
  340. };
  341. },
  342. computed: {
  343. properties() {
  344. return Array.from(
  345. new Set(
  346. this.sortedFilteredColumns.flatMap(
  347. column => column.properties
  348. )
  349. )
  350. );
  351. },
  352. lastPage() {
  353. return Math.ceil(this.count / this.pageSize);
  354. },
  355. sortedFilteredColumns() {
  356. return this.orderedColumns.filter(
  357. column => this.shownColumns.indexOf(column.name) !== -1
  358. );
  359. },
  360. hidableSortedColumns() {
  361. return this.orderedColumns.filter(column => column.hidable);
  362. },
  363. lastSelectedItemIndex() {
  364. return this.data.findIndex(item => item.highlighted);
  365. },
  366. filterTypes() {
  367. return this.filters
  368. .map(filter => filter.type)
  369. .filter((f, index, self) => self.indexOf(f) === index);
  370. },
  371. ...mapGetters({
  372. socket: "websockets/getSocket"
  373. })
  374. },
  375. mounted() {
  376. const columns = [
  377. {
  378. name: "select",
  379. displayName: "",
  380. properties: [],
  381. sortable: false,
  382. hidable: false,
  383. draggable: false
  384. },
  385. ...this.columns
  386. ];
  387. this.orderedColumns = columns;
  388. // A column will be shown if the defaultVisibility is set to shown, OR if the defaultVisibility is not set to shown and hidable is false
  389. this.shownColumns = columns
  390. .filter(column => column.defaultVisibility !== "hidden")
  391. .map(column => column.name);
  392. const pageSize = parseInt(localStorage.getItem("adminPageSize"));
  393. if (!Number.isNaN(pageSize)) this.pageSize = pageSize;
  394. ws.onConnect(this.init);
  395. },
  396. methods: {
  397. init() {
  398. this.getData();
  399. },
  400. getData() {
  401. this.socket.dispatch(
  402. this.dataAction,
  403. this.page,
  404. this.pageSize,
  405. this.properties,
  406. this.sort,
  407. this.filter,
  408. res => {
  409. console.log(111, res);
  410. new Toast(res.message);
  411. if (res.status === "success") {
  412. const { data, count } = res.data;
  413. this.data = data;
  414. this.count = count;
  415. }
  416. }
  417. );
  418. },
  419. changePageSize() {
  420. this.getData();
  421. localStorage.setItem("adminPageSize", this.pageSize);
  422. },
  423. changePage(page) {
  424. if (page < 1) return;
  425. if (page > this.lastPage) return;
  426. if (page === this.page) return;
  427. this.page = page;
  428. this.getData();
  429. },
  430. changeSort(column) {
  431. if (column.sortable) {
  432. const { sortProperty } = column;
  433. if (this.sort[sortProperty] === undefined)
  434. this.sort[sortProperty] = "ascending";
  435. else if (this.sort[sortProperty] === "ascending")
  436. this.sort[sortProperty] = "descending";
  437. else if (this.sort[sortProperty] === "descending")
  438. delete this.sort[sortProperty];
  439. this.getData();
  440. }
  441. },
  442. changeFilter() {
  443. this.advancedQuery.forEach(query => {
  444. const { regex } = query;
  445. const { name } = query.filter;
  446. if (this.filter[name] !== undefined && regex === "") {
  447. delete this.filter[name];
  448. } else if (this.filter[name] !== regex) {
  449. this.filter[name] = regex;
  450. }
  451. });
  452. this.getData();
  453. },
  454. toggleColumnVisibility(column) {
  455. if (this.shownColumns.indexOf(column.name) !== -1) {
  456. this.shownColumns.splice(
  457. this.shownColumns.indexOf(column.name),
  458. 1
  459. );
  460. } else {
  461. this.shownColumns.push(column.name);
  462. }
  463. this.getData();
  464. },
  465. clickItem(itemIndex, event) {
  466. const { shiftKey, ctrlKey } = event;
  467. // Shift was pressed, so attempt to select all items between the clicked item and last clicked item
  468. if (shiftKey) {
  469. // If there is a last clicked item
  470. if (this.lastSelectedItemIndex >= 0) {
  471. // Clicked item is lower than last item, so select upwards until it reaches the last selected item
  472. if (itemIndex > this.lastSelectedItemIndex) {
  473. for (
  474. let itemIndexUp = itemIndex;
  475. itemIndexUp > this.lastSelectedItemIndex;
  476. itemIndexUp -= 1
  477. ) {
  478. this.data[itemIndexUp].selected = true;
  479. }
  480. }
  481. // Clicked item is higher than last item, so select downwards until it reaches the last selected item
  482. else if (itemIndex < this.lastSelectedItemIndex) {
  483. for (
  484. let itemIndexDown = itemIndex;
  485. itemIndexDown < this.lastSelectedItemIndex;
  486. itemIndexDown += 1
  487. ) {
  488. this.data[itemIndexDown].selected = true;
  489. }
  490. }
  491. }
  492. }
  493. // Ctrl was pressed, so toggle selected on the clicked item
  494. else if (ctrlKey) {
  495. this.data[itemIndex].selected = !this.data[itemIndex].selected;
  496. }
  497. // Neither ctrl nor shift were pressed, so unselect all items and set the clicked item to selected
  498. else {
  499. this.data = this.data.map(item => ({
  500. ...item,
  501. selected: false
  502. }));
  503. this.data[itemIndex].selected = true;
  504. }
  505. // Set the last clicked item to no longer be highlighted, if it exists
  506. if (this.lastSelectedItemIndex >= 0)
  507. this.data[this.lastSelectedItemIndex].highlighted = false;
  508. // Set the clicked item to be highlighted
  509. this.data[itemIndex].highlighted = true;
  510. },
  511. addQueryItem() {
  512. this.advancedQuery.push({
  513. regex: "",
  514. filter: {},
  515. type: ""
  516. });
  517. },
  518. removeQueryItem(index) {
  519. this.advancedQuery.splice(index, 1);
  520. }
  521. }
  522. };
  523. </script>
  524. <style lang="scss" scoped>
  525. .night-mode .table-outer-container {
  526. .table-container .table {
  527. &,
  528. thead th {
  529. background-color: var(--dark-grey-3);
  530. color: var(--light-grey-2);
  531. }
  532. tr {
  533. &:nth-child(even) {
  534. background-color: var(--dark-grey-2);
  535. }
  536. &:hover,
  537. &:focus,
  538. &.highlighted {
  539. background-color: var(--dark-grey);
  540. }
  541. }
  542. }
  543. .table-header,
  544. .table-footer {
  545. background-color: var(--dark-grey-3);
  546. color: var(--light-grey-2);
  547. }
  548. }
  549. .table-outer-container {
  550. border-radius: 5px;
  551. box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1);
  552. margin: 10px 0;
  553. overflow: hidden;
  554. .table-container {
  555. overflow-x: auto;
  556. table {
  557. border-collapse: separate;
  558. thead {
  559. tr {
  560. th {
  561. height: 40px;
  562. line-height: 40px;
  563. border: 1px solid var(--light-grey-2);
  564. border-width: 1px 1px 1px 0;
  565. &:first-child,
  566. &:last-child {
  567. border-width: 1px 0 1px;
  568. }
  569. &.sortable {
  570. cursor: pointer;
  571. }
  572. & > div {
  573. display: flex;
  574. white-space: nowrap;
  575. & > span {
  576. & > .material-icons {
  577. font-size: 22px;
  578. position: relative;
  579. top: 6px;
  580. cursor: pointer;
  581. }
  582. &:first-child {
  583. margin-right: auto;
  584. }
  585. }
  586. }
  587. }
  588. }
  589. }
  590. tbody {
  591. tr {
  592. &.selected td:first-child {
  593. border-left: 5px solid var(--primary-color);
  594. padding-left: 0;
  595. }
  596. &.highlighted {
  597. background-color: var(--light-grey);
  598. td:first-child {
  599. border-left: 5px solid var(--red);
  600. padding-left: 0;
  601. }
  602. }
  603. &.selected.highlighted td:first-child {
  604. border-left: 5px solid var(--green);
  605. padding-left: 0;
  606. }
  607. td {
  608. border: 1px solid var(--light-grey-2);
  609. border-width: 0 1px 1px 0;
  610. &:first-child,
  611. &:last-child {
  612. border-width: 0 0 1px;
  613. }
  614. }
  615. }
  616. }
  617. }
  618. table thead tr th:first-child,
  619. table tbody tr td:first-child {
  620. position: sticky;
  621. left: 0;
  622. z-index: 2;
  623. padding: 0;
  624. padding-left: 5px;
  625. }
  626. }
  627. .table-header,
  628. .table-footer {
  629. display: flex;
  630. flex-direction: row;
  631. flex-wrap: wrap;
  632. justify-content: space-between;
  633. line-height: 36px;
  634. background-color: var(--white);
  635. }
  636. .table-header .table-buttons > span > .button {
  637. margin: 5px;
  638. }
  639. .table-footer {
  640. .page-controls,
  641. .page-size > .control {
  642. display: flex;
  643. flex-direction: row;
  644. margin-bottom: 0 !important;
  645. button {
  646. margin: 5px;
  647. font-size: 20px;
  648. }
  649. p,
  650. label {
  651. margin: 5px;
  652. font-size: 14px;
  653. font-weight: 600;
  654. }
  655. &.select::after {
  656. top: 18px;
  657. }
  658. }
  659. }
  660. }
  661. .advanced-query {
  662. display: flex;
  663. margin-bottom: 5px;
  664. & > .control {
  665. & > input,
  666. & > select,
  667. & > .button {
  668. border-radius: 0;
  669. }
  670. &:first-child {
  671. & > input,
  672. & > select,
  673. & > .button {
  674. border-radius: 5px 0 0 5px;
  675. }
  676. }
  677. &:last-child {
  678. & > input,
  679. & > select,
  680. & > .button {
  681. border-radius: 0 5px 5px 0;
  682. }
  683. }
  684. & > .button {
  685. font-size: 22px;
  686. }
  687. }
  688. }
  689. </style>