AdvancedTable.vue 58 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417
  1. <script setup lang="ts">
  2. import {
  3. defineAsyncComponent,
  4. PropType,
  5. useSlots,
  6. ref,
  7. computed,
  8. onMounted,
  9. onUnmounted,
  10. watch,
  11. nextTick
  12. } from "vue";
  13. import { useRoute, useRouter } from "vue-router";
  14. import Toast from "toasters";
  15. import { storeToRefs } from "pinia";
  16. import { DraggableList } from "vue-draggable-list";
  17. import { useWebsocketsStore } from "@/stores/websockets";
  18. import { useModalsStore } from "@/stores/modals";
  19. import keyboardShortcuts from "@/keyboardShortcuts";
  20. import { useDragBox } from "@/composables/useDragBox";
  21. import {
  22. TableColumn,
  23. TableFilter,
  24. TableEvents,
  25. TableBulkActions
  26. } from "@/types/advancedTable";
  27. const { dragBox, setInitialBox, onDragBox, resetBoxPosition } = useDragBox();
  28. const AutoSuggest = defineAsyncComponent(
  29. () => import("@/components/AutoSuggest.vue")
  30. );
  31. const props = defineProps({
  32. /*
  33. Column properties:
  34. name: Unique lowercase name
  35. displayName: Nice name for the column header
  36. properties: The properties this column needs to show data
  37. sortable: Boolean for whether the order of a particular column can be changed
  38. sortProperty: The property the backend will sort on if this column gets sorted, e.g. title
  39. hidable: Boolean for whether a column can be hidden
  40. defaultVisibility: Default visibility for a column, either "shown" or "hidden"
  41. draggable: Boolean for whether a column can be dragged/reordered,
  42. resizable: Boolean for whether a column can be resized
  43. minWidth: Minimum width of column, e.g. 50px
  44. width: Width of column, e.g. 100px
  45. maxWidth: Maximum width of column, e.g. 150px
  46. */
  47. columnDefault: {
  48. type: Object as PropType<TableColumn>,
  49. default: () => ({})
  50. },
  51. columns: {
  52. type: Array as PropType<TableColumn[]>,
  53. default: () => []
  54. },
  55. filters: {
  56. type: Array as PropType<TableFilter[]>,
  57. default: () => []
  58. },
  59. dataAction: { type: String, default: null },
  60. name: { type: String, default: null },
  61. maxWidth: { type: Number, default: 1880 },
  62. query: { type: Boolean, default: true },
  63. hasKeyboardShortcuts: { type: Boolean, default: true },
  64. events: { type: Object as PropType<TableEvents>, default: () => {} },
  65. bulkActions: {
  66. type: Object as PropType<TableBulkActions>,
  67. default: () => ({})
  68. }
  69. });
  70. const slots = useSlots();
  71. const route = useRoute();
  72. const router = useRouter();
  73. const modalsStore = useModalsStore();
  74. const { activeModals } = storeToRefs(modalsStore);
  75. const { socket } = useWebsocketsStore();
  76. const page = ref(1);
  77. const pageSize = ref(10);
  78. const rows = ref([]);
  79. const count = ref(0);
  80. const sort = ref({});
  81. const orderedColumns = ref([]);
  82. const shownColumns = ref([]);
  83. const editingFilters = ref([]);
  84. const appliedFilters = ref([]);
  85. const filterOperator = ref("or");
  86. const appliedFilterOperator = ref("or");
  87. const filterOperators = ref([
  88. {
  89. name: "or",
  90. displayName: "OR"
  91. },
  92. {
  93. name: "and",
  94. displayName: "AND"
  95. },
  96. {
  97. name: "nor",
  98. displayName: "NOR"
  99. }
  100. ]);
  101. const resizing = ref({
  102. resizing: false,
  103. width: 0,
  104. lastX: 0,
  105. resizingColumn: {
  106. width: 0,
  107. minWidth: 0,
  108. maxWidth: 0
  109. }
  110. });
  111. const allFilterTypes = ref({
  112. contains: {
  113. name: "contains",
  114. displayName: "Contains"
  115. },
  116. exact: {
  117. name: "exact",
  118. displayName: "Exact"
  119. },
  120. regex: {
  121. name: "regex",
  122. displayName: "Regex"
  123. },
  124. datetimeBefore: {
  125. name: "datetimeBefore",
  126. displayName: "Before"
  127. },
  128. datetimeAfter: {
  129. name: "datetimeAfter",
  130. displayName: "After"
  131. },
  132. numberLesserEqual: {
  133. name: "numberLesserEqual",
  134. displayName: "Less than or equal to"
  135. },
  136. numberLesser: {
  137. name: "numberLesser",
  138. displayName: "Less than"
  139. },
  140. numberGreater: {
  141. name: "numberGreater",
  142. displayName: "Greater than"
  143. },
  144. numberGreaterEqual: {
  145. name: "numberGreaterEqual",
  146. displayName: "Greater than or equal to"
  147. },
  148. numberEquals: {
  149. name: "numberEquals",
  150. displayName: "Equals"
  151. },
  152. boolean: {
  153. name: "boolean",
  154. displayName: "Boolean"
  155. },
  156. special: {
  157. name: "special",
  158. displayName: "Special"
  159. }
  160. });
  161. const addFilterValue = ref();
  162. const showFiltersDropdown = ref(false);
  163. const showColumnsDropdown = ref(false);
  164. const lastColumnResizerTapped = ref();
  165. const lastColumnResizerTappedDate = ref(0);
  166. const autosuggest = ref({
  167. allItems: {}
  168. });
  169. const storeTableSettingsDebounceTimeout = ref();
  170. const windowResizeDebounceTimeout = ref();
  171. const columnOrderChangedDebounceTimeout = ref();
  172. const lastSelectedItemIndex = ref(0);
  173. const bulkPopup = ref();
  174. const rowElements = ref([]);
  175. const lastPage = computed(() => Math.ceil(count.value / pageSize.value));
  176. const sortedFilteredColumns = computed(() =>
  177. orderedColumns.value.filter(
  178. column => shownColumns.value.indexOf(column.name) !== -1
  179. )
  180. );
  181. const hidableSortedColumns = computed(() =>
  182. orderedColumns.value.filter(column => column.hidable)
  183. );
  184. const selectedRows = computed(() => rows.value.filter(row => row.selected));
  185. const properties = computed(() =>
  186. Array.from(
  187. new Set(
  188. sortedFilteredColumns.value.flatMap(column => column.properties)
  189. )
  190. )
  191. );
  192. const hasCheckboxes = computed(
  193. () => slots["bulk-actions"] != null || slots["bulk-actions-right"] != null
  194. );
  195. const aModalIsOpen = computed(() => Object.keys(activeModals.value).length > 0);
  196. const getData = () => {
  197. socket.dispatch(
  198. props.dataAction,
  199. page.value,
  200. pageSize.value,
  201. properties.value,
  202. sort.value,
  203. appliedFilters.value.map(filter => ({
  204. ...filter,
  205. filterType: filter.filterType.name
  206. })),
  207. appliedFilterOperator.value,
  208. res => {
  209. if (res.status === "success") {
  210. rows.value = res.data.data.map(row => ({
  211. ...row,
  212. selected: false
  213. }));
  214. count.value = res.data.count;
  215. } else {
  216. new Toast(res.message);
  217. }
  218. }
  219. );
  220. };
  221. const setQuery = () => {
  222. const queryObject = {
  223. ...route.query,
  224. page: page.value,
  225. pageSize: pageSize.value,
  226. filter: JSON.stringify({
  227. appliedFilters: appliedFilters.value,
  228. appliedFilterOperator: appliedFilterOperator.value
  229. }),
  230. columnSort: JSON.stringify(sort.value),
  231. columnOrder: JSON.stringify(
  232. orderedColumns.value.map(column => column.name)
  233. ),
  234. columnWidths: JSON.stringify(
  235. orderedColumns.value.map(column => ({
  236. name: column.name,
  237. width: column.width
  238. }))
  239. ),
  240. shownColumns: JSON.stringify(shownColumns.value)
  241. };
  242. const queryString = `?${Object.keys(queryObject)
  243. .map(key => `${key}=${queryObject[key]}`)
  244. .join("&")}`;
  245. window.history.replaceState(window.history.state, null, queryString);
  246. };
  247. const setLocalStorage = () => {
  248. localStorage.setItem(
  249. `advancedTableSettings:${props.name}`,
  250. JSON.stringify({
  251. pageSize: pageSize.value,
  252. filter: {
  253. appliedFilters: appliedFilters.value,
  254. appliedFilterOperator: appliedFilterOperator.value
  255. },
  256. columnSort: sort.value,
  257. columnOrder: orderedColumns.value.map(column => column.name),
  258. columnWidths: orderedColumns.value.map(column => ({
  259. name: column.name,
  260. width: column.width
  261. })),
  262. shownColumns: shownColumns.value
  263. })
  264. );
  265. };
  266. const storeTableSettings = () => {
  267. // Clear debounce timeout
  268. if (storeTableSettingsDebounceTimeout.value)
  269. clearTimeout(storeTableSettingsDebounceTimeout.value);
  270. // Resizing calls this function a lot, so rather than saving dozens of times a second, use debouncing
  271. storeTableSettingsDebounceTimeout.value = setTimeout(() => {
  272. if (props.query) setQuery();
  273. setLocalStorage();
  274. }, 250);
  275. };
  276. const changePageSize = () => {
  277. page.value = 1;
  278. getData();
  279. storeTableSettings();
  280. };
  281. const changePage = newPage => {
  282. if (newPage < 1) return;
  283. if (newPage > lastPage.value) return;
  284. if (newPage === page.value) return;
  285. page.value = newPage;
  286. getData();
  287. if (props.query) setQuery();
  288. };
  289. const changeSort = column => {
  290. if (column.sortable) {
  291. const { sortProperty } = column;
  292. if (sort.value[sortProperty] === undefined)
  293. sort.value[sortProperty] = "ascending";
  294. else if (sort.value[sortProperty] === "ascending")
  295. sort.value[sortProperty] = "descending";
  296. else if (sort.value[sortProperty] === "descending")
  297. delete sort.value[sortProperty];
  298. getData();
  299. storeTableSettings();
  300. }
  301. };
  302. const recalculateWidths = () => {
  303. let noWidthCount = 0;
  304. let calculatedWidth = 0;
  305. orderedColumns.value.forEach(column => {
  306. if (orderedColumns.value.indexOf(column.name) !== -1)
  307. if (
  308. Number.isFinite(column.width) &&
  309. !Number.isFinite(column.calculatedWidth)
  310. ) {
  311. calculatedWidth += column.width;
  312. } else if (Number.isFinite(column.defaultWidth)) {
  313. calculatedWidth += column.defaultWidth;
  314. } else {
  315. noWidthCount += 1;
  316. }
  317. });
  318. calculatedWidth = Math.floor(
  319. (Math.min(props.maxWidth, document.body.clientWidth) -
  320. calculatedWidth) /
  321. (noWidthCount - 1)
  322. );
  323. orderedColumns.value = orderedColumns.value.map(column => {
  324. const orderedColumn = column;
  325. if (shownColumns.value.indexOf(orderedColumn.name) !== -1) {
  326. let newWidth;
  327. if (Number.isFinite(orderedColumn.defaultWidth)) {
  328. newWidth = orderedColumn.defaultWidth;
  329. } else {
  330. // eslint-disable-next-line no-param-reassign
  331. newWidth = orderedColumn.calculatedWidth = Math.min(
  332. Math.max(
  333. orderedColumn.minWidth || 100, // fallback 100px min width
  334. calculatedWidth
  335. ),
  336. orderedColumn.maxWidth || 1000 // fallback 1000px max width
  337. );
  338. }
  339. if (newWidth && !Number.isFinite(orderedColumn.width))
  340. orderedColumn.width = newWidth;
  341. }
  342. return orderedColumn;
  343. });
  344. };
  345. const toggleColumnVisibility = column => {
  346. if (!column.hidable) return false;
  347. if (shownColumns.value.indexOf(column.name) !== -1) {
  348. if (shownColumns.value.length <= 3)
  349. return new Toast(
  350. `Unable to hide column ${column.displayName}, there must be at least 1 visibile column`
  351. );
  352. shownColumns.value.splice(shownColumns.value.indexOf(column.name), 1);
  353. } else {
  354. shownColumns.value.push(column.name);
  355. }
  356. recalculateWidths();
  357. getData();
  358. return storeTableSettings();
  359. };
  360. const toggleSelectedRow = (itemIndex, event) => {
  361. const { shiftKey, ctrlKey } = event;
  362. // Shift was pressed, so attempt to select all items between the clicked item and last clicked item
  363. if (shiftKey && !ctrlKey) {
  364. // If the clicked item is already selected, prevent default, otherwise the checkbox will be unchecked
  365. if (rows.value[itemIndex].selected) event.preventDefault();
  366. rows.value[itemIndex].selected = true;
  367. // If there is a last clicked item
  368. if (lastSelectedItemIndex.value >= 0) {
  369. // Clicked item is lower than last item, so select upwards until it reaches the last selected item
  370. if (itemIndex > lastSelectedItemIndex.value) {
  371. for (
  372. let itemIndexUp = itemIndex;
  373. itemIndexUp > lastSelectedItemIndex.value;
  374. itemIndexUp -= 1
  375. ) {
  376. if (!rows.value[itemIndexUp].removed)
  377. rows.value[itemIndexUp].selected = true;
  378. }
  379. }
  380. // Clicked item is higher than last item, so select downwards until it reaches the last selected item
  381. else if (itemIndex < lastSelectedItemIndex.value) {
  382. for (
  383. let itemIndexDown = itemIndex;
  384. itemIndexDown < lastSelectedItemIndex.value;
  385. itemIndexDown += 1
  386. ) {
  387. if (!rows.value[itemIndexDown].removed)
  388. rows.value[itemIndexDown].selected = true;
  389. }
  390. }
  391. }
  392. }
  393. // Ctrl was pressed, so attempt to unselect all items between the clicked item and last clicked item
  394. else if (!shiftKey && ctrlKey) {
  395. // If the clicked item is already unselected, prevent default, otherwise the checkbox will be checked
  396. if (!rows.value[itemIndex].selected) event.preventDefault();
  397. rows.value[itemIndex].selected = false;
  398. // If there is a last clicked item
  399. if (lastSelectedItemIndex.value >= 0) {
  400. // Clicked item is lower than last item, so unselect upwards until it reaches the last selected item
  401. if (itemIndex >= lastSelectedItemIndex.value) {
  402. for (
  403. let itemIndexUp = itemIndex;
  404. itemIndexUp >= lastSelectedItemIndex.value;
  405. itemIndexUp -= 1
  406. ) {
  407. if (!rows.value[itemIndexUp].removed)
  408. rows.value[itemIndexUp].selected = false;
  409. }
  410. }
  411. // Clicked item is higher than last item, so unselect downwards until it reaches the last selected item
  412. else if (itemIndex < lastSelectedItemIndex.value) {
  413. for (
  414. let itemIndexDown = itemIndex;
  415. itemIndexDown <= lastSelectedItemIndex.value;
  416. itemIndexDown += 1
  417. ) {
  418. if (!rows.value[itemIndexDown].removed)
  419. rows.value[itemIndexDown].selected = false;
  420. }
  421. }
  422. }
  423. }
  424. // Neither ctrl nor shift were pressed, so toggle clicked item
  425. else {
  426. rows.value[itemIndex].selected = !rows.value[itemIndex].selected;
  427. }
  428. rows.value[itemIndex].highlighted = rows.value[itemIndex].selected;
  429. // Set the last clicked item to no longer be highlighted, if it exists
  430. if (lastSelectedItemIndex.value >= 0)
  431. rows.value[lastSelectedItemIndex.value].highlighted = false;
  432. lastSelectedItemIndex.value = itemIndex;
  433. };
  434. const toggleAllRows = () => {
  435. if (
  436. rows.value.filter(row => !row.removed).length >
  437. selectedRows.value.length
  438. ) {
  439. rows.value = rows.value.map(row => {
  440. if (row.removed) return row;
  441. return { ...row, selected: true };
  442. });
  443. } else {
  444. rows.value = rows.value.map(row => {
  445. if (row.removed) return row;
  446. return { ...row, selected: false };
  447. });
  448. }
  449. };
  450. const highlightRow = async itemIndex => {
  451. const rowElement = rowElements.value[`row-${itemIndex}`]
  452. ? rowElements.value[`row-${itemIndex}`][0]
  453. : null;
  454. // Set the last clicked item to no longer be highlighted, if it exists
  455. if (lastSelectedItemIndex.value >= 0)
  456. rows.value[lastSelectedItemIndex.value].highlighted = false;
  457. if (rowElement) {
  458. await nextTick();
  459. rowElement.focus();
  460. }
  461. // Set the item to be highlighted
  462. rows.value[itemIndex].highlighted = true;
  463. };
  464. const highlightUp = itemIndex => {
  465. if (itemIndex === 0) return;
  466. const newItemIndex = itemIndex - 1;
  467. highlightRow(newItemIndex);
  468. };
  469. const highlightDown = itemIndex => {
  470. if (itemIndex === rows.value.length - 1) return;
  471. const newItemIndex = itemIndex + 1;
  472. highlightRow(newItemIndex);
  473. };
  474. const unhighlightRow = async itemIndex => {
  475. const rowElement = rowElements.value[`row-${itemIndex}`]
  476. ? rowElements.value[`row-${itemIndex}`][0]
  477. : null;
  478. if (rowElement) {
  479. await nextTick();
  480. rowElement.blur();
  481. }
  482. // Set the item to no longer be highlighted
  483. rows.value[itemIndex].highlighted = false;
  484. };
  485. const selectUp = itemIndex => {
  486. if (itemIndex === 0) return;
  487. const newItemIndex = itemIndex - 1;
  488. if (!rows.value[itemIndex].removed) rows.value[itemIndex].selected = true;
  489. if (!rows.value[newItemIndex].removed)
  490. rows.value[newItemIndex].selected = true;
  491. highlightRow(newItemIndex);
  492. };
  493. const selectDown = itemIndex => {
  494. if (itemIndex === rows.value.length - 1) return;
  495. const newItemIndex = itemIndex + 1;
  496. if (!rows.value[itemIndex].removed) rows.value[itemIndex].selected = true;
  497. if (!rows.value[newItemIndex].removed)
  498. rows.value[newItemIndex].selected = true;
  499. highlightRow(newItemIndex);
  500. };
  501. const unselectUp = itemIndex => {
  502. if (itemIndex === 0) return;
  503. const newItemIndex = itemIndex - 1;
  504. if (!rows.value[itemIndex].removed) rows.value[itemIndex].selected = false;
  505. if (!rows.value[newItemIndex].removed)
  506. rows.value[newItemIndex].selected = false;
  507. highlightRow(newItemIndex);
  508. };
  509. const unselectDown = itemIndex => {
  510. if (itemIndex === rows.value.length - 1) return;
  511. const newItemIndex = itemIndex + 1;
  512. if (!rows.value[itemIndex].removed) rows.value[itemIndex].selected = false;
  513. if (!rows.value[newItemIndex].removed)
  514. rows.value[newItemIndex].selected = false;
  515. highlightRow(newItemIndex);
  516. };
  517. const addFilterItem = () => {
  518. let data = "";
  519. if (addFilterValue.value.defaultFilterType.startsWith("datetime")) {
  520. const now = new Date();
  521. now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
  522. data = now.toISOString().slice(0, 16);
  523. }
  524. editingFilters.value.push({
  525. data,
  526. filter: addFilterValue.value,
  527. filterType: allFilterTypes.value[addFilterValue.value.defaultFilterType]
  528. });
  529. };
  530. const removeFilterItem = index => {
  531. editingFilters.value.splice(index, 1);
  532. };
  533. const columnResetWidth = column => {
  534. const index = orderedColumns.value.indexOf(column);
  535. if (column.defaultWidth && !Number.isNaN(column.defaultWidth))
  536. orderedColumns.value[index].width = column.defaultWidth;
  537. else if (column.calculatedWidth && !Number.isNaN(column.calculatedWidth))
  538. orderedColumns.value[index].width = column.calculatedWidth;
  539. storeTableSettings();
  540. };
  541. const columnResizingStart = (column, event) => {
  542. const eventIsTouch = event.type === "touchstart";
  543. if (eventIsTouch) {
  544. // Handle double click from touch (if this method is called for the same column twice in a row within one second)
  545. if (
  546. lastColumnResizerTapped.value === column &&
  547. Date.now() - lastColumnResizerTappedDate.value <= 1000
  548. ) {
  549. columnResetWidth(column);
  550. lastColumnResizerTapped.value = null;
  551. lastColumnResizerTappedDate.value = 0;
  552. return;
  553. }
  554. lastColumnResizerTapped.value = column;
  555. lastColumnResizerTappedDate.value = Date.now();
  556. }
  557. resizing.value.resizing = true;
  558. resizing.value.resizingColumn = column;
  559. resizing.value.width = event.target.parentElement.offsetWidth;
  560. resizing.value.lastX = eventIsTouch
  561. ? event.targetTouches[0].clientX
  562. : event.x;
  563. };
  564. const columnResizing = event => {
  565. if (resizing.value.resizing) {
  566. const eventIsTouch = event.type === "touchmove";
  567. if (!eventIsTouch && event.buttons !== 1) {
  568. resizing.value.resizing = false;
  569. storeTableSettings();
  570. }
  571. const x = eventIsTouch ? event.changedTouches[0].clientX : event.x;
  572. resizing.value.width -= resizing.value.lastX - x;
  573. resizing.value.lastX = x;
  574. if (
  575. resizing.value.resizingColumn.minWidth &&
  576. resizing.value.resizingColumn.maxWidth
  577. ) {
  578. resizing.value.resizingColumn.width = Math.max(
  579. Math.min(
  580. resizing.value.resizingColumn.maxWidth,
  581. resizing.value.width
  582. ),
  583. resizing.value.resizingColumn.minWidth
  584. );
  585. } else if (resizing.value.resizingColumn.minWidth) {
  586. resizing.value.resizingColumn.width = Math.max(
  587. resizing.value.width,
  588. resizing.value.resizingColumn.minWidth
  589. );
  590. } else if (resizing.value.resizingColumn.maxWidth) {
  591. resizing.value.resizingColumn.width = Math.min(
  592. resizing.value.resizingColumn.maxWidth,
  593. resizing.value.width
  594. );
  595. } else {
  596. resizing.value.resizingColumn.width = resizing.value.width;
  597. }
  598. resizing.value.width = resizing.value.resizingColumn.width;
  599. console.log(`New width: ${resizing.value.width}px`);
  600. storeTableSettings();
  601. }
  602. };
  603. const columnResizingStop = () => {
  604. resizing.value.resizing = false;
  605. storeTableSettings();
  606. };
  607. const filterTypes = filter => {
  608. if (!filter || !filter.filterTypes) return [];
  609. return filter.filterTypes.map(
  610. filterType => allFilterTypes.value[filterType]
  611. );
  612. };
  613. const changeFilterType = index => {
  614. editingFilters.value[index].filterType =
  615. allFilterTypes.value[
  616. editingFilters.value[index].filter.defaultFilterType
  617. ];
  618. };
  619. const applyFilterAndGetData = () => {
  620. appliedFilters.value = JSON.parse(JSON.stringify(editingFilters.value));
  621. appliedFilterOperator.value = filterOperator.value;
  622. page.value = 1;
  623. getData();
  624. storeTableSettings();
  625. };
  626. const columnOrderChanged = () => {
  627. storeTableSettings();
  628. };
  629. const getTableSettings = () => {
  630. const urlTableSettings: {
  631. page: number;
  632. pageSize: number;
  633. shownColumns: string[];
  634. columnOrder: string[];
  635. columnWidths: {
  636. name: string;
  637. width: number;
  638. }[];
  639. columnSort: Record<string, string>;
  640. filter: {
  641. appliedFilters: TableFilter[];
  642. appliedFilterOperator: string;
  643. };
  644. } = {};
  645. if (props.query) {
  646. if (route.query.page)
  647. urlTableSettings.page = Number.parseInt(route.query.page);
  648. if (route.query.pageSize)
  649. urlTableSettings.pageSize = Number.parseInt(route.query.pageSize);
  650. if (route.query.shownColumns)
  651. urlTableSettings.shownColumns = JSON.parse(
  652. route.query.shownColumns
  653. );
  654. if (route.query.columnOrder)
  655. urlTableSettings.columnOrder = JSON.parse(route.query.columnOrder);
  656. if (route.query.columnWidths)
  657. urlTableSettings.columnWidths = JSON.parse(
  658. route.query.columnWidths
  659. );
  660. if (route.query.columnSort)
  661. urlTableSettings.columnSort = JSON.parse(route.query.columnSort);
  662. if (route.query.filter)
  663. urlTableSettings.filter = JSON.parse(route.query.filter);
  664. }
  665. const localStorageTableSettings = JSON.parse(
  666. localStorage.getItem(`advancedTableSettings:${props.name}`)
  667. );
  668. return {
  669. ...localStorageTableSettings,
  670. ...urlTableSettings
  671. };
  672. };
  673. const onWindowResize = () => {
  674. if (windowResizeDebounceTimeout.value)
  675. clearTimeout(windowResizeDebounceTimeout.value);
  676. windowResizeDebounceTimeout.value = setTimeout(() => {
  677. // Only change the position if the popup is actually visible
  678. if (selectedRows.value.length === 0) return;
  679. const bulkActions = {
  680. height: 46,
  681. width: 400,
  682. ...props.bulkActions
  683. };
  684. setInitialBox(
  685. {
  686. top: document.body.clientHeight - bulkActions.height - 10,
  687. left: (document.body.clientWidth - bulkActions.width) / 2,
  688. ...bulkActions
  689. },
  690. true
  691. );
  692. }, 50);
  693. };
  694. const updateData = (index, data) => {
  695. rows.value[index] = { ...rows.value[index], ...data, updated: true };
  696. };
  697. const removeData = index => {
  698. rows.value[index] = {
  699. ...rows.value[index],
  700. selected: false,
  701. removed: true
  702. };
  703. };
  704. onMounted(async () => {
  705. const tableSettings = getTableSettings();
  706. const columns = [
  707. ...props.columns.map(column => ({
  708. ...(typeof props.columnDefault === "object"
  709. ? props.columnDefault
  710. : {}),
  711. ...column
  712. })),
  713. {
  714. name: "placeholder",
  715. displayName: "",
  716. properties: [],
  717. sortable: false,
  718. hidable: false,
  719. draggable: false,
  720. resizable: false,
  721. minWidth: "auto",
  722. width: "auto",
  723. maxWidth: "auto"
  724. }
  725. ];
  726. if (hasCheckboxes.value)
  727. columns.unshift({
  728. name: "select",
  729. displayName: "",
  730. properties: [],
  731. sortable: false,
  732. hidable: false,
  733. draggable: false,
  734. resizable: false,
  735. minWidth: 47,
  736. defaultWidth: 47,
  737. maxWidth: 47
  738. });
  739. if (props.events && props.events.updated)
  740. columns.unshift({
  741. name: "updatedPlaceholder",
  742. displayName: "",
  743. properties: [],
  744. sortable: false,
  745. hidable: false,
  746. draggable: false,
  747. resizable: false,
  748. minWidth: 5,
  749. width: 5,
  750. maxWidth: 5
  751. });
  752. orderedColumns.value = columns.sort((columnA, columnB) => {
  753. // Always places updatedPlaceholder column in the first position
  754. if (columnA.name === "updatedPlaceholder") return -1;
  755. if (columnB.name === "updatedPlaceholder") return 1;
  756. // Always places select column in the second position
  757. if (columnA.name === "select") return -1;
  758. if (columnB.name === "select") return 1;
  759. // Always places placeholder column in the last position
  760. if (columnA.name === "placeholder") return 1;
  761. if (columnB.name === "placeholder") return -1;
  762. // If there are no table settings stored, use default ordering
  763. if (!tableSettings || !tableSettings.columnOrder) return 0;
  764. const indexA = tableSettings.columnOrder.indexOf(columnA.name);
  765. const indexB = tableSettings.columnOrder.indexOf(columnB.name);
  766. // If either of the columns is not stored in the table settings, use default ordering
  767. if (indexA === -1 || indexB === -1) return 0;
  768. return indexA - indexB;
  769. });
  770. shownColumns.value = orderedColumns.value
  771. .filter(column => {
  772. // If table settings exist, use shownColumns from settings to determine which columns to show
  773. if (tableSettings && tableSettings.shownColumns)
  774. return tableSettings.shownColumns.indexOf(column.name) !== -1;
  775. // Table settings don't exist, only show if the default visibility isn't hidden
  776. return column.defaultVisibility !== "hidden";
  777. })
  778. .map(column => column.name);
  779. recalculateWidths();
  780. if (tableSettings) {
  781. // If table settings' page is an integer, use it for the page
  782. if (Number.isInteger(tableSettings?.page))
  783. page.value = tableSettings.page;
  784. // If table settings' pageSize is an integer, use it for the pageSize
  785. if (Number.isInteger(tableSettings?.pageSize))
  786. pageSize.value = tableSettings.pageSize;
  787. // If table settings' columnSort exists, sort all still existing columns based on table settings' columnSort object
  788. if (tableSettings.columnSort) {
  789. Object.entries(tableSettings.columnSort).forEach(
  790. ([columnName, sortDirection]) => {
  791. if (
  792. props.columns.find(column => column.name === columnName)
  793. )
  794. sort.value[columnName] = sortDirection;
  795. }
  796. );
  797. }
  798. // If table settings' columnWidths exists, load the stored widths into the columns
  799. if (tableSettings.columnWidths) {
  800. orderedColumns.value = orderedColumns.value.map(orderedColumn => {
  801. const columnWidth = tableSettings.columnWidths.find(
  802. column => column.name === orderedColumn.name
  803. )?.width;
  804. if (orderedColumn.resizable && columnWidth)
  805. return { ...orderedColumn, width: columnWidth };
  806. return orderedColumn;
  807. });
  808. }
  809. if (
  810. tableSettings.filter &&
  811. tableSettings.filter.appliedFilters &&
  812. tableSettings.filter.appliedFilterOperator
  813. ) {
  814. // Set the applied filter operator and filter operator to the value stored in table settings
  815. appliedFilterOperator.value = filterOperator.value =
  816. tableSettings.filter.appliedFilterOperator;
  817. // Set the applied filters and editing filters to the value stored in table settings, for all filters that are allowed
  818. appliedFilters.value = tableSettings.filter.appliedFilters.filter(
  819. appliedFilter =>
  820. props.filters.find(
  821. (filter: { name: string }) =>
  822. appliedFilter.filter.name === filter.name
  823. )
  824. );
  825. editingFilters.value = tableSettings.filter.appliedFilters.filter(
  826. appliedFilter =>
  827. props.filters.find(
  828. (filter: { name: string }) =>
  829. appliedFilter.filter.name === filter.name
  830. )
  831. );
  832. }
  833. }
  834. socket.onConnect(() => {
  835. getData();
  836. if (props.query) setQuery();
  837. if (props.events) {
  838. // if (props.events.room)
  839. // socket.dispatch("apis.joinRoom", props.events.room, () => {});
  840. if (props.events.adminRoom)
  841. socket.dispatch(
  842. "apis.joinAdminRoom",
  843. props.events.adminRoom,
  844. () => {}
  845. );
  846. }
  847. props.filters.forEach(filter => {
  848. if (filter.autosuggest && filter.autosuggestDataAction) {
  849. socket.dispatch(filter.autosuggestDataAction, res => {
  850. if (res.status === "success") {
  851. const { items } = res.data;
  852. autosuggest.value.allItems[filter.name] = items;
  853. } else {
  854. new Toast(res.message);
  855. }
  856. });
  857. }
  858. });
  859. });
  860. // TODO, this doesn't address special properties
  861. if (props.events && props.events.updated)
  862. socket.on(`event:${props.events.updated.event}`, res => {
  863. const index = rows.value
  864. .map(row => row._id)
  865. .indexOf(
  866. props.events.updated.id
  867. .split(".")
  868. .reduce(
  869. (previous, current) =>
  870. previous &&
  871. previous[current] !== null &&
  872. previous[current] !== undefined
  873. ? previous[current]
  874. : null,
  875. res.data
  876. )
  877. );
  878. const row = props.events.updated.item
  879. .split(".")
  880. .reduce(
  881. (previous, current) =>
  882. previous &&
  883. previous[current] !== null &&
  884. previous[current] !== undefined
  885. ? previous[current]
  886. : null,
  887. res.data
  888. );
  889. updateData(index, row);
  890. });
  891. if (props.events && props.events.removed)
  892. socket.on(`event:${props.events.removed.event}`, res => {
  893. const index = rows.value
  894. .map(row => row._id)
  895. .indexOf(
  896. props.events.removed.id
  897. .split(".")
  898. .reduce(
  899. (previous, current) =>
  900. previous &&
  901. previous[current] !== null &&
  902. previous[current] !== undefined
  903. ? previous[current]
  904. : null,
  905. res.data
  906. )
  907. );
  908. removeData(index);
  909. });
  910. if (props.hasKeyboardShortcuts) {
  911. // Navigation section
  912. // Page navigation section
  913. keyboardShortcuts.registerShortcut("advancedTable.previousPage", {
  914. keyCode: 37, // 'Left arrow' key
  915. ctrl: true,
  916. preventDefault: false,
  917. handler: event => {
  918. // Previous page
  919. if (aModalIsOpen.value) return;
  920. if (
  921. document.activeElement.nodeName === "INPUT" ||
  922. document.activeElement.nodeName === "TEXTAREA"
  923. )
  924. return;
  925. event.preventDefault();
  926. changePage(page.value - 1);
  927. }
  928. });
  929. keyboardShortcuts.registerShortcut("advancedTable.nextPage", {
  930. keyCode: 39, // 'Right arrow' key
  931. ctrl: true,
  932. preventDefault: false,
  933. handler: event => {
  934. // Next page
  935. if (aModalIsOpen.value) return;
  936. if (
  937. document.activeElement.nodeName === "INPUT" ||
  938. document.activeElement.nodeName === "TEXTAREA"
  939. )
  940. return;
  941. event.preventDefault();
  942. changePage(page.value + 1);
  943. }
  944. });
  945. keyboardShortcuts.registerShortcut("advancedTable.firstPage", {
  946. keyCode: 37, // 'Left arrow' key
  947. ctrl: true,
  948. shift: true,
  949. preventDefault: false,
  950. handler: event => {
  951. // First page
  952. if (aModalIsOpen.value) return;
  953. if (
  954. document.activeElement.nodeName === "INPUT" ||
  955. document.activeElement.nodeName === "TEXTAREA"
  956. )
  957. return;
  958. event.preventDefault();
  959. changePage(1);
  960. }
  961. });
  962. keyboardShortcuts.registerShortcut("advancedTable.lastPage", {
  963. keyCode: 39, // 'Right arrow' key
  964. ctrl: true,
  965. shift: true,
  966. preventDefault: false,
  967. handler: event => {
  968. // Last page
  969. if (aModalIsOpen.value) return;
  970. if (
  971. document.activeElement.nodeName === "INPUT" ||
  972. document.activeElement.nodeName === "TEXTAREA"
  973. )
  974. return;
  975. event.preventDefault();
  976. changePage(lastPage.value);
  977. }
  978. });
  979. // Reset localStorage section
  980. keyboardShortcuts.registerShortcut("advancedTable.resetLocalStorage", {
  981. keyCode: 116, // 'F5' key
  982. ctrl: true,
  983. preventDefault: false,
  984. handler: () => {
  985. // Reset local storage
  986. if (aModalIsOpen.value) return;
  987. console.log("Reset local storage");
  988. localStorage.removeItem(`advancedTableSettings:${props.name}`);
  989. router.push({ query: {} });
  990. }
  991. });
  992. // Selecting section
  993. keyboardShortcuts.registerShortcut("advancedTable.selectAll", {
  994. keyCode: 65, // 'A' key
  995. ctrl: true,
  996. preventDefault: false,
  997. handler: event => {
  998. if (aModalIsOpen.value) return;
  999. if (
  1000. document.activeElement.nodeName === "INPUT" ||
  1001. document.activeElement.nodeName === "TEXTAREA"
  1002. )
  1003. return;
  1004. event.preventDefault();
  1005. toggleAllRows();
  1006. }
  1007. });
  1008. // Popup actions section
  1009. for (let i = 1; i <= 9; i += 1) {
  1010. keyboardShortcuts.registerShortcut(
  1011. `advancedTable.executePopupAction${i}`,
  1012. {
  1013. keyCode: 48 + i, // '1-9' keys, where 49 is 1 and 57 is 9
  1014. ctrl: true,
  1015. preventDefault: true,
  1016. handler: () => {
  1017. // Execute popup action 1-9
  1018. if (aModalIsOpen.value) return;
  1019. if (selectedRows.value.length === 0) return;
  1020. const bulkActionsElement =
  1021. bulkPopup.value.querySelector(".bulk-actions");
  1022. bulkActionsElement.children[i - 1].click();
  1023. }
  1024. }
  1025. );
  1026. }
  1027. keyboardShortcuts.registerShortcut(`advancedTable.selectPopupAction1`, {
  1028. keyCode: 48, // '0' key
  1029. ctrl: true,
  1030. preventDefault: true,
  1031. handler: () => {
  1032. // Select popup action 0
  1033. if (aModalIsOpen.value) return;
  1034. if (selectedRows.value.length === 0) return;
  1035. const bulkActionsElement =
  1036. bulkPopup.value.querySelector(".bulk-actions");
  1037. bulkActionsElement.children[
  1038. bulkActionsElement.children.length - 1
  1039. ].focus();
  1040. }
  1041. });
  1042. }
  1043. await nextTick();
  1044. onWindowResize();
  1045. window.addEventListener("resize", onWindowResize);
  1046. });
  1047. onUnmounted(() => {
  1048. window.removeEventListener("resize", onWindowResize);
  1049. if (storeTableSettingsDebounceTimeout.value)
  1050. clearTimeout(storeTableSettingsDebounceTimeout.value);
  1051. if (windowResizeDebounceTimeout.value)
  1052. clearTimeout(windowResizeDebounceTimeout.value);
  1053. if (columnOrderChangedDebounceTimeout.value)
  1054. clearTimeout(columnOrderChangedDebounceTimeout.value);
  1055. if (props.hasKeyboardShortcuts) {
  1056. const shortcutNames = [
  1057. // Navigation
  1058. "advancedTable.previousPage",
  1059. "advancedTable.nextPage",
  1060. "advancedTable.firstPage",
  1061. "advancedTable.lastPage",
  1062. // Reset localStorage
  1063. "advancedTable.resetLocalStorage",
  1064. // Selecting
  1065. "advancedTable.selectAll",
  1066. // Popup actions
  1067. "advancedTable.executePopupAction1",
  1068. "advancedTable.executePopupAction2",
  1069. "advancedTable.executePopupAction3",
  1070. "advancedTable.executePopupAction4",
  1071. "advancedTable.executePopupAction5",
  1072. "advancedTable.executePopupAction6",
  1073. "advancedTable.executePopupAction7",
  1074. "advancedTable.executePopupAction8",
  1075. "advancedTable.executePopupAction9",
  1076. "advancedTable.selectPopupAction1"
  1077. ];
  1078. shortcutNames.forEach(shortcutName => {
  1079. keyboardShortcuts.unregisterShortcut(shortcutName);
  1080. });
  1081. }
  1082. });
  1083. watch(selectedRows, (newSelectedRows, oldSelectedRows) => {
  1084. // If selected rows goes from zero to one or more selected, trigger onWindowResize, as otherwise the popup could be out of bounds
  1085. if (oldSelectedRows.length === 0 && newSelectedRows.length > 0)
  1086. onWindowResize();
  1087. });
  1088. </script>
  1089. <template>
  1090. <div>
  1091. <div
  1092. class="table-outer-container"
  1093. @mousemove="columnResizing($event)"
  1094. @touchmove="columnResizing($event)"
  1095. >
  1096. <div class="table-header">
  1097. <div>
  1098. <tippy
  1099. v-if="filters.length > 0"
  1100. :touch="true"
  1101. :interactive="true"
  1102. placement="bottom-start"
  1103. theme="search"
  1104. ref="search"
  1105. trigger="click"
  1106. @show="
  1107. () => {
  1108. showFiltersDropdown = true;
  1109. }
  1110. "
  1111. @hide="
  1112. () => {
  1113. showFiltersDropdown = false;
  1114. }
  1115. "
  1116. >
  1117. <div class="control has-addons" ref="trigger">
  1118. <button class="button is-primary">
  1119. <i class="material-icons icon-with-button"
  1120. >filter_list</i
  1121. >
  1122. Filters
  1123. </button>
  1124. <button class="button dropdown-toggle">
  1125. <i class="material-icons">
  1126. {{
  1127. showFiltersDropdown
  1128. ? "expand_less"
  1129. : "expand_more"
  1130. }}
  1131. </i>
  1132. </button>
  1133. </div>
  1134. <template #content>
  1135. <div class="control is-grouped input-with-button">
  1136. <p class="control select is-expanded">
  1137. <select v-model="addFilterValue">
  1138. <option
  1139. v-for="type in filters"
  1140. :key="type.name"
  1141. :value="type"
  1142. >
  1143. {{ type.displayName }}
  1144. </option>
  1145. </select>
  1146. </p>
  1147. <p class="control">
  1148. <button
  1149. :disabled="!addFilterValue"
  1150. class="button material-icons is-success"
  1151. @click="addFilterItem()"
  1152. >
  1153. control_point
  1154. </button>
  1155. </p>
  1156. </div>
  1157. <div
  1158. v-for="(filter, index) in editingFilters"
  1159. :key="`filter-${index}`"
  1160. class="advanced-filter control is-grouped is-expanded"
  1161. >
  1162. <div class="control select">
  1163. <select
  1164. v-model="filter.filter"
  1165. @change="changeFilterType(index)"
  1166. >
  1167. <option
  1168. v-for="type in filters"
  1169. :key="type.name"
  1170. :value="type"
  1171. >
  1172. {{ type.displayName }}
  1173. </option>
  1174. </select>
  1175. </div>
  1176. <div class="control select">
  1177. <select
  1178. v-model="filter.filterType"
  1179. :disabled="!filter.filterType"
  1180. >
  1181. <option
  1182. v-for="filterType in filterTypes(
  1183. filter.filter
  1184. )"
  1185. :key="filterType.name"
  1186. :value="filterType"
  1187. :selected="
  1188. filter.filter
  1189. .defaultFilterType ===
  1190. filterType.name
  1191. "
  1192. >
  1193. {{ filterType.displayName }}
  1194. </option>
  1195. </select>
  1196. </div>
  1197. <div
  1198. v-if="
  1199. filter.filterType.name &&
  1200. (filter.filterType.name === 'exact' ||
  1201. filter.filterType.name ===
  1202. 'boolean') &&
  1203. filter.filter.dropdown
  1204. "
  1205. class="control is-expanded select"
  1206. >
  1207. <select
  1208. v-model="filter.data"
  1209. :disabled="!filter.filterType"
  1210. >
  1211. <option
  1212. v-for="[
  1213. dropdownValue,
  1214. dropdownDisplay
  1215. ] in filter.filter.dropdown"
  1216. :key="dropdownValue"
  1217. :value="dropdownValue"
  1218. >
  1219. {{ dropdownDisplay }}
  1220. </option>
  1221. </select>
  1222. </div>
  1223. <div
  1224. v-else-if="
  1225. filter.filterType.name &&
  1226. filter.filterType.name === 'boolean'
  1227. "
  1228. class="control is-expanded select"
  1229. >
  1230. <select
  1231. v-model="filter.data"
  1232. :disabled="!filter.filterType"
  1233. >
  1234. <option :value="true">true</option>
  1235. <option :value="false">false</option>
  1236. </select>
  1237. </div>
  1238. <div v-else class="control is-expanded">
  1239. <input
  1240. v-if="
  1241. filter.filterType.name &&
  1242. filter.filterType.name.startsWith(
  1243. 'datetime'
  1244. )
  1245. "
  1246. v-model="filter.data"
  1247. class="input"
  1248. type="datetime-local"
  1249. />
  1250. <input
  1251. v-else-if="
  1252. filter.filterType.name &&
  1253. filter.filterType.name.startsWith(
  1254. 'number'
  1255. )
  1256. "
  1257. v-model="filter.data"
  1258. class="input"
  1259. type="number"
  1260. :disabled="!filter.filterType"
  1261. @keydown.enter="applyFilterAndGetData()"
  1262. />
  1263. <auto-suggest
  1264. v-else
  1265. v-model="filter.data"
  1266. placeholder="Search value"
  1267. :disabled="!filter.filterType"
  1268. :all-items="
  1269. autosuggest.allItems[
  1270. filter.filter.name
  1271. ]
  1272. "
  1273. @submitted="applyFilterAndGetData()"
  1274. />
  1275. </div>
  1276. <div class="control">
  1277. <button
  1278. class="button material-icons is-danger"
  1279. @click="removeFilterItem(index)"
  1280. >
  1281. remove_circle_outline
  1282. </button>
  1283. </div>
  1284. </div>
  1285. <div
  1286. v-if="editingFilters.length > 0"
  1287. class="control is-expanded is-grouped"
  1288. >
  1289. <label class="control label"
  1290. >Filter operator</label
  1291. >
  1292. <div class="control select is-expanded">
  1293. <select v-model="filterOperator">
  1294. <option
  1295. v-for="operator in filterOperators"
  1296. :key="operator.name"
  1297. :value="operator.name"
  1298. >
  1299. {{ operator.displayName }}
  1300. </option>
  1301. </select>
  1302. </div>
  1303. </div>
  1304. <div
  1305. class="advanced-filter-bottom"
  1306. v-if="editingFilters.length > 0"
  1307. >
  1308. <div class="control is-expanded">
  1309. <button
  1310. class="button is-info"
  1311. @click="applyFilterAndGetData()"
  1312. >
  1313. <i
  1314. class="material-icons icon-with-button"
  1315. >filter_list</i
  1316. >
  1317. Apply filters
  1318. </button>
  1319. </div>
  1320. </div>
  1321. <div
  1322. class="advanced-filter-bottom"
  1323. v-else-if="editingFilters.length === 0"
  1324. >
  1325. <div class="control is-expanded">
  1326. <button
  1327. class="button is-info"
  1328. @click="applyFilterAndGetData()"
  1329. >
  1330. <i
  1331. class="material-icons icon-with-button"
  1332. >filter_list</i
  1333. >
  1334. Apply filters
  1335. </button>
  1336. </div>
  1337. </div>
  1338. </template>
  1339. </tippy>
  1340. <tippy
  1341. v-if="appliedFilters.length > 0"
  1342. :touch="true"
  1343. :interactive="true"
  1344. theme="info"
  1345. ref="activeFilters"
  1346. >
  1347. <div class="filters-indicator">
  1348. {{ appliedFilters.length }}
  1349. <i class="material-icons" @click.prevent="true"
  1350. >filter_list</i
  1351. >
  1352. </div>
  1353. <template #content>
  1354. <p
  1355. v-for="(filter, index) in appliedFilters"
  1356. :key="`filter-${index}`"
  1357. >
  1358. {{ filter.filter.displayName }}
  1359. {{
  1360. appliedFilters.length === 1 &&
  1361. appliedFilterOperator === "nor"
  1362. ? "not"
  1363. : ""
  1364. }}
  1365. {{
  1366. filter.filterType.displayName.toLowerCase()
  1367. }}
  1368. "{{ filter.data }}"
  1369. {{
  1370. appliedFilters.length === index + 1
  1371. ? ""
  1372. : appliedFilterOperator
  1373. }}
  1374. </p>
  1375. </template>
  1376. </tippy>
  1377. <i
  1378. v-else
  1379. class="filters-indicator material-icons"
  1380. content="No active filters"
  1381. v-tippy="{ theme: 'info' }"
  1382. >
  1383. filter_list_off
  1384. </i>
  1385. </div>
  1386. <div>
  1387. <tippy
  1388. v-if="hidableSortedColumns.length > 0"
  1389. :touch="true"
  1390. :interactive="true"
  1391. placement="bottom-end"
  1392. theme="dropdown"
  1393. ref="editColumns"
  1394. trigger="click"
  1395. @show="
  1396. () => {
  1397. showColumnsDropdown = true;
  1398. }
  1399. "
  1400. @hide="
  1401. () => {
  1402. showColumnsDropdown = false;
  1403. }
  1404. "
  1405. >
  1406. <div class="control has-addons" ref="trigger">
  1407. <button class="button is-primary">
  1408. <i class="material-icons icon-with-button"
  1409. >tune</i
  1410. >
  1411. Columns
  1412. </button>
  1413. <button class="button dropdown-toggle">
  1414. <i class="material-icons">
  1415. {{
  1416. showColumnsDropdown
  1417. ? "expand_less"
  1418. : "expand_more"
  1419. }}
  1420. </i>
  1421. </button>
  1422. </div>
  1423. <template #content>
  1424. <div class="nav-dropdown-items">
  1425. <draggable-list
  1426. v-model:list="orderedColumns"
  1427. item-key="name"
  1428. @update="columnOrderChanged"
  1429. :attributes="{
  1430. class: column => ({
  1431. sortable: column.sortable,
  1432. 'nav-item': true
  1433. })
  1434. }"
  1435. :disabled="column => !column.draggable"
  1436. tag="button"
  1437. >
  1438. <template #item="{ element: column }">
  1439. <template
  1440. v-if="
  1441. column.name !== 'select' &&
  1442. column.name !== 'placeholder' &&
  1443. column.name !==
  1444. 'updatedPlaceholder'
  1445. "
  1446. >
  1447. <div
  1448. @click.prevent="
  1449. toggleColumnVisibility(
  1450. column
  1451. )
  1452. "
  1453. >
  1454. <p
  1455. class="control is-expanded checkbox-control"
  1456. >
  1457. <label class="switch">
  1458. <input
  1459. type="checkbox"
  1460. :id="`column-dropdown-checkbox-${column.name}`"
  1461. :checked="
  1462. shownColumns.indexOf(
  1463. column.name
  1464. ) !== -1
  1465. "
  1466. @click="
  1467. toggleColumnVisibility(
  1468. column
  1469. )
  1470. "
  1471. />
  1472. <span
  1473. :class="{
  1474. slider: true,
  1475. round: true,
  1476. disabled:
  1477. !column.hidable
  1478. }"
  1479. ></span>
  1480. </label>
  1481. <label
  1482. :for="`column-dropdown-checkbox-${column.name}`"
  1483. >
  1484. <span></span>
  1485. <p>
  1486. {{
  1487. column.displayName
  1488. }}
  1489. </p>
  1490. </label>
  1491. </p>
  1492. </div>
  1493. </template>
  1494. </template>
  1495. </draggable-list>
  1496. </div>
  1497. </template>
  1498. </tippy>
  1499. </div>
  1500. </div>
  1501. <div class="table-container">
  1502. <table
  1503. :class="{
  1504. table: true,
  1505. 'has-checkboxes': hasCheckboxes
  1506. }"
  1507. >
  1508. <thead>
  1509. <tr>
  1510. <draggable-list
  1511. v-model:list="orderedColumns"
  1512. item-key="name"
  1513. @update="columnOrderChanged"
  1514. tag="th"
  1515. :attributes="{
  1516. style: column => ({
  1517. minWidth: Number.isNaN(column.minWidth)
  1518. ? column.minWidth
  1519. : `${column.minWidth}px`,
  1520. width: Number.isNaN(column.width)
  1521. ? column.width
  1522. : `${column.width}px`,
  1523. maxWidth: Number.isNaN(column.maxWidth)
  1524. ? column.maxWidth
  1525. : `${column.maxWidth}px`
  1526. }),
  1527. class: column => ({
  1528. sortable: column.sortable
  1529. })
  1530. }"
  1531. :disabled="column => !column.draggable"
  1532. >
  1533. <template #item="{ element: column }">
  1534. <template
  1535. v-if="
  1536. shownColumns.indexOf(
  1537. column.name
  1538. ) !== -1 &&
  1539. (column.name !==
  1540. 'updatedPlaceholder' ||
  1541. rows.length > 0)
  1542. "
  1543. >
  1544. <div v-if="column.name === 'select'">
  1545. <p class="checkbox">
  1546. <input
  1547. v-if="rows.length === 0"
  1548. type="checkbox"
  1549. disabled
  1550. />
  1551. <input
  1552. v-else
  1553. type="checkbox"
  1554. :checked="
  1555. rows.filter(
  1556. row => !row.removed
  1557. ).length ===
  1558. selectedRows.length
  1559. "
  1560. @click="toggleAllRows()"
  1561. />
  1562. </p>
  1563. </div>
  1564. <div v-else class="handle">
  1565. <span>
  1566. {{ column.displayName }}
  1567. </span>
  1568. <span
  1569. v-if="column.sortable"
  1570. :content="`Sort by ${column.displayName}`"
  1571. v-tippy
  1572. >
  1573. <span
  1574. v-if="
  1575. !sort[
  1576. column.sortProperty
  1577. ]
  1578. "
  1579. class="material-icons"
  1580. @click="changeSort(column)"
  1581. >
  1582. unfold_more
  1583. </span>
  1584. <span
  1585. v-if="
  1586. sort[
  1587. column.sortProperty
  1588. ] === 'ascending'
  1589. "
  1590. class="material-icons active"
  1591. @click="changeSort(column)"
  1592. >
  1593. expand_more
  1594. </span>
  1595. <span
  1596. v-if="
  1597. sort[
  1598. column.sortProperty
  1599. ] === 'descending'
  1600. "
  1601. class="material-icons active"
  1602. @click="changeSort(column)"
  1603. >
  1604. expand_less
  1605. </span>
  1606. </span>
  1607. </div>
  1608. <div
  1609. class="resizer"
  1610. v-if="column.resizable"
  1611. @mousedown.prevent.stop="
  1612. columnResizingStart(
  1613. column,
  1614. $event
  1615. )
  1616. "
  1617. @touchstart.prevent.stop="
  1618. columnResizingStart(
  1619. column,
  1620. $event
  1621. )
  1622. "
  1623. @mouseup="columnResizingStop()"
  1624. @touchend="columnResizingStop()"
  1625. @dblclick="columnResetWidth(column)"
  1626. ></div>
  1627. </template>
  1628. </template>
  1629. </draggable-list>
  1630. </tr>
  1631. </thead>
  1632. <tbody>
  1633. <tr
  1634. v-for="(item, itemIndex) in rows"
  1635. :key="item._id"
  1636. :class="{
  1637. selected: item.selected,
  1638. highlighted: item.highlighted,
  1639. updated: item.updated,
  1640. removed: item.removed
  1641. }"
  1642. :ref="el => (rowElements[`row-${itemIndex}`] = el)"
  1643. tabindex="0"
  1644. @blur="unhighlightRow(itemIndex)"
  1645. @keydown.up.prevent
  1646. @keydown.down.prevent
  1647. @keydown.space.prevent
  1648. @click="highlightRow(itemIndex)"
  1649. @keyup.up.exact="highlightUp(itemIndex)"
  1650. @keyup.down.exact="highlightDown(itemIndex)"
  1651. @keyup.shift.up.exact="selectUp(itemIndex)"
  1652. @keyup.shift.down.exact="selectDown(itemIndex)"
  1653. @keyup.ctrl.up.exact="unselectUp(itemIndex)"
  1654. @keyup.ctrl.down.exact="unselectDown(itemIndex)"
  1655. @keyup.space.exact="
  1656. toggleSelectedRow(itemIndex, {})
  1657. "
  1658. >
  1659. <td
  1660. v-for="column in sortedFilteredColumns"
  1661. :key="`${item._id}-${column.name}`"
  1662. >
  1663. <slot
  1664. :name="`column-${column.name}`"
  1665. :item="item"
  1666. v-if="
  1667. column.properties.length === 0 ||
  1668. column.properties.every(
  1669. property =>
  1670. property
  1671. .split('.')
  1672. .reduce(
  1673. (previous, current) =>
  1674. previous &&
  1675. previous[
  1676. current
  1677. ] !== null &&
  1678. previous[
  1679. current
  1680. ] !== undefined
  1681. ? previous[
  1682. current
  1683. ]
  1684. : null,
  1685. item
  1686. ) !== null
  1687. )
  1688. "
  1689. ></slot>
  1690. <div
  1691. v-if="
  1692. column.name === 'updatedPlaceholder' &&
  1693. item.updated
  1694. "
  1695. class="updated-tooltip"
  1696. content="Row updated"
  1697. v-tippy="{
  1698. theme: 'info',
  1699. placement: 'right'
  1700. }"
  1701. ></div>
  1702. <p
  1703. class="checkbox"
  1704. v-if="column.name === 'select'"
  1705. >
  1706. <input
  1707. type="checkbox"
  1708. :checked="item.selected"
  1709. @click="
  1710. toggleSelectedRow(itemIndex, $event)
  1711. "
  1712. :disabled="item.removed"
  1713. />
  1714. </p>
  1715. <span
  1716. v-if="item.removed"
  1717. class="removed-overlay"
  1718. content="Item removed"
  1719. v-tippy="{ theme: 'info' }"
  1720. ></span>
  1721. <div
  1722. class="resizer"
  1723. v-if="column.resizable"
  1724. @mousedown.prevent.stop="
  1725. columnResizingStart(column, $event)
  1726. "
  1727. @touchstart.prevent.stop="
  1728. columnResizingStart(column, $event)
  1729. "
  1730. @mouseup="columnResizingStop()"
  1731. @touchend="columnResizingStop()"
  1732. @dblclick="columnResetWidth(column)"
  1733. ></div>
  1734. </td>
  1735. </tr>
  1736. </tbody>
  1737. </table>
  1738. </div>
  1739. <div v-if="rows.length === 0" class="table-no-results">
  1740. No results found
  1741. </div>
  1742. <div class="table-footer">
  1743. <div class="page-controls">
  1744. <button
  1745. :class="{ disabled: page === 1 }"
  1746. class="button is-primary material-icons"
  1747. :disabled="page === 1"
  1748. @click="changePage(1)"
  1749. content="First Page"
  1750. v-tippy
  1751. >
  1752. skip_previous
  1753. </button>
  1754. <button
  1755. :class="{ disabled: page === 1 }"
  1756. class="button is-primary material-icons"
  1757. :disabled="page === 1"
  1758. @click="changePage(page - 1)"
  1759. content="Previous Page"
  1760. v-tippy
  1761. >
  1762. fast_rewind
  1763. </button>
  1764. <p>Page {{ page }} / {{ lastPage > 0 ? lastPage : 1 }}</p>
  1765. <button
  1766. :class="{
  1767. disabled: page === lastPage || lastPage === 0
  1768. }"
  1769. class="button is-primary material-icons"
  1770. :disabled="page === lastPage"
  1771. @click="changePage(page + 1)"
  1772. content="Next Page"
  1773. v-tippy
  1774. >
  1775. fast_forward
  1776. </button>
  1777. <button
  1778. :class="{
  1779. disabled: page === lastPage || lastPage === 0
  1780. }"
  1781. class="button is-primary material-icons"
  1782. :disabled="page === lastPage"
  1783. @click="changePage(lastPage)"
  1784. content="Last Page"
  1785. v-tippy
  1786. >
  1787. skip_next
  1788. </button>
  1789. </div>
  1790. <div class="page-size">
  1791. <div class="control">
  1792. <label class="label">Items per page</label>
  1793. <p class="control select">
  1794. <select
  1795. v-model.number="pageSize"
  1796. @change="changePageSize()"
  1797. >
  1798. <option value="10">10</option>
  1799. <option value="25">25</option>
  1800. <option value="50">50</option>
  1801. <option value="100">100</option>
  1802. <option value="250">250</option>
  1803. <option value="500">500</option>
  1804. <option value="1000">1000</option>
  1805. </select>
  1806. </p>
  1807. </div>
  1808. </div>
  1809. </div>
  1810. </div>
  1811. <div
  1812. v-if="hasCheckboxes && selectedRows.length > 0"
  1813. class="bulk-popup"
  1814. :style="{
  1815. top: dragBox.top + 'px',
  1816. left: dragBox.left + 'px',
  1817. width: dragBox.width + 'px',
  1818. height: dragBox.height + 'px'
  1819. }"
  1820. ref="bulkPopup"
  1821. >
  1822. <button
  1823. class="button is-primary"
  1824. :content="
  1825. selectedRows.length === 1
  1826. ? `${selectedRows.length} row selected`
  1827. : `${selectedRows.length} rows selected`
  1828. "
  1829. v-tippy="{ theme: 'info' }"
  1830. >
  1831. {{ selectedRows.length }}
  1832. </button>
  1833. <slot name="bulk-actions" :item="selectedRows" />
  1834. <div class="right">
  1835. <span
  1836. class="material-icons drag-icon"
  1837. @mousedown.left="onDragBox"
  1838. @touchstart="onDragBox"
  1839. @dblclick="resetBoxPosition()"
  1840. >
  1841. drag_indicator
  1842. </span>
  1843. </div>
  1844. </div>
  1845. </div>
  1846. </template>
  1847. <style lang="less" scoped>
  1848. .night-mode {
  1849. .table-outer-container {
  1850. .table-container .table {
  1851. &,
  1852. :deep(thead th) {
  1853. background-color: var(--dark-grey-3) !important;
  1854. color: var(--light-grey-2);
  1855. }
  1856. tr {
  1857. :deep(th),
  1858. td {
  1859. border-color: var(--dark-grey) !important;
  1860. background-color: var(--dark-grey-3) !important;
  1861. }
  1862. &:nth-child(even) td {
  1863. background-color: var(--dark-grey-2) !important;
  1864. }
  1865. &:hover,
  1866. &:focus,
  1867. &.highlighted {
  1868. :deep(th),
  1869. td {
  1870. background-color: var(--dark-grey-4) !important;
  1871. }
  1872. }
  1873. &.updated td:first-child {
  1874. background-color: var(--primary-color) !important;
  1875. }
  1876. }
  1877. &.has-checkboxes tbody tr {
  1878. td:nth-child(2) {
  1879. background-color: var(--dark-grey-3) !important;
  1880. }
  1881. &:nth-child(even) td:nth-child(2) {
  1882. background-color: var(--dark-grey-2) !important;
  1883. }
  1884. &.updated td:first-child {
  1885. background-color: var(--primary-color) !important;
  1886. }
  1887. &:hover,
  1888. &:focus,
  1889. &.highlighted {
  1890. th,
  1891. td {
  1892. &:nth-child(2) {
  1893. background-color: var(--dark-grey-4) !important;
  1894. }
  1895. }
  1896. }
  1897. }
  1898. }
  1899. .table-header,
  1900. .table-footer {
  1901. background-color: var(--dark-grey-3);
  1902. color: var(--light-grey-2);
  1903. }
  1904. .table-no-results {
  1905. background-color: var(--dark-grey-3);
  1906. color: var(--light-grey-2);
  1907. border-color: var(--dark-grey) !important;
  1908. }
  1909. .label.control {
  1910. background-color: var(--dark-grey) !important;
  1911. border-color: var(--grey-3) !important;
  1912. color: var(--white) !important;
  1913. }
  1914. }
  1915. .bulk-popup {
  1916. border: 0;
  1917. background-color: var(--dark-grey-2);
  1918. color: var(--white);
  1919. .material-icons {
  1920. color: var(--white);
  1921. }
  1922. }
  1923. }
  1924. .table-outer-container {
  1925. border-radius: @border-radius;
  1926. box-shadow: @box-shadow;
  1927. margin: 10px 0;
  1928. overflow: hidden;
  1929. .table-container {
  1930. overflow-x: auto;
  1931. table {
  1932. border-collapse: separate;
  1933. table-layout: fixed;
  1934. :deep(thead) {
  1935. tr {
  1936. th {
  1937. height: 40px;
  1938. line-height: 40px;
  1939. border: 1px solid var(--light-grey-2);
  1940. border-width: 1px 1px 1px 0;
  1941. padding: 0;
  1942. &:last-child {
  1943. border-width: 1px 0 1px;
  1944. }
  1945. &.sortable {
  1946. cursor: pointer;
  1947. }
  1948. & > div {
  1949. display: flex;
  1950. white-space: nowrap;
  1951. padding: 8px 10px;
  1952. & > span {
  1953. margin-left: 5px;
  1954. &:first-child {
  1955. margin-left: 0;
  1956. margin-right: auto;
  1957. }
  1958. & > .material-icons {
  1959. font-size: 22px;
  1960. position: relative;
  1961. top: 6px;
  1962. cursor: pointer;
  1963. &.active {
  1964. color: var(--primary-color);
  1965. }
  1966. &:hover,
  1967. &:focus {
  1968. filter: brightness(90%);
  1969. }
  1970. }
  1971. }
  1972. }
  1973. }
  1974. }
  1975. }
  1976. tbody {
  1977. tr {
  1978. &.updated {
  1979. td:first-child {
  1980. background-color: var(--primary-color) !important;
  1981. }
  1982. }
  1983. &:nth-child(even) td {
  1984. background-color: rgb(250, 250, 250);
  1985. }
  1986. td {
  1987. border: 1px solid var(--light-grey-2);
  1988. border-width: 0 1px 1px 0;
  1989. &:last-child {
  1990. border-width: 0 0 1px;
  1991. }
  1992. :deep(.row-options) {
  1993. display: flex;
  1994. justify-content: space-evenly;
  1995. .icon-with-button {
  1996. height: 30px;
  1997. width: 30px;
  1998. }
  1999. }
  2000. }
  2001. &.removed {
  2002. filter: grayscale(100%);
  2003. cursor: not-allowed;
  2004. user-select: none;
  2005. td .removed-overlay {
  2006. position: absolute;
  2007. top: 0;
  2008. left: 0;
  2009. bottom: 0;
  2010. right: 5px;
  2011. z-index: 5;
  2012. }
  2013. }
  2014. }
  2015. }
  2016. }
  2017. table {
  2018. :deep(thead tr),
  2019. tbody tr {
  2020. th,
  2021. td {
  2022. position: relative;
  2023. white-space: nowrap;
  2024. text-overflow: ellipsis;
  2025. overflow: hidden;
  2026. background-color: var(--white);
  2027. &:first-child {
  2028. display: table-cell;
  2029. position: sticky;
  2030. left: 0;
  2031. z-index: 2;
  2032. & > .updated-tooltip {
  2033. position: absolute;
  2034. top: 0;
  2035. left: 0;
  2036. bottom: 0;
  2037. right: 0;
  2038. }
  2039. }
  2040. .resizer {
  2041. height: 100%;
  2042. width: 5px;
  2043. background-color: transparent;
  2044. cursor: col-resize;
  2045. position: absolute;
  2046. right: 0;
  2047. top: 0;
  2048. }
  2049. }
  2050. &:hover,
  2051. &:focus,
  2052. &.highlighted {
  2053. th,
  2054. td {
  2055. background-color: rgb(240, 240, 240);
  2056. }
  2057. }
  2058. }
  2059. &.has-checkboxes {
  2060. :deep(thead),
  2061. tbody {
  2062. tr {
  2063. th,
  2064. td {
  2065. &:nth-child(2) {
  2066. display: table-cell;
  2067. position: sticky;
  2068. left: 5px;
  2069. z-index: 2;
  2070. }
  2071. }
  2072. &.updated td:first-child {
  2073. background-color: var(--primary-color);
  2074. }
  2075. }
  2076. }
  2077. }
  2078. }
  2079. }
  2080. .table-header,
  2081. .table-footer {
  2082. display: flex;
  2083. flex-direction: row;
  2084. flex-wrap: wrap;
  2085. justify-content: space-between;
  2086. line-height: 36px;
  2087. background-color: var(--white);
  2088. }
  2089. .table-header {
  2090. & > div {
  2091. display: flex;
  2092. flex-direction: row;
  2093. > span > .control {
  2094. margin: 5px;
  2095. }
  2096. .filters-indicator {
  2097. line-height: 46px;
  2098. display: flex;
  2099. align-items: center;
  2100. column-gap: 4px;
  2101. }
  2102. }
  2103. @media screen and (max-width: 400px) {
  2104. flex-direction: column;
  2105. & > div {
  2106. justify-content: center;
  2107. }
  2108. }
  2109. }
  2110. .table-footer {
  2111. .page-controls,
  2112. .page-size > .control {
  2113. display: flex;
  2114. flex-direction: row;
  2115. margin-bottom: 0 !important;
  2116. button {
  2117. margin: 5px;
  2118. font-size: 20px;
  2119. }
  2120. p,
  2121. label {
  2122. margin: 5px;
  2123. font-size: 14px;
  2124. font-weight: 600;
  2125. }
  2126. &.select::after {
  2127. top: 18px;
  2128. }
  2129. }
  2130. @media screen and (max-width: 600px) {
  2131. flex-direction: column;
  2132. .page-controls,
  2133. .page-size > .control {
  2134. justify-content: center;
  2135. }
  2136. }
  2137. }
  2138. .table-no-results {
  2139. display: flex;
  2140. flex-direction: row;
  2141. justify-content: center;
  2142. border-bottom: 1px solid var(--light-grey-2);
  2143. font-size: 18px;
  2144. line-height: 50px;
  2145. background-color: var(--white);
  2146. }
  2147. }
  2148. .control.is-grouped {
  2149. display: flex;
  2150. & > .control {
  2151. &.label {
  2152. height: 36px;
  2153. background-color: var(--white);
  2154. border: 1px solid var(--light-grey-2);
  2155. color: var(--dark-grey-2);
  2156. appearance: none;
  2157. font-size: 14px;
  2158. line-height: 34px;
  2159. padding-left: 8px;
  2160. padding-right: 8px;
  2161. }
  2162. :deep(& > div > input) {
  2163. border-radius: 0;
  2164. }
  2165. & > .button {
  2166. font-size: 22px;
  2167. }
  2168. }
  2169. @media screen and (max-width: 600px) {
  2170. &.advanced-filter {
  2171. flex-wrap: wrap;
  2172. .control.select {
  2173. width: 50%;
  2174. }
  2175. .control {
  2176. margin-bottom: 0 !important;
  2177. &:nth-child(1) > select {
  2178. border-radius: @border-radius 0 0 0;
  2179. }
  2180. &:nth-child(2) > select {
  2181. border-radius: 0 @border-radius 0 0;
  2182. }
  2183. :deep(&:nth-child(3)) {
  2184. & > input,
  2185. & > div > input,
  2186. & > select {
  2187. border-radius: 0 0 0 @border-radius;
  2188. }
  2189. }
  2190. &:nth-child(4) > button {
  2191. border-radius: 0 0 @border-radius 0;
  2192. }
  2193. }
  2194. }
  2195. }
  2196. }
  2197. .advanced-filter {
  2198. .control {
  2199. position: relative;
  2200. }
  2201. }
  2202. .advanced-filter-bottom {
  2203. display: flex;
  2204. .button {
  2205. font-size: 16px !important;
  2206. width: 100%;
  2207. }
  2208. .control {
  2209. margin: 0 !important;
  2210. }
  2211. }
  2212. :deep(.bulk-popup) {
  2213. display: flex;
  2214. position: fixed;
  2215. flex-direction: row;
  2216. width: 100%;
  2217. max-width: 400px;
  2218. line-height: 36px;
  2219. z-index: 5;
  2220. border: 1px solid var(--light-grey-3);
  2221. border-radius: @border-radius;
  2222. box-shadow: @box-shadow-dropdown;
  2223. background-color: var(--white);
  2224. color: var(--dark-grey);
  2225. padding: 5px;
  2226. .right {
  2227. display: flex;
  2228. flex-direction: row;
  2229. margin-left: auto;
  2230. }
  2231. .drag-icon {
  2232. position: relative;
  2233. top: 6px;
  2234. color: var(--dark-grey);
  2235. cursor: move;
  2236. }
  2237. .bulk-actions {
  2238. display: flex;
  2239. flex-direction: row;
  2240. width: 100%;
  2241. justify-content: space-evenly;
  2242. .material-icons {
  2243. position: relative;
  2244. top: 6px;
  2245. margin-left: 5px;
  2246. cursor: pointer;
  2247. color: var(--primary-color);
  2248. height: 25px;
  2249. &:hover,
  2250. &:focus {
  2251. filter: brightness(90%);
  2252. }
  2253. }
  2254. .delete-icon {
  2255. color: var(--dark-red);
  2256. }
  2257. .import-album-icon {
  2258. color: var(--purple);
  2259. }
  2260. }
  2261. }
  2262. </style>