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