Coverage for src / lexigram / admin / ui / organisms / userbox.py: 13%

45 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 UserBox(Component): 

9 """ 

10 A compact user profile display for headers or sidebars. 

11 """ 

12 

13 def __init__( 

14 self, 

15 username: str, 

16 avatar_url: str | None = None, 

17 direction: str = "down", 

18 position: str = "right", 

19 roles: list[str] | None = None, 

20 user_menu_items: list[dict] | None = None, 

21 user: Any | None = None, 

22 **props, 

23 ) -> None: 

24 super().__init__( 

25 username=username, 

26 avatar_url=avatar_url, 

27 direction=direction, 

28 position=position, 

29 roles=roles, 

30 user_menu_items=user_menu_items, 

31 user=user, 

32 **props, 

33 ) 

34 self.username = username 

35 self.avatar_url = avatar_url 

36 self.direction = direction 

37 self.position = position 

38 self.roles = roles or [] 

39 self.user_menu_items = user_menu_items or [] 

40 self.user = user 

41 

42 def render(self) -> Any: 

43 from lexigram.ui import Dropdown 

44 

45 if not self.avatar_url: 

46 avatar = el( 

47 "div", 

48 self.username[0].upper(), 

49 class_="w-8 h-8 rounded-full bg-primary-100 text-primary-600 flex items-center justify-center font-bold text-sm dark:bg-primary-900/30 dark:text-primary-400 border border-border", 

50 ) 

51 else: 

52 avatar = el( 

53 "img", 

54 src=self.avatar_url, 

55 class_="w-8 h-8 rounded-full border border-border", 

56 ) 

57 

58 name = el( 

59 "div", 

60 self.username, 

61 class_="font-medium text-sm text-foreground truncate flex-1 text-left", 

62 x_show="!sidebarMini", 

63 ) 

64 

65 trigger = el( 

66 "div", 

67 avatar, 

68 name, 

69 el( 

70 "svg", 

71 el("path", d="M19 9l-7 7-7-7"), 

72 class_="ml-auto h-4 w-4 text-muted-foreground", 

73 fill="none", 

74 viewBox="0 0 24 24", 

75 stroke="currentColor", 

76 ), 

77 class_="flex items-center space-x-3 p-2 hover:bg-card transition-colors cursor-pointer w-full border border-transparent hover:border-border", 

78 **{"x-bind:class": "sidebarMini ? 'justify-center' : ''"}, 

79 ) 

80 

81 # Dropdown Items 

82 items = [] 

83 

84 # RBACChecker logic removed 

85 

86 for menu_item in self.user_menu_items: 

87 required_permission = menu_item.get("permission") 

88 if required_permission and self.user: 

89 try: 

90 # Check permission using AuthenticatedUserProtocol.has_permission 

91 has_perm = False 

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

93 has_perm = self.user.has_permission(required_permission) 

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

95 # Basic check 

96 has_perm = required_permission in self.user.permissions 

97 

98 if not has_perm: 

99 continue 

100 except (AttributeError, TypeError): 

101 continue 

102 

103 # Allow menu items to supply attributes (data-test hooks, etc.) 

104 attrs = dict(menu_item.get("attrs", {})) 

105 href = menu_item.get("href", "#") 

106 

107 # By default render as a block item in dropdown and make it HTMX-enabled 

108 # so clicking updates the main content area (consistent with system menu) 

109 # Icon rendering 

110 icon_node: Any = "" 

111 if menu_item.get("icon"): 

112 from lexigram.ui import Icon 

113 

114 icon_node = Icon( 

115 menu_item["icon"], 

116 class_name="w-4 h-4 mr-2 text-muted-foreground group-hover:text-muted-foreground dark:text-muted-foreground", 

117 ) 

118 

119 items.append( 

120 el( 

121 "a", 

122 icon_node, 

123 menu_item.get("label", ""), 

124 href=href, 

125 class_="group flex items-center px-4 py-2 text-sm text-foreground hover:bg-muted dark:hover:bg-card rounded-md", 

126 hx_get=href, 

127 hx_target="#main-content", 

128 hx_swap="innerHTML", 

129 hx_push_url="true", 

130 **attrs, 

131 ), 

132 ) 

133 

134 if items: 

135 items.append(el("div", class_="h-px my-1 bg-muted")) 

136 

137 items.append( 

138 el( 

139 "a", 

140 "Sign out", 

141 href="/admin/logout", 

142 class_="block px-4 py-2 text-sm text-destructive hover:bg-destructive/10 rounded-md", 

143 ), 

144 ) 

145 

146 return Dropdown( 

147 trigger=trigger, 

148 items=items, 

149 direction=self.direction, 

150 position=self.position, 

151 )