dbviewer 0.5.6 → 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 98bab0acf6c5aef237e7ef161661cfdfa11c9067973efe1f189e01a7fad51175
4
- data.tar.gz: 0b35c2802191d5f23f8a5baf2740cd006e7b9dbf109f35c9cec77605b004bcf8
3
+ metadata.gz: 3385fea543f18e7af2bd9a924b5095f76235612d569fc1e1a828222b8b1a3b53
4
+ data.tar.gz: 762f1ee1b3774c9186cbdac735beb0717c6a3feb64ffecd7833ec8732ce91898
5
5
  SHA512:
6
- metadata.gz: a220184a4186cf107ffeb0a12a93b252370cd704be1ee54f8c3040c9818ec82fa7cd866dc170ede1180cf2ad640efb9605669d5f918f7c9b1f94d170b1e843e8
7
- data.tar.gz: 7a30db71097c9066c8a0069ec8c4223d705e88e0e6c83266b87ab34112987c088cd5749abfae71f519efa47daeadbb25a4baafe8c024cf0de07631ac3486fce1
6
+ metadata.gz: a1552e63407dd92eb3bfa821a4712f6cb57b5c68462c6028a9e3debe14f2e72ce073c631ec1616aa50a5158939ae667899ef4999c2ac5feff8a6f22edd783b08
7
+ data.tar.gz: 2852a40ddec1a2a86d2b9764bf527fd21dd140aa57a7ef36da6d7dcf312362c6b24b753c6badd04aee89328557136c954adbad95a442540a233f53affac5489c
@@ -40,13 +40,7 @@
40
40
  <% @tables.each do |table| %>
41
41
  <tr>
42
42
  <td class="fw-medium">
43
- <%
44
- # Include creation filter params in table links
45
- filter_params = {}
46
- filter_params[:creation_filter_start] = session[:creation_filter_start] if session[:creation_filter_start].present?
47
- filter_params[:creation_filter_end] = session[:creation_filter_end] if session[:creation_filter_end].present?
48
- %>
49
- <%= link_to table[:name], table_path(table[:name], filter_params), class: "text-decoration-none" %>
43
+ <%= link_to table[:name], table_path(table[:name]), class: "text-decoration-none" %>
50
44
  </td>
51
45
  <td class="text-end">
52
46
  <span class="badge bg-secondary-subtle"><%= table[:record_count] %></span>
@@ -1525,3 +1525,1006 @@
1525
1525
  return section;
1526
1526
  }
1527
1527
  </script>
