Coverage for src/lexigram/admin/ui/organisms/pagination.py: 96%

28 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-21 15:04 +0800

1from __future__ import annotations 

2 

3from typing import Any 

4 

5from lexigram.ui import ( 

6 Component, 

7 JumpToPage, 

8 Link, 

9 PageSizeSelector, 

10 PaginationLinks, 

11 Zones, 

12 el, 

13) 

14 

15 

16class Pagination(Component): 

17 """ 

18 Standalone pagination component with page size selector and jump to page. 

19 Compiles molecules into a functional organism. 

20 Uses Zone-based targeting for consistent HTMX behavior. 

21 """ 

22 

23 def __init__( 

24 self, 

25 page: int = 1, 

26 total: int = 0, 

27 per_page: int = 20, 

28 base_url: str = "", 

29 show_size_selector: bool = True, 

30 show_jump_to_page: bool = True, 

31 extra_query: str = "", 

32 hx_target: str | None = None, 

33 hx_swap: str = "innerHTML", 

34 hx_push_url: str = "true", 

35 next_cursor: str | None = None, 

36 state: Any | None = None, 

37 **props, 

38 ) -> None: 

39 super().__init__( 

40 page=page, 

41 total=total, 

42 per_page=per_page, 

43 base_url=base_url, 

44 **props, 

45 ) 

46 self.state = state 

47 self.page = page 

48 self.total = total 

49 self.per_page = per_page 

50 self.base_url = base_url 

51 self.next_cursor = next_cursor 

52 self.show_size_selector = show_size_selector 

53 self.show_jump_to_page = show_jump_to_page 

54 self.extra_query = extra_query 

55 # Use Zones.DATA by default 

56 self.hx_target = hx_target or Zones.DATA.selector 

57 self.hx_swap = hx_swap 

58 self.hx_push_url = hx_push_url 

59 

60 def render(self) -> Any: 

61 if self.total <= 0: 

62 return "" 

63 

64 total_pages = (self.total + self.per_page - 1) // self.per_page 

65 

66 start_item = (self.page - 1) * self.per_page + 1 

67 end_item = min(self.page * self.per_page, self.total) 

68 

69 # Render Modular Components 

70 size_selector = ( 

71 PageSizeSelector( 

72 per_page=self.per_page, 

73 base_url=self.base_url, 

74 extra_query=self.extra_query, 

75 hx_target=self.hx_target, 

76 hx_swap=self.hx_swap, 

77 hx_push_url=self.hx_push_url, 

78 state=self.state, 

79 ) 

80 if self.show_size_selector 

81 else "" 

82 ) 

83 

84 jump_to_page = ( 

85 JumpToPage( 

86 page=self.page, 

87 total_pages=total_pages, 

88 per_page=self.per_page, 

89 base_url=self.base_url, 

90 extra_query=self.extra_query, 

91 hx_target=self.hx_target, 

92 hx_swap=self.hx_swap, 

93 hx_push_url=self.hx_push_url, 

94 state=self.state, 

95 ) 

96 if self.show_jump_to_page 

97 else "" 

98 ) 

99 

100 page_links = PaginationLinks( 

101 page=self.page, 

102 total_pages=total_pages, 

103 per_page=self.per_page, 

104 base_url=self.base_url, 

105 extra_query=self.extra_query, 

106 hx_target=self.hx_target, 

107 hx_swap=self.hx_swap, 

108 hx_push_url=self.hx_push_url, 

109 state=self.state, 

110 ) 

111 

112 return el( 

113 "div", 

114 { 

115 "id": "table-pagination", 

116 "class": "flex items-center justify-between border-t border-border bg-background px-4 py-3 mt-4", 

117 }, 

118 # Mobile View 

119 el( 

120 "div", 

121 {"class": "flex flex-1 justify-between sm:hidden"}, 

122 # Previous (mobile). Render as span when disabled to avoid HTMX attributes on inactive controls. 

123 ( 

124 el( 

125 "span", 

126 "Previous", 

127 class_=f"relative inline-flex items-center rounded-md border border-border bg-card px-4 py-2 text-sm font-medium text-foreground {'invisible' if self.page <= 1 else ''}", 

128 ) 

129 if self.page <= 1 

130 else Link( 

131 "Previous", 

132 f"{self.base_url}?page={max(1, self.page - 1)}&per_page={self.per_page}{self.extra_query}", 

133 color="muted", 

134 class_=f"relative inline-flex items-center rounded-md border border-border bg-card px-4 py-2 text-sm font-medium text-foreground hover:bg-muted {'invisible' if self.page <= 1 else ''}", 

135 hx_get=f"{self.base_url}?page={max(1, self.page - 1)}&per_page={self.per_page}{self.extra_query}", 

136 hx_trigger="click", 

137 hx_target=self.hx_target, 

138 hx_swap=self.hx_swap, 

139 hx_push_url=self.hx_push_url, 

140 hx_include="this", 

141 onclick="return false", 

142 preload="mouseover", 

143 ) 

144 ), 

145 # Next (mobile). Render as span when disabled to avoid HTMX attributes on inactive controls. 

146 ( 

147 el( 

148 "span", 

149 "Next", 

150 class_=f"relative ml-3 inline-flex items-center rounded-md border border-border bg-card px-4 py-2 text-sm font-medium text-foreground {'invisible' if not self.next_cursor and self.page >= total_pages else ''}", 

151 ) 

152 if (not self.next_cursor and self.page >= total_pages) 

153 else Link( 

154 "Next", 

155 f"{self.base_url}?cursor={self.next_cursor}&per_page={self.per_page}{self.extra_query}" 

156 if self.next_cursor 

157 else f"{self.base_url}?page={min(total_pages, self.page + 1)}&per_page={self.per_page}{self.extra_query}", 

158 class_=f"relative ml-3 inline-flex items-center rounded-md border border-border bg-card px-4 py-2 text-sm font-medium text-foreground hover:bg-muted {'invisible' if not self.next_cursor and self.page >= total_pages else ''}", 

159 hx_get=f"{self.base_url}?cursor={self.next_cursor}&per_page={self.per_page}{self.extra_query}" 

160 if self.next_cursor 

161 else f"{self.base_url}?page={min(total_pages, self.page + 1)}&per_page={self.per_page}{self.extra_query}", 

162 hx_trigger="click", 

163 hx_target=self.hx_target, 

164 hx_swap=self.hx_swap, 

165 hx_push_url=self.hx_push_url, 

166 hx_include="this", 

167 onclick="return false", 

168 preload="mouseover", 

169 ) 

170 ), 

171 ), 

172 # Desktop View 

173 el( 

174 "div", 

175 { 

176 "class": "hidden sm:flex sm:flex-1 sm:items-center sm:justify-between px-4", 

177 }, 

178 # Results text 

179 el( 

180 "div", 

181 el( 

182 "p", 

183 "Showing ", 

184 el("span", str(start_item), class_="font-bold"), 

185 " to ", 

186 el("span", str(end_item), class_="font-bold"), 

187 " of ", 

188 el("span", str(self.total), class_="font-bold"), 

189 " results", 

190 class_="text-[11px] uppercase tracking-wider text-muted-foreground font-semibold", 

191 ), 

192 ), 

193 # Pagination Controls 

194 el( 

195 "div", 

196 page_links, 

197 size_selector, 

198 jump_to_page, 

199 class_="flex items-center space-x-4", 

200 ), 

201 ), 

202 )