@memberjunction/server 5.44.0 → 5.45.0

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.
Files changed (254) hide show
  1. package/dist/agentSessions/ReturningVisitorRecap.d.ts +39 -0
  2. package/dist/agentSessions/ReturningVisitorRecap.d.ts.map +1 -0
  3. package/dist/agentSessions/ReturningVisitorRecap.js +198 -0
  4. package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -0
  5. package/dist/agentSessions/SessionJanitor.d.ts +13 -0
  6. package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
  7. package/dist/agentSessions/SessionJanitor.js +65 -7
  8. package/dist/agentSessions/SessionJanitor.js.map +1 -1
  9. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  10. package/dist/agentSessions/SessionManager.js +37 -0
  11. package/dist/agentSessions/SessionManager.js.map +1 -1
  12. package/dist/auth/magicLink/MagicLinkRouter.d.ts +10 -0
  13. package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
  14. package/dist/auth/magicLink/MagicLinkRouter.js +16 -0
  15. package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
  16. package/dist/auth/magicLink/index.d.ts +1 -1
  17. package/dist/auth/magicLink/index.d.ts.map +1 -1
  18. package/dist/auth/magicLink/index.js +1 -1
  19. package/dist/auth/magicLink/index.js.map +1 -1
  20. package/dist/auth/magicLink/types.d.ts +26 -0
  21. package/dist/auth/magicLink/types.d.ts.map +1 -1
  22. package/dist/config.d.ts +1883 -144
  23. package/dist/config.d.ts.map +1 -1
  24. package/dist/config.js +160 -36
  25. package/dist/config.js.map +1 -1
  26. package/dist/context.d.ts.map +1 -1
  27. package/dist/context.js +45 -2
  28. package/dist/context.js.map +1 -1
  29. package/dist/generated/generated.d.ts +522 -0
  30. package/dist/generated/generated.d.ts.map +1 -1
  31. package/dist/generated/generated.js +2860 -0
  32. package/dist/generated/generated.js.map +1 -1
  33. package/dist/generic/ResolverBase.d.ts +13 -0
  34. package/dist/generic/ResolverBase.d.ts.map +1 -1
  35. package/dist/generic/ResolverBase.js +22 -0
  36. package/dist/generic/ResolverBase.js.map +1 -1
  37. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  38. package/dist/generic/RunViewResolver.js +4 -0
  39. package/dist/generic/RunViewResolver.js.map +1 -1
  40. package/dist/index.d.ts +0 -2
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +101 -3
  43. package/dist/index.js.map +1 -1
  44. package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
  45. package/dist/integration/CustomColumnPromoter.js +6 -0
  46. package/dist/integration/CustomColumnPromoter.js.map +1 -1
  47. package/dist/realtimeWidget/WidgetRouter.d.ts +26 -0
  48. package/dist/realtimeWidget/WidgetRouter.d.ts.map +1 -0
  49. package/dist/realtimeWidget/WidgetRouter.js +182 -0
  50. package/dist/realtimeWidget/WidgetRouter.js.map +1 -0
  51. package/dist/realtimeWidget/WidgetSessionService.d.ts +265 -0
  52. package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -0
  53. package/dist/realtimeWidget/WidgetSessionService.js +477 -0
  54. package/dist/realtimeWidget/WidgetSessionService.js.map +1 -0
  55. package/dist/realtimeWidget/host-identity.d.ts +40 -0
  56. package/dist/realtimeWidget/host-identity.d.ts.map +1 -0
  57. package/dist/realtimeWidget/host-identity.js +58 -0
  58. package/dist/realtimeWidget/host-identity.js.map +1 -0
  59. package/dist/realtimeWidget/index.d.ts +10 -0
  60. package/dist/realtimeWidget/index.d.ts.map +1 -0
  61. package/dist/realtimeWidget/index.js +9 -0
  62. package/dist/realtimeWidget/index.js.map +1 -0
  63. package/dist/realtimeWidget/visitorIdentity.d.ts +76 -0
  64. package/dist/realtimeWidget/visitorIdentity.d.ts.map +1 -0
  65. package/dist/realtimeWidget/visitorIdentity.js +202 -0
  66. package/dist/realtimeWidget/visitorIdentity.js.map +1 -0
  67. package/dist/realtimeWidget/widgetCore.d.ts +106 -0
  68. package/dist/realtimeWidget/widgetCore.d.ts.map +1 -0
  69. package/dist/realtimeWidget/widgetCore.js +173 -0
  70. package/dist/realtimeWidget/widgetCore.js.map +1 -0
  71. package/dist/realtimeWidget/widgetGuestElevation.d.ts +51 -0
  72. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -0
  73. package/dist/realtimeWidget/widgetGuestElevation.js +79 -0
  74. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -0
  75. package/dist/resolvers/ComponentRegistryResolver.d.ts +7 -0
  76. package/dist/resolvers/ComponentRegistryResolver.d.ts.map +1 -1
  77. package/dist/resolvers/ComponentRegistryResolver.js +31 -8
  78. package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
  79. package/dist/resolvers/EntityPermissionResolver.d.ts.map +1 -1
  80. package/dist/resolvers/EntityPermissionResolver.js +1 -2
  81. package/dist/resolvers/EntityPermissionResolver.js.map +1 -1
  82. package/dist/resolvers/QuerySystemUserResolver.d.ts +9 -6
  83. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  84. package/dist/resolvers/QuerySystemUserResolver.js +15 -13
  85. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  86. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +10 -0
  87. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  88. package/dist/resolvers/RealtimeClientSessionResolver.js +47 -3
  89. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  90. package/dist/resolvers/RingCentralTelephonyResolver.d.ts +27 -0
  91. package/dist/resolvers/RingCentralTelephonyResolver.d.ts.map +1 -0
  92. package/dist/resolvers/RingCentralTelephonyResolver.js +87 -0
  93. package/dist/resolvers/RingCentralTelephonyResolver.js.map +1 -0
  94. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  95. package/dist/resolvers/RunAIAgentResolver.js +14 -3
  96. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  97. package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
  98. package/dist/resolvers/SearchKnowledgeResolver.js +2 -0
  99. package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
  100. package/dist/resolvers/TeamsMeetingsResolver.d.ts +28 -0
  101. package/dist/resolvers/TeamsMeetingsResolver.d.ts.map +1 -0
  102. package/dist/resolvers/TeamsMeetingsResolver.js +91 -0
  103. package/dist/resolvers/TeamsMeetingsResolver.js.map +1 -0
  104. package/dist/resolvers/TelephonyResolver.d.ts +26 -0
  105. package/dist/resolvers/TelephonyResolver.d.ts.map +1 -0
  106. package/dist/resolvers/TelephonyResolver.js +86 -0
  107. package/dist/resolvers/TelephonyResolver.js.map +1 -0
  108. package/dist/resolvers/TestQuerySQLResolver.d.ts +1 -1
  109. package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
  110. package/dist/resolvers/TestQuerySQLResolver.js +2 -3
  111. package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
  112. package/dist/resolvers/VonageTelephonyResolver.d.ts +26 -0
  113. package/dist/resolvers/VonageTelephonyResolver.d.ts.map +1 -0
  114. package/dist/resolvers/VonageTelephonyResolver.js +86 -0
  115. package/dist/resolvers/VonageTelephonyResolver.js.map +1 -0
  116. package/dist/rest/MediaStreamHandler.js +4 -1
  117. package/dist/rest/MediaStreamHandler.js.map +1 -1
  118. package/dist/telephony/RingCentralTelephonyService.d.ts +115 -0
  119. package/dist/telephony/RingCentralTelephonyService.d.ts.map +1 -0
  120. package/dist/telephony/RingCentralTelephonyService.js +262 -0
  121. package/dist/telephony/RingCentralTelephonyService.js.map +1 -0
  122. package/dist/telephony/TeamsMeetingsRouter.d.ts +40 -0
  123. package/dist/telephony/TeamsMeetingsRouter.d.ts.map +1 -0
  124. package/dist/telephony/TeamsMeetingsRouter.js +96 -0
  125. package/dist/telephony/TeamsMeetingsRouter.js.map +1 -0
  126. package/dist/telephony/TeamsMeetingsService.d.ts +113 -0
  127. package/dist/telephony/TeamsMeetingsService.d.ts.map +1 -0
  128. package/dist/telephony/TeamsMeetingsService.js +196 -0
  129. package/dist/telephony/TeamsMeetingsService.js.map +1 -0
  130. package/dist/telephony/TwilioTelephonyRouter.d.ts +36 -0
  131. package/dist/telephony/TwilioTelephonyRouter.d.ts.map +1 -0
  132. package/dist/telephony/TwilioTelephonyRouter.js +143 -0
  133. package/dist/telephony/TwilioTelephonyRouter.js.map +1 -0
  134. package/dist/telephony/TwilioTelephonyService.d.ts +95 -0
  135. package/dist/telephony/TwilioTelephonyService.d.ts.map +1 -0
  136. package/dist/telephony/TwilioTelephonyService.js +193 -0
  137. package/dist/telephony/TwilioTelephonyService.js.map +1 -0
  138. package/dist/telephony/VonageTelephonyRouter.d.ts +41 -0
  139. package/dist/telephony/VonageTelephonyRouter.d.ts.map +1 -0
  140. package/dist/telephony/VonageTelephonyRouter.js +201 -0
  141. package/dist/telephony/VonageTelephonyRouter.js.map +1 -0
  142. package/dist/telephony/VonageTelephonyService.d.ts +90 -0
  143. package/dist/telephony/VonageTelephonyService.d.ts.map +1 -0
  144. package/dist/telephony/VonageTelephonyService.js +191 -0
  145. package/dist/telephony/VonageTelephonyService.js.map +1 -0
  146. package/dist/telephony/calendar-scheduler.d.ts +67 -0
  147. package/dist/telephony/calendar-scheduler.d.ts.map +1 -0
  148. package/dist/telephony/calendar-scheduler.js +133 -0
  149. package/dist/telephony/calendar-scheduler.js.map +1 -0
  150. package/dist/telephony/index.d.ts +29 -0
  151. package/dist/telephony/index.d.ts.map +1 -0
  152. package/dist/telephony/index.js +38 -0
  153. package/dist/telephony/index.js.map +1 -0
  154. package/dist/telephony/media-upgrade-router.d.ts +33 -0
  155. package/dist/telephony/media-upgrade-router.d.ts.map +1 -0
  156. package/dist/telephony/media-upgrade-router.js +56 -0
  157. package/dist/telephony/media-upgrade-router.js.map +1 -0
  158. package/dist/telephony/ringcentral-runtime.d.ts +14 -0
  159. package/dist/telephony/ringcentral-runtime.d.ts.map +1 -0
  160. package/dist/telephony/ringcentral-runtime.js +18 -0
  161. package/dist/telephony/ringcentral-runtime.js.map +1 -0
  162. package/dist/telephony/teams-meetings-runtime.d.ts +16 -0
  163. package/dist/telephony/teams-meetings-runtime.d.ts.map +1 -0
  164. package/dist/telephony/teams-meetings-runtime.js +20 -0
  165. package/dist/telephony/teams-meetings-runtime.js.map +1 -0
  166. package/dist/telephony/teamsAcsMediaRegistry.d.ts +69 -0
  167. package/dist/telephony/teamsAcsMediaRegistry.d.ts.map +1 -0
  168. package/dist/telephony/teamsAcsMediaRegistry.js +118 -0
  169. package/dist/telephony/teamsAcsMediaRegistry.js.map +1 -0
  170. package/dist/telephony/telephony-runtime.d.ts +14 -0
  171. package/dist/telephony/telephony-runtime.d.ts.map +1 -0
  172. package/dist/telephony/telephony-runtime.js +18 -0
  173. package/dist/telephony/telephony-runtime.js.map +1 -0
  174. package/dist/telephony/twilioMediaRegistry.d.ts +60 -0
  175. package/dist/telephony/twilioMediaRegistry.d.ts.map +1 -0
  176. package/dist/telephony/twilioMediaRegistry.js +106 -0
  177. package/dist/telephony/twilioMediaRegistry.js.map +1 -0
  178. package/dist/telephony/vonage-runtime.d.ts +14 -0
  179. package/dist/telephony/vonage-runtime.d.ts.map +1 -0
  180. package/dist/telephony/vonage-runtime.js +18 -0
  181. package/dist/telephony/vonage-runtime.js.map +1 -0
  182. package/dist/telephony/vonageMediaRegistry.d.ts +78 -0
  183. package/dist/telephony/vonageMediaRegistry.d.ts.map +1 -0
  184. package/dist/telephony/vonageMediaRegistry.js +158 -0
  185. package/dist/telephony/vonageMediaRegistry.js.map +1 -0
  186. package/package.json +88 -83
  187. package/src/__tests__/search-knowledge-tags.test.ts +9 -9
  188. package/src/__tests__/teams-meetings-service.test.ts +65 -0
  189. package/src/__tests__/teamsAcsMediaRegistry.test.ts +82 -0
  190. package/src/__tests__/twilio-telephony-service.test.ts +23 -0
  191. package/src/__tests__/twilioMediaRegistry.test.ts +103 -0
  192. package/src/__tests__/vonageMediaRegistry.test.ts +180 -0
  193. package/src/__tests__/widget.test.ts +204 -0
  194. package/src/__tests__/widgetGuestElevation.test.ts +57 -0
  195. package/src/__tests__/widgetHostIdentity.test.ts +117 -0
  196. package/src/agentSessions/ReturningVisitorRecap.ts +242 -0
  197. package/src/agentSessions/SessionJanitor.ts +66 -6
  198. package/src/agentSessions/SessionManager.ts +38 -0
  199. package/src/auth/magicLink/MagicLinkRouter.ts +17 -0
  200. package/src/auth/magicLink/index.ts +1 -0
  201. package/src/auth/magicLink/types.ts +26 -0
  202. package/src/config.ts +172 -38
  203. package/src/context.ts +50 -3
  204. package/src/generated/generated.ts +1991 -1
  205. package/src/generic/ResolverBase.ts +28 -0
  206. package/src/generic/RunViewResolver.ts +4 -0
  207. package/src/index.ts +109 -3
  208. package/src/integration/CustomColumnPromoter.ts +6 -0
  209. package/src/realtimeWidget/WidgetRouter.ts +204 -0
  210. package/src/realtimeWidget/WidgetSessionService.ts +714 -0
  211. package/src/realtimeWidget/host-identity.ts +84 -0
  212. package/src/realtimeWidget/index.ts +15 -0
  213. package/src/realtimeWidget/visitorIdentity.ts +258 -0
  214. package/src/realtimeWidget/widgetCore.ts +231 -0
  215. package/src/realtimeWidget/widgetGuestElevation.ts +115 -0
  216. package/src/resolvers/ComponentRegistryResolver.ts +36 -10
  217. package/src/resolvers/EntityPermissionResolver.ts +1 -2
  218. package/src/resolvers/QuerySystemUserResolver.ts +14 -10
  219. package/src/resolvers/RealtimeClientSessionResolver.ts +59 -2
  220. package/src/resolvers/RingCentralTelephonyResolver.ts +64 -0
  221. package/src/resolvers/RunAIAgentResolver.ts +16 -4
  222. package/src/resolvers/SearchKnowledgeResolver.ts +2 -0
  223. package/src/resolvers/TeamsMeetingsResolver.ts +68 -0
  224. package/src/resolvers/TelephonyResolver.ts +63 -0
  225. package/src/resolvers/TestQuerySQLResolver.ts +2 -3
  226. package/src/resolvers/VonageTelephonyResolver.ts +63 -0
  227. package/src/rest/MediaStreamHandler.ts +4 -1
  228. package/src/telephony/RingCentralTelephonyService.ts +342 -0
  229. package/src/telephony/TeamsMeetingsRouter.ts +124 -0
  230. package/src/telephony/TeamsMeetingsService.ts +268 -0
  231. package/src/telephony/TwilioTelephonyRouter.ts +184 -0
  232. package/src/telephony/TwilioTelephonyService.ts +261 -0
  233. package/src/telephony/VonageTelephonyRouter.ts +239 -0
  234. package/src/telephony/VonageTelephonyService.ts +259 -0
  235. package/src/telephony/calendar-scheduler.ts +176 -0
  236. package/src/telephony/index.ts +43 -0
  237. package/src/telephony/media-upgrade-router.ts +62 -0
  238. package/src/telephony/ringcentral-runtime.ts +22 -0
  239. package/src/telephony/teams-meetings-runtime.ts +24 -0
  240. package/src/telephony/teamsAcsMediaRegistry.ts +152 -0
  241. package/src/telephony/telephony-runtime.ts +22 -0
  242. package/src/telephony/twilioMediaRegistry.ts +137 -0
  243. package/src/telephony/vonage-runtime.ts +22 -0
  244. package/src/telephony/vonageMediaRegistry.ts +200 -0
  245. package/dist/agents/skip-agent.d.ts +0 -111
  246. package/dist/agents/skip-agent.d.ts.map +0 -1
  247. package/dist/agents/skip-agent.js +0 -1487
  248. package/dist/agents/skip-agent.js.map +0 -1
  249. package/dist/agents/skip-sdk.d.ts +0 -261
  250. package/dist/agents/skip-sdk.d.ts.map +0 -1
  251. package/dist/agents/skip-sdk.js +0 -909
  252. package/dist/agents/skip-sdk.js.map +0 -1
  253. package/src/agents/skip-agent.ts +0 -1594
  254. package/src/agents/skip-sdk.ts +0 -1210