1528
+
1529
+ <!-- Floating Creation Filter - Only visible on desktop and on table details page -->
1530
+ <% if has_timestamp_column?(@table_name) %>
1531
+ <div class="floating-creation-filter d-none d-lg-block">
1532
+ <button class="btn btn-primary btn-lg shadow-lg floating-filter-btn"
1533
+ type="button"
1534
+ data-bs-toggle="offcanvas"
1535
+ data-bs-target="#creationFilterOffcanvas"
1536
+ aria-controls="creationFilterOffcanvas"
1537
+ title="Creation Date Filter">
1538
+ <i class="bi bi-calendar-range"></i>
1539
+ <% if @creation_filter_start.present? || @creation_filter_end.present? %>
1540
+ <span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-success">
1541
+ <i class="bi bi-check"></i>
1542
+ </span>
1543
+ <% end %>
1544
+ </button>
1545
+ </div>
1546
+
1547
+ <!-- Creation Filter Offcanvas -->
1548
+ <div class="offcanvas offcanvas-end" tabindex="-1" id="creationFilterOffcanvas" aria-labelledby="creationFilterOffcanvasLabel">
1549
+ <div class="offcanvas-header">
1550
+ <h5 class="offcanvas-title" id="creationFilterOffcanvasLabel">
1551
+ <i class="bi bi-calendar-range me-2"></i>Creation Date Filter
1552
+ </h5>
1553
+ <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
1554
+ </div>
1555
+ <div class="offcanvas-body">
1556
+ <form id="floatingCreationFilterForm" action="<%= request.path %>" method="get" class="mb-0">
1557
+ <!-- Preserve existing query parameters -->
1558
+ <input type="hidden" name="page" value="<%= @current_page %>">
1559
+ <input type="hidden" name="per_page" value="<%= @per_page %>">
1560
+ <input type="hidden" name="order_by" value="<%= @order_by %>">
1561
+ <input type="hidden" name="order_direction" value="<%= @order_direction %>">
1562
+
1563
+ <!-- Preserve column filters -->
1564
+ <% @column_filters.each do |key, value| %>
1565
+ <% unless key.to_s.start_with?('created_at') %>
1566
+ <input type="hidden" name="column_filters[<%= key %>]" value="<%= value %>">
1567
+ <% end %>
1568
+ <% end %>
1569
+
1570
+ <div class="mb-3">
1571
+ <p class="text-muted small">
1572
+ <i class="bi bi-info-circle me-1"></i>
1573
+ Filter records by their creation date and time. This applies to the <code>created_at</code> column.
1574
+ </p>
1575
+ </div>
1576
+
1577
+ <!-- Date range picker -->
1578
+ <div class="mb-3">
1579
+ <label for="floatingCreationFilterRange" class="form-label">Date Range</label>
1580
+ <input type="text"
1581
+ id="floatingCreationFilterRange"
1582
+ name="creation_filter_range"
1583
+ class="form-control"
1584
+ placeholder="Select date range..."
1585
+ readonly>
1586
+ <!-- Hidden inputs for form submission -->
1587
+ <input type="hidden" id="creation_filter_start" name="creation_filter_start" value="<%= @creation_filter_start %>">
1588
+ <input type="hidden" id="creation_filter_end" name="creation_filter_end" value="<%= @creation_filter_end %>">
1589
+ </div>
1590
+
1591
+ <!-- Quick preset buttons -->
1592
+ <div class="mb-3">
1593
+ <label class="form-label">Quick Presets</label>
1594
+ <div class="d-grid gap-1">
1595
+ <button type="button" class="btn btn-outline-secondary btn-sm preset-btn" data-preset="lastminute">
1596
+ <i class="bi bi-clock me-1"></i>Last Minute
1597
+ </button>
1598
+ <button type="button" class="btn btn-outline-secondary btn-sm preset-btn" data-preset="last5minutes">
1599
+ <i class="bi bi-clock-history me-1"></i>Last 5 Minutes
1600
+ </button>
1601
+ <button type="button" class="btn btn-outline-secondary btn-sm preset-btn" data-preset="today">
1602
+ <i class="bi bi-calendar-day me-1"></i>Today
1603
+ </button>
1604
+ <button type="button" class="btn btn-outline-secondary btn-sm preset-btn" data-preset="yesterday">
1605
+ <i class="bi bi-calendar-minus me-1"></i>Yesterday
1606
+ </button>
1607
+ <button type="button" class="btn btn-outline-secondary btn-sm preset-btn" data-preset="last7days">
1608
+ <i class="bi bi-calendar-week me-1"></i>Last 7 Days
1609
+ </button>
1610
+ <button type="button" class="btn btn-outline-secondary btn-sm preset-btn" data-preset="last30days">
1611
+ <i class="bi bi-calendar-month me-1"></i>Last 30 Days
1612
+ </button>
1613
+ <button type="button" class="btn btn-outline-secondary btn-sm preset-btn" data-preset="thismonth">
1614
+ <i class="bi bi-calendar3 me-1"></i>This Month
1615
+ </button>
1616
+ </div>
1617
+ </div>
1618
+
1619
+ <div class="d-grid gap-2">
1620
+ <button type="submit" class="btn btn-primary">
1621
+ <i class="bi bi-funnel me-1"></i>Apply Filter
1622
+ </button>
1623
+ <% if @creation_filter_start.present? || @creation_filter_end.present? %>
1624
+ <%
1625
+ # Preserve other query params when clearing creation filter
1626
+ clear_params = {
1627
+ clear_creation_filter: true,
1628
+ page: @current_page,
1629
+ per_page: @per_page,
1630
+ order_by: @order_by,
1631
+ order_direction: @order_direction
1632
+ }
1633
+ # Add column filters except created_at ones
1634
+ @column_filters.each do |key, value|
1635
+ unless key.to_s.start_with?('created_at')
1636
+ clear_params["column_filters[#{key}]"] = value
1637
+ end
1638
+ end
1639
+ %>
1640
+ <a href="<%= request.path %>?<%= clear_params.to_query %>" class="btn btn-outline-secondary">
1641
+ <i class="bi bi-x-circle me-1"></i>Clear Filter
1642
+ </a>
1643
+ <% end %>
1644
+ </div>
1645
+
1646
+ <div class="mt-3">
1647
+ <% if @creation_filter_start.present? || @creation_filter_end.present? %>
1648
+ <div class="alert alert-success alert-sm">
1649
+ <i class="bi bi-check-circle-fill me-1"></i>
1650
+ <strong>Filter Active:</strong> Showing records
1651
+ <% if @creation_filter_start.present? && @creation_filter_end.present? %>
1652
+ between <%= DateTime.parse(@creation_filter_start).strftime("%b %d, %Y %I:%M %p") %>
1653
+ and <%= DateTime.parse(@creation_filter_end).strftime("%b %d, %Y %I:%M %p") %>
1654
+ <% elsif @creation_filter_start.present? %>
1655
+ from <%= DateTime.parse(@creation_filter_start).strftime("%b %d, %Y %I:%M %p") %> onwards
1656
+ <% elsif @creation_filter_end.present? %>
1657
+ up to <%= DateTime.parse(@creation_filter_end).strftime("%b %d, %Y %I:%M %p") %>
1658
+ <% end %>
1659
+
1660
+ <% if @current_page == 1 && @records && @records.rows && @records.rows.empty? %>
1661
+ <div class="mt-2 text-warning">
1662
+ <i class="bi bi-exclamation-triangle-fill me-1"></i>
1663
+ No records match the current filter criteria.
1664
+ </div>
1665
+ <% end %>
1666
+ </div>
1667
+ <% end %>
1668
+ </div>
1669
+ </form>
1670
+ </div>
1671
+ </div>
1672
+
1673
+ <style>
1674
+ /* Floating creation filter button */
1675
+ .floating-creation-filter {
1676
+ position: fixed;
1677
+ bottom: 30px;
1678
+ right: 30px;
1679
+ z-index: 1050;
1680
+ }
1681
+
1682
+ .floating-filter-btn {
1683
+ width: 60px;
1684
+ height: 60px;
1685
+ border-radius: 50%;
1686
+ display: flex;
1687
+ align-items: center;
1688
+ justify-content: center;
1689
+ font-size: 1.2rem;
1690
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1691
+ border: none;
1692
+ position: relative;
1693
+ background: var(--bs-primary);
1694
+ color: white;
1695
+ box-shadow: 0 4px 12px rgba(var(--bs-primary-rgb), 0.4);
1696
+ }
1697
+
1698
+ .floating-filter-btn:hover {
1699
+ transform: translateY(-3px) scale(1.05);
1700
+ box-shadow: 0 8px 25px rgba(var(--bs-primary-rgb), 0.5) !important;
1701
+ background: var(--bs-primary) !important;
1702
+ color: white !important;
1703
+ }
1704
+
1705
+ .floating-filter-btn:active {
1706
+ transform: translateY(-1px) scale(1.02);
1707
+ transition: all 0.1s ease;
1708
+ }
1709
+
1710
+ .floating-filter-btn:focus {
1711
+ outline: 2px solid rgba(var(--bs-primary-rgb), 0.5);
1712
+ outline-offset: 2px;
1713
+ }
1714
+
1715
+ /* Badge for active filter indicator */
1716
+ .floating-filter-btn .badge {
1717
+ font-size: 0.6rem;
1718
+ width: 18px;
1719
+ height: 18px;
1720
+ display: flex;
1721
+ align-items: center;
1722
+ justify-content: center;
1723
+ background: var(--bs-success) !important;
1724
+ animation: pulse 2s infinite;
1725
+ }
1726
+
1727
+ @keyframes pulse {
1728
+ 0% { transform: scale(1); }
1729
+ 50% { transform: scale(1.1); }
1730
+ 100% { transform: scale(1); }
1731
+ }
1732
+
1733
+ /* Better datetime input styling for the floating filter */
1734
+ #creationFilterOffcanvas .form-control {
1735
+ border-radius: 6px;
1736
+ border: 1px solid var(--bs-border-color);
1737
+ background-color: var(--bs-body-bg);
1738
+ color: var(--bs-body-color);
1739
+ transition: all 0.15s ease-in-out;
1740
+ }
1741
+
1742
+ #creationFilterOffcanvas .form-control:focus {
1743
+ border-color: var(--bs-primary);
1744
+ box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
1745
+ background-color: var(--bs-body-bg);
1746
+ }
1747
+
1748
+ /* Offcanvas enhancements */
1749
+ #creationFilterOffcanvas {
1750
+ backdrop-filter: blur(10px);
1751
+ }
1752
+
1753
+ #creationFilterOffcanvas .offcanvas-header {
1754
+ background: var(--bs-body-bg);
1755
+ border-bottom: 1px solid var(--bs-border-color);
1756
+ padding: 1.25rem;
1757
+ }
1758
+
1759
+ #creationFilterOffcanvas .offcanvas-body {
1760
+ background: var(--bs-body-bg);
1761
+ padding: 1.25rem;
1762
+ }
1763
+
1764
+ #creationFilterOffcanvas .offcanvas-title {
1765
+ color: var(--bs-body-color);
1766
+ font-weight: 600;
1767
+ }
1768
+
1769
+ /* Dark mode specific enhancements */
1770
+ [data-bs-theme="dark"] .floating-filter-btn {
1771
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(var(--bs-primary-rgb), 0.2);
1772
+ }
1773
+
1774
+ [data-bs-theme="dark"] .floating-filter-btn:hover {
1775
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(var(--bs-primary-rgb), 0.3) !important;
1776
+ }
1777
+
1778
+ [data-bs-theme="dark"] #creationFilterOffcanvas .offcanvas-header {
1779
+ background: var(--bs-dark);
1780
+ border-bottom-color: var(--bs-border-color-translucent);
1781
+ }
1782
+
1783
+ [data-bs-theme="dark"] #creationFilterOffcanvas .offcanvas-body {
1784
+ background: var(--bs-dark);
1785
+ }
1786
+
1787
+ [data-bs-theme="dark"] #creationFilterOffcanvas .form-control {
1788
+ background-color: var(--bs-body-bg);
1789
+ border-color: var(--bs-border-color-translucent);
1790
+ }
1791
+
1792
+ [data-bs-theme="dark"] #creationFilterOffcanvas .form-control:focus {
1793
+ background-color: var(--bs-body-bg);
1794
+ border-color: var(--bs-primary);
1795
+ }
1796
+
1797
+ /* Date range picker styling */
1798
+ #floatingCreationFilterRange {
1799
+ cursor: pointer;
1800
+ background-color: var(--bs-body-bg);
1801
+ color: var(--bs-body-color);
1802
+ border: 1px solid var(--bs-border-color);
1803
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, background-color 0.15s ease-in-out;
1804
+ }
1805
+
1806
+ #floatingCreationFilterRange:focus {
1807
+ border-color: var(--bs-primary);
1808
+ box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
1809
+ background-color: var(--bs-body-bg);
1810
+ }
1811
+
1812
+ #floatingCreationFilterRange::placeholder {
1813
+ color: var(--bs-secondary-color);
1814
+ opacity: 0.7;
1815
+ }
1816
+
1817
+ /* Enhanced dark mode support for input */
1818
+ [data-bs-theme="dark"] #floatingCreationFilterRange {
1819
+ background-color: var(--bs-body-bg);
1820
+ border-color: var(--bs-border-color-translucent);
1821
+ color: var(--bs-body-color);
1822
+ }
1823
+
1824
+ [data-bs-theme="dark"] #floatingCreationFilterRange:focus {
1825
+ background-color: var(--bs-body-bg);
1826
+ border-color: var(--bs-primary);
1827
+ box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
1828
+ }
1829
+
1830
+ /* Preset buttons styling */
1831
+ .preset-btn {
1832
+ font-size: 0.8rem;
1833
+ padding: 0.35rem 0.75rem;
1834
+ text-align: left;
1835
+ justify-content: flex-start;
1836
+ border: 1px solid var(--bs-border-color);
1837
+ background-color: var(--bs-body-bg);
1838
+ color: var(--bs-body-color);
1839
+ transition: all 0.2s ease;
1840
+ position: relative;
1841
+ overflow: hidden;
1842
+ }
1843
+
1844
+ .preset-btn::before {
1845
+ content: '';
1846
+ position: absolute;
1847
+ top: 0;
1848
+ left: -100%;
1849
+ width: 100%;
1850
+ height: 100%;
1851
+ background: linear-gradient(90deg, transparent, rgba(var(--bs-primary-rgb), 0.1), transparent);
1852
+ transition: left 0.3s ease;
1853
+ }
1854
+
1855
+ .preset-btn:hover {
1856
+ background-color: var(--bs-primary);
1857
+ color: white;
1858
+ border-color: var(--bs-primary);
1859
+ transform: translateY(-1px);
1860
+ box-shadow: 0 2px 4px rgba(var(--bs-primary-rgb), 0.2);
1861
+ }
1862
+
1863
+ .preset-btn:hover::before {
1864
+ left: 100%;
1865
+ }
1866
+
1867
+ .preset-btn:active {
1868
+ transform: translateY(0);
1869
+ box-shadow: 0 1px 2px rgba(var(--bs-primary-rgb), 0.2);
1870
+ }
1871
+
1872
+ .preset-btn i {
1873
+ opacity: 0.8;
1874
+ transition: opacity 0.2s ease;
1875
+ }
1876
+
1877
+ .preset-btn:hover i {
1878
+ opacity: 1;
1879
+ }
1880
+
1881
+ /* Dark mode enhancements for preset buttons */
1882
+ [data-bs-theme="dark"] .preset-btn {
1883
+ background-color: var(--bs-dark);
1884
+ border-color: var(--bs-border-color-translucent);
1885
+ color: var(--bs-body-color);
1886
+ }
1887
+
1888
+ [data-bs-theme="dark"] .preset-btn:hover {
1889
+ background-color: var(--bs-primary);
1890
+ color: white;
1891
+ border-color: var(--bs-primary);
1892
+ box-shadow: 0 2px 8px rgba(var(--bs-primary-rgb), 0.3);
1893
+ }
1894
+
1895
+ /* Flatpickr theme adjustments */
1896
+ .flatpickr-calendar {
1897
+ border-radius: 8px;
1898
+ box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.2);
1899
+ border: 1px solid var(--bs-border-color);
1900
+ background: var(--bs-body-bg);
1901
+ font-family: var(--bs-body-font-family);
1902
+ }
1903
+
1904
+ .flatpickr-months {
1905
+ background: var(--bs-body-bg);
1906
+ border-bottom: 1px solid var(--bs-border-color);
1907
+ border-radius: 8px 8px 0 0;
1908
+ }
1909
+
1910
+ .flatpickr-month {
1911
+ color: var(--bs-body-color);
1912
+ fill: var(--bs-body-color);
1913
+ }
1914
+
1915
+ .flatpickr-current-month {
1916
+ color: var(--bs-body-color);
1917
+ }
1918
+
1919
+ .flatpickr-current-month .flatpickr-monthDropdown-month {
1920
+ background: var(--bs-body-bg);
1921
+ color: var(--bs-body-color);
1922
+ }
1923
+
1924
+ .flatpickr-weekdays {
1925
+ background: var(--bs-body-bg);
1926
+ }
1927
+
1928
+ .flatpickr-weekday {
1929
+ color: var(--bs-secondary-color);
1930
+ font-weight: 600;
1931
+ font-size: 0.75rem;
1932
+ }
1933
+
1934
+ .flatpickr-day {
1935
+ color: var(--bs-body-color);
1936
+ border-radius: 4px;
1937
+ transition: all 0.2s ease;
1938
+ }
1939
+
1940
+ .flatpickr-day:hover {
1941
+ background: var(--bs-primary-bg-subtle);
1942
+ color: var(--bs-primary);
1943
+ border-color: var(--bs-primary-border-subtle);
1944
+ }
1945
+
1946
+ .flatpickr-day.selected {
1947
+ background: var(--bs-primary);
1948
+ color: white;
1949
+ border-color: var(--bs-primary);
1950
+ box-shadow: 0 2px 4px rgba(var(--bs-primary-rgb), 0.3);
1951
+ }
1952
+
1953
+ .flatpickr-day.selected:hover {
1954
+ background: var(--bs-primary);
1955
+ color: white;
1956
+ }
1957
+
1958
+ .flatpickr-day.inRange {
1959
+ background: var(--bs-primary-bg-subtle);
1960
+ color: var(--bs-primary);
1961
+ border-color: transparent;
1962
+ }
1963
+
1964
+ .flatpickr-day.startRange {
1965
+ background: var(--bs-primary);
1966
+ color: white;
1967
+ border-radius: 4px 0 0 4px;
1968
+ }
1969
+
1970
+ .flatpickr-day.endRange {
1971
+ background: var(--bs-primary);
1972
+ color: white;
1973
+ border-radius: 0 4px 4px 0;
1974
+ }
1975
+
1976
+ .flatpickr-day.startRange.endRange {
1977
+ border-radius: 4px;
1978
+ }
1979
+
1980
+ .flatpickr-day.today {
1981
+ border-color: var(--bs-primary);
1982
+ color: var(--bs-primary);
1983
+ font-weight: 600;
1984
+ }
1985
+
1986
+ .flatpickr-day.today:hover {
1987
+ background: var(--bs-primary);
1988
+ color: white;
1989
+ }
1990
+
1991
+ .flatpickr-day.disabled {
1992
+ color: var(--bs-secondary-color);
1993
+ opacity: 0.5;
1994
+ }
1995
+
1996
+ .flatpickr-time {
1997
+ background: var(--bs-body-bg);
1998
+ border-radius: 0 0 8px 8px;
1999
+ }
2000
+
2001
+ .flatpickr-time input {
2002
+ background: var(--bs-body-bg);
2003
+ color: var(--bs-body-color);
2004
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
2005
+ }
2006
+
2007
+ .flatpickr-time input:focus {
2008
+ border-color: var(--bs-primary);
2009
+ box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
2010
+ outline: 0;
2011
+ }
2012
+
2013
+ .flatpickr-time .flatpickr-time-separator {
2014
+ color: var(--bs-body-color);
2015
+ }
2016
+
2017
+ .flatpickr-prev-month,
2018
+ .flatpickr-next-month {
2019
+ color: var(--bs-body-color);
2020
+ fill: var(--bs-body-color);
2021
+ transition: color 0.2s ease;
2022
+ }
2023
+
2024
+ .flatpickr-prev-month:hover,
2025
+ .flatpickr-next-month:hover {
2026
+ color: var(--bs-primary);
2027
+ fill: var(--bs-primary);
2028
+ }
2029
+
2030
+ /* Dark mode specific enhancements */
2031
+ [data-bs-theme="dark"] .flatpickr-calendar {
2032
+ background: var(--bs-dark);
2033
+ border-color: var(--bs-border-color-translucent);
2034
+ box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.4);
2035
+ }
2036
+
2037
+ [data-bs-theme="dark"] .flatpickr-months {
2038
+ background: var(--bs-dark);
2039
+ border-bottom-color: var(--bs-border-color-translucent);
2040
+ }
2041
+
2042
+ [data-bs-theme="dark"] .flatpickr-weekdays {
2043
+ background: var(--bs-dark);
2044
+ }
2045
+
2046
+ /* Enhanced dark mode day styling for better contrast */
2047
+ [data-bs-theme="dark"] .flatpickr-day {
2048
+ color: #e9ecef !important;
2049
+ background: transparent;
2050
+ border: 1px solid transparent;
2051
+ }
2052
+
2053
+ [data-bs-theme="dark"] .flatpickr-day:hover {
2054
+ background: rgba(var(--bs-primary-rgb), 0.25) !important;
2055
+ color: #ffffff !important;
2056
+ border-color: rgba(var(--bs-primary-rgb), 0.4);
2057
+ }
2058
+
2059
+ [data-bs-theme="dark"] .flatpickr-day.inRange {
2060
+ background: rgba(var(--bs-primary-rgb), 0.2) !important;
2061
+ color: #ffffff !important;
2062
+ border-color: transparent;
2063
+ }
2064
+
2065
+ [data-bs-theme="dark"] .flatpickr-day.selected {
2066
+ background: var(--bs-primary) !important;
2067
+ color: #ffffff !important;
2068
+ border-color: var(--bs-primary);
2069
+ box-shadow: 0 2px 6px rgba(var(--bs-primary-rgb), 0.4);
2070
+ }
2071
+
2072
+ [data-bs-theme="dark"] .flatpickr-day.selected:hover {
2073
+ background: var(--bs-primary) !important;
2074
+ color: #ffffff !important;
2075
+ }
2076
+
2077
+ [data-bs-theme="dark"] .flatpickr-day.startRange {
2078
+ background: var(--bs-primary) !important;
2079
+ color: #ffffff !important;
2080
+ border-radius: 4px 0 0 4px;
2081
+ }
2082
+
2083
+ [data-bs-theme="dark"] .flatpickr-day.endRange {
2084
+ background: var(--bs-primary) !important;
2085
+ color: #ffffff !important;
2086
+ border-radius: 0 4px 4px 0;
2087
+ }
2088
+
2089
+ [data-bs-theme="dark"] .flatpickr-day.startRange.endRange {
2090
+ border-radius: 4px;
2091
+ }
2092
+
2093
+ [data-bs-theme="dark"] .flatpickr-day.today {
2094
+ border-color: var(--bs-primary) !important;
2095
+ color: var(--bs-primary) !important;
2096
+ font-weight: 600;
2097
+ background: rgba(var(--bs-primary-rgb), 0.1);
2098
+ }
2099
+
2100
+ [data-bs-theme="dark"] .flatpickr-day.today:hover {
2101
+ background: var(--bs-primary) !important;
2102
+ color: #ffffff !important;
2103
+ }
2104
+
2105
+ [data-bs-theme="dark"] .flatpickr-day.disabled {
2106
+ color: #6c757d !important;
2107
+ opacity: 0.4;
2108
+ background: transparent !important;
2109
+ }
2110
+
2111
+ [data-bs-theme="dark"] .flatpickr-day.disabled:hover {
2112
+ background: transparent !important;
2113
+ color: #6c757d !important;
2114
+ cursor: not-allowed;
2115
+ }
2116
+
2117
+ /* Dark mode other day states */
2118
+ [data-bs-theme="dark"] .flatpickr-day.nextMonthDay,
2119
+ [data-bs-theme="dark"] .flatpickr-day.prevMonthDay {
2120
+ color: #6c757d !important;
2121
+ opacity: 0.6;
2122
+ }
2123
+
2124
+ [data-bs-theme="dark"] .flatpickr-day.nextMonthDay:hover,
2125
+ [data-bs-theme="dark"] .flatpickr-day.prevMonthDay:hover {
2126
+ background: rgba(var(--bs-primary-rgb), 0.15) !important;
2127
+ color: #adb5bd !important;
2128
+ }
2129
+
2130
+ [data-bs-theme="dark"] .flatpickr-time {
2131
+ background: var(--bs-dark);
2132
+ border-top-color: var(--bs-border-color-translucent);
2133
+ }
2134
+
2135
+ [data-bs-theme="dark"] .flatpickr-time input {
2136
+ background: var(--bs-body-bg);
2137
+ border-color: var(--bs-border-color-translucent);
2138
+ color: #e9ecef !important;
2139
+ }
2140
+
2141
+ .flatpickr-calendar.hasTime .flatpickr-time {
2142
+ border-top: var(--bs-border-color-translucent);
2143
+ }
2144
+
2145
+ .flatpickr-next-month {
2146
+ color: var(--bs-body-color);
2147
+ fill: var(--bs-body-color);
2148
+ }
2149
+
2150
+ span.flatpickr-weekday {
2151
+ color: var(--bs-secondary-color);
2152
+ font-weight: 600;
2153
+ font-size: 0.75rem;
2154
+ }
2155
+
2156
+ [data-bs-theme="dark"] .flatpickr-time input:focus {
2157
+ border-color: var(--bs-primary);
2158
+ background: var(--bs-body-bg);
2159
+ color: #ffffff !important;
2160
+ }
2161
+
2162
+ [data-bs-theme="dark"] .flatpickr-time .flatpickr-time-separator {
2163
+ color: #e9ecef !important;
2164
+ }
2165
+
2166
+ /* Better focus states */
2167
+ .flatpickr-day:focus {
2168
+ outline: 2px solid var(--bs-primary);
2169
+ outline-offset: -2px;
2170
+ z-index: 10;
2171
+ }
2172
+
2173
+ /* Animation for calendar appearance */
2174
+ .flatpickr-calendar.open {
2175
+ animation: flatpickr-slideDown 0.2s ease-out;
2176
+ }
2177
+
2178
+ @keyframes flatpickr-slideDown {
2179
+ from {
2180
+ opacity: 0;
2181
+ transform: translateY(-10px) scale(0.98);
2182
+ }
2183
+ to {
2184
+ opacity: 1;
2185
+ transform: translateY(0) scale(1);
2186
+ }
2187
+ }
2188
+
2189
+ /* Small alert styling */
2190
+ .alert-sm {
2191
+ padding: 0.5rem;
2192
+ font-size: 0.875rem;
2193
+ }
2194
+
2195
+ /* Responsive adjustments - hide on smaller screens */
2196
+ @media (max-width: 991.98px) {
2197
+ .floating-creation-filter {
2198
+ display: none !important;
2199
+ }
2200
+ }
2201
+
2202
+ /* Flatpickr positioning and responsive adjustments */
2203
+ .flatpickr-calendar.arrowTop:before,
2204
+ .flatpickr-calendar.arrowTop:after {
2205
+ border-bottom-color: var(--bs-border-color);
2206
+ }
2207
+
2208
+ .flatpickr-calendar.arrowBottom:before,
2209
+ .flatpickr-calendar.arrowBottom:after {
2210
+ border-top-color: var(--bs-border-color);
2211
+ }
2212
+
2213
+ [data-bs-theme="dark"] .flatpickr-calendar.arrowTop:before,
2214
+ [data-bs-theme="dark"] .flatpickr-calendar.arrowTop:after {
2215
+ border-bottom-color: var(--bs-border-color-translucent);
2216
+ }
2217
+
2218
+ [data-bs-theme="dark"] .flatpickr-calendar.arrowBottom:before,
2219
+ [data-bs-theme="dark"] .flatpickr-calendar.arrowBottom:after {
2220
+ border-top-color: var(--bs-border-color-translucent);
2221
+ }
2222
+
2223
+ /* Ensure calendar stays within viewport on mobile */
2224
+ @media (max-width: 576px) {
2225
+ .flatpickr-calendar {
2226
+ max-width: calc(100vw - 20px);
2227
+ font-size: 14px;
2228
+ }
2229
+
2230
+ .flatpickr-day {
2231
+ height: 35px;
2232
+ line-height: 35px;
2233
+ }
2234
+
2235
+ .flatpickr-time input {
2236
+ font-size: 14px;
2237
+ }
2238
+ }
2239
+
2240
+ /* Improved accessibility */
2241
+ .flatpickr-calendar {
2242
+ font-family: inherit;
2243
+ }
2244
+
2245
+ .flatpickr-day[aria-label] {
2246
+ position: relative;
2247
+ }
2248
+
2249
+ /* Smooth scroll behavior for time inputs */
2250
+ .flatpickr-time input {
2251
+ scroll-behavior: smooth;
2252
+ }
2253
+
2254
+ /* Enhanced visual feedback for interactive elements */
2255
+ .flatpickr-prev-month,
2256
+ .flatpickr-next-month {
2257
+ border-radius: 4px;
2258
+ padding: 4px;
2259
+ margin: 2px;
2260
+ }
2261
+
2262
+ .flatpickr-prev-month:hover,
2263
+ .flatpickr-next-month:hover {
2264
+ background: rgba(var(--bs-primary-rgb), 0.1);
2265
+ }
2266
+ </style>
2267
+
2268
+ <script>
2269
+ document.addEventListener('DOMContentLoaded', function() {
2270
+ // Initialize Flatpickr date range picker
2271
+ const dateRangeInput = document.getElementById('floatingCreationFilterRange');
2272
+ const startHidden = document.getElementById('creation_filter_start');
2273
+ const endHidden = document.getElementById('creation_filter_end');
2274
+
2275
+ if (dateRangeInput && typeof flatpickr !== 'undefined') {
2276
+ console.log('Flatpickr library loaded, initializing date range picker');
2277
+ // Store the Flatpickr instance in a variable accessible to all handlers
2278
+ let fp;
2279
+
2280
+ // Function to initialize Flatpickr
2281
+ function initializeFlatpickr(theme) {
2282
+ // Determine theme based on current document theme or passed parameter
2283
+ const currentTheme = theme || (document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'light');
2284
+
2285
+ const config = {
2286
+ mode: 'range',
2287
+ enableTime: true,
2288
+ dateFormat: 'Y-m-d H:i',
2289
+ time_24hr: true,
2290
+ allowInput: false,
2291
+ clickOpens: true,
2292
+ theme: currentTheme,
2293
+ animate: true,
2294
+ position: 'auto',
2295
+ static: false,
2296
+ appendTo: document.body, // Ensure it renders above other elements
2297
+ locale: {
2298
+ rangeSeparator: ' to ',
2299
+ weekdays: {
2300
+ shorthand: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
2301
+ longhand: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
2302
+ },
2303
+ months: {
2304
+ shorthand: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
2305
+ longhand: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
2306
+ }
2307
+ },
2308
+ onOpen: function(selectedDates, dateStr, instance) {
2309
+ // Add a slight delay to apply theme-specific styling after calendar opens
2310
+ setTimeout(() => {
2311
+ const calendar = instance.calendarContainer;
2312
+ if (calendar) {
2313
+ // Apply theme-specific class for additional styling control
2314
+ calendar.classList.add(`flatpickr-${currentTheme}`);
2315
+
2316
+ // Ensure proper z-index for offcanvas overlay
2317
+ calendar.style.zIndex = '1070';
2318
+
2319
+ // Add elegant entrance animation
2320
+ calendar.classList.add('open');
2321
+ }
2322
+ }, 10);
2323
+ },
2324
+ onClose: function(selectedDates, dateStr, instance) {
2325
+ const calendar = instance.calendarContainer;
2326
+ if (calendar) {
2327
+ calendar.classList.remove('open');
2328
+ }
2329
+ },
2330
+ onChange: function(selectedDates, dateStr, instance) {
2331
+ console.log('Date range changed:', selectedDates);
2332
+
2333
+ if (selectedDates.length === 2) {
2334
+ // Format dates for hidden inputs (Rails expects ISO format)
2335
+ startHidden.value = selectedDates[0].toISOString().slice(0, 16);
2336
+ endHidden.value = selectedDates[1].toISOString().slice(0, 16);
2337
+
2338
+ // Update display with elegant formatting
2339
+ const formatOptions = {
2340
+ year: 'numeric',
2341
+ month: 'short',
2342
+ day: 'numeric',
2343
+ hour: '2-digit',
2344
+ minute: '2-digit',
2345
+ hour12: false
2346
+ };
2347
+
2348
+ const startFormatted = selectedDates[0].toLocaleDateString('en-US', formatOptions);
2349
+ const endFormatted = selectedDates[1].toLocaleDateString('en-US', formatOptions);
2350
+ dateRangeInput.value = `${startFormatted} to ${endFormatted}`;
2351
+
2352
+ } else if (selectedDates.length === 1) {
2353
+ startHidden.value = selectedDates[0].toISOString().slice(0, 16);
2354
+ endHidden.value = '';
2355
+
2356
+ const formatOptions = {
2357
+ year: 'numeric',
2358
+ month: 'short',
2359
+ day: 'numeric',
2360
+ hour: '2-digit',
2361
+ minute: '2-digit',
2362
+ hour12: false
2363
+ };
2364
+
2365
+ const startFormatted = selectedDates[0].toLocaleDateString('en-US', formatOptions);
2366
+ dateRangeInput.value = `${startFormatted} (select end date)`;
2367
+
2368
+ } else {
2369
+ startHidden.value = '';
2370
+ endHidden.value = '';
2371
+ dateRangeInput.value = '';
2372
+ }
2373
+ }
2374
+ };
2375
+
2376
+ return flatpickr(dateRangeInput, config);
2377
+ }
2378
+
2379
+ // Initialize date range picker
2380
+ fp = initializeFlatpickr();
2381
+
2382
+ // Set initial values if they exist
2383
+ if (startHidden.value || endHidden.value) {
2384
+ const dates = [];
2385
+ if (startHidden.value) {
2386
+ dates.push(new Date(startHidden.value));
2387
+ }
2388
+ if (endHidden.value) {
2389
+ dates.push(new Date(endHidden.value));
2390
+ }
2391
+ fp.setDate(dates);
2392
+ }
2393
+
2394
+ // Preset button functionality
2395
+ const presetButtons = document.querySelectorAll('.preset-btn');
2396
+ presetButtons.forEach(button => {
2397
+ button.addEventListener('click', function(event) {
2398
+ event.preventDefault(); // Prevent any form submission
2399
+
2400
+ const preset = this.getAttribute('data-preset');
2401
+ const now = new Date();
2402
+ let startDate, endDate;
2403
+
2404
+ console.log('Preset button clicked:', preset); // Debug log
2405
+
2406
+ switch (preset) {
2407
+ case 'lastminute':
2408
+ startDate = new Date(now);
2409
+ startDate.setMinutes(startDate.getMinutes() - 1);
2410
+ endDate = new Date(now);
2411
+ break;
2412
+ case 'last5minutes':
2413
+ startDate = new Date(now);
2414
+ startDate.setMinutes(startDate.getMinutes() - 5);
2415
+ endDate = new Date(now);
2416
+ break;
2417
+ case 'today':
2418
+ startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);
2419
+ endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59);
2420
+ break;
2421
+ case 'yesterday':
2422
+ const yesterday = new Date(now);
2423
+ yesterday.setDate(yesterday.getDate() - 1);
2424
+ startDate = new Date(yesterday.getFullYear(), yesterday.getMonth(), yesterday.getDate(), 0, 0, 0);
2425
+ endDate = new Date(yesterday.getFullYear(), yesterday.getMonth(), yesterday.getDate(), 23, 59, 59);
2426
+ break;
2427
+ case 'last7days':
2428
+ startDate = new Date(now);
2429
+ startDate.setDate(startDate.getDate() - 7);
2430
+ startDate.setHours(0, 0, 0, 0);
2431
+ endDate = new Date(now);
2432
+ endDate.setHours(23, 59, 59, 999);
2433
+ break;
2434
+ case 'last30days':
2435
+ startDate = new Date(now);
2436
+ startDate.setDate(startDate.getDate() - 30);
2437
+ startDate.setHours(0, 0, 0, 0);
2438
+ endDate = new Date(now);
2439
+ endDate.setHours(23, 59, 59, 999);
2440
+ break;
2441
+ case 'thismonth':
2442
+ startDate = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0);
2443
+ endDate = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59);
2444
+ break;
2445
+ }
2446
+
2447
+ if (startDate && endDate && fp) {
2448
+ console.log('Setting dates:', startDate, endDate); // Debug log
2449
+ fp.setDate([startDate, endDate]);
2450
+
2451
+ // Also update the hidden inputs directly as a fallback
2452
+ startHidden.value = startDate.toISOString().slice(0, 16);
2453
+ endHidden.value = endDate.toISOString().slice(0, 16);
2454
+
2455
+ // Update the display value
2456
+ const formattedStart = startDate.toLocaleDateString() + ' ' + startDate.toLocaleTimeString();
2457
+ const formattedEnd = endDate.toLocaleDateString() + ' ' + endDate.toLocaleTimeString();
2458
+ dateRangeInput.value = formattedStart + ' to ' + formattedEnd;
2459
+ } else {
2460
+ console.error('Failed to set dates - startDate:', startDate, 'endDate:', endDate, 'fp:', fp);
2461
+ }
2462
+ });
2463
+ });
2464
+
2465
+ // Listen for theme changes and update Flatpickr theme
2466
+ document.addEventListener('dbviewerThemeChanged', function(e) {
2467
+ const newTheme = e.detail.theme === 'dark' ? 'dark' : 'light';
2468
+ console.log('Theme changed to:', newTheme);
2469
+
2470
+ // Destroy and recreate with new theme
2471
+ if (fp) {
2472
+ const currentDates = fp.selectedDates;
2473
+ fp.destroy();
2474
+ fp = initializeFlatpickr(newTheme);
2475
+
2476
+ // Restore previous values if they existed
2477
+ if (currentDates && currentDates.length > 0) {
2478
+ fp.setDate(currentDates);
2479
+ }
2480
+ }
2481
+ });
2482
+
2483
+ // Also listen for direct data-bs-theme attribute changes using MutationObserver
2484
+ const themeObserver = new MutationObserver(function(mutations) {
2485
+ mutations.forEach(function(mutation) {
2486
+ if (mutation.type === 'attributes' && mutation.attributeName === 'data-bs-theme') {
2487
+ const newTheme = document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'light';
2488
+ console.log('Theme attribute changed to:', newTheme);
2489
+
2490
+ if (fp) {
2491
+ const currentDates = fp.selectedDates;
2492
+ fp.destroy();
2493
+ fp = initializeFlatpickr(newTheme);
2494
+
2495
+ // Restore previous values if they existed
2496
+ if (currentDates && currentDates.length > 0) {
2497
+ fp.setDate(currentDates);
2498
+ }
2499
+ }
2500
+ }
2501
+ });
2502
+ });
2503
+
2504
+ // Start observing theme changes
2505
+ themeObserver.observe(document.documentElement, {
2506
+ attributes: true,
2507
+ attributeFilter: ['data-bs-theme']
2508
+ });
2509
+ } else {
2510
+ console.error('Date range picker initialization failed:', {
2511
+ dateRangeInput: !!dateRangeInput,
2512
+ flatpickr: typeof flatpickr !== 'undefined'
2513
+ });
2514
+ }
2515
+
2516
+ // Close offcanvas after form submission
2517
+ const form = document.getElementById('floatingCreationFilterForm');
2518
+ if (form) {
2519
+ form.addEventListener('submit', function() {
2520
+ const offcanvas = bootstrap.Offcanvas.getInstance(document.getElementById('creationFilterOffcanvas'));
2521
+ if (offcanvas) {
2522
+ setTimeout(() => {
2523
+ offcanvas.hide();
2524
+ }, 100);
2525
+ }
2526
+ });
2527
+ }
2528
+ });
2529
+ </script>
2530
+ <% end %>
@@ -23,6 +23,8 @@
23
23
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
24
24
  <!-- Bootstrap Icons -->
