@memberjunction/server 2.112.0 → 2.113.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.
- package/dist/agents/skip-agent.d.ts +4 -4
- package/dist/agents/skip-agent.d.ts.map +1 -1
- package/dist/agents/skip-agent.js +951 -808
- package/dist/agents/skip-agent.js.map +1 -1
- package/dist/agents/skip-sdk.d.ts +1 -1
- package/dist/agents/skip-sdk.d.ts.map +1 -1
- package/dist/agents/skip-sdk.js +43 -53
- package/dist/agents/skip-sdk.js.map +1 -1
- package/dist/apolloServer/index.js +1 -1
- package/dist/auth/AuthProviderFactory.d.ts +1 -1
- package/dist/auth/AuthProviderFactory.d.ts.map +1 -1
- package/dist/auth/AuthProviderFactory.js +3 -1
- package/dist/auth/AuthProviderFactory.js.map +1 -1
- package/dist/auth/BaseAuthProvider.d.ts +1 -1
- package/dist/auth/BaseAuthProvider.d.ts.map +1 -1
- package/dist/auth/BaseAuthProvider.js +2 -3
- package/dist/auth/BaseAuthProvider.js.map +1 -1
- package/dist/auth/IAuthProvider.d.ts +1 -1
- package/dist/auth/IAuthProvider.d.ts.map +1 -1
- package/dist/auth/exampleNewUserSubClass.d.ts.map +1 -1
- package/dist/auth/exampleNewUserSubClass.js +1 -1
- package/dist/auth/exampleNewUserSubClass.js.map +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +6 -6
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/initializeProviders.js +1 -1
- package/dist/auth/initializeProviders.js.map +1 -1
- package/dist/auth/newUsers.d.ts +1 -1
- package/dist/auth/newUsers.d.ts.map +1 -1
- package/dist/auth/newUsers.js +7 -7
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/auth/providers/Auth0Provider.d.ts +1 -1
- package/dist/auth/providers/Auth0Provider.d.ts.map +1 -1
- package/dist/auth/providers/Auth0Provider.js +1 -1
- package/dist/auth/providers/Auth0Provider.js.map +1 -1
- package/dist/auth/providers/CognitoProvider.d.ts +1 -1
- package/dist/auth/providers/CognitoProvider.d.ts.map +1 -1
- package/dist/auth/providers/CognitoProvider.js +6 -3
- package/dist/auth/providers/CognitoProvider.js.map +1 -1
- package/dist/auth/providers/GoogleProvider.d.ts +1 -1
- package/dist/auth/providers/GoogleProvider.d.ts.map +1 -1
- package/dist/auth/providers/GoogleProvider.js +1 -1
- package/dist/auth/providers/GoogleProvider.js.map +1 -1
- package/dist/auth/providers/MSALProvider.d.ts +1 -1
- package/dist/auth/providers/MSALProvider.d.ts.map +1 -1
- package/dist/auth/providers/MSALProvider.js +1 -1
- package/dist/auth/providers/MSALProvider.js.map +1 -1
- package/dist/auth/providers/OktaProvider.d.ts +1 -1
- package/dist/auth/providers/OktaProvider.d.ts.map +1 -1
- package/dist/auth/providers/OktaProvider.js +1 -1
- package/dist/auth/providers/OktaProvider.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +10 -22
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +7 -9
- package/dist/context.js.map +1 -1
- package/dist/entitySubclasses/entityPermissions.server.d.ts +1 -1
- package/dist/entitySubclasses/entityPermissions.server.d.ts.map +1 -1
- package/dist/entitySubclasses/entityPermissions.server.js +1 -1
- package/dist/entitySubclasses/entityPermissions.server.js.map +1 -1
- package/dist/generated/generated.d.ts +788 -658
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +2050 -3054
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/KeyInputOutputTypes.d.ts +1 -1
- package/dist/generic/KeyInputOutputTypes.d.ts.map +1 -1
- package/dist/generic/KeyInputOutputTypes.js +1 -1
- package/dist/generic/KeyInputOutputTypes.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +1 -1
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +10 -15
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +1 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +15 -15
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -18
- package/dist/index.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts +2 -2
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +30 -28
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/AskSkipResolver.d.ts +2 -2
- package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
- package/dist/resolvers/AskSkipResolver.js +50 -60
- package/dist/resolvers/AskSkipResolver.js.map +1 -1
- package/dist/resolvers/ComponentRegistryResolver.d.ts.map +1 -1
- package/dist/resolvers/ComponentRegistryResolver.js +38 -36
- package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
- package/dist/resolvers/CreateQueryResolver.d.ts +1 -1
- package/dist/resolvers/CreateQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/CreateQueryResolver.js +40 -43
- package/dist/resolvers/CreateQueryResolver.js.map +1 -1
- package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
- package/dist/resolvers/DatasetResolver.js +1 -1
- package/dist/resolvers/DatasetResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/EntityResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityResolver.js +1 -1
- package/dist/resolvers/EntityResolver.js.map +1 -1
- package/dist/resolvers/FileCategoryResolver.js +1 -1
- package/dist/resolvers/FileCategoryResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.js +1 -1
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.d.ts +1 -1
- package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.js +5 -5
- package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +6 -8
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.d.ts +3 -3
- package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.js +3 -3
- package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts.map +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.js +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +11 -11
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/ReportResolver.js +1 -1
- package/dist/resolvers/ReportResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +28 -27
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunAIPromptResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIPromptResolver.js +31 -31
- package/dist/resolvers/RunAIPromptResolver.js.map +1 -1
- package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTemplateResolver.js +9 -9
- package/dist/resolvers/RunTemplateResolver.js.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js +10 -10
- package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
- package/dist/resolvers/SyncDataResolver.d.ts +1 -1
- package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncDataResolver.js +14 -15
- package/dist/resolvers/SyncDataResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.d.ts +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +44 -48
- package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
- package/dist/resolvers/TaskResolver.d.ts.map +1 -1
- package/dist/resolvers/TaskResolver.js +7 -7
- package/dist/resolvers/TaskResolver.js.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.d.ts +1 -1
- package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.js +12 -12
- package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
- package/dist/resolvers/UserFavoriteResolver.d.ts +1 -1
- package/dist/resolvers/UserFavoriteResolver.d.ts.map +1 -1
- package/dist/resolvers/UserFavoriteResolver.js +1 -1
- package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
- package/dist/resolvers/UserViewResolver.d.ts.map +1 -1
- package/dist/resolvers/UserViewResolver.js.map +1 -1
- package/dist/rest/EntityCRUDHandler.d.ts +1 -1
- package/dist/rest/EntityCRUDHandler.d.ts.map +1 -1
- package/dist/rest/EntityCRUDHandler.js +16 -14
- package/dist/rest/EntityCRUDHandler.js.map +1 -1
- package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
- package/dist/rest/RESTEndpointHandler.js +25 -23
- package/dist/rest/RESTEndpointHandler.js.map +1 -1
- package/dist/rest/ViewOperationsHandler.d.ts +1 -1
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +21 -17
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/scheduler/LearningCycleScheduler.d.ts.map +1 -1
- package/dist/scheduler/LearningCycleScheduler.js.map +1 -1
- package/dist/services/ScheduledJobsService.d.ts.map +1 -1
- package/dist/services/ScheduledJobsService.js +6 -4
- package/dist/services/ScheduledJobsService.js.map +1 -1
- package/dist/services/TaskOrchestrator.d.ts +1 -1
- package/dist/services/TaskOrchestrator.d.ts.map +1 -1
- package/dist/services/TaskOrchestrator.js +30 -30
- package/dist/services/TaskOrchestrator.js.map +1 -1
- package/dist/types.d.ts +3 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/dist/util.d.ts +1 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +2 -2
- package/dist/util.js.map +1 -1
- package/package.json +39 -36
- package/src/agents/skip-agent.ts +1200 -1067
- package/src/agents/skip-sdk.ts +851 -877
- package/src/apolloServer/index.ts +2 -2
- package/src/auth/AuthProviderFactory.ts +14 -8
- package/src/auth/BaseAuthProvider.ts +4 -5
- package/src/auth/IAuthProvider.ts +2 -2
- package/src/auth/exampleNewUserSubClass.ts +2 -9
- package/src/auth/index.ts +26 -31
- package/src/auth/initializeProviders.ts +3 -3
- package/src/auth/newUsers.ts +134 -166
- package/src/auth/providers/Auth0Provider.ts +5 -5
- package/src/auth/providers/CognitoProvider.ts +10 -7
- package/src/auth/providers/GoogleProvider.ts +5 -4
- package/src/auth/providers/MSALProvider.ts +5 -5
- package/src/auth/providers/OktaProvider.ts +7 -6
- package/src/config.ts +54 -63
- package/src/context.ts +30 -42
- package/src/entitySubclasses/entityPermissions.server.ts +3 -3
- package/src/generated/generated.ts +40442 -48106
- package/src/generic/KeyInputOutputTypes.ts +6 -3
- package/src/generic/ResolverBase.ts +78 -119
- package/src/generic/RunViewResolver.ts +23 -27
- package/src/index.ts +48 -66
- package/src/resolvers/ActionResolver.ts +57 -46
- package/src/resolvers/AskSkipResolver.ts +533 -607
- package/src/resolvers/ComponentRegistryResolver.ts +562 -547
- package/src/resolvers/CreateQueryResolver.ts +655 -683
- package/src/resolvers/DatasetResolver.ts +6 -5
- package/src/resolvers/EntityCommunicationsResolver.ts +1 -1
- package/src/resolvers/EntityRecordNameResolver.ts +5 -9
- package/src/resolvers/EntityResolver.ts +7 -9
- package/src/resolvers/FileCategoryResolver.ts +2 -2
- package/src/resolvers/FileResolver.ts +4 -4
- package/src/resolvers/GetDataContextDataResolver.ts +118 -106
- package/src/resolvers/GetDataResolver.ts +205 -194
- package/src/resolvers/MergeRecordsResolver.ts +5 -5
- package/src/resolvers/PotentialDuplicateRecordResolver.ts +1 -1
- package/src/resolvers/QueryResolver.ts +78 -95
- package/src/resolvers/ReportResolver.ts +2 -2
- package/src/resolvers/RunAIAgentResolver.ts +828 -818
- package/src/resolvers/RunAIPromptResolver.ts +709 -693
- package/src/resolvers/RunTemplateResolver.ts +103 -105
- package/src/resolvers/SqlLoggingConfigResolver.ts +72 -69
- package/src/resolvers/SyncDataResolver.ts +352 -386
- package/src/resolvers/SyncRolesUsersResolver.ts +350 -387
- package/src/resolvers/TaskResolver.ts +115 -110
- package/src/resolvers/TransactionGroupResolver.ts +138 -143
- package/src/resolvers/UserFavoriteResolver.ts +8 -17
- package/src/resolvers/UserViewResolver.ts +12 -17
- package/src/rest/EntityCRUDHandler.ts +268 -291
- package/src/rest/RESTEndpointHandler.ts +776 -782
- package/src/rest/ViewOperationsHandler.ts +195 -191
- package/src/scheduler/LearningCycleScheduler.ts +52 -8
- package/src/services/ScheduledJobsService.ts +132 -129
- package/src/services/TaskOrchestrator.ts +776 -792
- package/src/types.ts +9 -15
- package/src/util.ts +109 -112
package/src/agents/skip-agent.ts
CHANGED
|
@@ -6,49 +6,53 @@
|
|
|
6
6
|
* while maintaining compatibility with the existing Skip infrastructure.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { BaseAgent } from
|
|
10
|
-
import { ExecuteAgentParams, AgentConfiguration, BaseAgentNextStep } from '@memberjunction/ai-core-plus';
|
|
9
|
+
import { BaseAgent } from "@memberjunction/ai-agents";
|
|
11
10
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
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";
|
|
23
27
|
|
|
24
28
|
/**
|
|
25
29
|
* Context type for Skip agent execution
|
|
26
30
|
*/
|
|
27
31
|
export interface SkipAgentContext {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Optional data context ID to load
|
|
34
|
+
*/
|
|
35
|
+
dataContextId?: string;
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Optional pre-loaded data context
|
|
39
|
+
*/
|
|
40
|
+
dataContext?: DataContext;
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Conversation ID for tracking Skip conversations
|
|
44
|
+
*/
|
|
45
|
+
conversationId?: string;
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Force entity metadata refresh
|
|
49
|
+
*/
|
|
50
|
+
forceEntityRefresh?: boolean;
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Database connection (injected by caller)
|
|
54
|
+
*/
|
|
55
|
+
dataSource?: any;
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
/**
|
|
@@ -56,25 +60,25 @@ export interface SkipAgentContext {
|
|
|
56
60
|
* Contains the full Skip API response for downstream consumers
|
|
57
61
|
*/
|
|
58
62
|
export interface SkipAgentPayload {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
/**
|
|
64
|
+
* The full Skip API response
|
|
65
|
+
*/
|
|
66
|
+
skipResponse: SkipAPIResponse;
|
|
63
67
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Response phase from Skip
|
|
70
|
+
*/
|
|
71
|
+
responsePhase: string;
|
|
68
72
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Conversation ID
|
|
75
|
+
*/
|
|
76
|
+
conversationId: string;
|
|
73
77
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
/**
|
|
79
|
+
* User-facing message (title or clarifying question)
|
|
80
|
+
*/
|
|
81
|
+
message?: string;
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
/**
|
|
@@ -89,1286 +93,1415 @@ export interface SkipAgentPayload {
|
|
|
89
93
|
*/
|
|
90
94
|
@RegisterClass(BaseAgent, 'SkipProxyAgent')
|
|
91
95
|
export class SkipProxyAgent extends BaseAgent {
|
|
92
|
-
|
|
96
|
+
private skipSDK: SkipSDK;
|
|
93
97
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
constructor() {
|
|
99
|
+
super();
|
|
100
|
+
this.skipSDK = new SkipSDK();
|
|
101
|
+
}
|
|
98
102
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
LogStatus(`[SkipProxyAgent] Starting Skip agent execution`);
|
|
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; }> {
|
|
107
110
|
|
|
108
|
-
|
|
109
|
-
const context = params.context || ({} as SkipAgentContext);
|
|
110
|
-
const conversationId = params.data?.conversationId;
|
|
111
|
+
LogStatus(`[SkipProxyAgent] Starting Skip agent execution`);
|
|
111
112
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
finalStep: {
|
|
116
|
-
terminate: true,
|
|
117
|
-
step: 'Failed',
|
|
118
|
-
message: 'Missing required contextUser',
|
|
119
|
-
errorMessage: 'Missing required contextUser',
|
|
120
|
-
} as BaseAgentNextStep<P>,
|
|
121
|
-
stepCount: 1,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
113
|
+
// Extract context
|
|
114
|
+
const context = params.context || {} as SkipAgentContext;
|
|
115
|
+
const conversationId = params.data?.conversationId;
|
|
124
116
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
+
}
|
|
134
129
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
includeEntities: true,
|
|
144
|
-
includeQueries: true,
|
|
145
|
-
includeNotes: true,
|
|
146
|
-
includeRequests: false,
|
|
147
|
-
forceEntityRefresh: context.forceEntityRefresh || false,
|
|
148
|
-
includeCallbackAuth: true,
|
|
149
|
-
onStatusUpdate: (message: string, responsePhase?: string) => {
|
|
150
|
-
// Forward Skip status updates to MJ progress callback
|
|
151
|
-
if (params.onProgress) {
|
|
152
|
-
params.onProgress({
|
|
153
|
-
step: 'prompt_execution', // Skip execution is essentially a prompt to an external service
|
|
154
|
-
message,
|
|
155
|
-
percentage: 0, // Skip doesn't provide percentage
|
|
156
|
-
metadata: {
|
|
157
|
-
conversationId,
|
|
158
|
-
responsePhase,
|
|
159
|
-
},
|
|
160
|
-
});
|
|
130
|
+
// Load conversation messages from database if conversationId is provided
|
|
131
|
+
// This ensures we get real UUIDs from ConversationDetailEntity 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 || []);
|
|
161
138
|
}
|
|
162
|
-
},
|
|
163
|
-
};
|
|
164
139
|
|
|
165
|
-
|
|
166
|
-
|
|
140
|
+
// Prepare Skip SDK call options
|
|
141
|
+
const skipOptions: SkipCallOptions = {
|
|
142
|
+
messages: skipMessages,
|
|
143
|
+
conversationId,
|
|
144
|
+
dataContext: context.dataContext,
|
|
145
|
+
requestPhase: 'initial_request', // Could be parameterized if needed
|
|
146
|
+
contextUser: params.contextUser,
|
|
147
|
+
dataSource: context.dataSource,
|
|
148
|
+
includeEntities: true,
|
|
149
|
+
includeQueries: true,
|
|
150
|
+
includeNotes: true,
|
|
151
|
+
includeRequests: false,
|
|
152
|
+
forceEntityRefresh: context.forceEntityRefresh || false,
|
|
153
|
+
includeCallbackAuth: true,
|
|
154
|
+
onStatusUpdate: (message: string, responsePhase?: string) => {
|
|
155
|
+
// Forward Skip status updates to MJ progress callback
|
|
156
|
+
if (params.onProgress) {
|
|
157
|
+
params.onProgress({
|
|
158
|
+
step: 'prompt_execution', // Skip execution is essentially a prompt to an external service
|
|
159
|
+
message,
|
|
160
|
+
percentage: 0, // Skip doesn't provide percentage
|
|
161
|
+
metadata: {
|
|
162
|
+
conversationId,
|
|
163
|
+
responsePhase
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
167
169
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
LogError(`[SkipProxyAgent] Skip API call failed: ${result.error}`);
|
|
171
|
-
return {
|
|
172
|
-
finalStep: {
|
|
173
|
-
terminate: true,
|
|
174
|
-
step: 'Failed',
|
|
175
|
-
message: 'Skip API call failed',
|
|
176
|
-
errorMessage: result.error,
|
|
177
|
-
} as BaseAgentNextStep<P>,
|
|
178
|
-
stepCount: 1,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
170
|
+
// Call Skip API
|
|
171
|
+
const result = await this.skipSDK.chat(skipOptions);
|
|
181
172
|
|
|
182
|
-
|
|
183
|
-
|
|
173
|
+
// Handle Skip API errors
|
|
174
|
+
if (!result.success || !result.response) {
|
|
175
|
+
LogError(`[SkipProxyAgent] Skip API call failed: ${result.error}`);
|
|
176
|
+
return {
|
|
177
|
+
finalStep: {
|
|
178
|
+
terminate: true,
|
|
179
|
+
step: 'Failed',
|
|
180
|
+
message: 'Skip API call failed',
|
|
181
|
+
errorMessage: result.error
|
|
182
|
+
} as BaseAgentNextStep<P>,
|
|
183
|
+
stepCount: 1
|
|
184
|
+
};
|
|
185
|
+
}
|
|
184
186
|
|
|
185
|
-
|
|
187
|
+
// Map Skip response to MJ agent next step
|
|
188
|
+
const nextStep = this.mapSkipResponseToNextStep(result.response, conversationId);
|
|
186
189
|
|
|
187
|
-
|
|
188
|
-
finalStep: nextStep as BaseAgentNextStep<P>,
|
|
189
|
-
stepCount: 1, // Skip is a single-step proxy
|
|
190
|
-
};
|
|
191
|
-
}
|
|
190
|
+
LogStatus(`[SkipProxyAgent] Skip execution completed with phase: ${result.responsePhase}`);
|
|
192
191
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
try {
|
|
199
|
-
const rv = new RunView();
|
|
200
|
-
const result = await rv.RunView(
|
|
201
|
-
{
|
|
202
|
-
EntityName: 'Conversation Details',
|
|
203
|
-
ExtraFilter: `ConversationID='${conversationId}'`,
|
|
204
|
-
OrderBy: '__mj_CreatedAt ASC',
|
|
205
|
-
},
|
|
206
|
-
contextUser
|
|
207
|
-
);
|
|
192
|
+
return {
|
|
193
|
+
finalStep: nextStep as BaseAgentNextStep<P>,
|
|
194
|
+
stepCount: 1 // Skip is a single-step proxy
|
|
195
|
+
};
|
|
196
|
+
}
|
|
208
197
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
198
|
+
/**
|
|
199
|
+
* Load conversation messages from database with real UUIDs using MemberJunction's RunView pattern
|
|
200
|
+
* This is the preferred method as it ensures all messages have proper conversationDetailIDs
|
|
201
|
+
*/
|
|
202
|
+
private async loadMessagesFromDatabase(conversationId: string, contextUser: UserInfo): Promise<SkipMessage[]> {
|
|
203
|
+
try {
|
|
204
|
+
const rv = new RunView();
|
|
205
|
+
const result = await rv.RunView({
|
|
206
|
+
EntityName: 'Conversation Details',
|
|
207
|
+
ExtraFilter: `ConversationID='${conversationId}'`,
|
|
208
|
+
OrderBy: '__mj_CreatedAt ASC'
|
|
209
|
+
}, contextUser);
|
|
212
210
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const skipRole: 'user' | 'system' = dbRole === 'ai' || dbRole === 'system' || dbRole === 'assistant' ? 'system' : 'user';
|
|
211
|
+
if (!result.Success) {
|
|
212
|
+
throw new Error(`Failed to load conversation details: ${result.ErrorMessage}`);
|
|
213
|
+
}
|
|
217
214
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
215
|
+
const allMessages = (result.Results || []).map((r: any) => {
|
|
216
|
+
// Map database role to Skip role
|
|
217
|
+
const dbRole = (r.Role || '').trim().toLowerCase();
|
|
218
|
+
const skipRole: 'user' | 'system' =
|
|
219
|
+
(dbRole === 'ai' || dbRole === 'system' || dbRole === 'assistant') ? 'system' : 'user';
|
|
222
220
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
hiddenToUser: r.HiddenToUser,
|
|
228
|
-
userRating: r.UserRating,
|
|
229
|
-
userFeedback: r.UserFeedback,
|
|
230
|
-
reflectionInsights: r.ReflectionInsights,
|
|
231
|
-
summaryOfEarlierConveration: r.SummaryOfEarlierConversation,
|
|
232
|
-
createdAt: r.__mj_CreatedAt,
|
|
233
|
-
updatedAt: r.__mj_UpdatedAt,
|
|
234
|
-
};
|
|
235
|
-
return message;
|
|
236
|
-
});
|
|
221
|
+
// For system messages, always send the raw Message from database
|
|
222
|
+
// Skip Brain needs the full JSON response to extract component specs for modification
|
|
223
|
+
// For user messages, use the message as-is
|
|
224
|
+
const content = r.Message;
|
|
237
225
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
226
|
+
const message: SkipMessage = {
|
|
227
|
+
content: content,
|
|
228
|
+
role: skipRole,
|
|
229
|
+
conversationDetailID: r.ID,
|
|
230
|
+
hiddenToUser: r.HiddenToUser,
|
|
231
|
+
userRating: r.UserRating,
|
|
232
|
+
userFeedback: r.UserFeedback,
|
|
233
|
+
reflectionInsights: r.ReflectionInsights,
|
|
234
|
+
summaryOfEarlierConveration: r.SummaryOfEarlierConversation,
|
|
235
|
+
createdAt: r.__mj_CreatedAt,
|
|
236
|
+
updatedAt: r.__mj_UpdatedAt,
|
|
237
|
+
};
|
|
238
|
+
return message;
|
|
239
|
+
});
|
|
244
240
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
241
|
+
// Find the index of the last user message
|
|
242
|
+
// We only want to include messages up to and including the most recent user message
|
|
243
|
+
// This filters out status messages and incomplete AI responses
|
|
244
|
+
const lastUserMessageIndex = allMessages.reduce((lastIndex, msg, currentIndex) => {
|
|
245
|
+
return msg.role === 'user' ? currentIndex : lastIndex;
|
|
246
|
+
}, -1);
|
|
249
247
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
248
|
+
if (lastUserMessageIndex === -1) {
|
|
249
|
+
// No user messages found, return all messages (shouldn't happen in practice)
|
|
250
|
+
return allMessages;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Return messages up to and including the last user message
|
|
254
|
+
return allMessages.slice(0, lastUserMessageIndex + 1);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
LogError(`[SkipProxyAgent] Error loading messages from database: ${error}`);
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
255
259
|
}
|
|
256
|
-
}
|
|
257
260
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
261
|
+
/**
|
|
262
|
+
* Convert MJ ChatMessage format to Skip SkipMessage format
|
|
263
|
+
* This is a fallback method when database loading is not available
|
|
264
|
+
*/
|
|
265
|
+
private convertMessagesToSkipFormat(messages: ChatMessage[]): SkipMessage[] {
|
|
266
|
+
return messages.map((msg, index) => {
|
|
267
|
+
// Extract conversationDetailID from metadata if available
|
|
268
|
+
const conversationDetailID = msg.metadata?.conversationDetailID || `temp-${index}`;
|
|
266
269
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
270
|
+
return {
|
|
271
|
+
// Skip only accepts 'user' or 'system' roles, map 'assistant' to 'system'
|
|
272
|
+
role: (msg.role === 'assistant' ? 'system' : msg.role) as 'user' | 'system',
|
|
273
|
+
content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
|
|
274
|
+
conversationDetailID,
|
|
275
|
+
// Include other SkipMessage fields from metadata if available
|
|
276
|
+
hiddenToUser: msg.metadata?.hiddenToUser,
|
|
277
|
+
userRating: msg.metadata?.userRating,
|
|
278
|
+
userFeedback: msg.metadata?.userFeedback,
|
|
279
|
+
reflectionInsights: msg.metadata?.reflectionInsights,
|
|
280
|
+
summaryOfEarlierConveration: msg.metadata?.summaryOfEarlierConversation,
|
|
281
|
+
createdAt: msg.metadata?.createdAt,
|
|
282
|
+
updatedAt: msg.metadata?.updatedAt
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
}
|
|
283
286
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
287
|
+
/**
|
|
288
|
+
* Map Skip API response to MJ agent next step
|
|
289
|
+
*/
|
|
290
|
+
private mapSkipResponseToNextStep(
|
|
291
|
+
apiResponse: SkipAPIResponse,
|
|
292
|
+
conversationId: string
|
|
293
|
+
): BaseAgentNextStep<ComponentSpec> {
|
|
294
|
+
//return this.tempHack();
|
|
289
295
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
296
|
+
switch (apiResponse.responsePhase) {
|
|
297
|
+
case 'analysis_complete': {
|
|
298
|
+
// Skip has completed analysis and returned results
|
|
299
|
+
const completeResponse = apiResponse as SkipAPIAnalysisCompleteResponse;
|
|
300
|
+
const componentSpec = completeResponse.componentOptions[0].option;
|
|
301
|
+
return {
|
|
302
|
+
terminate: true,
|
|
303
|
+
step: 'Success',
|
|
304
|
+
message: completeResponse.title || 'Analysis complete',
|
|
305
|
+
newPayload: componentSpec
|
|
306
|
+
};
|
|
307
|
+
}
|
|
302
308
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
309
|
+
case 'clarifying_question': {
|
|
310
|
+
// Skip needs more information from the user
|
|
311
|
+
const clarifyResponse = apiResponse as SkipAPIClarifyingQuestionResponse;
|
|
306
312
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
+
return {
|
|
314
|
+
terminate: true,
|
|
315
|
+
step: 'Chat',
|
|
316
|
+
message: clarifyResponse.clarifyingQuestion,
|
|
317
|
+
newPayload: undefined
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
default: {
|
|
322
|
+
// Unknown or unexpected response phase
|
|
323
|
+
LogError(`[SkipProxyAgent] Unknown Skip response phase: ${apiResponse.responsePhase}`);
|
|
324
|
+
return {
|
|
325
|
+
terminate: true,
|
|
326
|
+
step: 'Failed',
|
|
327
|
+
message: `Unknown Skip response phase: ${apiResponse.responsePhase}`,
|
|
328
|
+
errorMessage: `Unknown Skip response phase: ${apiResponse.responsePhase}`,
|
|
329
|
+
newPayload: undefined
|
|
330
|
+
};
|
|
331
|
+
}
|
|
313
332
|
}
|
|
333
|
+
}
|
|
314
334
|
|
|
315
|
-
|
|
316
|
-
// Unknown or unexpected response phase
|
|
317
|
-
LogError(`[SkipProxyAgent] Unknown Skip response phase: ${apiResponse.responsePhase}`);
|
|
335
|
+
private tempHack(): BaseAgentNextStep<SkipAgentPayload> {
|
|
318
336
|
return {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
newPayload: undefined,
|
|
337
|
+
terminate: true,
|
|
338
|
+
step: 'Success',
|
|
339
|
+
message: "Demo Report (not real)",
|
|
340
|
+
newPayload: demoSpecJson as unknown as SkipAgentPayload
|
|
324
341
|
};
|
|
325
|
-
}
|
|
326
342
|
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
private tempHack(): BaseAgentNextStep<SkipAgentPayload> {
|
|
330
|
-
return {
|
|
331
|
-
terminate: true,
|
|
332
|
-
step: 'Success',
|
|
333
|
-
message: 'Demo Report (not real)',
|
|
334
|
-
newPayload: demoSpecJson as unknown as SkipAgentPayload,
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
343
|
}
|
|
338
344
|
|
|
339
345
|
const demoSpecJson = {
|
|
340
|
-
name:
|
|
341
|
-
title:
|
|
342
|
-
description:
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
"
|
|
347
|
-
|
|
348
|
-
mode: 'views',
|
|
349
|
-
entities: [
|
|
346
|
+
"name": "EntityBrowser",
|
|
347
|
+
"title": "Entity Browser",
|
|
348
|
+
"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.",
|
|
349
|
+
"type": "dashboard",
|
|
350
|
+
"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",
|
|
351
|
+
"dataRequirements": {
|
|
352
|
+
"mode": "views",
|
|
353
|
+
"entities": [
|
|
350
354
|
{
|
|
351
|
-
name:
|
|
352
|
-
description:
|
|
353
|
-
displayFields: [
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
355
|
+
"name": "Entities",
|
|
356
|
+
"description": "Metadata about all entities in the system",
|
|
357
|
+
"displayFields": [
|
|
358
|
+
"ID",
|
|
359
|
+
"Name",
|
|
360
|
+
"DisplayName",
|
|
361
|
+
"NameSuffix",
|
|
362
|
+
"Description",
|
|
363
|
+
"SchemaName",
|
|
364
|
+
"BaseTable",
|
|
365
|
+
"BaseView"
|
|
366
|
+
],
|
|
367
|
+
"filterFields": [
|
|
368
|
+
"SchemaName",
|
|
369
|
+
"BaseTable"
|
|
370
|
+
],
|
|
371
|
+
"sortFields": [
|
|
372
|
+
"Name",
|
|
373
|
+
"DisplayName"
|
|
374
|
+
],
|
|
375
|
+
"fieldMetadata": [
|
|
357
376
|
{
|
|
358
|
-
name:
|
|
359
|
-
sequence: 1,
|
|
360
|
-
defaultInView: false,
|
|
361
|
-
type:
|
|
362
|
-
allowsNull: false,
|
|
363
|
-
isPrimaryKey: true,
|
|
364
|
-
description:
|
|
377
|
+
"name": "ID",
|
|
378
|
+
"sequence": 1,
|
|
379
|
+
"defaultInView": false,
|
|
380
|
+
"type": "uniqueidentifier",
|
|
381
|
+
"allowsNull": false,
|
|
382
|
+
"isPrimaryKey": true,
|
|
383
|
+
"description": "Unique identifier for the entity"
|
|
365
384
|
},
|
|
366
385
|
{
|
|
367
|
-
name:
|
|
368
|
-
sequence: 2,
|
|
369
|
-
defaultInView: true,
|
|
370
|
-
type:
|
|
371
|
-
allowsNull: false,
|
|
372
|
-
isPrimaryKey: false,
|
|
373
|
-
description:
|
|
386
|
+
"name": "Name",
|
|
387
|
+
"sequence": 2,
|
|
388
|
+
"defaultInView": true,
|
|
389
|
+
"type": "nvarchar",
|
|
390
|
+
"allowsNull": false,
|
|
391
|
+
"isPrimaryKey": false,
|
|
392
|
+
"description": "System name of the entity"
|
|
374
393
|
},
|
|
375
394
|
{
|
|
376
|
-
name:
|
|
377
|
-
sequence: 3,
|
|
378
|
-
defaultInView: true,
|
|
379
|
-
type:
|
|
380
|
-
allowsNull: true,
|
|
381
|
-
isPrimaryKey: false,
|
|
382
|
-
description:
|
|
395
|
+
"name": "DisplayName",
|
|
396
|
+
"sequence": 3,
|
|
397
|
+
"defaultInView": true,
|
|
398
|
+
"type": "nvarchar",
|
|
399
|
+
"allowsNull": true,
|
|
400
|
+
"isPrimaryKey": false,
|
|
401
|
+
"description": "User-friendly display name for the entity"
|
|
383
402
|
},
|
|
384
403
|
{
|
|
385
|
-
name:
|
|
386
|
-
sequence: 4,
|
|
387
|
-
defaultInView: true,
|
|
388
|
-
type:
|
|
389
|
-
allowsNull: true,
|
|
390
|
-
isPrimaryKey: false,
|
|
391
|
-
description:
|
|
404
|
+
"name": "NameSuffix",
|
|
405
|
+
"sequence": 4,
|
|
406
|
+
"defaultInView": true,
|
|
407
|
+
"type": "nvarchar",
|
|
408
|
+
"allowsNull": true,
|
|
409
|
+
"isPrimaryKey": false,
|
|
410
|
+
"description": "Optional suffix appended to entity names for display purposes"
|
|
392
411
|
},
|
|
393
412
|
{
|
|
394
|
-
name:
|
|
395
|
-
sequence: 5,
|
|
396
|
-
defaultInView: true,
|
|
397
|
-
type:
|
|
398
|
-
allowsNull: true,
|
|
399
|
-
isPrimaryKey: false,
|
|
400
|
-
description:
|
|
413
|
+
"name": "Description",
|
|
414
|
+
"sequence": 5,
|
|
415
|
+
"defaultInView": true,
|
|
416
|
+
"type": "nvarchar",
|
|
417
|
+
"allowsNull": true,
|
|
418
|
+
"isPrimaryKey": false,
|
|
419
|
+
"description": "Description of the entity"
|
|
401
420
|
},
|
|
402
421
|
{
|
|
403
|
-
name:
|
|
404
|
-
sequence: 6,
|
|
405
|
-
defaultInView: true,
|
|
406
|
-
type:
|
|
407
|
-
allowsNull: true,
|
|
408
|
-
isPrimaryKey: false,
|
|
409
|
-
description:
|
|
422
|
+
"name": "SchemaName",
|
|
423
|
+
"sequence": 6,
|
|
424
|
+
"defaultInView": true,
|
|
425
|
+
"type": "nvarchar",
|
|
426
|
+
"allowsNull": true,
|
|
427
|
+
"isPrimaryKey": false,
|
|
428
|
+
"description": "Database schema name"
|
|
410
429
|
},
|
|
411
430
|
{
|
|
412
|
-
name:
|
|
413
|
-
sequence: 7,
|
|
414
|
-
defaultInView: true,
|
|
415
|
-
type:
|
|
416
|
-
allowsNull: true,
|
|
417
|
-
isPrimaryKey: false,
|
|
418
|
-
description:
|
|
431
|
+
"name": "BaseTable",
|
|
432
|
+
"sequence": 7,
|
|
433
|
+
"defaultInView": true,
|
|
434
|
+
"type": "nvarchar",
|
|
435
|
+
"allowsNull": true,
|
|
436
|
+
"isPrimaryKey": false,
|
|
437
|
+
"description": "Base table in the database"
|
|
419
438
|
},
|
|
420
439
|
{
|
|
421
|
-
name:
|
|
422
|
-
sequence: 8,
|
|
423
|
-
defaultInView: true,
|
|
424
|
-
type:
|
|
425
|
-
allowsNull: false,
|
|
426
|
-
isPrimaryKey: false,
|
|
427
|
-
description:
|
|
428
|
-
}
|
|
440
|
+
"name": "BaseView",
|
|
441
|
+
"sequence": 8,
|
|
442
|
+
"defaultInView": true,
|
|
443
|
+
"type": "nvarchar",
|
|
444
|
+
"allowsNull": false,
|
|
445
|
+
"isPrimaryKey": false,
|
|
446
|
+
"description": "Base view used for the entity"
|
|
447
|
+
}
|
|
448
|
+
],
|
|
449
|
+
"permissionLevelNeeded": [
|
|
450
|
+
"read"
|
|
429
451
|
],
|
|
430
|
-
|
|
431
|
-
usageContext: 'Main entity list display and filtering',
|
|
452
|
+
"usageContext": "Main entity list display and filtering"
|
|
432
453
|
},
|
|
433
454
|
{
|
|
434
|
-
name:
|
|
435
|
-
description:
|
|
436
|
-
displayFields: [
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
455
|
+
"name": "Entity Fields",
|
|
456
|
+
"description": "Fields belonging to each entity",
|
|
457
|
+
"displayFields": [
|
|
458
|
+
"Name",
|
|
459
|
+
"DisplayName",
|
|
460
|
+
"Type",
|
|
461
|
+
"Length",
|
|
462
|
+
"AllowsNull",
|
|
463
|
+
"IsPrimaryKey",
|
|
464
|
+
"IsUnique"
|
|
465
|
+
],
|
|
466
|
+
"filterFields": [
|
|
467
|
+
"EntityID"
|
|
468
|
+
],
|
|
469
|
+
"sortFields": [
|
|
470
|
+
"Sequence",
|
|
471
|
+
"Name"
|
|
472
|
+
],
|
|
473
|
+
"fieldMetadata": [
|
|
440
474
|
{
|
|
441
|
-
name:
|
|
442
|
-
sequence: 1,
|
|
443
|
-
defaultInView: false,
|
|
444
|
-
type:
|
|
445
|
-
allowsNull: false,
|
|
446
|
-
isPrimaryKey: false,
|
|
447
|
-
description:
|
|
475
|
+
"name": "EntityID",
|
|
476
|
+
"sequence": 1,
|
|
477
|
+
"defaultInView": false,
|
|
478
|
+
"type": "uniqueidentifier",
|
|
479
|
+
"allowsNull": false,
|
|
480
|
+
"isPrimaryKey": false,
|
|
481
|
+
"description": "Reference to parent entity"
|
|
448
482
|
},
|
|
449
483
|
{
|
|
450
|
-
name:
|
|
451
|
-
sequence: 2,
|
|
452
|
-
defaultInView: true,
|
|
453
|
-
type:
|
|
454
|
-
allowsNull: false,
|
|
455
|
-
isPrimaryKey: false,
|
|
456
|
-
description:
|
|
484
|
+
"name": "Name",
|
|
485
|
+
"sequence": 2,
|
|
486
|
+
"defaultInView": true,
|
|
487
|
+
"type": "nvarchar",
|
|
488
|
+
"allowsNull": false,
|
|
489
|
+
"isPrimaryKey": false,
|
|
490
|
+
"description": "Field name"
|
|
457
491
|
},
|
|
458
492
|
{
|
|
459
|
-
name:
|
|
460
|
-
sequence: 3,
|
|
461
|
-
defaultInView: true,
|
|
462
|
-
type:
|
|
463
|
-
allowsNull: true,
|
|
464
|
-
isPrimaryKey: false,
|
|
465
|
-
description:
|
|
493
|
+
"name": "DisplayName",
|
|
494
|
+
"sequence": 3,
|
|
495
|
+
"defaultInView": true,
|
|
496
|
+
"type": "nvarchar",
|
|
497
|
+
"allowsNull": true,
|
|
498
|
+
"isPrimaryKey": false,
|
|
499
|
+
"description": "User-friendly field name"
|
|
466
500
|
},
|
|
467
501
|
{
|
|
468
|
-
name:
|
|
469
|
-
sequence: 4,
|
|
470
|
-
defaultInView: true,
|
|
471
|
-
type:
|
|
472
|
-
allowsNull: false,
|
|
473
|
-
isPrimaryKey: false,
|
|
474
|
-
description:
|
|
502
|
+
"name": "Type",
|
|
503
|
+
"sequence": 4,
|
|
504
|
+
"defaultInView": true,
|
|
505
|
+
"type": "nvarchar",
|
|
506
|
+
"allowsNull": false,
|
|
507
|
+
"isPrimaryKey": false,
|
|
508
|
+
"description": "Data type of the field"
|
|
475
509
|
},
|
|
476
510
|
{
|
|
477
|
-
name:
|
|
478
|
-
sequence: 5,
|
|
479
|
-
defaultInView: true,
|
|
480
|
-
type:
|
|
481
|
-
allowsNull: true,
|
|
482
|
-
isPrimaryKey: false,
|
|
483
|
-
description:
|
|
511
|
+
"name": "Length",
|
|
512
|
+
"sequence": 5,
|
|
513
|
+
"defaultInView": true,
|
|
514
|
+
"type": "int",
|
|
515
|
+
"allowsNull": true,
|
|
516
|
+
"isPrimaryKey": false,
|
|
517
|
+
"description": "Maximum length for string fields"
|
|
484
518
|
},
|
|
485
519
|
{
|
|
486
|
-
name:
|
|
487
|
-
sequence: 6,
|
|
488
|
-
defaultInView: true,
|
|
489
|
-
type:
|
|
490
|
-
allowsNull: false,
|
|
491
|
-
isPrimaryKey: false,
|
|
492
|
-
description:
|
|
520
|
+
"name": "AllowsNull",
|
|
521
|
+
"sequence": 6,
|
|
522
|
+
"defaultInView": true,
|
|
523
|
+
"type": "bit",
|
|
524
|
+
"allowsNull": false,
|
|
525
|
+
"isPrimaryKey": false,
|
|
526
|
+
"description": "Whether field allows null values"
|
|
493
527
|
},
|
|
494
528
|
{
|
|
495
|
-
name:
|
|
496
|
-
sequence: 7,
|
|
497
|
-
defaultInView: true,
|
|
498
|
-
type:
|
|
499
|
-
allowsNull: false,
|
|
500
|
-
isPrimaryKey: false,
|
|
501
|
-
description:
|
|
529
|
+
"name": "IsPrimaryKey",
|
|
530
|
+
"sequence": 7,
|
|
531
|
+
"defaultInView": true,
|
|
532
|
+
"type": "bit",
|
|
533
|
+
"allowsNull": false,
|
|
534
|
+
"isPrimaryKey": false,
|
|
535
|
+
"description": "Whether field is part of primary key"
|
|
502
536
|
},
|
|
503
537
|
{
|
|
504
|
-
name:
|
|
505
|
-
sequence: 8,
|
|
506
|
-
defaultInView: true,
|
|
507
|
-
type:
|
|
508
|
-
allowsNull: false,
|
|
509
|
-
isPrimaryKey: false,
|
|
510
|
-
description:
|
|
538
|
+
"name": "IsUnique",
|
|
539
|
+
"sequence": 8,
|
|
540
|
+
"defaultInView": true,
|
|
541
|
+
"type": "bit",
|
|
542
|
+
"allowsNull": false,
|
|
543
|
+
"isPrimaryKey": false,
|
|
544
|
+
"description": "Whether field must be unique"
|
|
511
545
|
},
|
|
512
546
|
{
|
|
513
|
-
name:
|
|
514
|
-
sequence: 9,
|
|
515
|
-
defaultInView: false,
|
|
516
|
-
type:
|
|
517
|
-
allowsNull: false,
|
|
518
|
-
isPrimaryKey: false,
|
|
519
|
-
description:
|
|
520
|
-
}
|
|
547
|
+
"name": "Sequence",
|
|
548
|
+
"sequence": 9,
|
|
549
|
+
"defaultInView": false,
|
|
550
|
+
"type": "int",
|
|
551
|
+
"allowsNull": false,
|
|
552
|
+
"isPrimaryKey": false,
|
|
553
|
+
"description": "Display order of the field"
|
|
554
|
+
}
|
|
555
|
+
],
|
|
556
|
+
"permissionLevelNeeded": [
|
|
557
|
+
"read"
|
|
521
558
|
],
|
|
522
|
-
|
|
523
|
-
usageContext: 'Details panel to show entity fields',
|
|
559
|
+
"usageContext": "Details panel to show entity fields"
|
|
524
560
|
},
|
|
525
561
|
{
|
|
526
|
-
name:
|
|
527
|
-
description:
|
|
528
|
-
displayFields: [
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
562
|
+
"name": "Entity Relationships",
|
|
563
|
+
"description": "Relationships between entities",
|
|
564
|
+
"displayFields": [
|
|
565
|
+
"RelatedEntity",
|
|
566
|
+
"Type",
|
|
567
|
+
"DisplayName",
|
|
568
|
+
"RelatedEntityJoinField"
|
|
569
|
+
],
|
|
570
|
+
"filterFields": [
|
|
571
|
+
"EntityID"
|
|
572
|
+
],
|
|
573
|
+
"sortFields": [
|
|
574
|
+
"Sequence",
|
|
575
|
+
"RelatedEntity"
|
|
576
|
+
],
|
|
577
|
+
"fieldMetadata": [
|
|
532
578
|
{
|
|
533
|
-
name:
|
|
534
|
-
sequence: 1,
|
|
535
|
-
defaultInView: false,
|
|
536
|
-
type:
|
|
537
|
-
allowsNull: false,
|
|
538
|
-
isPrimaryKey: false,
|
|
539
|
-
description:
|
|
579
|
+
"name": "EntityID",
|
|
580
|
+
"sequence": 1,
|
|
581
|
+
"defaultInView": false,
|
|
582
|
+
"type": "uniqueidentifier",
|
|
583
|
+
"allowsNull": false,
|
|
584
|
+
"isPrimaryKey": false,
|
|
585
|
+
"description": "Reference to parent entity"
|
|
540
586
|
},
|
|
541
587
|
{
|
|
542
|
-
name:
|
|
543
|
-
sequence: 2,
|
|
544
|
-
defaultInView: true,
|
|
545
|
-
type:
|
|
546
|
-
allowsNull: false,
|
|
547
|
-
isPrimaryKey: false,
|
|
548
|
-
description:
|
|
588
|
+
"name": "RelatedEntity",
|
|
589
|
+
"sequence": 2,
|
|
590
|
+
"defaultInView": true,
|
|
591
|
+
"type": "nvarchar",
|
|
592
|
+
"allowsNull": false,
|
|
593
|
+
"isPrimaryKey": false,
|
|
594
|
+
"description": "The related entity in the relationship"
|
|
549
595
|
},
|
|
550
596
|
{
|
|
551
|
-
name:
|
|
552
|
-
sequence: 3,
|
|
553
|
-
defaultInView: true,
|
|
554
|
-
type:
|
|
555
|
-
allowsNull: false,
|
|
556
|
-
isPrimaryKey: false,
|
|
557
|
-
description:
|
|
597
|
+
"name": "Type",
|
|
598
|
+
"sequence": 3,
|
|
599
|
+
"defaultInView": true,
|
|
600
|
+
"type": "nvarchar",
|
|
601
|
+
"allowsNull": false,
|
|
602
|
+
"isPrimaryKey": false,
|
|
603
|
+
"description": "Type of relationship (One to Many, Many to One, etc.)"
|
|
558
604
|
},
|
|
559
605
|
{
|
|
560
|
-
name:
|
|
561
|
-
sequence: 4,
|
|
562
|
-
defaultInView: true,
|
|
563
|
-
type:
|
|
564
|
-
allowsNull: true,
|
|
565
|
-
isPrimaryKey: false,
|
|
566
|
-
description:
|
|
606
|
+
"name": "DisplayName",
|
|
607
|
+
"sequence": 4,
|
|
608
|
+
"defaultInView": true,
|
|
609
|
+
"type": "nvarchar",
|
|
610
|
+
"allowsNull": true,
|
|
611
|
+
"isPrimaryKey": false,
|
|
612
|
+
"description": "User-friendly name for the relationship"
|
|
567
613
|
},
|
|
568
614
|
{
|
|
569
|
-
name:
|
|
570
|
-
sequence: 5,
|
|
571
|
-
defaultInView: true,
|
|
572
|
-
type:
|
|
573
|
-
allowsNull: true,
|
|
574
|
-
isPrimaryKey: false,
|
|
575
|
-
description:
|
|
615
|
+
"name": "RelatedEntityJoinField",
|
|
616
|
+
"sequence": 5,
|
|
617
|
+
"defaultInView": true,
|
|
618
|
+
"type": "nvarchar",
|
|
619
|
+
"allowsNull": true,
|
|
620
|
+
"isPrimaryKey": false,
|
|
621
|
+
"description": "The field in the related entity that joins to this entity"
|
|
576
622
|
},
|
|
577
623
|
{
|
|
578
|
-
name:
|
|
579
|
-
sequence: 6,
|
|
580
|
-
defaultInView: false,
|
|
581
|
-
type:
|
|
582
|
-
allowsNull: false,
|
|
583
|
-
isPrimaryKey: false,
|
|
584
|
-
description:
|
|
585
|
-
}
|
|
624
|
+
"name": "Sequence",
|
|
625
|
+
"sequence": 6,
|
|
626
|
+
"defaultInView": false,
|
|
627
|
+
"type": "int",
|
|
628
|
+
"allowsNull": false,
|
|
629
|
+
"isPrimaryKey": false,
|
|
630
|
+
"description": "Display order"
|
|
631
|
+
}
|
|
586
632
|
],
|
|
587
|
-
permissionLevelNeeded: [
|
|
588
|
-
|
|
589
|
-
|
|
633
|
+
"permissionLevelNeeded": [
|
|
634
|
+
"read"
|
|
635
|
+
],
|
|
636
|
+
"usageContext": "Details panel to show entity relationships"
|
|
637
|
+
}
|
|
590
638
|
],
|
|
591
|
-
queries: [],
|
|
592
|
-
description:
|
|
593
|
-
'This component requires access to entity metadata including entities, their fields, and relationships to provide a comprehensive entity browsing experience',
|
|
639
|
+
"queries": [],
|
|
640
|
+
"description": "This component requires access to entity metadata including entities, their fields, and relationships to provide a comprehensive entity browsing experience"
|
|
594
641
|
},
|
|
595
|
-
technicalDesign:
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
'<EntityBrowser\n utilities={utilities}\n styles={styles}\n components={components}\n callbacks={callbacks}\n savedUserSettings={savedUserSettings}\n onSaveUserSettings={onSaveUserSettings}\n/>',
|
|
601
|
-
dependencies: [
|
|
642
|
+
"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",
|
|
643
|
+
"properties": [],
|
|
644
|
+
"events": [],
|
|
645
|
+
"exampleUsage": "<EntityBrowser\n utilities={utilities}\n styles={styles}\n components={components}\n callbacks={callbacks}\n savedUserSettings={savedUserSettings}\n onSaveUserSettings={onSaveUserSettings}\n/>",
|
|
646
|
+
"dependencies": [
|
|
602
647
|
{
|
|
603
|
-
name:
|
|
604
|
-
title:
|
|
605
|
-
description:
|
|
606
|
-
type:
|
|
607
|
-
functionalRequirements:
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
entities: [
|
|
648
|
+
"name": "EntityList",
|
|
649
|
+
"title": "Entity List",
|
|
650
|
+
"description": "Displays entities in a grid or card layout with sorting capabilities",
|
|
651
|
+
"type": "table",
|
|
652
|
+
"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",
|
|
653
|
+
"dataRequirements": {
|
|
654
|
+
"mode": "views",
|
|
655
|
+
"entities": [
|
|
612
656
|
{
|
|
613
|
-
name:
|
|
614
|
-
description:
|
|
615
|
-
displayFields: [
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
657
|
+
"name": "Entities",
|
|
658
|
+
"description": "Metadata about all entities in the system",
|
|
659
|
+
"displayFields": [
|
|
660
|
+
"ID",
|
|
661
|
+
"Name",
|
|
662
|
+
"DisplayName",
|
|
663
|
+
"NameSuffix",
|
|
664
|
+
"Description",
|
|
665
|
+
"SchemaName",
|
|
666
|
+
"BaseTable",
|
|
667
|
+
"BaseView"
|
|
668
|
+
],
|
|
669
|
+
"filterFields": [
|
|
670
|
+
"SchemaName",
|
|
671
|
+
"BaseTable"
|
|
672
|
+
],
|
|
673
|
+
"sortFields": [
|
|
674
|
+
"Name",
|
|
675
|
+
"DisplayName"
|
|
676
|
+
],
|
|
677
|
+
"fieldMetadata": [
|
|
619
678
|
{
|
|
620
|
-
name:
|
|
621
|
-
sequence: 1,
|
|
622
|
-
defaultInView: false,
|
|
623
|
-
type:
|
|
624
|
-
allowsNull: false,
|
|
625
|
-
isPrimaryKey: true,
|
|
626
|
-
description:
|
|
679
|
+
"name": "ID",
|
|
680
|
+
"sequence": 1,
|
|
681
|
+
"defaultInView": false,
|
|
682
|
+
"type": "uniqueidentifier",
|
|
683
|
+
"allowsNull": false,
|
|
684
|
+
"isPrimaryKey": true,
|
|
685
|
+
"description": "Unique identifier for the entity"
|
|
627
686
|
},
|
|
628
687
|
{
|
|
629
|
-
name:
|
|
630
|
-
sequence: 2,
|
|
631
|
-
defaultInView: true,
|
|
632
|
-
type:
|
|
633
|
-
allowsNull: false,
|
|
634
|
-
isPrimaryKey: false,
|
|
635
|
-
description:
|
|
688
|
+
"name": "Name",
|
|
689
|
+
"sequence": 2,
|
|
690
|
+
"defaultInView": true,
|
|
691
|
+
"type": "nvarchar",
|
|
692
|
+
"allowsNull": false,
|
|
693
|
+
"isPrimaryKey": false,
|
|
694
|
+
"description": "System name of the entity"
|
|
636
695
|
},
|
|
637
696
|
{
|
|
638
|
-
name:
|
|
639
|
-
sequence: 3,
|
|
640
|
-
defaultInView: true,
|
|
641
|
-
type:
|
|
642
|
-
allowsNull: true,
|
|
643
|
-
isPrimaryKey: false,
|
|
644
|
-
description:
|
|
697
|
+
"name": "DisplayName",
|
|
698
|
+
"sequence": 3,
|
|
699
|
+
"defaultInView": true,
|
|
700
|
+
"type": "nvarchar",
|
|
701
|
+
"allowsNull": true,
|
|
702
|
+
"isPrimaryKey": false,
|
|
703
|
+
"description": "User-friendly display name for the entity"
|
|
645
704
|
},
|
|
646
705
|
{
|
|
647
|
-
name:
|
|
648
|
-
sequence: 4,
|
|
649
|
-
defaultInView: true,
|
|
650
|
-
type:
|
|
651
|
-
allowsNull: true,
|
|
652
|
-
isPrimaryKey: false,
|
|
653
|
-
description:
|
|
706
|
+
"name": "NameSuffix",
|
|
707
|
+
"sequence": 4,
|
|
708
|
+
"defaultInView": true,
|
|
709
|
+
"type": "nvarchar",
|
|
710
|
+
"allowsNull": true,
|
|
711
|
+
"isPrimaryKey": false,
|
|
712
|
+
"description": "Optional suffix appended to entity names for display purposes"
|
|
654
713
|
},
|
|
655
714
|
{
|
|
656
|
-
name:
|
|
657
|
-
sequence: 5,
|
|
658
|
-
defaultInView: true,
|
|
659
|
-
type:
|
|
660
|
-
allowsNull: true,
|
|
661
|
-
isPrimaryKey: false,
|
|
662
|
-
description:
|
|
715
|
+
"name": "Description",
|
|
716
|
+
"sequence": 5,
|
|
717
|
+
"defaultInView": true,
|
|
718
|
+
"type": "nvarchar",
|
|
719
|
+
"allowsNull": true,
|
|
720
|
+
"isPrimaryKey": false,
|
|
721
|
+
"description": "Description of the entity"
|
|
663
722
|
},
|
|
664
723
|
{
|
|
665
|
-
name:
|
|
666
|
-
sequence: 6,
|
|
667
|
-
defaultInView: true,
|
|
668
|
-
type:
|
|
669
|
-
allowsNull: true,
|
|
670
|
-
isPrimaryKey: false,
|
|
671
|
-
description:
|
|
724
|
+
"name": "SchemaName",
|
|
725
|
+
"sequence": 6,
|
|
726
|
+
"defaultInView": true,
|
|
727
|
+
"type": "nvarchar",
|
|
728
|
+
"allowsNull": true,
|
|
729
|
+
"isPrimaryKey": false,
|
|
730
|
+
"description": "Database schema name"
|
|
672
731
|
},
|
|
673
732
|
{
|
|
674
|
-
name:
|
|
675
|
-
sequence: 7,
|
|
676
|
-
defaultInView: true,
|
|
677
|
-
type:
|
|
678
|
-
allowsNull: true,
|
|
679
|
-
isPrimaryKey: false,
|
|
680
|
-
description:
|
|
733
|
+
"name": "BaseTable",
|
|
734
|
+
"sequence": 7,
|
|
735
|
+
"defaultInView": true,
|
|
736
|
+
"type": "nvarchar",
|
|
737
|
+
"allowsNull": true,
|
|
738
|
+
"isPrimaryKey": false,
|
|
739
|
+
"description": "Base table in the database"
|
|
681
740
|
},
|
|
682
741
|
{
|
|
683
|
-
name:
|
|
684
|
-
sequence: 8,
|
|
685
|
-
defaultInView: true,
|
|
686
|
-
type:
|
|
687
|
-
allowsNull: false,
|
|
688
|
-
isPrimaryKey: false,
|
|
689
|
-
description:
|
|
690
|
-
}
|
|
742
|
+
"name": "BaseView",
|
|
743
|
+
"sequence": 8,
|
|
744
|
+
"defaultInView": true,
|
|
745
|
+
"type": "nvarchar",
|
|
746
|
+
"allowsNull": false,
|
|
747
|
+
"isPrimaryKey": false,
|
|
748
|
+
"description": "Base view used for the entity"
|
|
749
|
+
}
|
|
691
750
|
],
|
|
692
|
-
permissionLevelNeeded: [
|
|
693
|
-
|
|
751
|
+
"permissionLevelNeeded": [
|
|
752
|
+
"read"
|
|
753
|
+
],
|
|
754
|
+
"usageContext": "Main entity list display and filtering"
|
|
694
755
|
},
|
|
695
756
|
{
|
|
696
|
-
name:
|
|
697
|
-
description:
|
|
698
|
-
displayFields: [
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
757
|
+
"name": "Entity Fields",
|
|
758
|
+
"description": "Fields belonging to each entity",
|
|
759
|
+
"displayFields": [
|
|
760
|
+
"Name",
|
|
761
|
+
"DisplayName",
|
|
762
|
+
"Type",
|
|
763
|
+
"Length",
|
|
764
|
+
"AllowsNull",
|
|
765
|
+
"IsPrimaryKey",
|
|
766
|
+
"IsUnique"
|
|
767
|
+
],
|
|
768
|
+
"filterFields": [
|
|
769
|
+
"EntityID"
|
|
770
|
+
],
|
|
771
|
+
"sortFields": [
|
|
772
|
+
"Sequence",
|
|
773
|
+
"Name"
|
|
774
|
+
],
|
|
775
|
+
"fieldMetadata": [
|
|
702
776
|
{
|
|
703
|
-
name:
|
|
704
|
-
sequence: 1,
|
|
705
|
-
defaultInView: false,
|
|
706
|
-
type:
|
|
707
|
-
allowsNull: false,
|
|
708
|
-
isPrimaryKey: false,
|
|
709
|
-
description:
|
|
777
|
+
"name": "EntityID",
|
|
778
|
+
"sequence": 1,
|
|
779
|
+
"defaultInView": false,
|
|
780
|
+
"type": "uniqueidentifier",
|
|
781
|
+
"allowsNull": false,
|
|
782
|
+
"isPrimaryKey": false,
|
|
783
|
+
"description": "Reference to parent entity"
|
|
710
784
|
},
|
|
711
785
|
{
|
|
712
|
-
name:
|
|
713
|
-
sequence: 2,
|
|
714
|
-
defaultInView: true,
|
|
715
|
-
type:
|
|
716
|
-
allowsNull: false,
|
|
717
|
-
isPrimaryKey: false,
|
|
718
|
-
description:
|
|
786
|
+
"name": "Name",
|
|
787
|
+
"sequence": 2,
|
|
788
|
+
"defaultInView": true,
|
|
789
|
+
"type": "nvarchar",
|
|
790
|
+
"allowsNull": false,
|
|
791
|
+
"isPrimaryKey": false,
|
|
792
|
+
"description": "Field name"
|
|
719
793
|
},
|
|
720
794
|
{
|
|
721
|
-
name:
|
|
722
|
-
sequence: 3,
|
|
723
|
-
defaultInView: true,
|
|
724
|
-
type:
|
|
725
|
-
allowsNull: true,
|
|
726
|
-
isPrimaryKey: false,
|
|
727
|
-
description:
|
|
795
|
+
"name": "DisplayName",
|
|
796
|
+
"sequence": 3,
|
|
797
|
+
"defaultInView": true,
|
|
798
|
+
"type": "nvarchar",
|
|
799
|
+
"allowsNull": true,
|
|
800
|
+
"isPrimaryKey": false,
|
|
801
|
+
"description": "User-friendly field name"
|
|
728
802
|
},
|
|
729
803
|
{
|
|
730
|
-
name:
|
|
731
|
-
sequence: 4,
|
|
732
|
-
defaultInView: true,
|
|
733
|
-
type:
|
|
734
|
-
allowsNull: false,
|
|
735
|
-
isPrimaryKey: false,
|
|
736
|
-
description:
|
|
804
|
+
"name": "Type",
|
|
805
|
+
"sequence": 4,
|
|
806
|
+
"defaultInView": true,
|
|
807
|
+
"type": "nvarchar",
|
|
808
|
+
"allowsNull": false,
|
|
809
|
+
"isPrimaryKey": false,
|
|
810
|
+
"description": "Data type of the field"
|
|
737
811
|
},
|
|
738
812
|
{
|
|
739
|
-
name:
|
|
740
|
-
sequence: 5,
|
|
741
|
-
defaultInView: true,
|
|
742
|
-
type:
|
|
743
|
-
allowsNull: true,
|
|
744
|
-
isPrimaryKey: false,
|
|
745
|
-
description:
|
|
813
|
+
"name": "Length",
|
|
814
|
+
"sequence": 5,
|
|
815
|
+
"defaultInView": true,
|
|
816
|
+
"type": "int",
|
|
817
|
+
"allowsNull": true,
|
|
818
|
+
"isPrimaryKey": false,
|
|
819
|
+
"description": "Maximum length for string fields"
|
|
746
820
|
},
|
|
747
821
|
{
|
|
748
|
-
name:
|
|
749
|
-
sequence: 6,
|
|
750
|
-
defaultInView: true,
|
|
751
|
-
type:
|
|
752
|
-
allowsNull: false,
|
|
753
|
-
isPrimaryKey: false,
|
|
754
|
-
description:
|
|
822
|
+
"name": "AllowsNull",
|
|
823
|
+
"sequence": 6,
|
|
824
|
+
"defaultInView": true,
|
|
825
|
+
"type": "bit",
|
|
826
|
+
"allowsNull": false,
|
|
827
|
+
"isPrimaryKey": false,
|
|
828
|
+
"description": "Whether field allows null values"
|
|
755
829
|
},
|
|
756
830
|
{
|
|
757
|
-
name:
|
|
758
|
-
sequence: 7,
|
|
759
|
-
defaultInView: true,
|
|
760
|
-
type:
|
|
761
|
-
allowsNull: false,
|
|
762
|
-
isPrimaryKey: false,
|
|
763
|
-
description:
|
|
831
|
+
"name": "IsPrimaryKey",
|
|
832
|
+
"sequence": 7,
|
|
833
|
+
"defaultInView": true,
|
|
834
|
+
"type": "bit",
|
|
835
|
+
"allowsNull": false,
|
|
836
|
+
"isPrimaryKey": false,
|
|
837
|
+
"description": "Whether field is part of primary key"
|
|
764
838
|
},
|
|
765
839
|
{
|
|
766
|
-
name:
|
|
767
|
-
sequence: 8,
|
|
768
|
-
defaultInView: true,
|
|
769
|
-
type:
|
|
770
|
-
allowsNull: false,
|
|
771
|
-
isPrimaryKey: false,
|
|
772
|
-
description:
|
|
840
|
+
"name": "IsUnique",
|
|
841
|
+
"sequence": 8,
|
|
842
|
+
"defaultInView": true,
|
|
843
|
+
"type": "bit",
|
|
844
|
+
"allowsNull": false,
|
|
845
|
+
"isPrimaryKey": false,
|
|
846
|
+
"description": "Whether field must be unique"
|
|
773
847
|
},
|
|
774
848
|
{
|
|
775
|
-
name:
|
|
776
|
-
sequence: 9,
|
|
777
|
-
defaultInView: false,
|
|
778
|
-
type:
|
|
779
|
-
allowsNull: false,
|
|
780
|
-
isPrimaryKey: false,
|
|
781
|
-
description:
|
|
782
|
-
}
|
|
849
|
+
"name": "Sequence",
|
|
850
|
+
"sequence": 9,
|
|
851
|
+
"defaultInView": false,
|
|
852
|
+
"type": "int",
|
|
853
|
+
"allowsNull": false,
|
|
854
|
+
"isPrimaryKey": false,
|
|
855
|
+
"description": "Display order of the field"
|
|
856
|
+
}
|
|
857
|
+
],
|
|
858
|
+
"permissionLevelNeeded": [
|
|
859
|
+
"read"
|
|
783
860
|
],
|
|
784
|
-
|
|
785
|
-
usageContext: 'Details panel to show entity fields',
|
|
861
|
+
"usageContext": "Details panel to show entity fields"
|
|
786
862
|
},
|
|
787
863
|
{
|
|
788
|
-
name:
|
|
789
|
-
description:
|
|
790
|
-
displayFields: [
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
864
|
+
"name": "Entity Relationships",
|
|
865
|
+
"description": "Relationships between entities",
|
|
866
|
+
"displayFields": [
|
|
867
|
+
"RelatedEntity",
|
|
868
|
+
"Type",
|
|
869
|
+
"DisplayName",
|
|
870
|
+
"RelatedEntityJoinField"
|
|
871
|
+
],
|
|
872
|
+
"filterFields": [
|
|
873
|
+
"EntityID"
|
|
874
|
+
],
|
|
875
|
+
"sortFields": [
|
|
876
|
+
"Sequence",
|
|
877
|
+
"RelatedEntity"
|
|
878
|
+
],
|
|
879
|
+
"fieldMetadata": [
|
|
794
880
|
{
|
|
795
|
-
name:
|
|
796
|
-
sequence: 1,
|
|
797
|
-
defaultInView: false,
|
|
798
|
-
type:
|
|
799
|
-
allowsNull: false,
|
|
800
|
-
isPrimaryKey: false,
|
|
801
|
-
description:
|
|
881
|
+
"name": "EntityID",
|
|
882
|
+
"sequence": 1,
|
|
883
|
+
"defaultInView": false,
|
|
884
|
+
"type": "uniqueidentifier",
|
|
885
|
+
"allowsNull": false,
|
|
886
|
+
"isPrimaryKey": false,
|
|
887
|
+
"description": "Reference to parent entity"
|
|
802
888
|
},
|
|
803
889
|
{
|
|
804
|
-
name:
|
|
805
|
-
sequence: 2,
|
|
806
|
-
defaultInView: true,
|
|
807
|
-
type:
|
|
808
|
-
allowsNull: false,
|
|
809
|
-
isPrimaryKey: false,
|
|
810
|
-
description:
|
|
890
|
+
"name": "RelatedEntity",
|
|
891
|
+
"sequence": 2,
|
|
892
|
+
"defaultInView": true,
|
|
893
|
+
"type": "nvarchar",
|
|
894
|
+
"allowsNull": false,
|
|
895
|
+
"isPrimaryKey": false,
|
|
896
|
+
"description": "The related entity in the relationship"
|
|
811
897
|
},
|
|
812
898
|
{
|
|
813
|
-
name:
|
|
814
|
-
sequence: 3,
|
|
815
|
-
defaultInView: true,
|
|
816
|
-
type:
|
|
817
|
-
allowsNull: false,
|
|
818
|
-
isPrimaryKey: false,
|
|
819
|
-
description:
|
|
899
|
+
"name": "Type",
|
|
900
|
+
"sequence": 3,
|
|
901
|
+
"defaultInView": true,
|
|
902
|
+
"type": "nvarchar",
|
|
903
|
+
"allowsNull": false,
|
|
904
|
+
"isPrimaryKey": false,
|
|
905
|
+
"description": "Type of relationship (One to Many, Many to One, etc.)"
|
|
820
906
|
},
|
|
821
907
|
{
|
|
822
|
-
name:
|
|
823
|
-
sequence: 4,
|
|
824
|
-
defaultInView: true,
|
|
825
|
-
type:
|
|
826
|
-
allowsNull: true,
|
|
827
|
-
isPrimaryKey: false,
|
|
828
|
-
description:
|
|
908
|
+
"name": "DisplayName",
|
|
909
|
+
"sequence": 4,
|
|
910
|
+
"defaultInView": true,
|
|
911
|
+
"type": "nvarchar",
|
|
912
|
+
"allowsNull": true,
|
|
913
|
+
"isPrimaryKey": false,
|
|
914
|
+
"description": "User-friendly name for the relationship"
|
|
829
915
|
},
|
|
830
916
|
{
|
|
831
|
-
name:
|
|
832
|
-
sequence: 5,
|
|
833
|
-
defaultInView: true,
|
|
834
|
-
type:
|
|
835
|
-
allowsNull: true,
|
|
836
|
-
isPrimaryKey: false,
|
|
837
|
-
description:
|
|
917
|
+
"name": "RelatedEntityJoinField",
|
|
918
|
+
"sequence": 5,
|
|
919
|
+
"defaultInView": true,
|
|
920
|
+
"type": "nvarchar",
|
|
921
|
+
"allowsNull": true,
|
|
922
|
+
"isPrimaryKey": false,
|
|
923
|
+
"description": "The field in the related entity that joins to this entity"
|
|
838
924
|
},
|
|
839
925
|
{
|
|
840
|
-
name:
|
|
841
|
-
sequence: 6,
|
|
842
|
-
defaultInView: false,
|
|
843
|
-
type:
|
|
844
|
-
allowsNull: false,
|
|
845
|
-
isPrimaryKey: false,
|
|
846
|
-
description:
|
|
847
|
-
}
|
|
926
|
+
"name": "Sequence",
|
|
927
|
+
"sequence": 6,
|
|
928
|
+
"defaultInView": false,
|
|
929
|
+
"type": "int",
|
|
930
|
+
"allowsNull": false,
|
|
931
|
+
"isPrimaryKey": false,
|
|
932
|
+
"description": "Display order"
|
|
933
|
+
}
|
|
848
934
|
],
|
|
849
|
-
permissionLevelNeeded: [
|
|
850
|
-
|
|
851
|
-
|
|
935
|
+
"permissionLevelNeeded": [
|
|
936
|
+
"read"
|
|
937
|
+
],
|
|
938
|
+
"usageContext": "Details panel to show entity relationships"
|
|
939
|
+
}
|
|
852
940
|
],
|
|
853
|
-
queries: [],
|
|
854
|
-
description:
|
|
855
|
-
'This component requires access to entity metadata including entities, their fields, and relationships to provide a comprehensive entity browsing experience',
|
|
941
|
+
"queries": [],
|
|
942
|
+
"description": "This component requires access to entity metadata including entities, their fields, and relationships to provide a comprehensive entity browsing experience"
|
|
856
943
|
},
|
|
857
|
-
technicalDesign:
|
|
858
|
-
|
|
859
|
-
properties: [
|
|
944
|
+
"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",
|
|
945
|
+
"properties": [
|
|
860
946
|
{
|
|
861
|
-
name:
|
|
862
|
-
description:
|
|
863
|
-
type:
|
|
864
|
-
required: true
|
|
947
|
+
"name": "entities",
|
|
948
|
+
"description": "Array of entity objects to display",
|
|
949
|
+
"type": "array",
|
|
950
|
+
"required": true
|
|
865
951
|
},
|
|
866
952
|
{
|
|
867
|
-
name:
|
|
868
|
-
description:
|
|
869
|
-
type:
|
|
870
|
-
required: true,
|
|
871
|
-
possibleValues: [
|
|
953
|
+
"name": "viewMode",
|
|
954
|
+
"description": "Display mode - grid or card view",
|
|
955
|
+
"type": "string",
|
|
956
|
+
"required": true,
|
|
957
|
+
"possibleValues": [
|
|
958
|
+
"grid",
|
|
959
|
+
"card"
|
|
960
|
+
]
|
|
872
961
|
},
|
|
873
962
|
{
|
|
874
|
-
name:
|
|
875
|
-
description:
|
|
876
|
-
type:
|
|
877
|
-
required: true
|
|
963
|
+
"name": "selectedEntityId",
|
|
964
|
+
"description": "ID of the currently selected entity",
|
|
965
|
+
"type": "string",
|
|
966
|
+
"required": true
|
|
878
967
|
},
|
|
879
968
|
{
|
|
880
|
-
name:
|
|
881
|
-
description:
|
|
882
|
-
type:
|
|
883
|
-
required: true
|
|
969
|
+
"name": "onSelectEntity",
|
|
970
|
+
"description": "Callback when an entity is selected",
|
|
971
|
+
"type": "function",
|
|
972
|
+
"required": true
|
|
884
973
|
},
|
|
885
974
|
{
|
|
886
|
-
name:
|
|
887
|
-
description:
|
|
888
|
-
type:
|
|
889
|
-
required: true,
|
|
890
|
-
defaultValue:
|
|
975
|
+
"name": "sortBy",
|
|
976
|
+
"description": "Field to sort by",
|
|
977
|
+
"type": "string",
|
|
978
|
+
"required": true,
|
|
979
|
+
"defaultValue": "Name"
|
|
891
980
|
},
|
|
892
981
|
{
|
|
893
|
-
name:
|
|
894
|
-
description:
|
|
895
|
-
type:
|
|
896
|
-
required: true,
|
|
897
|
-
defaultValue:
|
|
898
|
-
possibleValues: [
|
|
982
|
+
"name": "sortDirection",
|
|
983
|
+
"description": "Sort direction",
|
|
984
|
+
"type": "string",
|
|
985
|
+
"required": true,
|
|
986
|
+
"defaultValue": "asc",
|
|
987
|
+
"possibleValues": [
|
|
988
|
+
"asc",
|
|
989
|
+
"desc"
|
|
990
|
+
]
|
|
899
991
|
},
|
|
900
992
|
{
|
|
901
|
-
name:
|
|
902
|
-
description:
|
|
903
|
-
type:
|
|
904
|
-
required: true
|
|
905
|
-
}
|
|
993
|
+
"name": "onSortChange",
|
|
994
|
+
"description": "Callback when sort changes",
|
|
995
|
+
"type": "function",
|
|
996
|
+
"required": true
|
|
997
|
+
}
|
|
906
998
|
],
|
|
907
|
-
events: [
|
|
999
|
+
"events": [
|
|
908
1000
|
{
|
|
909
|
-
name:
|
|
910
|
-
description:
|
|
911
|
-
parameters: [
|
|
1001
|
+
"name": "onSelectEntity",
|
|
1002
|
+
"description": "Fired when an entity is selected",
|
|
1003
|
+
"parameters": [
|
|
912
1004
|
{
|
|
913
|
-
name:
|
|
914
|
-
description:
|
|
915
|
-
type:
|
|
916
|
-
}
|
|
917
|
-
]
|
|
1005
|
+
"name": "entityId",
|
|
1006
|
+
"description": "ID of the selected entity",
|
|
1007
|
+
"type": "string"
|
|
1008
|
+
}
|
|
1009
|
+
]
|
|
918
1010
|
},
|
|
919
1011
|
{
|
|
920
|
-
name:
|
|
921
|
-
description:
|
|
922
|
-
parameters: [
|
|
1012
|
+
"name": "onSortChange",
|
|
1013
|
+
"description": "Fired when sort configuration changes",
|
|
1014
|
+
"parameters": [
|
|
923
1015
|
{
|
|
924
|
-
name:
|
|
925
|
-
description:
|
|
926
|
-
type:
|
|
1016
|
+
"name": "sortBy",
|
|
1017
|
+
"description": "Field to sort by",
|
|
1018
|
+
"type": "string"
|
|
927
1019
|
},
|
|
928
1020
|
{
|
|
929
|
-
name:
|
|
930
|
-
description:
|
|
931
|
-
type:
|
|
932
|
-
}
|
|
933
|
-
]
|
|
934
|
-
}
|
|
1021
|
+
"name": "sortDirection",
|
|
1022
|
+
"description": "Sort direction",
|
|
1023
|
+
"type": "string"
|
|
1024
|
+
}
|
|
1025
|
+
]
|
|
1026
|
+
}
|
|
935
1027
|
],
|
|
936
|
-
exampleUsage:
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
dependencies: [
|
|
1028
|
+
"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/>",
|
|
1029
|
+
"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}",
|
|
1030
|
+
"dependencies": [
|
|
940
1031
|
{
|
|
941
|
-
name:
|
|
942
|
-
location:
|
|
943
|
-
namespace:
|
|
944
|
-
version:
|
|
945
|
-
}
|
|
1032
|
+
"name": "DataGrid",
|
|
1033
|
+
"location": "registry",
|
|
1034
|
+
"namespace": "Generic/UI/Table",
|
|
1035
|
+
"version": "^1.0.0"
|
|
1036
|
+
}
|
|
946
1037
|
],
|
|
947
|
-
libraries: []
|
|
1038
|
+
"libraries": []
|
|
948
1039
|
},
|
|
949
1040
|
{
|
|
950
|
-
name:
|
|
951
|
-
title:
|
|
952
|
-
description:
|
|
953
|
-
type:
|
|
954
|
-
functionalRequirements:
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
"
|
|
958
|
-
|
|
959
|
-
mode: 'views',
|
|
960
|
-
entities: [
|
|
1041
|
+
"name": "EntityDetails",
|
|
1042
|
+
"title": "Entity Details Panel",
|
|
1043
|
+
"description": "Sliding panel that displays detailed information about a selected entity including fields and relationships",
|
|
1044
|
+
"type": "form",
|
|
1045
|
+
"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",
|
|
1046
|
+
"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",
|
|
1047
|
+
"dataRequirements": {
|
|
1048
|
+
"mode": "views",
|
|
1049
|
+
"entities": [
|
|
961
1050
|
{
|
|
962
|
-
name:
|
|
963
|
-
description:
|
|
964
|
-
displayFields: [
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1051
|
+
"name": "Entities",
|
|
1052
|
+
"description": "Metadata about all entities in the system",
|
|
1053
|
+
"displayFields": [
|
|
1054
|
+
"ID",
|
|
1055
|
+
"Name",
|
|
1056
|
+
"DisplayName",
|
|
1057
|
+
"NameSuffix",
|
|
1058
|
+
"Description",
|
|
1059
|
+
"SchemaName",
|
|
1060
|
+
"BaseTable",
|
|
1061
|
+
"BaseView"
|
|
1062
|
+
],
|
|
1063
|
+
"filterFields": [
|
|
1064
|
+
"SchemaName",
|
|
1065
|
+
"BaseTable"
|
|
1066
|
+
],
|
|
1067
|
+
"sortFields": [
|
|
1068
|
+
"Name",
|
|
1069
|
+
"DisplayName"
|
|
1070
|
+
],
|
|
1071
|
+
"fieldMetadata": [
|
|
968
1072
|
{
|
|
969
|
-
name:
|
|
970
|
-
sequence: 1,
|
|
971
|
-
defaultInView: false,
|
|
972
|
-
type:
|
|
973
|
-
allowsNull: false,
|
|
974
|
-
isPrimaryKey: true,
|
|
975
|
-
description:
|
|
1073
|
+
"name": "ID",
|
|
1074
|
+
"sequence": 1,
|
|
1075
|
+
"defaultInView": false,
|
|
1076
|
+
"type": "uniqueidentifier",
|
|
1077
|
+
"allowsNull": false,
|
|
1078
|
+
"isPrimaryKey": true,
|
|
1079
|
+
"description": "Unique identifier for the entity"
|
|
976
1080
|
},
|
|
977
1081
|
{
|
|
978
|
-
name:
|
|
979
|
-
sequence: 2,
|
|
980
|
-
defaultInView: true,
|
|
981
|
-
type:
|
|
982
|
-
allowsNull: false,
|
|
983
|
-
isPrimaryKey: false,
|
|
984
|
-
description:
|
|
1082
|
+
"name": "Name",
|
|
1083
|
+
"sequence": 2,
|
|
1084
|
+
"defaultInView": true,
|
|
1085
|
+
"type": "nvarchar",
|
|
1086
|
+
"allowsNull": false,
|
|
1087
|
+
"isPrimaryKey": false,
|
|
1088
|
+
"description": "System name of the entity"
|
|
985
1089
|
},
|
|
986
1090
|
{
|
|
987
|
-
name:
|
|
988
|
-
sequence: 3,
|
|
989
|
-
defaultInView: true,
|
|
990
|
-
type:
|
|
991
|
-
allowsNull: true,
|
|
992
|
-
isPrimaryKey: false,
|
|
993
|
-
description:
|
|
1091
|
+
"name": "DisplayName",
|
|
1092
|
+
"sequence": 3,
|
|
1093
|
+
"defaultInView": true,
|
|
1094
|
+
"type": "nvarchar",
|
|
1095
|
+
"allowsNull": true,
|
|
1096
|
+
"isPrimaryKey": false,
|
|
1097
|
+
"description": "User-friendly display name for the entity"
|
|
994
1098
|
},
|
|
995
1099
|
{
|
|
996
|
-
name:
|
|
997
|
-
sequence: 4,
|
|
998
|
-
defaultInView: true,
|
|
999
|
-
type:
|
|
1000
|
-
allowsNull: true,
|
|
1001
|
-
isPrimaryKey: false,
|
|
1002
|
-
description:
|
|
1100
|
+
"name": "NameSuffix",
|
|
1101
|
+
"sequence": 4,
|
|
1102
|
+
"defaultInView": true,
|
|
1103
|
+
"type": "nvarchar",
|
|
1104
|
+
"allowsNull": true,
|
|
1105
|
+
"isPrimaryKey": false,
|
|
1106
|
+
"description": "Optional suffix appended to entity names for display purposes"
|
|
1003
1107
|
},
|
|
1004
1108
|
{
|
|
1005
|
-
name:
|
|
1006
|
-
sequence: 5,
|
|
1007
|
-
defaultInView: true,
|
|
1008
|
-
type:
|
|
1009
|
-
allowsNull: true,
|
|
1010
|
-
isPrimaryKey: false,
|
|
1011
|
-
description:
|
|
1109
|
+
"name": "Description",
|
|
1110
|
+
"sequence": 5,
|
|
1111
|
+
"defaultInView": true,
|
|
1112
|
+
"type": "nvarchar",
|
|
1113
|
+
"allowsNull": true,
|
|
1114
|
+
"isPrimaryKey": false,
|
|
1115
|
+
"description": "Description of the entity"
|
|
1012
1116
|
},
|
|
1013
1117
|
{
|
|
1014
|
-
name:
|
|
1015
|
-
sequence: 6,
|
|
1016
|
-
defaultInView: true,
|
|
1017
|
-
type:
|
|
1018
|
-
allowsNull: true,
|
|
1019
|
-
isPrimaryKey: false,
|
|
1020
|
-
description:
|
|
1118
|
+
"name": "SchemaName",
|
|
1119
|
+
"sequence": 6,
|
|
1120
|
+
"defaultInView": true,
|
|
1121
|
+
"type": "nvarchar",
|
|
1122
|
+
"allowsNull": true,
|
|
1123
|
+
"isPrimaryKey": false,
|
|
1124
|
+
"description": "Database schema name"
|
|
1021
1125
|
},
|
|
1022
1126
|
{
|
|
1023
|
-
name:
|
|
1024
|
-
sequence: 7,
|
|
1025
|
-
defaultInView: true,
|
|
1026
|
-
type:
|
|
1027
|
-
allowsNull: true,
|
|
1028
|
-
isPrimaryKey: false,
|
|
1029
|
-
description:
|
|
1127
|
+
"name": "BaseTable",
|
|
1128
|
+
"sequence": 7,
|
|
1129
|
+
"defaultInView": true,
|
|
1130
|
+
"type": "nvarchar",
|
|
1131
|
+
"allowsNull": true,
|
|
1132
|
+
"isPrimaryKey": false,
|
|
1133
|
+
"description": "Base table in the database"
|
|
1030
1134
|
},
|
|
1031
1135
|
{
|
|
1032
|
-
name:
|
|
1033
|
-
sequence: 8,
|
|
1034
|
-
defaultInView: true,
|
|
1035
|
-
type:
|
|
1036
|
-
allowsNull: false,
|
|
1037
|
-
isPrimaryKey: false,
|
|
1038
|
-
description:
|
|
1039
|
-
}
|
|
1136
|
+
"name": "BaseView",
|
|
1137
|
+
"sequence": 8,
|
|
1138
|
+
"defaultInView": true,
|
|
1139
|
+
"type": "nvarchar",
|
|
1140
|
+
"allowsNull": false,
|
|
1141
|
+
"isPrimaryKey": false,
|
|
1142
|
+
"description": "Base view used for the entity"
|
|
1143
|
+
}
|
|
1040
1144
|
],
|
|
1041
|
-
permissionLevelNeeded: [
|
|
1042
|
-
|
|
1145
|
+
"permissionLevelNeeded": [
|
|
1146
|
+
"read"
|
|
1147
|
+
],
|
|
1148
|
+
"usageContext": "Main entity list display and filtering"
|
|
1043
1149
|
},
|
|
1044
1150
|
{
|
|
1045
|
-
name:
|
|
1046
|
-
description:
|
|
1047
|
-
displayFields: [
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1151
|
+
"name": "Entity Fields",
|
|
1152
|
+
"description": "Fields belonging to each entity",
|
|
1153
|
+
"displayFields": [
|
|
1154
|
+
"Name",
|
|
1155
|
+
"DisplayName",
|
|
1156
|
+
"Type",
|
|
1157
|
+
"Length",
|
|
1158
|
+
"AllowsNull",
|
|
1159
|
+
"IsPrimaryKey",
|
|
1160
|
+
"IsUnique"
|
|
1161
|
+
],
|
|
1162
|
+
"filterFields": [
|
|
1163
|
+
"EntityID"
|
|
1164
|
+
],
|
|
1165
|
+
"sortFields": [
|
|
1166
|
+
"Sequence",
|
|
1167
|
+
"Name"
|
|
1168
|
+
],
|
|
1169
|
+
"fieldMetadata": [
|
|
1051
1170
|
{
|
|
1052
|
-
name:
|
|
1053
|
-
sequence: 1,
|
|
1054
|
-
defaultInView: false,
|
|
1055
|
-
type:
|
|
1056
|
-
allowsNull: false,
|
|
1057
|
-
isPrimaryKey: false,
|
|
1058
|
-
description:
|
|
1171
|
+
"name": "EntityID",
|
|
1172
|
+
"sequence": 1,
|
|
1173
|
+
"defaultInView": false,
|
|
1174
|
+
"type": "uniqueidentifier",
|
|
1175
|
+
"allowsNull": false,
|
|
1176
|
+
"isPrimaryKey": false,
|
|
1177
|
+
"description": "Reference to parent entity"
|
|
1059
1178
|
},
|
|
1060
1179
|
{
|
|
1061
|
-
name:
|
|
1062
|
-
sequence: 2,
|
|
1063
|
-
defaultInView: true,
|
|
1064
|
-
type:
|
|
1065
|
-
allowsNull: false,
|
|
1066
|
-
isPrimaryKey: false,
|
|
1067
|
-
description:
|
|
1180
|
+
"name": "Name",
|
|
1181
|
+
"sequence": 2,
|
|
1182
|
+
"defaultInView": true,
|
|
1183
|
+
"type": "nvarchar",
|
|
1184
|
+
"allowsNull": false,
|
|
1185
|
+
"isPrimaryKey": false,
|
|
1186
|
+
"description": "Field name"
|
|
1068
1187
|
},
|
|
1069
1188
|
{
|
|
1070
|
-
name:
|
|
1071
|
-
sequence: 3,
|
|
1072
|
-
defaultInView: true,
|
|
1073
|
-
type:
|
|
1074
|
-
allowsNull: true,
|
|
1075
|
-
isPrimaryKey: false,
|
|
1076
|
-
description:
|
|
1189
|
+
"name": "DisplayName",
|
|
1190
|
+
"sequence": 3,
|
|
1191
|
+
"defaultInView": true,
|
|
1192
|
+
"type": "nvarchar",
|
|
1193
|
+
"allowsNull": true,
|
|
1194
|
+
"isPrimaryKey": false,
|
|
1195
|
+
"description": "User-friendly field name"
|
|
1077
1196
|
},
|
|
1078
1197
|
{
|
|
1079
|
-
name:
|
|
1080
|
-
sequence: 4,
|
|
1081
|
-
defaultInView: true,
|
|
1082
|
-
type:
|
|
1083
|
-
allowsNull: false,
|
|
1084
|
-
isPrimaryKey: false,
|
|
1085
|
-
description:
|
|
1198
|
+
"name": "Type",
|
|
1199
|
+
"sequence": 4,
|
|
1200
|
+
"defaultInView": true,
|
|
1201
|
+
"type": "nvarchar",
|
|
1202
|
+
"allowsNull": false,
|
|
1203
|
+
"isPrimaryKey": false,
|
|
1204
|
+
"description": "Data type of the field"
|
|
1086
1205
|
},
|
|
1087
1206
|
{
|
|
1088
|
-
name:
|
|
1089
|
-
sequence: 5,
|
|
1090
|
-
defaultInView: true,
|
|
1091
|
-
type:
|
|
1092
|
-
allowsNull: true,
|
|
1093
|
-
isPrimaryKey: false,
|
|
1094
|
-
description:
|
|
1207
|
+
"name": "Length",
|
|
1208
|
+
"sequence": 5,
|
|
1209
|
+
"defaultInView": true,
|
|
1210
|
+
"type": "int",
|
|
1211
|
+
"allowsNull": true,
|
|
1212
|
+
"isPrimaryKey": false,
|
|
1213
|
+
"description": "Maximum length for string fields"
|
|
1095
1214
|
},
|
|
1096
1215
|
{
|
|
1097
|
-
name:
|
|
1098
|
-
sequence: 6,
|
|
1099
|
-
defaultInView: true,
|
|
1100
|
-
type:
|
|
1101
|
-
allowsNull: false,
|
|
1102
|
-
isPrimaryKey: false,
|
|
1103
|
-
description:
|
|
1216
|
+
"name": "AllowsNull",
|
|
1217
|
+
"sequence": 6,
|
|
1218
|
+
"defaultInView": true,
|
|
1219
|
+
"type": "bit",
|
|
1220
|
+
"allowsNull": false,
|
|
1221
|
+
"isPrimaryKey": false,
|
|
1222
|
+
"description": "Whether field allows null values"
|
|
1104
1223
|
},
|
|
1105
1224
|
{
|
|
1106
|
-
name:
|
|
1107
|
-
sequence: 7,
|
|
1108
|
-
defaultInView: true,
|
|
1109
|
-
type:
|
|
1110
|
-
allowsNull: false,
|
|
1111
|
-
isPrimaryKey: false,
|
|
1112
|
-
description:
|
|
1225
|
+
"name": "IsPrimaryKey",
|
|
1226
|
+
"sequence": 7,
|
|
1227
|
+
"defaultInView": true,
|
|
1228
|
+
"type": "bit",
|
|
1229
|
+
"allowsNull": false,
|
|
1230
|
+
"isPrimaryKey": false,
|
|
1231
|
+
"description": "Whether field is part of primary key"
|
|
1113
1232
|
},
|
|
1114
1233
|
{
|
|
1115
|
-
name:
|
|
1116
|
-
sequence: 8,
|
|
1117
|
-
defaultInView: true,
|
|
1118
|
-
type:
|
|
1119
|
-
allowsNull: false,
|
|
1120
|
-
isPrimaryKey: false,
|
|
1121
|
-
description:
|
|
1234
|
+
"name": "IsUnique",
|
|
1235
|
+
"sequence": 8,
|
|
1236
|
+
"defaultInView": true,
|
|
1237
|
+
"type": "bit",
|
|
1238
|
+
"allowsNull": false,
|
|
1239
|
+
"isPrimaryKey": false,
|
|
1240
|
+
"description": "Whether field must be unique"
|
|
1122
1241
|
},
|
|
1123
1242
|
{
|
|
1124
|
-
name:
|
|
1125
|
-
sequence: 9,
|
|
1126
|
-
defaultInView: false,
|
|
1127
|
-
type:
|
|
1128
|
-
allowsNull: false,
|
|
1129
|
-
isPrimaryKey: false,
|
|
1130
|
-
description:
|
|
1131
|
-
}
|
|
1243
|
+
"name": "Sequence",
|
|
1244
|
+
"sequence": 9,
|
|
1245
|
+
"defaultInView": false,
|
|
1246
|
+
"type": "int",
|
|
1247
|
+
"allowsNull": false,
|
|
1248
|
+
"isPrimaryKey": false,
|
|
1249
|
+
"description": "Display order of the field"
|
|
1250
|
+
}
|
|
1132
1251
|
],
|
|
1133
|
-
permissionLevelNeeded: [
|
|
1134
|
-
|
|
1252
|
+
"permissionLevelNeeded": [
|
|
1253
|
+
"read"
|
|
1254
|
+
],
|
|
1255
|
+
"usageContext": "Details panel to show entity fields"
|
|
1135
1256
|
},
|
|
1136
1257
|
{
|
|
1137
|
-
name:
|
|
1138
|
-
description:
|
|
1139
|
-
displayFields: [
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1258
|
+
"name": "Entity Relationships",
|
|
1259
|
+
"description": "Relationships between entities",
|
|
1260
|
+
"displayFields": [
|
|
1261
|
+
"RelatedEntity",
|
|
1262
|
+
"Type",
|
|
1263
|
+
"DisplayName",
|
|
1264
|
+
"RelatedEntityJoinField"
|
|
1265
|
+
],
|
|
1266
|
+
"filterFields": [
|
|
1267
|
+
"EntityID"
|
|
1268
|
+
],
|
|
1269
|
+
"sortFields": [
|
|
1270
|
+
"Sequence",
|
|
1271
|
+
"RelatedEntity"
|
|
1272
|
+
],
|
|
1273
|
+
"fieldMetadata": [
|
|
1143
1274
|
{
|
|
1144
|
-
name:
|
|
1145
|
-
sequence: 1,
|
|
1146
|
-
defaultInView: false,
|
|
1147
|
-
type:
|
|
1148
|
-
allowsNull: false,
|
|
1149
|
-
isPrimaryKey: false,
|
|
1150
|
-
description:
|
|
1275
|
+
"name": "EntityID",
|
|
1276
|
+
"sequence": 1,
|
|
1277
|
+
"defaultInView": false,
|
|
1278
|
+
"type": "uniqueidentifier",
|
|
1279
|
+
"allowsNull": false,
|
|
1280
|
+
"isPrimaryKey": false,
|
|
1281
|
+
"description": "Reference to parent entity"
|
|
1151
1282
|
},
|
|
1152
1283
|
{
|
|
1153
|
-
name:
|
|
1154
|
-
sequence: 2,
|
|
1155
|
-
defaultInView: true,
|
|
1156
|
-
type:
|
|
1157
|
-
allowsNull: false,
|
|
1158
|
-
isPrimaryKey: false,
|
|
1159
|
-
description:
|
|
1284
|
+
"name": "RelatedEntity",
|
|
1285
|
+
"sequence": 2,
|
|
1286
|
+
"defaultInView": true,
|
|
1287
|
+
"type": "nvarchar",
|
|
1288
|
+
"allowsNull": false,
|
|
1289
|
+
"isPrimaryKey": false,
|
|
1290
|
+
"description": "The related entity in the relationship"
|
|
1160
1291
|
},
|
|
1161
1292
|
{
|
|
1162
|
-
name:
|
|
1163
|
-
sequence: 3,
|
|
1164
|
-
defaultInView: true,
|
|
1165
|
-
type:
|
|
1166
|
-
allowsNull: false,
|
|
1167
|
-
isPrimaryKey: false,
|
|
1168
|
-
description:
|
|
1293
|
+
"name": "Type",
|
|
1294
|
+
"sequence": 3,
|
|
1295
|
+
"defaultInView": true,
|
|
1296
|
+
"type": "nvarchar",
|
|
1297
|
+
"allowsNull": false,
|
|
1298
|
+
"isPrimaryKey": false,
|
|
1299
|
+
"description": "Type of relationship (One to Many, Many to One, etc.)"
|
|
1169
1300
|
},
|
|
1170
1301
|
{
|
|
1171
|
-
name:
|
|
1172
|
-
sequence: 4,
|
|
1173
|
-
defaultInView: true,
|
|
1174
|
-
type:
|
|
1175
|
-
allowsNull: true,
|
|
1176
|
-
isPrimaryKey: false,
|
|
1177
|
-
description:
|
|
1302
|
+
"name": "DisplayName",
|
|
1303
|
+
"sequence": 4,
|
|
1304
|
+
"defaultInView": true,
|
|
1305
|
+
"type": "nvarchar",
|
|
1306
|
+
"allowsNull": true,
|
|
1307
|
+
"isPrimaryKey": false,
|
|
1308
|
+
"description": "User-friendly name for the relationship"
|
|
1178
1309
|
},
|
|
1179
1310
|
{
|
|
1180
|
-
name:
|
|
1181
|
-
sequence: 5,
|
|
1182
|
-
defaultInView: true,
|
|
1183
|
-
type:
|
|
1184
|
-
allowsNull: true,
|
|
1185
|
-
isPrimaryKey: false,
|
|
1186
|
-
description:
|
|
1311
|
+
"name": "RelatedEntityJoinField",
|
|
1312
|
+
"sequence": 5,
|
|
1313
|
+
"defaultInView": true,
|
|
1314
|
+
"type": "nvarchar",
|
|
1315
|
+
"allowsNull": true,
|
|
1316
|
+
"isPrimaryKey": false,
|
|
1317
|
+
"description": "The field in the related entity that joins to this entity"
|
|
1187
1318
|
},
|
|
1188
1319
|
{
|
|
1189
|
-
name:
|
|
1190
|
-
sequence: 6,
|
|
1191
|
-
defaultInView: false,
|
|
1192
|
-
type:
|
|
1193
|
-
allowsNull: false,
|
|
1194
|
-
isPrimaryKey: false,
|
|
1195
|
-
description:
|
|
1196
|
-
}
|
|
1320
|
+
"name": "Sequence",
|
|
1321
|
+
"sequence": 6,
|
|
1322
|
+
"defaultInView": false,
|
|
1323
|
+
"type": "int",
|
|
1324
|
+
"allowsNull": false,
|
|
1325
|
+
"isPrimaryKey": false,
|
|
1326
|
+
"description": "Display order"
|
|
1327
|
+
}
|
|
1197
1328
|
],
|
|
1198
|
-
permissionLevelNeeded: [
|
|
1199
|
-
|
|
1200
|
-
|
|
1329
|
+
"permissionLevelNeeded": [
|
|
1330
|
+
"read"
|
|
1331
|
+
],
|
|
1332
|
+
"usageContext": "Details panel to show entity relationships"
|
|
1333
|
+
}
|
|
1201
1334
|
],
|
|
1202
|
-
queries: [],
|
|
1203
|
-
description:
|
|
1204
|
-
'This component requires access to entity metadata including entities, their fields, and relationships to provide a comprehensive entity browsing experience',
|
|
1335
|
+
"queries": [],
|
|
1336
|
+
"description": "This component requires access to entity metadata including entities, their fields, and relationships to provide a comprehensive entity browsing experience"
|
|
1205
1337
|
},
|
|
1206
|
-
properties: [
|
|
1338
|
+
"properties": [
|
|
1207
1339
|
{
|
|
1208
|
-
name:
|
|
1209
|
-
description:
|
|
1210
|
-
type:
|
|
1211
|
-
required: true
|
|
1340
|
+
"name": "entity",
|
|
1341
|
+
"description": "The selected entity object",
|
|
1342
|
+
"type": "object",
|
|
1343
|
+
"required": true
|
|
1212
1344
|
},
|
|
1213
1345
|
{
|
|
1214
|
-
name:
|
|
1215
|
-
description:
|
|
1216
|
-
type:
|
|
1217
|
-
required: true,
|
|
1218
|
-
defaultValue: []
|
|
1346
|
+
"name": "fields",
|
|
1347
|
+
"description": "Array of fields for the entity",
|
|
1348
|
+
"type": "array",
|
|
1349
|
+
"required": true,
|
|
1350
|
+
"defaultValue": []
|
|
1219
1351
|
},
|
|
1220
1352
|
{
|
|
1221
|
-
name:
|
|
1222
|
-
description:
|
|
1223
|
-
type:
|
|
1224
|
-
required: true,
|
|
1225
|
-
defaultValue: []
|
|
1353
|
+
"name": "relationships",
|
|
1354
|
+
"description": "Array of relationships for the entity",
|
|
1355
|
+
"type": "array",
|
|
1356
|
+
"required": true,
|
|
1357
|
+
"defaultValue": []
|
|
1226
1358
|
},
|
|
1227
1359
|
{
|
|
1228
|
-
name:
|
|
1229
|
-
description:
|
|
1230
|
-
type:
|
|
1231
|
-
required: true
|
|
1360
|
+
"name": "isOpen",
|
|
1361
|
+
"description": "Whether the panel is open",
|
|
1362
|
+
"type": "boolean",
|
|
1363
|
+
"required": true
|
|
1232
1364
|
},
|
|
1233
1365
|
{
|
|
1234
|
-
name:
|
|
1235
|
-
description:
|
|
1236
|
-
type:
|
|
1237
|
-
required: true
|
|
1366
|
+
"name": "onClose",
|
|
1367
|
+
"description": "Callback to close the panel",
|
|
1368
|
+
"type": "function",
|
|
1369
|
+
"required": true
|
|
1238
1370
|
},
|
|
1239
1371
|
{
|
|
1240
|
-
name:
|
|
1241
|
-
description:
|
|
1242
|
-
type:
|
|
1243
|
-
required: true
|
|
1244
|
-
}
|
|
1372
|
+
"name": "onOpenRecord",
|
|
1373
|
+
"description": "Callback to open the entity record",
|
|
1374
|
+
"type": "function",
|
|
1375
|
+
"required": true
|
|
1376
|
+
}
|
|
1245
1377
|
],
|
|
1246
|
-
events: [
|
|
1378
|
+
"events": [
|
|
1247
1379
|
{
|
|
1248
|
-
name:
|
|
1249
|
-
description:
|
|
1250
|
-
parameters: []
|
|
1380
|
+
"name": "onClose",
|
|
1381
|
+
"description": "Fired when the panel should close",
|
|
1382
|
+
"parameters": []
|
|
1251
1383
|
},
|
|
1252
1384
|
{
|
|
1253
|
-
name:
|
|
1254
|
-
description:
|
|
1255
|
-
parameters: [
|
|
1385
|
+
"name": "onOpenRecord",
|
|
1386
|
+
"description": "Fired when the open record button is clicked",
|
|
1387
|
+
"parameters": [
|
|
1256
1388
|
{
|
|
1257
|
-
name:
|
|
1258
|
-
description:
|
|
1259
|
-
type:
|
|
1260
|
-
}
|
|
1261
|
-
]
|
|
1262
|
-
}
|
|
1389
|
+
"name": "entityName",
|
|
1390
|
+
"description": "Name of the entity to open",
|
|
1391
|
+
"type": "string"
|
|
1392
|
+
}
|
|
1393
|
+
]
|
|
1394
|
+
}
|
|
1263
1395
|
],
|
|
1264
|
-
exampleUsage:
|
|
1265
|
-
'<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/>',
|
|
1266
|
-
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}",
|
|
1267
|
-
dependencies: [
|
|
1396
|
+
"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/>",
|
|
1397
|
+
"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}",
|
|
1398
|
+
"dependencies": [
|
|
1268
1399
|
{
|
|
1269
|
-
name:
|
|
1270
|
-
location:
|
|
1271
|
-
namespace:
|
|
1272
|
-
version:
|
|
1273
|
-
}
|
|
1400
|
+
"name": "OpenRecordButton",
|
|
1401
|
+
"location": "registry",
|
|
1402
|
+
"namespace": "Generic/Navigation",
|
|
1403
|
+
"version": "^1.0.0"
|
|
1404
|
+
}
|
|
1274
1405
|
],
|
|
1275
|
-
libraries: []
|
|
1406
|
+
"libraries": []
|
|
1276
1407
|
},
|
|
1277
1408
|
{
|
|
1278
|
-
name:
|
|
1279
|
-
title:
|
|
1280
|
-
description:
|
|
1281
|
-
type:
|
|
1282
|
-
functionalRequirements:
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
entities: [
|
|
1409
|
+
"name": "EntityFilter",
|
|
1410
|
+
"title": "Entity Filter Panel",
|
|
1411
|
+
"description": "Collapsible filter panel for filtering entities by various criteria",
|
|
1412
|
+
"type": "form",
|
|
1413
|
+
"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",
|
|
1414
|
+
"dataRequirements": {
|
|
1415
|
+
"mode": "views",
|
|
1416
|
+
"description": "Receives filter options derived from Entities metadata",
|
|
1417
|
+
"entities": [
|
|
1288
1418
|
{
|
|
1289
|
-
name:
|
|
1290
|
-
description:
|
|
1291
|
-
displayFields: [
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1419
|
+
"name": "Entities",
|
|
1420
|
+
"description": "Source of schema and table filter options",
|
|
1421
|
+
"displayFields": [
|
|
1422
|
+
"SchemaName",
|
|
1423
|
+
"BaseTable"
|
|
1424
|
+
],
|
|
1425
|
+
"filterFields": [],
|
|
1426
|
+
"sortFields": [],
|
|
1427
|
+
"fieldMetadata": [],
|
|
1428
|
+
"permissionLevelNeeded": [
|
|
1429
|
+
"read"
|
|
1430
|
+
],
|
|
1431
|
+
"usageContext": "Extracts unique schema names and base tables for filter dropdowns"
|
|
1432
|
+
}
|
|
1298
1433
|
],
|
|
1299
|
-
queries: []
|
|
1434
|
+
"queries": []
|
|
1300
1435
|
},
|
|
1301
|
-
technicalDesign:
|
|
1302
|
-
|
|
1303
|
-
properties: [
|
|
1436
|
+
"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",
|
|
1437
|
+
"properties": [
|
|
1304
1438
|
{
|
|
1305
|
-
name:
|
|
1306
|
-
description:
|
|
1307
|
-
type:
|
|
1308
|
-
required: true
|
|
1439
|
+
"name": "filters",
|
|
1440
|
+
"description": "Current filter values",
|
|
1441
|
+
"type": "{schema?: string, table?: string, search?: string}",
|
|
1442
|
+
"required": true
|
|
1309
1443
|
},
|
|
1310
1444
|
{
|
|
1311
|
-
name:
|
|
1312
|
-
description:
|
|
1313
|
-
type:
|
|
1314
|
-
required: true
|
|
1445
|
+
"name": "onFilterChange",
|
|
1446
|
+
"description": "Callback when filters change",
|
|
1447
|
+
"type": "(filters: {schema?: string, table?: string, search?: string}) => void",
|
|
1448
|
+
"required": true
|
|
1315
1449
|
},
|
|
1316
1450
|
{
|
|
1317
|
-
name:
|
|
1318
|
-
description:
|
|
1319
|
-
type:
|
|
1320
|
-
required: true
|
|
1451
|
+
"name": "schemas",
|
|
1452
|
+
"description": "Available schema options",
|
|
1453
|
+
"type": "Array<string>",
|
|
1454
|
+
"required": true
|
|
1321
1455
|
},
|
|
1322
1456
|
{
|
|
1323
|
-
name:
|
|
1324
|
-
description:
|
|
1325
|
-
type:
|
|
1326
|
-
required: true
|
|
1457
|
+
"name": "tables",
|
|
1458
|
+
"description": "Available table options",
|
|
1459
|
+
"type": "Array<string>",
|
|
1460
|
+
"required": true
|
|
1327
1461
|
},
|
|
1328
1462
|
{
|
|
1329
|
-
name:
|
|
1330
|
-
description:
|
|
1331
|
-
type:
|
|
1332
|
-
required: true
|
|
1463
|
+
"name": "isCollapsed",
|
|
1464
|
+
"description": "Whether the panel is collapsed",
|
|
1465
|
+
"type": "boolean",
|
|
1466
|
+
"required": true
|
|
1333
1467
|
},
|
|
1334
1468
|
{
|
|
1335
|
-
name:
|
|
1336
|
-
description:
|
|
1337
|
-
type:
|
|
1338
|
-
required: true
|
|
1339
|
-
}
|
|
1469
|
+
"name": "onToggleCollapse",
|
|
1470
|
+
"description": "Callback to toggle collapse state",
|
|
1471
|
+
"type": "() => void",
|
|
1472
|
+
"required": true
|
|
1473
|
+
}
|
|
1340
1474
|
],
|
|
1341
|
-
events: [
|
|
1475
|
+
"events": [
|
|
1342
1476
|
{
|
|
1343
|
-
name:
|
|
1344
|
-
description:
|
|
1345
|
-
parameters: [
|
|
1477
|
+
"name": "onFilterChange",
|
|
1478
|
+
"description": "Fired when filter values change",
|
|
1479
|
+
"parameters": [
|
|
1346
1480
|
{
|
|
1347
|
-
name:
|
|
1348
|
-
description:
|
|
1349
|
-
type:
|
|
1350
|
-
}
|
|
1351
|
-
]
|
|
1481
|
+
"name": "filters",
|
|
1482
|
+
"description": "Updated filter object",
|
|
1483
|
+
"type": "object"
|
|
1484
|
+
}
|
|
1485
|
+
]
|
|
1352
1486
|
},
|
|
1353
1487
|
{
|
|
1354
|
-
name:
|
|
1355
|
-
description:
|
|
1356
|
-
parameters: []
|
|
1357
|
-
}
|
|
1488
|
+
"name": "onToggleCollapse",
|
|
1489
|
+
"description": "Fired when collapse state should toggle",
|
|
1490
|
+
"parameters": []
|
|
1491
|
+
}
|
|
1358
1492
|
],
|
|
1359
|
-
exampleUsage:
|
|
1360
|
-
'<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/>',
|
|
1361
|
-
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}",
|
|
1362
|
-
|
|
1363
|
-
libraries: [],
|
|
1493
|
+
"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/>",
|
|
1494
|
+
"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}",
|
|
1495
|
+
"dependencies": [],
|
|
1496
|
+
"libraries": []
|
|
1364
1497
|
},
|
|
1365
1498
|
{
|
|
1366
|
-
name:
|
|
1367
|
-
location:
|
|
1368
|
-
namespace:
|
|
1369
|
-
version:
|
|
1370
|
-
}
|
|
1499
|
+
"name": "OpenRecordButton",
|
|
1500
|
+
"location": "registry",
|
|
1501
|
+
"namespace": "Generic/Navigation",
|
|
1502
|
+
"version": "^1.0.0"
|
|
1503
|
+
}
|
|
1371
1504
|
],
|
|
1372
|
-
libraries: [],
|
|
1373
|
-
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: '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: '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: '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:', 'Entities', entityName);\n // Open the Entities entity record for the selected entity\n callbacks.OpenEntityRecord('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}",
|
|
1374
|
-
}
|
|
1505
|
+
"libraries": [],
|
|
1506
|
+
"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: '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: '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: '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:', 'Entities', entityName);\n // Open the Entities entity record for the selected entity\n callbacks.OpenEntityRecord('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}"
|
|
1507
|
+
}
|