@@ -1,1594 +0,0 @@
1
- /**
2
- * Skip Proxy Agent
3
- *
4
- * A proxy agent that integrates Skip SaaS API into the MemberJunction agent framework.
5
- * This agent acts as a bridge, allowing Skip to be invoked like any other MJ agent
6
- * while maintaining compatibility with the existing Skip infrastructure.
7
- */
8
-
9
- import { BaseAgent } from "@memberjunction/ai-agents";
10
- import {
11
- ExecuteAgentParams,
12
- AgentConfiguration,
13
- BaseAgentNextStep
14
- } from "@memberjunction/ai-core-plus";
15
- import {
16
- SkipAPIResponse,
17
- SkipAPIAnalysisCompleteResponse,
18
- SkipAPIClarifyingQuestionResponse,
19
- SkipMessage
20
- } from "@memberjunction/skip-types";
21
- import { SkipSDK, SkipCallOptions } from "./skip-sdk.js";
22
- import { DataContext } from "@memberjunction/data-context";
23
- import { LogStatus, LogError, RunView, UserInfo } from "@memberjunction/core";
24
- import { ChatMessage } from "@memberjunction/ai";
25
- import { RegisterClass } from "@memberjunction/global";
26
- import { ComponentSpec } from "@memberjunction/interactive-component-types";
27
-
28
- /**
29
- * Context type for Skip agent execution
30
- */
31
- export interface SkipAgentContext {
32
- /**
33
- * Optional data context ID to load
34
- */
35
- dataContextId?: string;
36
-
37
- /**
38
- * Optional pre-loaded data context
39
- */
40
- dataContext?: DataContext;
41
-
42
- /**
43
- * Conversation ID for tracking Skip conversations
44
- */
45
- conversationId?: string;
46
-
47
- /**
48
- * Force entity metadata refresh
49
- */
50
- forceEntityRefresh?: boolean;
51
-
52
- /**
53
- * Database connection (injected by caller)
54
- */
55
- dataSource?: any;
56
- }
57
-
58
- /**
59
- * Payload returned from Skip agent execution
60
- * Contains the full Skip API response for downstream consumers
61
- */
62
- export interface SkipAgentPayload {
63
- /**
64
- * The full Skip API response
65
- */
66
- skipResponse: SkipAPIResponse;
67
-
68
- /**
69
- * Response phase from Skip
70
- */
71
- responsePhase: string;
72
-
73
- /**
74
- * Conversation ID
75
- */
76
- conversationId: string;
77
-
78
- /**
79
- * User-facing message (title or clarifying question)
80
- */
81
- message?: string;
82
- }
83
-
84
- /**
85
- * Skip Proxy Agent
86
- *
87
- * This agent provides a simple proxy to the Skip SaaS API, allowing Skip to be
88
- * invoked through the standard MJ agent framework. It handles:
89
- * - Converting MJ conversation messages to Skip format
90
- * - Streaming progress updates from Skip
91
- * - Mapping Skip responses to MJ agent next steps
92
- * - Returning full Skip responses in the payload
93
- */
94
- @RegisterClass(BaseAgent, 'SkipProxyAgent')
95
- export class SkipProxyAgent extends BaseAgent {
96
- private skipSDK: SkipSDK;
97
-
98
- constructor() {
99
- super();
100
- this.skipSDK = new SkipSDK();
101
- }
102
-
103
- /**
104
- * Execute the Skip agent - proxies to Skip SaaS API
105
- */
106
- protected override async executeAgentInternal<P = SkipAgentPayload>(
107
- params: ExecuteAgentParams<SkipAgentContext, P>,
108
- config: AgentConfiguration
109
- ): Promise<{ finalStep: BaseAgentNextStep<P>; stepCount: number; }> {
110
-
111
- LogStatus(`[SkipProxyAgent] Starting Skip agent execution`);
112
-
113
- // Extract context
114
- const context = params.context || {} as SkipAgentContext;
115
- const conversationId = params.data?.conversationId;
116
-
117
- if (!params.contextUser) {
118
- LogError('[SkipProxyAgent] contextUser is required');
119
- return {
120
- finalStep: {
121
- terminate: true,
122
- step: 'Failed',
123
- message: 'Missing required contextUser',
124
- errorMessage: 'Missing required contextUser'
125
- } as BaseAgentNextStep<P>,
126
- stepCount: 1
127
- };
128
- }
129
-
130
- // Load conversation messages from database if conversationId is provided
131
- // This ensures we get real UUIDs from MJConversationDetailEntity records
132
- let skipMessages: SkipMessage[];
133
- if (conversationId && params.contextUser) {
134
- skipMessages = await this.loadMessagesFromDatabase(conversationId, params.contextUser);
135
- } else {
136
- // Fallback to converting provided conversation messages
137
- skipMessages = this.convertMessagesToSkipFormat(params.conversationMessages || []);
138
- }
139
-
140
- // Prepare Skip SDK call options
141
- const skipOptions: SkipCallOptions = {
142
- payload: params.payload,
143
- messages: skipMessages,
144
- conversationId,
145
- dataContext: context.dataContext,
146
- requestPhase: 'initial_request', // Could be parameterized if needed
147
- contextUser: params.contextUser,
148
- dataSource: context.dataSource,
149
- includeEntities: true,
150
- includeQueries: true,
151
- includeNotes: true,
152
- includeRequests: false,
153
- forceEntityRefresh: context.forceEntityRefresh || false,
154
- includeCallbackAuth: true,
155
- externalReferenceID: this.AgentRun?.ID ?? undefined,
156
- onStatusUpdate: (message: string, responsePhase?: string) => {
157
- // Forward Skip status updates to MJ progress callback
158
- if (params.onProgress) {
159
- params.onProgress({
160
- step: 'prompt_execution', // Skip execution is essentially a prompt to an external service
161
- message,
162
- percentage: 0, // Skip doesn't provide percentage
163
- metadata: {
164
- conversationId,
165
- responsePhase
166
- }
167
- });
168
- }
169
- }
170
- };
171
-
172
- // Call Skip API
173
- const result = await this.skipSDK.chat(skipOptions);
174
-
175
- // Handle Skip API errors — surface the actual error message, not a generic wrapper
176
- if (!result.success || !result.response) {
177
- const errorMsg = result.error || 'No response received from Skip API';
178
- LogError(`[SkipProxyAgent] Skip API call failed: ${errorMsg}`);
179
- return {
180
- finalStep: {
181
- terminate: true,
182
- step: 'Failed',
183
- message: errorMsg,
184
- errorMessage: errorMsg
185
- } as BaseAgentNextStep<P>,
186
- stepCount: 1
187
- };
188
- }
189
-
190
- // Map Skip response to MJ agent next step
191
- const nextStep = this.mapSkipResponseToNextStep(result.response, conversationId);
192
-
193
- LogStatus(`[SkipProxyAgent] Skip execution completed with phase: ${result.responsePhase}`);
194
-
195
- return {
196
- finalStep: nextStep as BaseAgentNextStep<P>,
197
- stepCount: 1 // Skip is a single-step proxy
198
- };
199
- }
200
-
201
- /**
202
- * Load conversation messages from database with real UUIDs using MemberJunction's RunView pattern
203
- * This is the preferred method as it ensures all messages have proper conversationDetailIDs
204
- */
205
- private async loadMessagesFromDatabase(conversationId: string, contextUser: UserInfo): Promise<SkipMessage[]> {
206
- try {
207
- const rv = new RunView();
208
- const result = await rv.RunView({
209
- EntityName: 'MJ: Conversation Details',
210
- ExtraFilter: `ConversationID='${conversationId}'`,
211
- OrderBy: '__mj_CreatedAt ASC'
212
- }, contextUser);
213
-
214
- if (!result.Success) {
215
- throw new Error(`Failed to load conversation details: ${result.ErrorMessage}`);
216
- }
217
-
218
- const allMessages = (result.Results || []).map((r: any) => {
219
- // Map database role to Skip role
220
- const dbRole = (r.Role || '').trim().toLowerCase();
221
- const skipRole: 'user' | 'system' =
222
- (dbRole === 'ai' || dbRole === 'system' || dbRole === 'assistant') ? 'system' : 'user';
223
-
224
- // For system messages, always send the raw Message from database
225
- // Skip Brain needs the full JSON response to extract component specs for modification
226
- // For user messages, use the message as-is
227
- const content = r.Message;
228
-
229
- const message: SkipMessage = {
230
- content: content,
231
- role: skipRole,
232
- conversationDetailID: r.ID,
233
- hiddenToUser: r.HiddenToUser,
234
- userRating: r.UserRating,
235
- userFeedback: r.UserFeedback,
236
- reflectionInsights: r.ReflectionInsights,
237
- summaryOfEarlierConveration: r.SummaryOfEarlierConversation,
238
- createdAt: r.__mj_CreatedAt,
239
- updatedAt: r.__mj_UpdatedAt,
240
- };
241
- return message;
242
- });
243
-
244
- // Find the index of the last user message
245
- // We only want to include messages up to and including the most recent user message
246
- // This filters out status messages and incomplete AI responses
247
- const lastUserMessageIndex = allMessages.reduce((lastIndex, msg, currentIndex) => {
248
- return msg.role === 'user' ? currentIndex : lastIndex;
249
- }, -1);
250
-
251
- if (lastUserMessageIndex === -1) {
252
- // No user messages found, return all messages (shouldn't happen in practice)
253
- return allMessages;
254
- }
255
-
256
- // Return messages up to and including the last user message
257
- return allMessages.slice(0, lastUserMessageIndex + 1);
258
- } catch (error) {
259
- LogError(`[SkipProxyAgent] Error loading messages from database: ${error}`);
260
- throw error;
261
- }
262
- }
263
-
264
- /**
265
- * Convert MJ ChatMessage format to Skip SkipMessage format
266
- * This is a fallback method when database loading is not available
267
- */
268
- private convertMessagesToSkipFormat(messages: ChatMessage[]): SkipMessage[] {
269
- return messages.map((msg, index) => {
270
- // Extract conversationDetailID from metadata if available
271
- const conversationDetailID = msg.metadata?.conversationDetailID || `temp-${index}`;
272
-
273
- return {
274
- // Skip only accepts 'user' or 'system' roles, map 'assistant' to 'system'
275
- role: (msg.role === 'assistant' ? 'system' : msg.role) as 'user' | 'system',
276
- content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
277
- conversationDetailID,
278
- // Include other SkipMessage fields from metadata if available
279
- hiddenToUser: msg.metadata?.hiddenToUser,
280
- userRating: msg.metadata?.userRating,
281
- userFeedback: msg.metadata?.userFeedback,
282
- reflectionInsights: msg.metadata?.reflectionInsights,
283
- summaryOfEarlierConveration: msg.metadata?.summaryOfEarlierConversation,
284
- createdAt: msg.metadata?.createdAt,
285
- updatedAt: msg.metadata?.updatedAt
286
- };
287
- });
288
- }
289
-
290
- /**
291
- * Map Skip API response to MJ agent next step.
292
- *
293
- * Checks for Skip-level errors first, then delegates to phase-specific handlers.
294
- */
295
- private mapSkipResponseToNextStep(
296
- apiResponse: SkipAPIResponse,
297
- conversationId: string
298
- ): BaseAgentNextStep<ComponentSpec> {
299
- // Check if Skip reported an error (success: false with any responsePhase)
300
- if (!apiResponse.success) {
301
- return this.handleSkipError(apiResponse);
302
- }
303
-
304
- switch (apiResponse.responsePhase) {
305
- case 'analysis_complete':
306
- return this.handleAnalysisComplete(apiResponse as SkipAPIAnalysisCompleteResponse);
307
-
308
- case 'clarifying_question':
309
- return this.handleClarifyingQuestion(apiResponse as SkipAPIClarifyingQuestionResponse);
310
-
311
- default: {
312
- const msg = `Unexpected Skip response phase: ${apiResponse.responsePhase}`;
313
- LogError(`[SkipProxyAgent] ${msg}`);
314
- return {
315
- terminate: true,
316
- step: 'Failed',
317
- message: msg,
318
- errorMessage: msg,
319
- newPayload: undefined
320
- };
321
- }
322
- }
323
- }
324
-
325
- /**
326
- * Handle Skip error responses — extracts the most descriptive error available
327
- */
328
- private handleSkipError(apiResponse: SkipAPIResponse): BaseAgentNextStep<ComponentSpec> {
329
- // Try to get a meaningful error: explicit error field, last system message, or fallback
330
- const lastSystemMessage = apiResponse.messages
331
- ?.filter(m => m.role === 'system')
332
- .pop();
333
-
334
- const errorDetail = apiResponse.error
335
- || lastSystemMessage?.content
336
- || 'Skip returned an error with no details';
337
-
338
- LogError(`[SkipProxyAgent] Skip error (phase: ${apiResponse.responsePhase}): ${errorDetail}`);
339
-
340
- return {
341
- terminate: true,
342
- step: 'Failed',
343
- message: errorDetail,
344
- errorMessage: errorDetail,
345
- newPayload: undefined
346
- };
347
- }
348
-
349
- /**
350
- * Handle analysis_complete phase — supports both component-bearing responses
351
- * (componentOptions populated) and analysis-only responses (markdown in `analysis`
352
- * with no component, produced by Skip's Analysis Agent path).
353
- */
354
- private handleAnalysisComplete(
355
- response: SkipAPIAnalysisCompleteResponse
356
- ): BaseAgentNextStep<ComponentSpec> {
357
- const hasComponentOptions = response.componentOptions && response.componentOptions.length > 0;
358
- const skipMessage = response.messages?.filter(msg => msg.role === 'system').pop();
359
- const analysisText = response.analysis?.trim();
360
-
361
- // Skip-Brain attaches actionableCommands as an extension field on the
362
- // analysis_complete response (e.g. `client:capture-data-snapshot` when
363
- // the Analysis Agent needs the user's current view). Forward to the
364
- // next step so AgentRunner persists them to ConversationDetail.ActionableCommands
365
- // — which the chat UI's actionable-commands.component reads to render buttons.
366
- const responseExt = response as unknown as { actionableCommands?: unknown[] };
367
- const actionableCommands = Array.isArray(responseExt.actionableCommands) && responseExt.actionableCommands.length > 0
368
- ? (responseExt.actionableCommands as BaseAgentNextStep<ComponentSpec>['actionableCommands'])
369
- : undefined;
370
- LogStatus(`[SkipProxyAgent DEBUG] handleAnalysisComplete actionableCommands from response: ${JSON.stringify(responseExt.actionableCommands)}`);
371
-
372
- if (!hasComponentOptions) {
373
- if (analysisText || skipMessage?.content) {
374
- return {
375
- terminate: true,
376
- step: 'Success',
377
- message: analysisText || skipMessage!.content,
378
- newPayload: undefined,
379
- actionableCommands
380
- };
381
- }
382
-
383
- const msg = 'Skip completed analysis but returned no component options. '
384
- + `Title: "${response.title || 'none'}". `
385
- + `Result type: "${response.resultType || 'none'}"`;
386
- LogError(`[SkipProxyAgent] ${msg}`);
387
- return {
388
- terminate: true,
389
- step: 'Failed',
390
- message: msg,
391
- errorMessage: msg,
392
- newPayload: undefined
393
- };
394
- }
395
-
396
- const componentSpec = response.componentOptions![0].option;
397
-
398
- return {
399
- terminate: true,
400
- step: 'Success',
401
- message: skipMessage?.content || response.title || 'Analysis complete',
402
- newPayload: componentSpec,
403
- actionableCommands
404
- };
405
- }
406
-
407
- /**
408
- * Handle clarifying_question phase
409
- */
410
- private handleClarifyingQuestion(
411
- response: SkipAPIClarifyingQuestionResponse
412
- ): BaseAgentNextStep<ComponentSpec> {
413
- return {
414
- terminate: true,
415
- step: 'Chat',
416
- message: response.clarifyingQuestion,
417
- responseForm: response.responseForm,
418
- newPayload: response.payload as ComponentSpec
419
- };
420
- }
421
-
422
- private tempHack(): BaseAgentNextStep<SkipAgentPayload> {
423
- return {
424
- terminate: true,
425
- step: 'Success',
426
- message: "Demo Report (not real)",
427
- newPayload: demoSpecJson as unknown as SkipAgentPayload
428
- };
429
- }
430
- }
431
-
432
- const demoSpecJson = {
433
- "name": "EntityBrowser",
434
- "title": "Entity Browser",
435
- "description": "A comprehensive entity browser with multi-panel display showing entities in a grid or card view with a sliding details panel, collapsible filters, sorting, and entity record opening capability.",
436
- "type": "dashboard",
437
- "functionalRequirements": "## Entity Browser Requirements\n\n### Core Functionality\n- Display entities in a responsive grid or card layout based on user preference\n- Allow users to select view mode (grid vs card)\n- Click on an entity to slide in a details panel from the right\n- Show entity metadata including fields and relationships in the details panel\n- Provide a collapsible filter panel on the left side\n- Support sorting by multiple fields with visual indicators\n- Include a search bar for quick entity filtering\n- Provide an 'Open' button to trigger the OpenEntityRecord callback\n- Remember user's last selected entity and view preferences\n\n### UX Considerations\n- Smooth animations for panel transitions\n- Responsive design that works on different screen sizes\n- Loading states while fetching data\n- Empty states with helpful messages\n- Keyboard navigation support (arrow keys, tab, enter)\n- Visual feedback for hover and selection states\n- Maintain scroll position when switching between entities",
438
- "dataRequirements": {
439
- "mode": "views",
440
- "entities": [
441
- {
442
- "name": "Entities",
443
- "description": "Metadata about all entities in the system",
444
- "displayFields": [
445
- "ID",
446
- "Name",
447
- "DisplayName",
448
- "NameSuffix",
449
- "Description",
450
- "SchemaName",
451
- "BaseTable",
452
- "BaseView"
453
- ],
454
- "filterFields": [
455
- "SchemaName",
456
- "BaseTable"
457
- ],
458
- "sortFields": [
459
- "Name",
460
- "DisplayName"
461
- ],
462
- "fieldMetadata": [
463
- {
464
- "name": "ID",
465
- "sequence": 1,
466
- "defaultInView": false,
467
- "type": "uniqueidentifier",
468
- "allowsNull": false,
469
- "isPrimaryKey": true,
470
- "description": "Unique identifier for the entity"
471
- },
472
- {
473
- "name": "Name",
474
- "sequence": 2,
475
- "defaultInView": true,
476
- "type": "nvarchar",
477
- "allowsNull": false,
478
- "isPrimaryKey": false,
479
- "description": "System name of the entity"
480
- },
481
- {
482
- "name": "DisplayName",
483
- "sequence": 3,
484
- "defaultInView": true,
485
- "type": "nvarchar",
486
- "allowsNull": true,
487
- "isPrimaryKey": false,
488
- "description": "User-friendly display name for the entity"
489
- },
490
- {
491
- "name": "NameSuffix",
492
- "sequence": 4,
493
- "defaultInView": true,
494
- "type": "nvarchar",
495
- "allowsNull": true,
496
- "isPrimaryKey": false,
497
- "description": "Optional suffix appended to entity names for display purposes"
498
- },
499
- {
500
- "name": "Description",
501
- "sequence": 5,
502
- "defaultInView": true,
503
- "type": "nvarchar",
504
- "allowsNull": true,
505
- "isPrimaryKey": false,
506
- "description": "Description of the entity"
507
- },
508
- {
509
- "name": "SchemaName",
510
- "sequence": 6,
511
- "defaultInView": true,
512
- "type": "nvarchar",
513
- "allowsNull": true,
514
- "isPrimaryKey": false,
515
- "description": "Database schema name"
516
- },
517
- {
518
- "name": "BaseTable",
519
- "sequence": 7,
520
- "defaultInView": true,
521
- "type": "nvarchar",
522
- "allowsNull": true,
523
- "isPrimaryKey": false,
524
- "description": "Base table in the database"
525
- },
526
- {
527
- "name": "BaseView",
528
- "sequence": 8,
529
- "defaultInView": true,
530
- "type": "nvarchar",
531
- "allowsNull": false,
532
- "isPrimaryKey": false,
533
- "description": "Base view used for the entity"
534
- }
535
- ],
536
- "permissionLevelNeeded": [
537
- "read"
538
- ],
539
- "usageContext": "Main entity list display and filtering"
540
- },
541
- {
542
- "name": "MJ: Entity Fields",
543
- "description": "Fields belonging to each entity",
544
- "displayFields": [
545
- "Name",
546
- "DisplayName",
547
- "Type",
548
- "Length",
549
- "AllowsNull",
550
- "IsPrimaryKey",
551
- "IsUnique"
552
- ],
553
- "filterFields": [
554
- "EntityID"
555
- ],
556
- "sortFields": [
557
- "Sequence",
558
- "Name"
559
- ],
560
- "fieldMetadata": [
561
- {
562
- "name": "EntityID",
563
- "sequence": 1,
564
- "defaultInView": false,
565
- "type": "uniqueidentifier",
566
- "allowsNull": false,
567
- "isPrimaryKey": false,
568
- "description": "Reference to parent entity"
569
- },
570
- {
571
- "name": "Name",
572
- "sequence": 2,
573
- "defaultInView": true,
574
- "type": "nvarchar",
575
- "allowsNull": false,
576
- "isPrimaryKey": false,
577
- "description": "Field name"
578
- },
579
- {
580
- "name": "DisplayName",
581
- "sequence": 3,
582
- "defaultInView": true,
583
- "type": "nvarchar",
584
- "allowsNull": true,
585
- "isPrimaryKey": false,
586
- "description": "User-friendly field name"
587
- },
588
- {
589
- "name": "Type",
590
- "sequence": 4,
591
- "defaultInView": true,
592
- "type": "nvarchar",
593
- "allowsNull": false,
594
- "isPrimaryKey": false,
595
- "description": "Data type of the field"
596
- },
597
- {
598
- "name": "Length",
599
- "sequence": 5,
600
- "defaultInView": true,
601
- "type": "int",
602
- "allowsNull": true,
603
- "isPrimaryKey": false,
604
- "description": "Maximum length for string fields"
605
- },
606
- {
607
- "name": "AllowsNull",
608
- "sequence": 6,
609
- "defaultInView": true,
610
- "type": "bit",
611
- "allowsNull": false,
612
- "isPrimaryKey": false,
613
- "description": "Whether field allows null values"
614
- },
615
- {
616
- "name": "IsPrimaryKey",
617
- "sequence": 7,
618
- "defaultInView": true,
619
- "type": "bit",
620
- "allowsNull": false,
621
- "isPrimaryKey": false,
622
- "description": "Whether field is part of primary key"
623
- },
624
- {
625
- "name": "IsUnique",
626
- "sequence": 8,
627
- "defaultInView": true,
628
- "type": "bit",
629
- "allowsNull": false,
630
- "isPrimaryKey": false,
631
- "description": "Whether field must be unique"
632
- },
633
- {
634
- "name": "Sequence",
635
- "sequence": 9,
636
- "defaultInView": false,
637
- "type": "int",
638
- "allowsNull": false,
639
- "isPrimaryKey": false,
640
- "description": "Display order of the field"
641
- }
642
- ],
643
- "permissionLevelNeeded": [
644
- "read"
645
- ],
646
- "usageContext": "Details panel to show entity fields"
647
- },
648
- {
649
- "name": "MJ: Entity Relationships",
650
- "description": "Relationships between entities",
651
- "displayFields": [
652
- "RelatedEntity",
653
- "Type",
654
- "DisplayName",
655
- "RelatedEntityJoinField"
656
- ],
657
- "filterFields": [
658
- "EntityID"
659
- ],
660
- "sortFields": [
661
- "Sequence",
662
- "RelatedEntity"
663
- ],
664
- "fieldMetadata": [
665
- {
666
- "name": "EntityID",
667
- "sequence": 1,
668
- "defaultInView": false,
669
- "type": "uniqueidentifier",
670
- "allowsNull": false,
671
- "isPrimaryKey": false,
672
- "description": "Reference to parent entity"
673
- },
674
- {
675
- "name": "RelatedEntity",
676
- "sequence": 2,
677
- "defaultInView": true,
678
- "type": "nvarchar",
679
- "allowsNull": false,
680
- "isPrimaryKey": false,
681
- "description": "The related entity in the relationship"
682
- },
683
- {
684
- "name": "Type",
685
- "sequence": 3,
686
- "defaultInView": true,
687
- "type": "nvarchar",
688
- "allowsNull": false,
689
- "isPrimaryKey": false,
690
- "description": "Type of relationship (One to Many, Many to One, etc.)"
691
- },
692
- {
693
- "name": "DisplayName",
694
- "sequence": 4,
695
- "defaultInView": true,
696
- "type": "nvarchar",
697
- "allowsNull": true,
698
- "isPrimaryKey": false,
699
- "description": "User-friendly name for the relationship"
700
- },
701
- {
702
- "name": "RelatedEntityJoinField",
703
- "sequence": 5,
704
- "defaultInView": true,
705
- "type": "nvarchar",
706
- "allowsNull": true,
707
- "isPrimaryKey": false,
708
- "description": "The field in the related entity that joins to this entity"
709
- },
710
- {
711
- "name": "Sequence",
712
- "sequence": 6,
713
- "defaultInView": false,
714
- "type": "int",
715
- "allowsNull": false,
716
- "isPrimaryKey": false,
717
- "description": "Display order"
718
- }
719
- ],
720
- "permissionLevelNeeded": [
721
- "read"
722
- ],
723
- "usageContext": "Details panel to show entity relationships"
724
- }
725
- ],
726
- "queries": [],
727
- "description": "This component requires access to entity metadata including entities, their fields, and relationships to provide a comprehensive entity browsing experience"
728
- },
729
- "technicalDesign": "## Technical Architecture\n\n### Component Structure\n- **Root Component (EntityBrowser)**: Manages overall layout and state coordination\n- **EntityList (Child)**: Displays entities in grid/card view with sorting\n- **EntityDetails (Child)**: Sliding panel showing entity fields and relationships\n- **EntityFilter (Child)**: Collapsible filter panel with dynamic filters\n\n### State Management\n- Selected entity ID (persisted in savedUserSettings)\n- View mode (grid/card) (persisted)\n- Active filters (persisted)\n- Sort configuration (persisted)\n- Panel visibility states (details open, filters collapsed)\n- Search query\n- Loading states for async operations\n\n### Layout\n```\n+------------------+------------------------+------------------+\n| | | |\n| Filter Panel | Entity Grid/Cards | Details Panel |\n| (Collapsible) | (Main Content) | (Sliding) |\n| | | |\n| [Schema Filter] | +-----+ +-----+ | Entity: Orders |\n| [Table Filter] | | Card | | Card | | |\n| [Search Box] | +-----+ +-----+ | Fields: |\n| | | - ID |\n| Sort By: | +-----+ +-----+ | - CustomerID |\n| [Name ↓] | | Card | | Card | | - OrderDate |\n| | +-----+ +-----+ | |\n| | | Relationships: |\n| | | → Customers |\n| | | → OrderItems |\n| | | |\n| | | [Open Record] |\n+------------------+------------------------+------------------+\n```\n\n### Data Flow\n1. Root component loads entities on mount\n2. Passes entity data to EntityList\n3. EntityList handles selection and passes selectedId up\n4. Root loads fields/relationships for selected entity\n5. Passes detailed data to EntityDetails\n6. Filter changes trigger data reload\n7. All user preferences saved via onSaveUserSettings\n\n### Interaction Patterns\n- Click entity card → Select and open details\n- Click filter → Apply and reload data\n- Click sort → Update sort and reload\n- Click 'Open' → Trigger OpenEntityRecord callback\n- Press Escape → Close details panel\n- Click outside → Close details panel",
730
- "properties": [],
731
- "events": [],
732
- "exampleUsage": "<EntityBrowser\n utilities={utilities}\n styles={styles}\n components={components}\n callbacks={callbacks}\n savedUserSettings={savedUserSettings}\n onSaveUserSettings={onSaveUserSettings}\n/>",
733
- "dependencies": [
734
- {
735
- "name": "EntityList",
736
- "title": "Entity List",
737
- "description": "Displays entities in a grid or card layout with sorting capabilities",
738
- "type": "table",
739
- "functionalRequirements": "## Entity List Requirements\n\n- Display entities in grid or card view based on viewMode prop\n- Support sorting by multiple fields\n- Handle entity selection and notify parent\n- Show loading state while data loads\n- Display record count badges\n- Highlight selected entity\n- Support keyboard navigation",
740
- "dataRequirements": {
741
- "mode": "views",
742
- "entities": [
743
- {
744
- "name": "Entities",
745
- "description": "Metadata about all entities in the system",
746
- "displayFields": [
747
- "ID",
748
- "Name",
749
- "DisplayName",
750
- "NameSuffix",
751
- "Description",
752
- "SchemaName",
753
- "BaseTable",
754
- "BaseView"
755
- ],
756
- "filterFields": [
757
- "SchemaName",
758
- "BaseTable"
759
- ],
760
- "sortFields": [
761
- "Name",
762
- "DisplayName"
763
- ],
764
- "fieldMetadata": [
765
- {
766
- "name": "ID",
767
- "sequence": 1,
768
- "defaultInView": false,
769
- "type": "uniqueidentifier",
770
- "allowsNull": false,
771
- "isPrimaryKey": true,
772
- "description": "Unique identifier for the entity"
773
- },
774
- {
775
- "name": "Name",
776
- "sequence": 2,
777
- "defaultInView": true,
778
- "type": "nvarchar",
779
- "allowsNull": false,
780
- "isPrimaryKey": false,
781
- "description": "System name of the entity"
782
- },
783
- {
784
- "name": "DisplayName",
785
- "sequence": 3,
786
- "defaultInView": true,
787
- "type": "nvarchar",
788
- "allowsNull": true,
789
- "isPrimaryKey": false,
790
- "description": "User-friendly display name for the entity"
791
- },
792
- {
793
- "name": "NameSuffix",
794
- "sequence": 4,
795
- "defaultInView": true,
796
- "type": "nvarchar",
797
- "allowsNull": true,
798
- "isPrimaryKey": false,
799
- "description": "Optional suffix appended to entity names for display purposes"
800
- },
801
- {
802
- "name": "Description",
803
- "sequence": 5,
804
- "defaultInView": true,
805
- "type": "nvarchar",
806
- "allowsNull": true,
807
- "isPrimaryKey": false,
808
- "description": "Description of the entity"
809
- },
810
- {
811
- "name": "SchemaName",
812
- "sequence": 6,
813
- "defaultInView": true,
814
- "type": "nvarchar",
815
- "allowsNull": true,
816
- "isPrimaryKey": false,
817
- "description": "Database schema name"
818
- },
819
- {
820
- "name": "BaseTable",
821
- "sequence": 7,
822
- "defaultInView": true,
823
- "type": "nvarchar",
824
- "allowsNull": true,
825
- "isPrimaryKey": false,
826
- "description": "Base table in the database"
827
- },
828
- {
829
- "name": "BaseView",
830
- "sequence": 8,
831
- "defaultInView": true,
832
- "type": "nvarchar",
833
- "allowsNull": false,
834
- "isPrimaryKey": false,
835
- "description": "Base view used for the entity"
836
- }
837
- ],
838
- "permissionLevelNeeded": [
839
- "read"
840
- ],
841
- "usageContext": "Main entity list display and filtering"
842
- },
843
- {
844
- "name": "MJ: Entity Fields",
845
- "description": "Fields belonging to each entity",
846
- "displayFields": [
847
- "Name",
848
- "DisplayName",
849
- "Type",
850
- "Length",
851
- "AllowsNull",
852
- "IsPrimaryKey",
853
- "IsUnique"
854
- ],
855
- "filterFields": [
856
- "EntityID"
857
- ],
858
- "sortFields": [
859
- "Sequence",
860
- "Name"
861
- ],
862
- "fieldMetadata": [
863
- {
864
- "name": "EntityID",
865
- "sequence": 1,
866
- "defaultInView": false,
867
- "type": "uniqueidentifier",
868
- "allowsNull": false,
869
- "isPrimaryKey": false,
870
- "description": "Reference to parent entity"
871
- },
872
- {
873
- "name": "Name",
874
- "sequence": 2,
875
- "defaultInView": true,
876
- "type": "nvarchar",
877
- "allowsNull": false,
878
- "isPrimaryKey": false,
879
- "description": "Field name"
880
- },
881
- {
882
- "name": "DisplayName",
883
- "sequence": 3,
884
- "defaultInView": true,
885
- "type": "nvarchar",
886
- "allowsNull": true,
887
- "isPrimaryKey": false,
888
- "description": "User-friendly field name"
889
- },
890
- {
891
- "name": "Type",
892
- "sequence": 4,
893
- "defaultInView": true,
894
- "type": "nvarchar",
895
- "allowsNull": false,
896
- "isPrimaryKey": false,
897
- "description": "Data type of the field"
898
- },
899
- {
900
- "name": "Length",
901
- "sequence": 5,
902
- "defaultInView": true,
903
- "type": "int",
904
- "allowsNull": true,
905
- "isPrimaryKey": false,
906
- "description": "Maximum length for string fields"
907
- },
908
- {
909
- "name": "AllowsNull",
910
- "sequence": 6,
911
- "defaultInView": true,
912
- "type": "bit",
913
- "allowsNull": false,
914
- "isPrimaryKey": false,
915
- "description": "Whether field allows null values"
916
- },
917
- {
918
- "name": "IsPrimaryKey",
919
- "sequence": 7,
920
- "defaultInView": true,
921
- "type": "bit",
922
- "allowsNull": false,
923
- "isPrimaryKey": false,
924
- "description": "Whether field is part of primary key"
925
- },
926
- {
927
- "name": "IsUnique",
928
- "sequence": 8,
929
- "defaultInView": true,
930
- "type": "bit",
931
- "allowsNull": false,
932
- "isPrimaryKey": false,
933
- "description": "Whether field must be unique"
934
- },
935
- {
936
- "name": "Sequence",
937
- "sequence": 9,
938
- "defaultInView": false,
939
- "type": "int",
940
- "allowsNull": false,
941
- "isPrimaryKey": false,
942
- "description": "Display order of the field"
943
- }
944
- ],
945
- "permissionLevelNeeded": [
946
- "read"
947
- ],
948
- "usageContext": "Details panel to show entity fields"
949
- },
950
- {
951
- "name": "MJ: Entity Relationships",
952
- "description": "Relationships between entities",
953
- "displayFields": [
954
- "RelatedEntity",
955
- "Type",
956
- "DisplayName",
957
- "RelatedEntityJoinField"
958
- ],
959
- "filterFields": [
960
- "EntityID"
961
- ],
962
- "sortFields": [
963
- "Sequence",
964
- "RelatedEntity"
965
- ],
966
- "fieldMetadata": [
967
- {
968
- "name": "EntityID",
969
- "sequence": 1,
970
- "defaultInView": false,
971
- "type": "uniqueidentifier",
972
- "allowsNull": false,
973
- "isPrimaryKey": false,
974
- "description": "Reference to parent entity"
975
- },
976
- {
977
- "name": "RelatedEntity",
978
- "sequence": 2,
979
- "defaultInView": true,
980
- "type": "nvarchar",
981
- "allowsNull": false,
982
- "isPrimaryKey": false,
983
- "description": "The related entity in the relationship"
984
- },
985
- {
986
- "name": "Type",
987
- "sequence": 3,
988
- "defaultInView": true,
989
- "type": "nvarchar",
990
- "allowsNull": false,
991
- "isPrimaryKey": false,
992
- "description": "Type of relationship (One to Many, Many to One, etc.)"
993
- },
994
- {
995
- "name": "DisplayName",
996
- "sequence": 4,
997
- "defaultInView": true,
998
- "type": "nvarchar",
999
- "allowsNull": true,
1000
- "isPrimaryKey": false,
1001
- "description": "User-friendly name for the relationship"
1002
- },
1003
- {
1004
- "name": "RelatedEntityJoinField",
1005
- "sequence": 5,
1006
- "defaultInView": true,
1007
- "type": "nvarchar",
1008
- "allowsNull": true,
1009
- "isPrimaryKey": false,
1010
- "description": "The field in the related entity that joins to this entity"
1011
- },
1012
- {
1013
- "name": "Sequence",
1014
- "sequence": 6,
1015
- "defaultInView": false,
1016
- "type": "int",
1017
- "allowsNull": false,
1018
- "isPrimaryKey": false,
1019
- "description": "Display order"
1020
- }
1021
- ],
1022
- "permissionLevelNeeded": [
1023
- "read"
1024
- ],
1025
- "usageContext": "Details panel to show entity relationships"
1026
- }
1027
- ],
1028
- "queries": [],
1029
- "description": "This component requires access to entity metadata including entities, their fields, and relationships to provide a comprehensive entity browsing experience"
1030
- },
1031
- "technicalDesign": "## Technical Design\n\n### Props\n- entities: Array of entity objects\n- viewMode: 'grid' | 'card'\n- selectedEntityId: Currently selected entity\n- onSelectEntity: Callback when entity selected\n- sortBy: Current sort field\n- sortDirection: 'asc' | 'desc'\n- onSortChange: Callback for sort changes\n\n### Rendering\n- Grid mode: Compact table with columns\n- Card mode: Cards with entity info\n- Sort indicators in headers\n- Selection highlighting",
1032
- "properties": [
1033
- {
1034
- "name": "entities",
1035
- "description": "Array of entity objects to display",
1036
- "type": "array",
1037
- "required": true
1038
- },
1039
- {
1040
- "name": "viewMode",
1041
- "description": "Display mode - grid or card view",
1042
- "type": "string",
1043
- "required": true,
1044
- "possibleValues": [
1045
- "grid",
1046
- "card"
1047
- ]
1048
- },
1049
- {
1050
- "name": "selectedEntityId",
1051
- "description": "ID of the currently selected entity",
1052
- "type": "string",
1053
- "required": true
1054
- },
1055
- {
1056
- "name": "onSelectEntity",
1057
- "description": "Callback when an entity is selected",
1058
- "type": "function",
1059
- "required": true
1060
- },
1061
- {
1062
- "name": "sortBy",
1063
- "description": "Field to sort by",
1064
- "type": "string",
1065
- "required": true,
1066
- "defaultValue": "Name"
1067
- },
1068
- {
1069
- "name": "sortDirection",
1070
- "description": "Sort direction",
1071
- "type": "string",
1072
- "required": true,
1073
- "defaultValue": "asc",
1074
- "possibleValues": [
1075
- "asc",
1076
- "desc"
1077
- ]
1078
- },
1079
- {
1080
- "name": "onSortChange",
1081
- "description": "Callback when sort changes",
1082
- "type": "function",
1083
- "required": true
1084
- }
1085
- ],
1086
- "events": [
1087
- {
1088
- "name": "onSelectEntity",
1089
- "description": "Fired when an entity is selected",
1090
- "parameters": [
1091
- {
1092
- "name": "entityId",
1093
- "description": "ID of the selected entity",
1094
- "type": "string"
1095
- }
1096
- ]
1097
- },
1098
- {
1099
- "name": "onSortChange",
1100
- "description": "Fired when sort configuration changes",
1101
- "parameters": [
1102
- {
1103
- "name": "sortBy",
1104
- "description": "Field to sort by",
1105
- "type": "string"
1106
- },
1107
- {
1108
- "name": "sortDirection",
1109
- "description": "Sort direction",
1110
- "type": "string"
1111
- }
1112
- ]
1113
- }
1114
- ],
1115
- "exampleUsage": "<EntityList\n entities={entities}\n viewMode={viewMode}\n selectedEntityId={selectedEntityId}\n onSelectEntity={handleSelectEntity}\n sortBy={sortBy}\n sortDirection={sortDirection}\n onSortChange={handleSortChange}\n utilities={utilities}\n styles={styles}\n components={components}\n callbacks={callbacks}\n/>",
1116
- "code": "function EntityList({\n entities,\n viewMode,\n selectedEntityId,\n onSelectEntity,\n sortBy,\n sortDirection,\n onSortChange,\n utilities,\n styles,\n components,\n callbacks,\n savedUserSettings,\n onSaveUserSettings\n}) {\n // Load DataGrid component from registry\n const DataGrid = components['DataGrid'];\n\n // Helper function to get border radius value\n const getBorderRadius = (size) => {\n return typeof styles.borders.radius === 'object' ? styles.borders.radius[size] : styles.borders.radius;\n };\n\n // Handle entity selection\n const handleEntityClick = useCallback((entityId) => {\n onSelectEntity?.(entityId);\n }, [onSelectEntity]);\n\n // Define columns for DataGrid\n const gridColumns = [\n {\n field: 'Name',\n header: 'Name',\n sortable: true,\n width: '150px'\n },\n {\n field: 'DisplayName',\n header: 'Display Name',\n sortable: true,\n width: '150px',\n render: (value, row) => value || row.Name\n },\n {\n field: 'Description',\n header: 'Description',\n sortable: false,\n width: '300px',\n render: (value) => value || '-'\n },\n {\n field: 'SchemaName',\n header: 'Schema',\n sortable: false,\n width: '120px',\n render: (value) => value || '-'\n },\n {\n field: 'BaseTable',\n header: 'Table',\n sortable: false,\n width: '150px',\n render: (value) => value || '-'\n },\n {\n field: 'BaseView',\n header: 'Base View',\n sortable: false,\n width: '150px',\n render: (value) => value || '-'\n }\n ];\n\n // Handle row click to open entity details\n const handleRowClick = useCallback((row) => {\n // When a row is clicked, select the entity and open its details\n handleEntityClick(row.ID);\n }, [handleEntityClick]);\n\n // Grid View\n if (viewMode === 'grid') {\n return (\n <div style={{\n width: '100%',\n overflowX: 'auto'\n }}>\n {DataGrid ? (\n <DataGrid\n data={entities}\n columns={gridColumns}\n pageSize={50}\n showFilters={true}\n showExport={false}\n selectionMode=\"none\" // Disable selection mode since we're using row clicks\n onRowClick={handleRowClick} // Handle row clicks to open the entity\n sortBy={sortBy}\n sortDirection={sortDirection}\n onSortChange={onSortChange}\n utilities={utilities}\n styles={styles}\n components={components}\n callbacks={callbacks}\n savedUserSettings={savedUserSettings}\n onSaveUserSettings={onSaveUserSettings}\n />\n ) : (\n <div style={{\n padding: styles.spacing.lg,\n textAlign: 'center',\n color: styles.colors.textSecondary\n }}>\n DataGrid component not available\n </div>\n )}\n </div>\n );\n }\n \n // Card View\n return (\n <div style={{\n display: 'grid',\n gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',\n gap: styles.spacing.lg\n }}>\n {entities.map((entity) => (\n <div\n key={entity.ID}\n onClick={() => handleEntityClick(entity.ID)}\n style={{\n padding: styles.spacing.lg,\n backgroundColor: selectedEntityId === entity.ID \n ? styles.colors.primary + '20'\n : styles.colors.surface,\n border: selectedEntityId === entity.ID\n ? `2px solid ${styles.colors.primary}`\n : `1px solid ${styles.colors.border}`,\n borderRadius: getBorderRadius('md'),\n cursor: 'pointer',\n transition: 'all 0.2s',\n position: 'relative'\n }}\n onMouseEnter={(e) => {\n if (selectedEntityId !== entity.ID) {\n e.currentTarget.style.transform = 'translateY(-2px)';\n e.currentTarget.style.boxShadow = `0 4px 12px ${styles.colors.shadow || 'rgba(0, 0, 0, 0.1)'}`;\n }\n }}\n onMouseLeave={(e) => {\n if (selectedEntityId !== entity.ID) {\n e.currentTarget.style.transform = 'translateY(0)';\n e.currentTarget.style.boxShadow = 'none';\n }\n }}\n >\n {/* Card Header */}\n <div style={{\n marginBottom: styles.spacing.md,\n paddingBottom: styles.spacing.md,\n borderBottom: `1px solid ${styles.colors.borderLight || styles.colors.border}`\n }}>\n <h3 style={{\n margin: 0,\n fontSize: styles.typography.fontSize.lg,\n fontWeight: styles.typography.fontWeight?.semibold || '600',\n color: styles.colors.text,\n marginBottom: styles.spacing.xs\n }}>\n {entity.DisplayName || entity.Name}\n </h3>\n {entity.DisplayName && entity.DisplayName !== entity.Name && (\n <div style={{\n fontSize: styles.typography.fontSize.sm,\n color: styles.colors.textSecondary\n }}>\n {entity.Name}\n </div>\n )}\n </div>\n \n {/* Card Body */}\n {entity.Description && (\n <p style={{\n margin: 0,\n marginBottom: styles.spacing.md,\n fontSize: styles.typography.fontSize.md,\n color: styles.colors.textSecondary,\n lineHeight: 1.5,\n display: '-webkit-box',\n WebkitLineClamp: 2,\n WebkitBoxOrient: 'vertical',\n overflow: 'hidden'\n }}>\n {entity.Description}\n </p>\n )}\n \n {/* Card Footer */}\n <div style={{\n display: 'flex',\n justifyContent: 'space-between',\n alignItems: 'center',\n fontSize: styles.typography.fontSize.sm,\n color: styles.colors.textSecondary\n }}>\n <div>\n {entity.SchemaName && (\n <span style={{ marginRight: styles.spacing.md }}>\n Schema: <strong>{entity.SchemaName}</strong>\n </span>\n )}\n {entity.BaseTable && (\n <span>\n Table: <strong>{entity.BaseTable}</strong>\n </span>\n )}\n </div>\n {entity.BaseView && (\n <div style={{\n fontSize: styles.typography.fontSize.xs || styles.typography.fontSize.sm,\n color: styles.colors.textSecondary\n }}>\n View: {entity.BaseView}\n </div>\n )}\n </div>\n \n {/* Selection Indicator */}\n {selectedEntityId === entity.ID && (\n <div style={{\n position: 'absolute',\n top: styles.spacing.sm,\n right: styles.spacing.sm,\n width: '8px',\n height: '8px',\n backgroundColor: styles.colors.primary,\n borderRadius: '50%'\n }} />\n )}\n </div>\n ))}\n </div>\n );\n}",
1117
- "dependencies": [
1118
- {
1119
- "name": "DataGrid",
1120
- "location": "registry",
1121
- "namespace": "Generic/UI/Table",
1122
- "version": "^1.0.0"
1123
- }
1124
- ],
1125
- "libraries": []
1126
- },
1127
- {
1128
- "name": "EntityDetails",
1129
- "title": "Entity Details Panel",
1130
- "description": "Sliding panel that displays detailed information about a selected entity including fields and relationships",
1131
- "type": "form",
1132
- "functionalRequirements": "## Entity Details Requirements\n\n- Slide in from the right when an entity is selected\n- Display entity metadata at the top\n- Show fields in a formatted table\n- Display relationships with icons\n- Include 'Open Record' button\n- Support closing via X button or Escape key\n- Smooth slide animation\n- Scrollable content area",
1133
- "technicalDesign": "## Technical Design\n\n### Props\n- entity: Selected entity object\n- fields: Array of entity fields\n- relationships: Array of entity relationships\n- isOpen: Whether panel is visible\n- onClose: Callback to close panel\n- onOpenRecord: Callback to open entity record\n\n### Layout\n- Fixed position overlay\n- Slide animation using transform\n- Header with entity name and close button\n- Sections for metadata, fields, relationships\n- Sticky 'Open Record' button at bottom",
1134
- "dataRequirements": {
1135
- "mode": "views",
1136
- "entities": [
1137
- {
1138
- "name": "Entities",
1139
- "description": "Metadata about all entities in the system",
1140
- "displayFields": [
1141
- "ID",
1142
- "Name",
1143
- "DisplayName",
1144
- "NameSuffix",
1145
- "Description",
1146
- "SchemaName",
1147
- "BaseTable",
1148
- "BaseView"
1149
- ],
1150
- "filterFields": [
1151
- "SchemaName",
1152
- "BaseTable"
1153
- ],
1154
- "sortFields": [
1155
- "Name",
1156
- "DisplayName"
1157
- ],
1158
- "fieldMetadata": [
1159
- {
1160
- "name": "ID",
1161
- "sequence": 1,
1162
- "defaultInView": false,
1163
- "type": "uniqueidentifier",
1164
- "allowsNull": false,
1165
- "isPrimaryKey": true,
1166
- "description": "Unique identifier for the entity"
1167
- },
1168
- {
1169
- "name": "Name",
1170
- "sequence": 2,
1171
- "defaultInView": true,
1172
- "type": "nvarchar",
1173
- "allowsNull": false,
1174
- "isPrimaryKey": false,
1175
- "description": "System name of the entity"
1176
- },
1177
- {
1178
- "name": "DisplayName",
1179
- "sequence": 3,
1180
- "defaultInView": true,
1181
- "type": "nvarchar",
1182
- "allowsNull": true,
1183
- "isPrimaryKey": false,
1184
- "description": "User-friendly display name for the entity"
1185
- },
1186
- {
1187
- "name": "NameSuffix",
1188
- "sequence": 4,
1189
- "defaultInView": true,
1190
- "type": "nvarchar",
1191
- "allowsNull": true,
1192
- "isPrimaryKey": false,
1193
- "description": "Optional suffix appended to entity names for display purposes"
1194
- },
1195
- {
1196
- "name": "Description",
1197
- "sequence": 5,
1198
- "defaultInView": true,
1199
- "type": "nvarchar",
1200
- "allowsNull": true,
1201
- "isPrimaryKey": false,
1202
- "description": "Description of the entity"
1203
- },
1204
- {
1205
- "name": "SchemaName",
1206
- "sequence": 6,
1207
- "defaultInView": true,
1208
- "type": "nvarchar",
1209
- "allowsNull": true,
1210
- "isPrimaryKey": false,
1211
- "description": "Database schema name"
1212
- },
1213
- {
1214
- "name": "BaseTable",
1215
- "sequence": 7,
1216
- "defaultInView": true,
1217
- "type": "nvarchar",
1218
- "allowsNull": true,
1219
- "isPrimaryKey": false,
1220
- "description": "Base table in the database"
1221
- },
1222
- {
1223
- "name": "BaseView",
1224
- "sequence": 8,
1225
- "defaultInView": true,
1226
- "type": "nvarchar",
1227
- "allowsNull": false,
1228
- "isPrimaryKey": false,
1229
- "description": "Base view used for the entity"
1230
- }
1231
- ],
1232
- "permissionLevelNeeded": [
1233
- "read"
1234
- ],
1235
- "usageContext": "Main entity list display and filtering"
1236
- },
1237
- {
1238
- "name": "MJ: Entity Fields",
1239
- "description": "Fields belonging to each entity",
1240
- "displayFields": [
1241
- "Name",
1242
- "DisplayName",
1243
- "Type",
1244
- "Length",
1245
- "AllowsNull",
1246
- "IsPrimaryKey",
1247
- "IsUnique"
1248
- ],
1249
- "filterFields": [
1250
- "EntityID"
1251
- ],
1252
- "sortFields": [
1253
- "Sequence",
1254
- "Name"
1255
- ],
1256
- "fieldMetadata": [
1257
- {
1258
- "name": "EntityID",
1259
- "sequence": 1,
1260
- "defaultInView": false,
1261
- "type": "uniqueidentifier",
1262
- "allowsNull": false,
1263
- "isPrimaryKey": false,
1264
- "description": "Reference to parent entity"
1265
- },
1266
- {
1267
- "name": "Name",
1268
- "sequence": 2,
1269
- "defaultInView": true,
1270
- "type": "nvarchar",
1271
- "allowsNull": false,
1272
- "isPrimaryKey": false,
1273
- "description": "Field name"
1274
- },
1275
- {
1276
- "name": "DisplayName",
1277
- "sequence": 3,
1278
- "defaultInView": true,
1279
- "type": "nvarchar",
1280
- "allowsNull": true,
1281
- "isPrimaryKey": false,
1282
- "description": "User-friendly field name"
1283
- },
1284
- {
1285
- "name": "Type",
1286
- "sequence": 4,
1287
- "defaultInView": true,
1288
- "type": "nvarchar",
1289
- "allowsNull": false,
1290
- "isPrimaryKey": false,
1291
- "description": "Data type of the field"
1292
- },
1293
- {
1294
- "name": "Length",
1295
- "sequence": 5,
1296
- "defaultInView": true,
1297
- "type": "int",
1298
- "allowsNull": true,
1299
- "isPrimaryKey": false,
1300
- "description": "Maximum length for string fields"
1301
- },
1302
- {
1303
- "name": "AllowsNull",
1304
- "sequence": 6,
1305
- "defaultInView": true,
1306
- "type": "bit",
1307
- "allowsNull": false,
1308
- "isPrimaryKey": false,
1309
- "description": "Whether field allows null values"
1310
- },
1311
- {
1312
- "name": "IsPrimaryKey",
1313
- "sequence": 7,
1314
- "defaultInView": true,
1315
- "type": "bit",
1316
- "allowsNull": false,
1317
- "isPrimaryKey": false,
1318
- "description": "Whether field is part of primary key"
1319
- },
1320
- {
1321
- "name": "IsUnique",
1322
- "sequence": 8,
1323
- "defaultInView": true,
1324
- "type": "bit",
1325
- "allowsNull": false,
1326
- "isPrimaryKey": false,
1327
- "description": "Whether field must be unique"
1328
- },
1329
- {
1330
- "name": "Sequence",
1331
- "sequence": 9,
1332
- "defaultInView": false,
1333
- "type": "int",
1334
- "allowsNull": false,
1335
- "isPrimaryKey": false,
1336
- "description": "Display order of the field"
1337
- }
1338
- ],
1339
- "permissionLevelNeeded": [
1340
- "read"
1341
- ],
1342
- "usageContext": "Details panel to show entity fields"
1343
- },
1344
- {
1345
- "name": "MJ: Entity Relationships",
1346
- "description": "Relationships between entities",
1347
- "displayFields": [
1348
- "RelatedEntity",
1349
- "Type",
1350
- "DisplayName",
1351
- "RelatedEntityJoinField"
1352
- ],
1353
- "filterFields": [
1354
- "EntityID"
1355
- ],
1356
- "sortFields": [
1357
- "Sequence",
1358
- "RelatedEntity"
1359
- ],
1360
- "fieldMetadata": [
1361
- {
1362
- "name": "EntityID",
1363
- "sequence": 1,
1364
- "defaultInView": false,
1365
- "type": "uniqueidentifier",
1366
- "allowsNull": false,
1367
- "isPrimaryKey": false,
1368
- "description": "Reference to parent entity"
1369
- },
1370
- {
1371
- "name": "RelatedEntity",
1372
- "sequence": 2,
1373
- "defaultInView": true,
1374
- "type": "nvarchar",
1375
- "allowsNull": false,
1376
- "isPrimaryKey": false,
1377
- "description": "The related entity in the relationship"
1378
- },
1379
- {
1380
- "name": "Type",
1381
- "sequence": 3,
1382
- "defaultInView": true,
1383
- "type": "nvarchar",
1384
- "allowsNull": false,
1385
- "isPrimaryKey": false,
1386
- "description": "Type of relationship (One to Many, Many to One, etc.)"
1387
- },
1388
- {
1389
- "name": "DisplayName",
1390
- "sequence": 4,
1391
- "defaultInView": true,
1392
- "type": "nvarchar",
1393
- "allowsNull": true,
1394
- "isPrimaryKey": false,
1395
- "description": "User-friendly name for the relationship"
1396
- },
1397
- {
1398
- "name": "RelatedEntityJoinField",
1399
- "sequence": 5,
1400
- "defaultInView": true,
1401
- "type": "nvarchar",
1402
- "allowsNull": true,
1403
- "isPrimaryKey": false,
1404
- "description": "The field in the related entity that joins to this entity"
1405
- },
1406
- {
1407
- "name": "Sequence",
1408
- "sequence": 6,
1409
- "defaultInView": false,
1410
- "type": "int",
1411
- "allowsNull": false,
1412
- "isPrimaryKey": false,
1413
- "description": "Display order"
1414
- }
1415
- ],
1416
- "permissionLevelNeeded": [
1417
- "read"
1418
- ],
1419
- "usageContext": "Details panel to show entity relationships"
1420
- }
1421
- ],
1422
- "queries": [],
1423
- "description": "This component requires access to entity metadata including entities, their fields, and relationships to provide a comprehensive entity browsing experience"
1424
- },
1425
- "properties": [
1426
- {
1427
- "name": "entity",
1428
- "description": "The selected entity object",
1429
- "type": "object",
1430
- "required": true
1431
- },
1432
- {
1433
- "name": "fields",
1434
- "description": "Array of fields for the entity",
1435
- "type": "array",
1436
- "required": true,
1437
- "defaultValue": []
1438
- },
1439
- {
1440
- "name": "relationships",
1441
- "description": "Array of relationships for the entity",
1442
- "type": "array",
1443
- "required": true,
1444
- "defaultValue": []
1445
- },
1446
- {
1447
- "name": "isOpen",
1448
- "description": "Whether the panel is open",
1449
- "type": "boolean",
1450
- "required": true
1451
- },
1452
- {
1453
- "name": "onClose",
1454
- "description": "Callback to close the panel",
1455
- "type": "function",
1456
- "required": true
1457
- },
1458
- {
1459
- "name": "onOpenRecord",
1460
- "description": "Callback to open the entity record",
1461
- "type": "function",
1462
- "required": true
1463
- }
1464
- ],
1465
- "events": [
1466
- {
1467
- "name": "onClose",
1468
- "description": "Fired when the panel should close",
1469
- "parameters": []
1470
- },
1471
- {
1472
- "name": "onOpenRecord",
1473
- "description": "Fired when the open record button is clicked",
1474
- "parameters": [
1475
- {
1476
- "name": "entityName",
1477
- "description": "Name of the entity to open",
1478
- "type": "string"
1479
- }
1480
- ]
1481
- }
1482
- ],
1483
- "exampleUsage": "<EntityDetails\n entity={selectedEntity}\n fields={entityFields}\n relationships={entityRelationships}\n isOpen={detailsPanelOpen}\n onClose={handleCloseDetails}\n onOpenRecord={handleOpenRecord}\n utilities={utilities}\n styles={styles}\n components={components}\n callbacks={callbacks}\n/>",
1484
- "code": "function EntityDetails({ \n entity, \n fields, \n relationships, \n isOpen, \n onClose, \n onOpenRecord,\n utilities, \n styles, \n components, \n callbacks, \n savedUserSettings, \n onSaveUserSettings \n}) {\n // Helper function to get border radius value\n const getBorderRadius = (size) => {\n return typeof styles.borders.radius === 'object' ? styles.borders.radius[size] : styles.borders.radius;\n };\n \n // Handle escape key to close panel\n useEffect(() => {\n const handleEscape = (e) => {\n if (e.key === 'Escape' && isOpen) {\n onClose?.();\n }\n };\n \n document.addEventListener('keydown', handleEscape);\n return () => document.removeEventListener('keydown', handleEscape);\n }, [isOpen, onClose]);\n \n // Load OpenRecordButton component\n const OpenRecordButton = components['OpenRecordButton'];\n \n // Render field type badge\n const renderFieldType = (type) => {\n const typeColors = {\n 'nvarchar': styles.colors.info || styles.colors.primary,\n 'varchar': styles.colors.info || styles.colors.primary,\n 'int': styles.colors.success || styles.colors.primary,\n 'bigint': styles.colors.success || styles.colors.primary,\n 'decimal': styles.colors.success || styles.colors.primary,\n 'float': styles.colors.success || styles.colors.primary,\n 'bit': styles.colors.warning || styles.colors.secondary,\n 'datetime': styles.colors.secondary,\n 'uniqueidentifier': styles.colors.primary,\n 'text': styles.colors.info || styles.colors.primary,\n 'ntext': styles.colors.info || styles.colors.primary\n };\n \n const color = typeColors[type?.toLowerCase()] || styles.colors.textSecondary;\n \n return (\n <span style={{\n display: 'inline-block',\n padding: `${styles.spacing.xs} ${styles.spacing.sm}`,\n backgroundColor: color + '15',\n color: color,\n borderRadius: getBorderRadius('sm'),\n fontSize: styles.typography.fontSize.xs || styles.typography.fontSize.sm,\n fontWeight: styles.typography.fontWeight?.medium || '500'\n }}>\n {type}\n </span>\n );\n };\n \n // Render relationship type icon\n const renderRelationshipIcon = (type) => {\n const icons = {\n 'One to Many': '1:N',\n 'Many to One': 'N:1',\n 'Many to Many': 'N:N',\n 'One to One': '1:1'\n };\n \n return (\n <span style={{\n display: 'inline-block',\n padding: `${styles.spacing.xs} ${styles.spacing.sm}`,\n backgroundColor: styles.colors.primary + '15',\n color: styles.colors.primary,\n borderRadius: getBorderRadius('sm'),\n fontSize: styles.typography.fontSize.xs || styles.typography.fontSize.sm,\n fontWeight: styles.typography.fontWeight?.bold || '700',\n fontFamily: 'monospace'\n }}>\n {icons[type] || type}\n </span>\n );\n };\n \n return (\n <>\n {/* Backdrop */}\n {isOpen && (\n <div\n onClick={onClose}\n style={{\n position: 'fixed',\n top: 0,\n left: 0,\n right: 0,\n bottom: 0,\n backgroundColor: 'rgba(0, 0, 0, 0.3)',\n zIndex: 99999,\n opacity: isOpen ? 1 : 0,\n transition: 'opacity 0.3s',\n pointerEvents: isOpen ? 'auto' : 'none'\n }}\n />\n )}\n \n {/* Panel */}\n <div style={{\n position: 'fixed',\n top: '75px',\n right: 0,\n bottom: 0,\n width: '480px',\n backgroundColor: styles.colors.background,\n boxShadow: isOpen ? `-4px 0 24px ${styles.colors.shadow || 'rgba(0, 0, 0, 0.1)'}` : 'none',\n transform: isOpen ? 'translateX(0)' : 'translateX(100%)',\n transition: 'transform 0.3s ease-out',\n zIndex: 100000,\n display: 'flex',\n flexDirection: 'column',\n overflow: 'hidden'\n }}>\n {/* Header */}\n <div style={{\n padding: styles.spacing.lg,\n borderBottom: `1px solid ${styles.colors.border}`,\n backgroundColor: styles.colors.surface\n }}>\n <div style={{\n display: 'flex',\n justifyContent: 'space-between',\n alignItems: 'flex-start'\n }}>\n <div style={{ flex: 1 }}>\n <h2 style={{\n margin: 0,\n fontSize: styles.typography.fontSize.xl,\n fontWeight: styles.typography.fontWeight?.bold || '700',\n color: styles.colors.text,\n marginBottom: styles.spacing.xs\n }}>\n {entity?.DisplayName || entity?.Name || 'No Entity Selected'}\n </h2>\n {entity?.DisplayName && entity?.Name && entity.DisplayName !== entity.Name && (\n <div style={{\n fontSize: styles.typography.fontSize.sm,\n color: styles.colors.textSecondary,\n fontFamily: 'monospace'\n }}>\n {entity.Name}\n </div>\n )}\n </div>\n <button\n onClick={onClose}\n style={{\n width: '32px',\n height: '32px',\n borderRadius: getBorderRadius('sm'),\n border: 'none',\n backgroundColor: 'transparent',\n color: styles.colors.textSecondary,\n fontSize: styles.typography.fontSize.lg,\n cursor: 'pointer',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n transition: 'background-color 0.2s'\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.backgroundColor = styles.colors.surfaceHover || styles.colors.surface;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.backgroundColor = 'transparent';\n }}\n >\n ✕\n </button>\n </div>\n </div>\n \n {/* Content */}\n <div style={{\n flex: 1,\n overflow: 'auto',\n padding: styles.spacing.lg\n }}>\n {entity ? (\n <>\n {/* Entity Metadata */}\n {entity.Description && (\n <div style={{\n marginBottom: styles.spacing.xl,\n padding: styles.spacing.md,\n backgroundColor: styles.colors.surface,\n borderRadius: getBorderRadius('md'),\n borderLeft: `3px solid ${styles.colors.primary}`\n }}>\n <div style={{\n fontSize: styles.typography.fontSize.md,\n color: styles.colors.textSecondary,\n lineHeight: 1.6\n }}>\n {entity.Description}\n </div>\n </div>\n )}\n \n {/* Quick Info */}\n <div style={{\n display: 'grid',\n gridTemplateColumns: 'repeat(2, 1fr)',\n gap: styles.spacing.md,\n marginBottom: styles.spacing.xl\n }}>\n <div style={{\n padding: styles.spacing.md,\n backgroundColor: styles.colors.surface,\n borderRadius: getBorderRadius('sm')\n }}>\n <div style={{\n fontSize: styles.typography.fontSize.xs || styles.typography.fontSize.sm,\n color: styles.colors.textSecondary,\n marginBottom: styles.spacing.xs\n }}>\n Schema\n </div>\n <div style={{\n fontSize: styles.typography.fontSize.md,\n fontWeight: styles.typography.fontWeight?.semibold || '600',\n color: styles.colors.text\n }}>\n {entity.SchemaName || '-'}\n </div>\n </div>\n <div style={{\n padding: styles.spacing.md,\n backgroundColor: styles.colors.surface,\n borderRadius: getBorderRadius('sm')\n }}>\n <div style={{\n fontSize: styles.typography.fontSize.xs || styles.typography.fontSize.sm,\n color: styles.colors.textSecondary,\n marginBottom: styles.spacing.xs\n }}>\n Base Table\n </div>\n <div style={{\n fontSize: styles.typography.fontSize.md,\n fontWeight: styles.typography.fontWeight?.semibold || '600',\n color: styles.colors.text\n }}>\n {entity.BaseTable || '-'}\n </div>\n </div>\n <div style={{\n padding: styles.spacing.md,\n backgroundColor: styles.colors.surface,\n borderRadius: getBorderRadius('sm')\n }}>\n <div style={{\n fontSize: styles.typography.fontSize.xs || styles.typography.fontSize.sm,\n color: styles.colors.textSecondary,\n marginBottom: styles.spacing.xs\n }}>\n Base View\n </div>\n <div style={{\n fontSize: styles.typography.fontSize.md,\n fontWeight: styles.typography.fontWeight?.semibold || '600',\n color: styles.colors.text\n }}>\n {entity.BaseView || '-'}\n </div>\n </div>\n <div style={{\n padding: styles.spacing.md,\n backgroundColor: styles.colors.surface,\n borderRadius: getBorderRadius('sm')\n }}>\n <div style={{\n fontSize: styles.typography.fontSize.xs || styles.typography.fontSize.sm,\n color: styles.colors.textSecondary,\n marginBottom: styles.spacing.xs\n }}>\n Field Count\n </div>\n <div style={{\n fontSize: styles.typography.fontSize.md,\n fontWeight: styles.typography.fontWeight?.semibold || '600',\n color: styles.colors.text\n }}>\n {fields?.length || 0}\n </div>\n </div>\n </div>\n \n {/* Fields Section */}\n <div style={{ marginBottom: styles.spacing.xl }}>\n <h3 style={{\n margin: 0,\n marginBottom: styles.spacing.md,\n fontSize: styles.typography.fontSize.lg,\n fontWeight: styles.typography.fontWeight?.semibold || '600',\n color: styles.colors.text\n }}>\n Fields ({fields?.length || 0})\n </h3>\n <div style={{\n backgroundColor: styles.colors.surface,\n borderRadius: getBorderRadius('md'),\n overflow: 'hidden'\n }}>\n {fields && fields.length > 0 ? (\n <table style={{\n width: '100%',\n borderCollapse: 'collapse'\n }}>\n <thead>\n <tr style={{\n borderBottom: `1px solid ${styles.colors.border}`\n }}>\n <th style={{\n padding: styles.spacing.sm,\n textAlign: 'left',\n fontSize: styles.typography.fontSize.sm,\n fontWeight: styles.typography.fontWeight?.medium || '500',\n color: styles.colors.textSecondary\n }}>\n Field\n </th>\n <th style={{\n padding: styles.spacing.sm,\n textAlign: 'left',\n fontSize: styles.typography.fontSize.sm,\n fontWeight: styles.typography.fontWeight?.medium || '500',\n color: styles.colors.textSecondary\n }}>\n Type\n </th>\n <th style={{\n padding: styles.spacing.sm,\n textAlign: 'center',\n fontSize: styles.typography.fontSize.sm,\n fontWeight: styles.typography.fontWeight?.medium || '500',\n color: styles.colors.textSecondary\n }}>\n Attributes\n </th>\n </tr>\n </thead>\n <tbody>\n {fields.map((field, index) => (\n <tr\n key={index}\n style={{\n borderBottom: index < fields.length - 1 \n ? `1px solid ${styles.colors.borderLight || styles.colors.border}` \n : 'none'\n }}\n >\n <td style={{\n padding: styles.spacing.sm,\n fontSize: styles.typography.fontSize.sm,\n color: styles.colors.text\n }}>\n <div>\n <div style={{\n fontWeight: field.IsPrimaryKey \n ? (styles.typography.fontWeight?.semibold || '600')\n : (styles.typography.fontWeight?.regular || '400')\n }}>\n {field.DisplayName || field.Name}\n </div>\n {field.DisplayName && (\n <div style={{\n fontSize: styles.typography.fontSize.xs || styles.typography.fontSize.sm,\n color: styles.colors.textSecondary,\n fontFamily: 'monospace'\n }}>\n {field.Name}\n </div>\n )}\n </div>\n </td>\n <td style={{\n padding: styles.spacing.sm\n }}>\n {renderFieldType(field.Type)}\n {field.Length && (\n <span style={{\n marginLeft: styles.spacing.xs,\n fontSize: styles.typography.fontSize.xs || styles.typography.fontSize.sm,\n color: styles.colors.textSecondary\n }}>\n ({field.Length})\n </span>\n )}\n </td>\n <td style={{\n padding: styles.spacing.sm,\n textAlign: 'center'\n }}>\n <div style={{\n display: 'flex',\n gap: styles.spacing.xs,\n justifyContent: 'center',\n flexWrap: 'wrap'\n }}>\n {field.IsPrimaryKey && (\n <span style={{\n padding: `2px ${styles.spacing.xs}`,\n backgroundColor: (styles.colors.warning || styles.colors.secondary) + '15',\n color: styles.colors.warning || styles.colors.secondary,\n borderRadius: getBorderRadius('xs') || getBorderRadius('sm'),\n fontSize: styles.typography.fontSize.xs || styles.typography.fontSize.sm,\n fontWeight: styles.typography.fontWeight?.bold || '700'\n }}>\n PK\n </span>\n )}\n {field.IsUnique && (\n <span style={{\n padding: `2px ${styles.spacing.xs}`,\n backgroundColor: (styles.colors.info || styles.colors.primary) + '15',\n color: styles.colors.info || styles.colors.primary,\n borderRadius: getBorderRadius('xs') || getBorderRadius('sm'),\n fontSize: styles.typography.fontSize.xs || styles.typography.fontSize.sm,\n fontWeight: styles.typography.fontWeight?.bold || '700'\n }}>\n UQ\n </span>\n )}\n {!field.AllowsNull && !field.IsPrimaryKey && (\n <span style={{\n padding: `2px ${styles.spacing.xs}`,\n backgroundColor: (styles.colors.error || styles.colors.secondary) + '15',\n color: styles.colors.error || styles.colors.secondary,\n borderRadius: getBorderRadius('xs') || getBorderRadius('sm'),\n fontSize: styles.typography.fontSize.xs || styles.typography.fontSize.sm,\n fontWeight: styles.typography.fontWeight?.bold || '700'\n }}>\n NN\n </span>\n )}\n </div>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n ) : (\n <div style={{\n padding: styles.spacing.lg,\n textAlign: 'center',\n color: styles.colors.textSecondary,\n fontSize: styles.typography.fontSize.sm\n }}>\n No fields available\n </div>\n )}\n </div>\n </div>\n \n {/* Relationships Section */}\n <div style={{ marginBottom: styles.spacing.xl }}>\n <h3 style={{\n margin: 0,\n marginBottom: styles.spacing.md,\n fontSize: styles.typography.fontSize.lg,\n fontWeight: styles.typography.fontWeight?.semibold || '600',\n color: styles.colors.text\n }}>\n Relationships ({relationships?.length || 0})\n </h3>\n <div style={{\n display: 'flex',\n flexDirection: 'column',\n gap: styles.spacing.sm\n }}>\n {relationships && relationships.length > 0 ? (\n relationships.map((rel, index) => (\n <div\n key={index}\n style={{\n padding: styles.spacing.md,\n backgroundColor: styles.colors.surface,\n borderRadius: getBorderRadius('sm'),\n display: 'flex',\n alignItems: 'center',\n gap: styles.spacing.md\n }}\n >\n {renderRelationshipIcon(rel.Type)}\n <div style={{ flex: 1 }}>\n <div style={{\n fontSize: styles.typography.fontSize.md,\n fontWeight: styles.typography.fontWeight?.medium || '500',\n color: styles.colors.text\n }}>\n {rel.DisplayName || rel.RelatedEntity}\n </div>\n {rel.RelatedEntityJoinField && (\n <div style={{\n fontSize: styles.typography.fontSize.xs || styles.typography.fontSize.sm,\n color: styles.colors.textSecondary,\n fontFamily: 'monospace'\n }}>\n via {rel.RelatedEntityJoinField}\n </div>\n )}\n </div>\n </div>\n ))\n ) : (\n <div style={{\n padding: styles.spacing.lg,\n backgroundColor: styles.colors.surface,\n borderRadius: getBorderRadius('sm'),\n textAlign: 'center',\n color: styles.colors.textSecondary,\n fontSize: styles.typography.fontSize.sm\n }}>\n No relationships defined\n </div>\n )}\n </div>\n </div>\n </>\n ) : (\n <div style={{\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n justifyContent: 'center',\n height: '100%',\n color: styles.colors.textSecondary\n }}>\n <div style={{\n fontSize: styles.typography.fontSize.lg,\n marginBottom: styles.spacing.md\n }}>\n No Entity Selected\n </div>\n <div style={{\n fontSize: styles.typography.fontSize.md\n }}>\n Select an entity from the list to view its details\n </div>\n </div>\n )}\n </div>\n \n {/* Footer with Open Record Button */}\n {entity && OpenRecordButton && (\n <div style={{\n padding: styles.spacing.lg,\n borderTop: `1px solid ${styles.colors.border}`,\n backgroundColor: styles.colors.surface\n }}>\n <OpenRecordButton\n entityName=\"Entities\"\n record={entity}\n buttonText=\"Open Entity Record\"\n utilities={utilities}\n styles={styles}\n components={components}\n callbacks={callbacks}\n savedUserSettings={savedUserSettings}\n onSaveUserSettings={onSaveUserSettings}\n buttonStyle={{\n width: '100%',\n padding: styles.spacing.md,\n backgroundColor: styles.colors.primary,\n color: 'white',\n border: 'none',\n borderRadius: getBorderRadius('md'),\n fontSize: styles.typography.fontSize.md,\n fontWeight: styles.typography.fontWeight?.semibold || '600',\n cursor: 'pointer',\n transition: 'background-color 0.2s'\n }}\n />\n </div>\n )}\n </div>\n </>\n );\n}",
1485
- "dependencies": [
1486
- {
1487
- "name": "OpenRecordButton",
1488
- "location": "registry",
1489
- "namespace": "Generic/Navigation",
1490
- "version": "^1.0.0"
1491
- }
1492
- ],
1493
- "libraries": []
1494
- },
1495
- {
1496
- "name": "EntityFilter",
1497
- "title": "Entity Filter Panel",
1498
- "description": "Collapsible filter panel for filtering entities by various criteria",
1499
- "type": "form",
1500
- "functionalRequirements": "## Entity Filter Requirements\n\n- Collapsible panel on the left side\n- Filter by schema name (dropdown)\n- Filter by base table (dropdown)\n- Search box for text search\n- Clear all filters button\n- Show active filter count\n- Smooth collapse/expand animation\n- Remember collapsed state",
1501
- "dataRequirements": {
1502
- "mode": "views",
1503
- "description": "Receives filter options derived from Entities metadata",
1504
- "entities": [
1505
- {
1506
- "name": "Entities",
1507
- "description": "Source of schema and table filter options",
1508
- "displayFields": [
1509
- "SchemaName",
1510
- "BaseTable"
1511
- ],
1512
- "filterFields": [],
1513
- "sortFields": [],
1514
- "fieldMetadata": [],
1515
- "permissionLevelNeeded": [
1516
- "read"
1517
- ],
1518
- "usageContext": "Extracts unique schema names and base tables for filter dropdowns"
1519
- }
1520
- ],
1521
- "queries": []
1522
- },
1523
- "technicalDesign": "## Technical Design\n\n### Props\n- filters: Current filter values\n- onFilterChange: Callback when filters change\n- schemas: Available schema options\n- tables: Available table options\n- isCollapsed: Whether panel is collapsed\n- onToggleCollapse: Callback to toggle collapse\n\n### Components\n- Collapse toggle button\n- Schema dropdown\n- Table dropdown\n- Search input\n- Clear filters button\n- Active filter badges",
1524
- "properties": [
1525
- {
1526
- "name": "filters",
1527
- "description": "Current filter values",
1528
- "type": "{schema?: string, table?: string, search?: string}",
1529
- "required": true
1530
- },
1531
- {
1532
- "name": "onFilterChange",
1533
- "description": "Callback when filters change",
1534
- "type": "(filters: {schema?: string, table?: string, search?: string}) => void",
1535
- "required": true
1536
- },
1537
- {
1538
- "name": "schemas",
1539
- "description": "Available schema options",
1540
- "type": "Array<string>",
1541
- "required": true
1542
- },
1543
- {
1544
- "name": "tables",
1545
- "description": "Available table options",
1546
- "type": "Array<string>",
1547
- "required": true
1548
- },
1549
- {
1550
- "name": "isCollapsed",
1551
- "description": "Whether the panel is collapsed",
1552
- "type": "boolean",
1553
- "required": true
1554
- },
1555
- {
1556
- "name": "onToggleCollapse",
1557
- "description": "Callback to toggle collapse state",
1558
- "type": "() => void",
1559
- "required": true
1560
- }
1561
- ],
1562
- "events": [
1563
- {
1564
- "name": "onFilterChange",
1565
- "description": "Fired when filter values change",
1566
- "parameters": [
1567
- {
1568
- "name": "filters",
1569
- "description": "Updated filter object",
1570
- "type": "object"
1571
- }
1572
- ]
1573
- },
1574
- {
1575
- "name": "onToggleCollapse",
1576
- "description": "Fired when collapse state should toggle",
1577
- "parameters": []
1578
- }
1579
- ],
1580
- "exampleUsage": "<EntityFilter\n filters={filters}\n onFilterChange={handleFilterChange}\n schemas={uniqueSchemas}\n tables={uniqueTables}\n isCollapsed={filterPanelCollapsed}\n onToggleCollapse={handleToggleFilter}\n utilities={utilities}\n styles={styles}\n components={components}\n callbacks={callbacks}\n/>",
1581
- "code": "function EntityFilter({ \n filters, \n onFilterChange, \n schemas, \n tables, \n isCollapsed, \n onToggleCollapse,\n utilities, \n styles, \n components, \n callbacks, \n savedUserSettings, \n onSaveUserSettings \n}) {\n // Helper function to get border radius value\n const getBorderRadius = (size) => {\n return typeof styles.borders.radius === 'object' ? styles.borders.radius[size] : styles.borders.radius;\n };\n \n // Calculate active filter count\n const activeFilterCount = Object.values(filters || {}).filter(Boolean).length;\n \n // Handle schema filter change\n const handleSchemaChange = useCallback((e) => {\n const newFilters = {\n ...filters,\n schema: e.target.value || undefined\n };\n onFilterChange?.(newFilters);\n }, [filters, onFilterChange]);\n \n // Handle table filter change\n const handleTableChange = useCallback((e) => {\n const newFilters = {\n ...filters,\n table: e.target.value || undefined\n };\n onFilterChange?.(newFilters);\n }, [filters, onFilterChange]);\n \n // Handle clear all filters\n const handleClearFilters = useCallback(() => {\n onFilterChange?.({});\n }, [onFilterChange]);\n \n // Handle toggle collapse\n const handleToggle = useCallback(() => {\n onToggleCollapse?.();\n }, [onToggleCollapse]);\n \n return (\n <div style={{\n width: isCollapsed ? '48px' : '280px',\n minWidth: isCollapsed ? '48px' : '280px',\n backgroundColor: styles.colors.surface,\n borderRight: `1px solid ${styles.colors.border}`,\n transition: 'width 0.3s ease-out',\n display: 'flex',\n flexDirection: 'column',\n position: 'relative',\n overflow: 'hidden'\n }}>\n {/* Toggle Button */}\n <button\n onClick={handleToggle}\n style={{\n position: 'absolute',\n top: styles.spacing.md,\n right: styles.spacing.md,\n width: '32px',\n height: '32px',\n borderRadius: getBorderRadius('sm'),\n border: `1px solid ${styles.colors.border}`,\n backgroundColor: styles.colors.background,\n color: styles.colors.text,\n fontSize: styles.typography.fontSize.md,\n cursor: 'pointer',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n zIndex: 1,\n transition: 'all 0.2s'\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.backgroundColor = styles.colors.surfaceHover || styles.colors.surface;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.backgroundColor = styles.colors.background;\n }}\n >\n {isCollapsed ? '→' : '←'}\n </button>\n \n {/* Filter Icon when collapsed */}\n {isCollapsed && (\n <div style={{\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n justifyContent: 'center',\n flex: 1,\n opacity: 1,\n transition: 'opacity 0.3s'\n }}>\n <div style={{\n fontSize: styles.typography.fontSize.xl,\n color: styles.colors.textSecondary,\n marginBottom: styles.spacing.sm\n }}>\n 🔍\n </div>\n {activeFilterCount > 0 && (\n <div style={{\n width: '24px',\n height: '24px',\n borderRadius: '50%',\n backgroundColor: styles.colors.primary,\n color: 'white',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n fontSize: styles.typography.fontSize.xs,\n fontWeight: styles.typography.fontWeight?.bold || '700'\n }}>\n {activeFilterCount}\n </div>\n )}\n </div>\n )}\n \n {/* Filter Content */}\n <div style={{\n padding: styles.spacing.lg,\n opacity: isCollapsed ? 0 : 1,\n transition: 'opacity 0.3s',\n pointerEvents: isCollapsed ? 'none' : 'auto',\n flex: 1,\n display: 'flex',\n flexDirection: 'column'\n }}>\n {/* Header */}\n <div style={{\n marginBottom: styles.spacing.xl,\n paddingRight: '40px'\n }}>\n <h2 style={{\n margin: 0,\n fontSize: styles.typography.fontSize.lg,\n fontWeight: styles.typography.fontWeight?.semibold || '600',\n color: styles.colors.text,\n marginBottom: styles.spacing.xs\n }}>\n Filters\n </h2>\n {activeFilterCount > 0 && (\n <div style={{\n fontSize: styles.typography.fontSize.sm,\n color: styles.colors.textSecondary\n }}>\n {activeFilterCount} active filter{activeFilterCount !== 1 ? 's' : ''}\n </div>\n )}\n </div>\n \n {/* Filter Controls */}\n <div style={{\n flex: 1,\n display: 'flex',\n flexDirection: 'column',\n gap: styles.spacing.lg\n }}>\n {/* Schema Filter */}\n <div>\n <label style={{\n display: 'block',\n marginBottom: styles.spacing.sm,\n fontSize: styles.typography.fontSize.sm,\n fontWeight: styles.typography.fontWeight?.medium || '500',\n color: styles.colors.textSecondary\n }}>\n Schema\n </label>\n <select\n value={filters?.schema || ''}\n onChange={handleSchemaChange}\n style={{\n width: '100%',\n padding: styles.spacing.sm,\n fontSize: styles.typography.fontSize.md,\n border: `1px solid ${styles.colors.border}`,\n borderRadius: getBorderRadius('sm'),\n backgroundColor: styles.colors.background,\n color: styles.colors.text,\n cursor: 'pointer'\n }}\n >\n <option value=\"\">All Schemas</option>\n {schemas.map((schema) => (\n <option key={schema} value={schema}>\n {schema}\n </option>\n ))}\n </select>\n </div>\n \n {/* Table Filter */}\n <div>\n <label style={{\n display: 'block',\n marginBottom: styles.spacing.sm,\n fontSize: styles.typography.fontSize.sm,\n fontWeight: styles.typography.fontWeight?.medium || '500',\n color: styles.colors.textSecondary\n }}>\n Base Table\n </label>\n <select\n value={filters?.table || ''}\n onChange={handleTableChange}\n style={{\n width: '100%',\n padding: styles.spacing.sm,\n fontSize: styles.typography.fontSize.md,\n border: `1px solid ${styles.colors.border}`,\n borderRadius: getBorderRadius('sm'),\n backgroundColor: styles.colors.background,\n color: styles.colors.text,\n cursor: 'pointer'\n }}\n >\n <option value=\"\">All Tables</option>\n {tables.map((table) => (\n <option key={table} value={table}>\n {table}\n </option>\n ))}\n </select>\n </div>\n \n {/* Active Filters Display */}\n {activeFilterCount > 0 && (\n <div>\n <div style={{\n marginBottom: styles.spacing.sm,\n fontSize: styles.typography.fontSize.sm,\n fontWeight: styles.typography.fontWeight?.medium || '500',\n color: styles.colors.textSecondary\n }}>\n Active Filters\n </div>\n <div style={{\n display: 'flex',\n flexDirection: 'column',\n gap: styles.spacing.xs\n }}>\n {filters?.schema && (\n <div style={{\n padding: styles.spacing.sm,\n backgroundColor: styles.colors.primary + '15',\n borderRadius: getBorderRadius('sm'),\n display: 'flex',\n justifyContent: 'space-between',\n alignItems: 'center'\n }}>\n <div style={{\n fontSize: styles.typography.fontSize.sm,\n color: styles.colors.text\n }}>\n <span style={{\n color: styles.colors.textSecondary,\n marginRight: styles.spacing.xs\n }}>\n Schema:\n </span>\n <strong>{filters.schema}</strong>\n </div>\n <button\n onClick={() => handleSchemaChange({ target: { value: '' } })}\n style={{\n width: '20px',\n height: '20px',\n borderRadius: '50%',\n border: 'none',\n backgroundColor: 'transparent',\n color: styles.colors.textSecondary,\n fontSize: styles.typography.fontSize.sm,\n cursor: 'pointer',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n padding: 0\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.backgroundColor = styles.colors.surfaceHover || styles.colors.surface;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.backgroundColor = 'transparent';\n }}\n >\n ✕\n </button>\n </div>\n )}\n {filters?.table && (\n <div style={{\n padding: styles.spacing.sm,\n backgroundColor: styles.colors.primary + '15',\n borderRadius: getBorderRadius('sm'),\n display: 'flex',\n justifyContent: 'space-between',\n alignItems: 'center'\n }}>\n <div style={{\n fontSize: styles.typography.fontSize.sm,\n color: styles.colors.text\n }}>\n <span style={{\n color: styles.colors.textSecondary,\n marginRight: styles.spacing.xs\n }}>\n Table:\n </span>\n <strong>{filters.table}</strong>\n </div>\n <button\n onClick={() => handleTableChange({ target: { value: '' } })}\n style={{\n width: '20px',\n height: '20px',\n borderRadius: '50%',\n border: 'none',\n backgroundColor: 'transparent',\n color: styles.colors.textSecondary,\n fontSize: styles.typography.fontSize.sm,\n cursor: 'pointer',\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'center',\n padding: 0\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.backgroundColor = styles.colors.surfaceHover || styles.colors.surface;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.backgroundColor = 'transparent';\n }}\n >\n ✕\n </button>\n </div>\n )}\n </div>\n </div>\n )}\n </div>\n \n {/* Clear All Button */}\n {activeFilterCount > 0 && (\n <div style={{\n marginTop: styles.spacing.xl,\n paddingTop: styles.spacing.lg,\n borderTop: `1px solid ${styles.colors.borderLight || styles.colors.border}`\n }}>\n <button\n onClick={handleClearFilters}\n style={{\n width: '100%',\n padding: styles.spacing.md,\n backgroundColor: styles.colors.surface,\n color: styles.colors.text,\n border: `1px solid ${styles.colors.border}`,\n borderRadius: getBorderRadius('md'),\n fontSize: styles.typography.fontSize.md,\n fontWeight: styles.typography.fontWeight?.medium || '500',\n cursor: 'pointer',\n transition: 'background-color 0.2s'\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.backgroundColor = styles.colors.error + '15';\n e.currentTarget.style.color = styles.colors.error;\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.backgroundColor = styles.colors.surface;\n e.currentTarget.style.color = styles.colors.text;\n }}\n >\n Clear All Filters\n </button>\n </div>\n )}\n </div>\n </div>\n );\n}",
1582
- "dependencies": [],
1583
- "libraries": []
1584
- },
1585
- {
1586
- "name": "OpenRecordButton",
1587
- "location": "registry",
1588
- "namespace": "Generic/Navigation",
1589
- "version": "^1.0.0"
1590
- }
1591
- ],
1592
- "libraries": [],
1593
- "code": "function EntityBrowser({ utilities, styles, components, callbacks, savedUserSettings, onSaveUserSettings }) {\n // Extract child components\n const { EntityList, EntityDetails, EntityFilter } = components;\n \n // Initialize state from saved settings where appropriate\n const [selectedEntityId, setSelectedEntityId] = useState(savedUserSettings?.selectedEntityId);\n const [viewMode, setViewMode] = useState(savedUserSettings?.viewMode || 'grid');\n const [filters, setFilters] = useState(savedUserSettings?.filters || {});\n const [sortBy, setSortBy] = useState(savedUserSettings?.sortBy || 'Name');\n const [sortDirection, setSortDirection] = useState(savedUserSettings?.sortDirection || 'asc');\n const [filterPanelCollapsed, setFilterPanelCollapsed] = useState(savedUserSettings?.filterPanelCollapsed || false);\n \n // Runtime UI state (not persisted)\n const [entities, setEntities] = useState([]);\n const [entityFields, setEntityFields] = useState([]);\n const [entityRelationships, setEntityRelationships] = useState([]);\n const [loading, setLoading] = useState(true);\n const [detailsPanelOpen, setDetailsPanelOpen] = useState(false);\n const [searchQuery, setSearchQuery] = useState('');\n const [uniqueSchemas, setUniqueSchemas] = useState([]);\n const [uniqueTables, setUniqueTables] = useState([]);\n \n // Load entities on mount and when filters/sort change\n useEffect(() => {\n const loadEntities = async () => {\n setLoading(true);\n try {\n // Build filter string\n let filterParts = [];\n if (filters.schema) {\n filterParts.push(`SchemaName = '${filters.schema}'`);\n }\n if (filters.table) {\n filterParts.push(`BaseTable = '${filters.table}'`);\n }\n if (searchQuery) {\n filterParts.push(`(Name LIKE '%${searchQuery}%' OR DisplayName LIKE '%${searchQuery}%' OR Description LIKE '%${searchQuery}%')`);\n }\n \n const result = await utilities.rv.RunView({\n EntityName: 'MJ: Entities',\n Fields: ['ID', 'Name', 'DisplayName', 'NameSuffix', 'Description', 'SchemaName', 'BaseTable', 'BaseView'],\n OrderBy: `${sortBy} ${sortDirection.toUpperCase()}`,\n ExtraFilter: filterParts.length > 0 ? filterParts.join(' AND ') : undefined\n });\n \n if (result?.Success && result?.Results) {\n setEntities(result.Results);\n \n // Extract unique schemas and tables for filter dropdowns\n const schemas = [...new Set(result.Results.map(e => e.SchemaName).filter(Boolean))];\n const tables = [...new Set(result.Results.map(e => e.BaseTable).filter(Boolean))];\n setUniqueSchemas(schemas);\n setUniqueTables(tables);\n } else {\n console.error('Failed to load entities:', result?.ErrorMessage);\n setEntities([]);\n }\n } catch (error) {\n console.error('Error loading entities:', error);\n setEntities([]);\n } finally {\n setLoading(false);\n }\n };\n \n loadEntities();\n }, [filters, sortBy, sortDirection, searchQuery, utilities.rv]);\n \n // Load entity details when selection changes\n useEffect(() => {\n const loadEntityDetails = async () => {\n if (!selectedEntityId) {\n setEntityFields([]);\n setEntityRelationships([]);\n return;\n }\n \n try {\n // Load fields\n const fieldsResult = await utilities.rv.RunView({\n EntityName: 'MJ: Entity Fields',\n Fields: ['Name', 'DisplayName', 'Type', 'Length', 'AllowsNull', 'IsPrimaryKey', 'IsUnique', 'Sequence'],\n OrderBy: 'Sequence ASC, Name ASC',\n ExtraFilter: `EntityID = '${selectedEntityId}'`\n });\n \n if (fieldsResult?.Success && fieldsResult?.Results) {\n setEntityFields(fieldsResult.Results);\n } else {\n setEntityFields([]);\n }\n \n // Load relationships\n const relationshipsResult = await utilities.rv.RunView({\n EntityName: 'MJ: Entity Relationships',\n Fields: ['RelatedEntity', 'Type', 'DisplayName', 'RelatedEntityJoinField', 'Sequence'],\n OrderBy: 'Sequence ASC, RelatedEntity ASC',\n ExtraFilter: `EntityID = '${selectedEntityId}'`\n });\n \n if (relationshipsResult?.Success && relationshipsResult?.Results) {\n setEntityRelationships(relationshipsResult.Results);\n } else {\n setEntityRelationships([]);\n }\n } catch (error) {\n console.error('Error loading entity details:', error);\n setEntityFields([]);\n setEntityRelationships([]);\n }\n };\n \n loadEntityDetails();\n }, [selectedEntityId, utilities.rv]);\n \n // Handle entity selection\n const handleSelectEntity = useCallback((entityId) => {\n setSelectedEntityId(entityId);\n setDetailsPanelOpen(true);\n \n // Save user preference\n onSaveUserSettings?.({\n ...savedUserSettings,\n selectedEntityId: entityId\n });\n }, [savedUserSettings, onSaveUserSettings]);\n \n // Handle view mode change\n const handleViewModeChange = useCallback((mode) => {\n setViewMode(mode);\n \n // Save preference\n onSaveUserSettings?.({\n ...savedUserSettings,\n viewMode: mode\n });\n }, [savedUserSettings, onSaveUserSettings]);\n \n // Handle filter changes\n const handleFilterChange = useCallback((newFilters) => {\n setFilters(newFilters);\n \n // Save filter preferences\n onSaveUserSettings?.({\n ...savedUserSettings,\n filters: newFilters\n });\n }, [savedUserSettings, onSaveUserSettings]);\n \n // Handle sort changes\n const handleSortChange = useCallback((newSortBy, newSortDirection) => {\n setSortBy(newSortBy);\n setSortDirection(newSortDirection);\n \n // Save sort preferences\n onSaveUserSettings?.({\n ...savedUserSettings,\n sortBy: newSortBy,\n sortDirection: newSortDirection\n });\n }, [savedUserSettings, onSaveUserSettings]);\n \n // Handle filter panel toggle\n const handleToggleFilter = useCallback(() => {\n const newCollapsed = !filterPanelCollapsed;\n setFilterPanelCollapsed(newCollapsed);\n \n // Save collapsed state\n onSaveUserSettings?.({\n ...savedUserSettings,\n filterPanelCollapsed: newCollapsed\n });\n }, [filterPanelCollapsed, savedUserSettings, onSaveUserSettings]);\n \n // Handle opening entity record (kept for backward compatibility with details panel)\n const handleOpenRecord = useCallback((entityName) => {\n console.log('Root handleOpenRecord called with entityName:', entityName);\n console.log('Callbacks object:', callbacks);\n if (callbacks?.OpenEntityRecord && entityName) {\n console.log('Calling OpenEntityRecord callback with:', 'MJ: Entities', entityName);\n // Open the Entities entity record for the selected entity\n callbacks.OpenEntityRecord('MJ: Entities', [{ FieldName: 'Name', Value: entityName }]);\n } else {\n console.error('OpenEntityRecord callback not available or entityName missing');\n }\n }, [callbacks]);\n \n // Handle closing details panel\n const handleCloseDetails = useCallback(() => {\n setDetailsPanelOpen(false);\n }, []);\n \n // Handle search\n const handleSearch = useCallback((query) => {\n setSearchQuery(query);\n }, []);\n \n // Get selected entity object\n const selectedEntity = entities.find(e => e.ID === selectedEntityId);\n \n // Helper function to get border radius value\n const getBorderRadius = (size) => {\n return typeof styles.borders.radius === 'object' ? styles.borders.radius[size] : styles.borders.radius;\n };\n \n // Loading state\n if (loading && entities.length === 0) {\n return (\n <div style={{\n display: 'flex',\n justifyContent: 'center',\n alignItems: 'center',\n height: '100vh',\n fontSize: styles.typography.fontSize.lg,\n color: styles.colors.textSecondary\n }}>\n Loading entities...\n </div>\n );\n }\n \n return (\n <div style={{\n display: 'flex',\n height: '100vh',\n backgroundColor: styles.colors.background,\n overflow: 'hidden'\n }}>\n {/* Filter Panel */}\n {EntityFilter && (\n <EntityFilter\n filters={filters}\n onFilterChange={handleFilterChange}\n schemas={uniqueSchemas}\n tables={uniqueTables}\n isCollapsed={filterPanelCollapsed}\n onToggleCollapse={handleToggleFilter}\n savedUserSettings={savedUserSettings?.filterPanel}\n onSaveUserSettings={(settings) => onSaveUserSettings?.({\n ...savedUserSettings,\n filterPanel: settings\n })}\n utilities={utilities}\n styles={styles}\n components={components}\n callbacks={callbacks}\n />\n )}\n \n {/* Main Content Area */}\n <div style={{\n flex: 1,\n display: 'flex',\n flexDirection: 'column',\n overflow: 'hidden'\n }}>\n {/* Header */}\n <div style={{\n padding: styles.spacing.lg,\n borderBottom: `1px solid ${styles.colors.border}`,\n backgroundColor: styles.colors.surface\n }}>\n <div style={{\n display: 'flex',\n justifyContent: 'space-between',\n alignItems: 'center',\n marginBottom: styles.spacing.md\n }}>\n <h1 style={{\n margin: 0,\n fontSize: styles.typography.fontSize.xxl || styles.typography.fontSize.xl,\n fontWeight: styles.typography.fontWeight?.bold || '700',\n color: styles.colors.text\n }}>\n Entity Browser\n </h1>\n \n {/* View Mode Toggle */}\n <div style={{\n display: 'flex',\n gap: styles.spacing.sm,\n alignItems: 'center'\n }}>\n <span style={{\n fontSize: styles.typography.fontSize.md,\n color: styles.colors.textSecondary\n }}>\n View:\n </span>\n <button\n onClick={() => handleViewModeChange('grid')}\n style={{\n padding: `${styles.spacing.sm} ${styles.spacing.md}`,\n backgroundColor: viewMode === 'grid' ? styles.colors.primary : styles.colors.background,\n color: viewMode === 'grid' ? 'white' : styles.colors.text,\n border: `1px solid ${styles.colors.border}`,\n borderRadius: getBorderRadius('sm'),\n cursor: 'pointer',\n fontSize: styles.typography.fontSize.md\n }}\n >\n Grid\n </button>\n <button\n onClick={() => handleViewModeChange('card')}\n style={{\n padding: `${styles.spacing.sm} ${styles.spacing.md}`,\n backgroundColor: viewMode === 'card' ? styles.colors.primary : styles.colors.background,\n color: viewMode === 'card' ? 'white' : styles.colors.text,\n border: `1px solid ${styles.colors.border}`,\n borderRadius: getBorderRadius('sm'),\n cursor: 'pointer',\n fontSize: styles.typography.fontSize.md\n }}\n >\n Cards\n </button>\n </div>\n </div>\n \n {/* Search Bar */}\n <div style={{\n display: 'flex',\n gap: styles.spacing.md\n }}>\n <input\n type=\"text\"\n placeholder=\"Search entities...\"\n value={searchQuery}\n onChange={(e) => handleSearch(e.target.value)}\n style={{\n flex: 1,\n padding: styles.spacing.md,\n fontSize: styles.typography.fontSize.md,\n border: `1px solid ${styles.colors.border}`,\n borderRadius: getBorderRadius('sm'),\n backgroundColor: styles.colors.background\n }}\n />\n {searchQuery && (\n <button\n onClick={() => handleSearch('')}\n style={{\n padding: `${styles.spacing.sm} ${styles.spacing.md}`,\n backgroundColor: styles.colors.surfaceHover || styles.colors.surface,\n color: styles.colors.text,\n border: `1px solid ${styles.colors.border}`,\n borderRadius: getBorderRadius('sm'),\n cursor: 'pointer',\n fontSize: styles.typography.fontSize.md\n }}\n >\n Clear\n </button>\n )}\n </div>\n </div>\n \n {/* Entity List */}\n <div style={{\n flex: 1,\n overflow: 'auto',\n padding: styles.spacing.lg\n }}>\n {EntityList && (\n <EntityList\n entities={entities}\n viewMode={viewMode}\n selectedEntityId={selectedEntityId}\n onSelectEntity={handleSelectEntity}\n sortBy={sortBy}\n sortDirection={sortDirection}\n onSortChange={handleSortChange}\n savedUserSettings={savedUserSettings?.entityList}\n onSaveUserSettings={(settings) => onSaveUserSettings?.({\n ...savedUserSettings,\n entityList: settings\n })}\n utilities={utilities}\n styles={styles}\n components={components}\n callbacks={callbacks}\n />\n )}\n \n {/* Empty State */}\n {entities.length === 0 && !loading && (\n <div style={{\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n justifyContent: 'center',\n padding: styles.spacing.xxl || styles.spacing.xl,\n color: styles.colors.textSecondary\n }}>\n <div style={{\n fontSize: styles.typography.fontSize.xl,\n marginBottom: styles.spacing.md\n }}>\n No entities found\n </div>\n <div style={{\n fontSize: styles.typography.fontSize.md\n }}>\n {searchQuery || Object.keys(filters).length > 0\n ? 'Try adjusting your filters or search query'\n : 'No entities are available'}\n </div>\n </div>\n )}\n </div>\n </div>\n \n {/* Details Panel */}\n {EntityDetails && (\n <EntityDetails\n entity={selectedEntity}\n fields={entityFields}\n relationships={entityRelationships}\n isOpen={detailsPanelOpen}\n onClose={handleCloseDetails}\n onOpenRecord={() => handleOpenRecord(selectedEntity?.Name)}\n savedUserSettings={savedUserSettings?.detailsPanel}\n onSaveUserSettings={(settings) => onSaveUserSettings?.({\n ...savedUserSettings,\n detailsPanel: settings\n })}\n utilities={utilities}\n styles={styles}\n components={components}\n callbacks={callbacks}\n />\n )}\n </div>\n );\n}"
1594
- }