25
25
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
26
+ <!-- Flatpickr CSS for Date Range Picker -->
27
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
26
28
  <!-- Google Fonts -->
27
29
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
28
30
  <!-- Chart.js for Data Visualization -->
@@ -1407,6 +1409,8 @@
1407
1409
 
1408
1410
  <!-- Bootstrap JS -->
1409
1411
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
1412
+ <!-- Flatpickr JS for Date Range Picker -->
1413
+ <script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
1410
1414
 
1411
1415
  <!-- Dark Mode Toggle Script -->
1412
1416
  <script>
@@ -4,95 +4,7 @@
4
4
  id="tableSearch" placeholder="Filter tables..." aria-label="Filter tables">
5
5
  </div>
6
6
 
7
- <div class="p-2">
8
- <div class="accordion accordion-flush" id="creationFilterAccordion">
9
- <div class="accordion-item border-0">
10
- <h2 class="accordion-header" id="creationFilterHeading">
11
- <button class="accordion-button p-2 collapsed" type="button" data-bs-toggle="collapse"
12
- data-bs-target="#creationFilterCollapse" aria-expanded="false"
13
- aria-controls="creationFilterCollapse">
14
- <i class="bi bi-calendar-range me-2"></i> Creation Filter
15
- <% if @creation_filter_start.present? || @creation_filter_end.present? %>
16
- <% if @table_name.present? && has_timestamp_column?(@table_name) %>
17
- <span class="badge bg-success ms-2">Active</span>
18
- <% else %>
19
- <span class="badge bg-secondary ms-2">Set</span>
20
- <% end %>
21
- <% end %>
22
- </button>
23
- </h2>
24
- <div id="creationFilterCollapse" class="accordion-collapse collapse" aria-labelledby="creationFilterHeading">
25
- <div class="accordion-body p-2">
26
- <form id="creationFilterForm" action="<%= request.path %>" method="get" class="mb-0">
27
- <!-- Preserve existing query parameters -->
28
- <input type="hidden" name="page" value="<%= @current_page %>">
29
- <input type="hidden" name="per_page" value="<%= @per_page %>">
30
- <input type="hidden" name="order_by" value="<%= @order_by %>">
31
- <input type="hidden" name="order_direction" value="<%= @order_direction %>">
32
-
33
- <!-- Datetime range fields -->
34
- <div class="mb-2">
35
- <label for="creationFilterStart" class="form-label mb-1 small">Start Date/Time</label>
36
- <input type="datetime-local" id="creationFilterStart" name="creation_filter_start"
37
- class="form-control form-control-sm" value="<%= @creation_filter_start %>">
38
- </div>
39
- <div class="mb-2">
40
- <label for="creationFilterEnd" class="form-label mb-1 small">End Date/Time</label>
41
- <input type="datetime-local" id="creationFilterEnd" name="creation_filter_end"
42
- class="form-control form-control-sm" value="<%= @creation_filter_end %>">
43
- </div>
44
-
45
- <div class="d-flex justify-content-between">
46
- <button type="submit" class="btn btn-primary btn-sm">Apply</button>
47
- <% if @creation_filter_start.present? || @creation_filter_end.present? %>
48
- <%
49
- # Preserve other query params when clearing creation filter
50
- clear_params = {
51
- clear_creation_filter: true,
52
- page: @current_page,
53
- per_page: @per_page,
54
- order_by: @order_by,
55
- order_direction: @order_direction
56
- }
57
- %>
58
- <a href="<%= request.path %>?<%= clear_params.to_query %>" class="btn btn-outline-secondary btn-sm">Clear</a>
59
- <% end %>
60
- </div>
61
- <div class="mt-2 small">
62
- <% if @table_name.present? && has_timestamp_column?(@table_name) && (@creation_filter_start.present? || @creation_filter_end.present?) %>
63
- <div class="text-success">
64
- <i class="bi bi-check-circle-fill"></i>
65
- Filter active on this table.
66
- <% if @current_page == 1 && @records && @records.rows && @records.rows.empty? %>
67
- <div class="alert alert-warning p-1 mt-2 small">
68
- <i class="bi bi-exclamation-triangle-fill"></i>
69
- No records match the filter criteria.
70
- </div>
71
- <% end %>
72
- </div>
73
- <% elsif @table_name.present? && (@creation_filter_start.present? || @creation_filter_end.present?) %>
74
- <div class="text-warning">
75
- <i class="bi bi-exclamation-circle-fill"></i>
76
- This table has no <code>created_at</code> column.
77
- </div>
78
- <% elsif !@table_name.present? && (@creation_filter_start.present? || @creation_filter_end.present?) %>
79
- <div class="text-info">
80
- <i class="bi bi-info-circle-fill"></i>
81
- Filter will be applied on tables with <code>created_at</code> column.
82
- </div>
83
- <% else %>
84
- <div class="text-muted">
85
- <i class="bi bi-info-circle-fill"></i>
86
- Filters apply to tables with a <code>created_at</code> column.
87
- </div>
88
- <% end %>
89
- </div>
90
- </form>
91
- </div>
92
- </div>
93
- </div>
94
- </div>
95
- </div>
7
+
96
8
 
