Coverage for src / lexigram / admin / ui / organisms / systembox.py: 0%

63 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-11 02:25 +0800

1from __future__ import annotations 

2 

3from typing import Any 

4 

5from lexigram.ui import Component, el 

6 

7 

8class SystemBox(Component): 

9 """ 

10 A compact system menu display for the sidebar footer that mirrors the 

11 behavior of `UserBox`. It renders compact icons inline and exposes 

12 block-style items (e.g., Settings) in a dropdown for discoverability. 

13 """ 

14 

15 def __init__( 

16 self, 

17 system_menu_items: list[dict] | None = None, 

18 direction: str = "up", 

19 user: Any | None = None, 

20 **props, 

21 ) -> None: 

22 super().__init__( 

23 system_menu_items=system_menu_items, 

24 direction=direction, 

25 user=user, 

26 **props, 

27 ) 

28 self.system_menu_items = system_menu_items or [] 

29 self.direction = direction 

30 self.user = user 

31 

32 def render(self) -> Any: 

33 block_items = [] 

34 compact_nodes = [] 

35 

36 # Old RBACChecker logic removed 

37 

38 for m in self.system_menu_items: 

39 label = m.get("label") or "" 

40 href = m.get("href", "#") 

41 icon = m.get("icon") 

42 permission = m.get("permission") 

43 

44 if permission and self.user: 

45 try: 

46 # Check permission using AuthenticatedUserProtocol.has_permission 

47 has_perm = False 

48 if hasattr(self.user, "has_permission"): 

49 has_perm = self.user.has_permission(permission) 

50 elif hasattr(self.user, "permissions"): 

51 # Basic check 

52 has_perm = permission in self.user.permissions 

53 

54 if not has_perm: 

55 # Skip guarded items when user lacks permission 

56 continue 

57 except Exception as _perm_err: # noqa: BLE001 — user permission objects may raise anything; conservative skip 

58 from lexigram.logging import get_logger 

59 

60 logger = get_logger(__name__) 

61 logger.exception( 

62 "Error while evaluating permission '%s' for user", permission 

63 ) 

64 # Conservative: skip if we can't evaluate permission 

65 continue 

66 

67 # Build content 

68 if icon: 

69 try: 

70 from lexigram.ui import get_icon 

71 

72 content = get_icon(icon, class_name="w-5 h-5") 

73 except Exception as _icon_err: # noqa: BLE001 — icon rendering is best-effort; fall back to plain text 

74 from lexigram.logging import get_logger 

75 

76 logger = get_logger(__name__) 

77 logger.exception("Failed to render icon '%s'", icon) 

78 content = el("span", label) 

79 else: 

80 content = el( 

81 "span", 

82 label, 

83 class_="text-sm text-foreground", 

84 ) 

85 

86 # Use any attributes supplied on the menu item (e.g., data-test hooks) 

87 attrs = dict(m.get("attrs", {})) 

88 

89 # Block style (full-width) items are rendered in dropdown or as block anchors 

90 if m.get("render") == "block": 

91 # Create SidebarItem-like structure for block items 

92 first_letter = label[0] if label else "" 

93 

94 # Check if icon is available for this item to optionally invoke it? 

95 # For now, just use label logic as icons are usually in 'compact_nodes' if intended for existing system box. 

96 # But 'Configuration' might have an icon in metadata? 

97 # If icon exists, we might want to hide it in mini mode like SidebarItem. 

98 

99 # Icon (if present in metadata) 

100 icon_node = "" 

101 # Logic: If icon is present, show it always (adjust margin). Hide first letter. 

102 # If no icon, show first letter in mini mode. 

103 

104 has_icon = False 

105 if icon: 

106 has_icon = True 

107 try: 

108 from lexigram.ui import get_icon 

109 

110 # Icon Container: Show in both modes. Handle margin dynamically. 

111 icon_node = el( 

112 "div", 

113 get_icon(icon, class_name="w-5 h-5"), 

114 class_="transition-all duration-200", 

115 **{"x-bind:class": "sidebarMini ? 'mr-0' : 'mr-3'"}, 

116 ) 

117 except (KeyError, ValueError): 

118 pass 

119 

120 first_letter_node = "" 

121 if not has_icon: 

122 first_letter_node = el( 

123 "span", 

124 first_letter, 

125 class_="font-bold text-xs w-6 h-6 flex items-center justify-center shrink-0 rounded-lg bg-muted dark:bg-card text-muted-foreground", 

126 x_show="sidebarMini", 

127 ) 

128 

129 block_items.append( 

130 el( 

131 "a", 

132 icon_node, 

133 # Full Label 

134 el( 

135 "span", 

136 label, 

137 class_="truncate whitespace-nowrap", 

138 x_show="!sidebarMini", 

139 ), 

140 # Mini Label (First Letter) 

141 first_letter_node, 

142 href=href, 

143 class_=( 

144 "group flex items-center px-3 py-2 rounded-xl text-sm font-medium transition-colors duration-200 text-muted-foreground dark:text-muted-foreground hover:bg-muted dark:hover:bg-card/50 hover:text-primary-600 dark:hover:text-primary-400" 

145 ), 

146 **{ 

147 "x-bind:class": "sidebarMini ? 'justify-center' : ''", 

148 # Merge other attrs 

149 **attrs, 

150 }, 

151 hx_get=href, 

152 hx_target="#main-content", 

153 hx_swap="innerHTML", 

154 hx_push_url="true", 

155 ), 

156 ) 

157 else: 

158 compact_nodes.append( 

159 el( 

160 "a", 

161 content, 

162 href=href, 

163 title=label, 

164 class_=( 

165 "inline-flex items-center justify-center p-2 rounded-md text-muted-foreground hover:bg-muted dark:text-foreground dark:hover:bg-card" 

166 ), 

167 **attrs, 

168 ), 

169 ) 

170 

171 # Inline compact area 

172 compact_bar = ( 

173 el("div", *compact_nodes, class_="flex items-center space-x-2") 

174 if compact_nodes 

175 else "" 

176 ) 

177 

178 # Render block items as a stacked list (flat, not popup) 

179 block_area = "" 

180 if block_items: 

181 block_area = el( 

182 "div", 

183 *block_items, 

184 class_="px-3 py-2 space-y-1", 

185 ) 

186 

187 # Place block items above compact icons to emulate stacked sidebar layout 

188 return el( 

189 "div", 

190 block_area, 

191 compact_bar, 

192 class_="flex flex-col px-3 py-2 border-b border-border", 

193 )