97
9
  <!-- Add custom styling for datetime inputs -->
98
10
  <style>
@@ -207,6 +119,13 @@
207
119
  <script>
208
120
  document.addEventListener('DOMContentLoaded', function() {
209
121
  const searchInput = document.getElementById('tableSearch');
122
+ const sidebarContent = document.querySelector('.dbviewer-sidebar-content');
123
+
124
+ // Storage keys for persistence
125
+ const STORAGE_KEYS = {
126
+ searchFilter: 'dbviewer_sidebar_search_filter',
127
+ scrollPosition: 'dbviewer_sidebar_scroll_position'
128
+ };
210
129
 
211
130
  if (searchInput) {
212
131
  // Debounce function to limit how often the filter runs
@@ -228,6 +147,9 @@
228
147
  const tableItems = document.querySelectorAll('#tablesList .list-group-item-action');
229
148
  let visibleCount = 0;
230
149
 
150
+ // Save the current search filter to localStorage
151
+ localStorage.setItem(STORAGE_KEYS.searchFilter, searchInput.value);
152
+
231
153
  tableItems.forEach(function(item) {
232
154
  // Get the table name from the title attribute for more accurate matching
233
155
  const tableName = (item.getAttribute('title') || item.textContent).trim().toLowerCase();
@@ -266,26 +188,7 @@
266
188
  }
267
189
  }, 150); // Debounce for 150ms
268
190
 
269
- // Set up event listeners for the search input
270
- searchInput.addEventListener('input', filterTables);
271
- searchInput.addEventListener('keyup', function(e) {
272
- filterTables();
273
-
274
- // Add keyboard navigation for the filtered list
275
- if (e.key === 'Enter' || e.key === 'ArrowDown') {
276
- e.preventDefault();
277
- // Focus the first visible table item (not having d-none class)
278
- const firstVisibleItem = document.querySelector('#tablesList .list-group-item-action:not(.d-none)');
279
- if (firstVisibleItem) {
280
- firstVisibleItem.focus();
281
- // Make sure the item is visible in the scrollable area
282
- firstVisibleItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
283
- }
284
- }
285
- });
286
- searchInput.addEventListener('search', filterTables); // For clearing via the "x" in some browsers
287
-
288
- // Clear the search box when clicking the X button
191
+ // Set up clear button first
289
192
  const clearButton = document.createElement('button');
290
193
  clearButton.type = 'button';
291
194
  clearButton.className = 'btn btn-sm btn-link position-absolute';
@@ -301,6 +204,8 @@
301
204
  clearButton.innerHTML = '<i class="bi bi-x-circle"></i>';
302
205
  clearButton.addEventListener('click', function() {
303
206
  searchInput.value = '';
207
+ // Clear the saved filter from localStorage
208
+ localStorage.removeItem(STORAGE_KEYS.searchFilter);
304
209
  // Call filter directly without debouncing for immediate feedback
305
210
  filterTables();
306
211
  this.style.display = 'none';
@@ -314,13 +219,90 @@
314
219
  searchInput.addEventListener('input', function() {
315
220
  clearButton.style.display = this.value ? 'block' : 'none';
316
221
  });
222
+ }
223
+
224
+ // Restore saved search filter on page load and apply it immediately
225
+ const savedFilter = localStorage.getItem(STORAGE_KEYS.searchFilter);
226
+ if (savedFilter) {
227
+ searchInput.value = savedFilter;
228
+ // Show clear button immediately when filter is restored
229
+ clearButton.style.display = 'block';
230
+ // Apply filter immediately without debouncing to prevent blinking
231
+ const query = savedFilter.toLowerCase();
232
+ const tableItems = document.querySelectorAll('#tablesList .list-group-item-action');
233
+ let visibleCount = 0;
317
234
 
318
- // Apply filter initially in case there's a value already (e.g., from browser autofill)
319
- if (searchInput.value) {
320
- filterTables();
321
- clearButton.style.display = 'block';
235
+ tableItems.forEach(function(item) {
236
+ const tableName = (item.getAttribute('title') || item.textContent).trim().toLowerCase();
237
+ const displayedText = item.textContent.trim().toLowerCase();
238
+
239
+ if (tableName.includes(query) || displayedText.includes(query)) {
240
+ item.classList.remove('d-none');
241
+ visibleCount++;
242
+ } else {
243
+ item.classList.add('d-none');
244
+ }
245
+ });
246
+
247
+ // Update the tables count immediately
248
+ const tableCountElement = document.getElementById('table-count');
249
+ if (tableCountElement) {
250
+ tableCountElement.textContent = visibleCount;
251
+ }
252
+
253
+ // Handle no results message immediately
254
+ let noResultsEl = document.getElementById('dbviewer-no-filter-results');
255
+ if (visibleCount === 0 && query !== '') {
256
+ if (!noResultsEl) {
257
+ noResultsEl = document.createElement('div');
258
+ noResultsEl.id = 'dbviewer-no-filter-results';
259
+ noResultsEl.className = 'list-group-item text-muted text-center py-3';
260
+ noResultsEl.innerHTML = '<i class="bi bi-search me-1"></i> No tables match "<span class="fw-bold"></span>"';
261
+ document.getElementById('tablesList').appendChild(noResultsEl);
262
+ }
263
+ noResultsEl.querySelector('.fw-bold').textContent = query;
264
+ noResultsEl.style.display = 'block';
265
+ } else if (noResultsEl) {
266
+ noResultsEl.style.display = 'none';
322
267
  }
323
268
  }
269
+
270
+ // Restore saved scroll position on page load
271
+ if (sidebarContent) {
272
+ const savedScrollPosition = localStorage.getItem(STORAGE_KEYS.scrollPosition);
273
+ if (savedScrollPosition) {
274
+ // Use requestAnimationFrame to ensure DOM is fully rendered
275
+ requestAnimationFrame(() => {
276
+ sidebarContent.scrollTop = parseInt(savedScrollPosition, 10);
277
+ });
278
+ }
279
+
280
+ // Save scroll position on scroll
281
+ const saveScrollPosition = debounce(function() {
282
+ localStorage.setItem(STORAGE_KEYS.scrollPosition, sidebarContent.scrollTop);
283
+ }, 100);
284
+
285
+ sidebarContent.addEventListener('scroll', saveScrollPosition);
286
+ }
287
+
288
+ // Set up event listeners for the search input
289
+ searchInput.addEventListener('input', filterTables);
290
+ searchInput.addEventListener('keyup', function(e) {
291
+ filterTables();
292
+
293
+ // Add keyboard navigation for the filtered list
294
+ if (e.key === 'Enter' || e.key === 'ArrowDown') {
295
+ e.preventDefault();
296
+ // Focus the first visible table item (not having d-none class)
297
+ const firstVisibleItem = document.querySelector('#tablesList .list-group-item-action:not(.d-none)');
298
+ if (firstVisibleItem) {
299
+ firstVisibleItem.focus();
300
+ // Make sure the item is visible in the scrollable area
301
+ firstVisibleItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
302
+ }
303
+ }
304
+ });
305
+ searchInput.addEventListener('search', filterTables); // For clearing via the "x" in some browsers
324
306
  }
325
307
  });
326
308
  </script>
@@ -1,3 +1,3 @@
1
1
  module Dbviewer
2
- VERSION = "0.5.6"
2
+ VERSION = "0.5.7"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dbviewer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.6
4
+ version: 0.5.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wailan Tirajoh