@memberjunction/server 2.112.0 → 2.113.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/skip-agent.d.ts +4 -4
- package/dist/agents/skip-agent.d.ts.map +1 -1
- package/dist/agents/skip-agent.js +951 -808
- package/dist/agents/skip-agent.js.map +1 -1
- package/dist/agents/skip-sdk.d.ts +1 -1
- package/dist/agents/skip-sdk.d.ts.map +1 -1
- package/dist/agents/skip-sdk.js +43 -53
- package/dist/agents/skip-sdk.js.map +1 -1
- package/dist/apolloServer/index.js +1 -1
- package/dist/auth/AuthProviderFactory.d.ts +1 -1
- package/dist/auth/AuthProviderFactory.d.ts.map +1 -1
- package/dist/auth/AuthProviderFactory.js +3 -1
- package/dist/auth/AuthProviderFactory.js.map +1 -1
- package/dist/auth/BaseAuthProvider.d.ts +1 -1
- package/dist/auth/BaseAuthProvider.d.ts.map +1 -1
- package/dist/auth/BaseAuthProvider.js +2 -3
- package/dist/auth/BaseAuthProvider.js.map +1 -1
- package/dist/auth/IAuthProvider.d.ts +1 -1
- package/dist/auth/IAuthProvider.d.ts.map +1 -1
- package/dist/auth/exampleNewUserSubClass.d.ts.map +1 -1
- package/dist/auth/exampleNewUserSubClass.js +1 -1
- package/dist/auth/exampleNewUserSubClass.js.map +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +6 -6
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/initializeProviders.js +1 -1
- package/dist/auth/initializeProviders.js.map +1 -1
- package/dist/auth/newUsers.d.ts +1 -1
- package/dist/auth/newUsers.d.ts.map +1 -1
- package/dist/auth/newUsers.js +7 -7
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/auth/providers/Auth0Provider.d.ts +1 -1
- package/dist/auth/providers/Auth0Provider.d.ts.map +1 -1
- package/dist/auth/providers/Auth0Provider.js +1 -1
- package/dist/auth/providers/Auth0Provider.js.map +1 -1
- package/dist/auth/providers/CognitoProvider.d.ts +1 -1
- package/dist/auth/providers/CognitoProvider.d.ts.map +1 -1
- package/dist/auth/providers/CognitoProvider.js +6 -3
- package/dist/auth/providers/CognitoProvider.js.map +1 -1
- package/dist/auth/providers/GoogleProvider.d.ts +1 -1
- package/dist/auth/providers/GoogleProvider.d.ts.map +1 -1
- package/dist/auth/providers/GoogleProvider.js +1 -1
- package/dist/auth/providers/GoogleProvider.js.map +1 -1
- package/dist/auth/providers/MSALProvider.d.ts +1 -1
- package/dist/auth/providers/MSALProvider.d.ts.map +1 -1
- package/dist/auth/providers/MSALProvider.js +1 -1
- package/dist/auth/providers/MSALProvider.js.map +1 -1
- package/dist/auth/providers/OktaProvider.d.ts +1 -1
- package/dist/auth/providers/OktaProvider.d.ts.map +1 -1
- package/dist/auth/providers/OktaProvider.js +1 -1
- package/dist/auth/providers/OktaProvider.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +10 -22
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +7 -9
- package/dist/context.js.map +1 -1
- package/dist/entitySubclasses/entityPermissions.server.d.ts +1 -1
- package/dist/entitySubclasses/entityPermissions.server.d.ts.map +1 -1
- package/dist/entitySubclasses/entityPermissions.server.js +1 -1
- package/dist/entitySubclasses/entityPermissions.server.js.map +1 -1
- package/dist/generated/generated.d.ts +788 -658
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +2050 -3054
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/KeyInputOutputTypes.d.ts +1 -1
- package/dist/generic/KeyInputOutputTypes.d.ts.map +1 -1
- package/dist/generic/KeyInputOutputTypes.js +1 -1
- package/dist/generic/KeyInputOutputTypes.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +1 -1
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +10 -15
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +1 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +15 -15
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -18
- package/dist/index.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts +2 -2
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +30 -28
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/AskSkipResolver.d.ts +2 -2
- package/dist/resolvers/AskSkipResolver.d.ts.map +1 -1
- package/dist/resolvers/AskSkipResolver.js +50 -60
- package/dist/resolvers/AskSkipResolver.js.map +1 -1
- package/dist/resolvers/ComponentRegistryResolver.d.ts.map +1 -1
- package/dist/resolvers/ComponentRegistryResolver.js +38 -36
- package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
- package/dist/resolvers/CreateQueryResolver.d.ts +1 -1
- package/dist/resolvers/CreateQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/CreateQueryResolver.js +40 -43
- package/dist/resolvers/CreateQueryResolver.js.map +1 -1
- package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
- package/dist/resolvers/DatasetResolver.js +1 -1
- package/dist/resolvers/DatasetResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/EntityResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityResolver.js +1 -1
- package/dist/resolvers/EntityResolver.js.map +1 -1
- package/dist/resolvers/FileCategoryResolver.js +1 -1
- package/dist/resolvers/FileCategoryResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.js +1 -1
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.d.ts +1 -1
- package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.js +5 -5
- package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +6 -8
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.d.ts +3 -3
- package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.js +3 -3
- package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.d.ts.map +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.js +1 -1
- package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +11 -11
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/ReportResolver.js +1 -1
- package/dist/resolvers/ReportResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +28 -27
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunAIPromptResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIPromptResolver.js +31 -31
- package/dist/resolvers/RunAIPromptResolver.js.map +1 -1
- package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTemplateResolver.js +9 -9
- package/dist/resolvers/RunTemplateResolver.js.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js +10 -10
- package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
- package/dist/resolvers/SyncDataResolver.d.ts +1 -1
- package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncDataResolver.js +14 -15
- package/dist/resolvers/SyncDataResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.d.ts +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +44 -48
- package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
- package/dist/resolvers/TaskResolver.d.ts.map +1 -1
- package/dist/resolvers/TaskResolver.js +7 -7
- package/dist/resolvers/TaskResolver.js.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.d.ts +1 -1
- package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.js +12 -12
- package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
- package/dist/resolvers/UserFavoriteResolver.d.ts +1 -1
- package/dist/resolvers/UserFavoriteResolver.d.ts.map +1 -1
- package/dist/resolvers/UserFavoriteResolver.js +1 -1
- package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
- package/dist/resolvers/UserViewResolver.d.ts.map +1 -1
- package/dist/resolvers/UserViewResolver.js.map +1 -1
- package/dist/rest/EntityCRUDHandler.d.ts +1 -1
- package/dist/rest/EntityCRUDHandler.d.ts.map +1 -1
- package/dist/rest/EntityCRUDHandler.js +16 -14
- package/dist/rest/EntityCRUDHandler.js.map +1 -1
- package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
- package/dist/rest/RESTEndpointHandler.js +25 -23
- package/dist/rest/RESTEndpointHandler.js.map +1 -1
- package/dist/rest/ViewOperationsHandler.d.ts +1 -1
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +21 -17
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/scheduler/LearningCycleScheduler.d.ts.map +1 -1
- package/dist/scheduler/LearningCycleScheduler.js.map +1 -1
- package/dist/services/ScheduledJobsService.d.ts.map +1 -1
- package/dist/services/ScheduledJobsService.js +6 -4
- package/dist/services/ScheduledJobsService.js.map +1 -1
- package/dist/services/TaskOrchestrator.d.ts +1 -1
- package/dist/services/TaskOrchestrator.d.ts.map +1 -1
- package/dist/services/TaskOrchestrator.js +30 -30
- package/dist/services/TaskOrchestrator.js.map +1 -1
- package/dist/types.d.ts +3 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/dist/util.d.ts +1 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +2 -2
- package/dist/util.js.map +1 -1
- package/package.json +39 -36
- package/src/agents/skip-agent.ts +1200 -1067
- package/src/agents/skip-sdk.ts +851 -877
- package/src/apolloServer/index.ts +2 -2
- package/src/auth/AuthProviderFactory.ts +14 -8
- package/src/auth/BaseAuthProvider.ts +4 -5
- package/src/auth/IAuthProvider.ts +2 -2
- package/src/auth/exampleNewUserSubClass.ts +2 -9
- package/src/auth/index.ts +26 -31
- package/src/auth/initializeProviders.ts +3 -3
- package/src/auth/newUsers.ts +134 -166
- package/src/auth/providers/Auth0Provider.ts +5 -5
- package/src/auth/providers/CognitoProvider.ts +10 -7
- package/src/auth/providers/GoogleProvider.ts +5 -4
- package/src/auth/providers/MSALProvider.ts +5 -5
- package/src/auth/providers/OktaProvider.ts +7 -6
- package/src/config.ts +54 -63
- package/src/context.ts +30 -42
- package/src/entitySubclasses/entityPermissions.server.ts +3 -3
- package/src/generated/generated.ts +40442 -48106
- package/src/generic/KeyInputOutputTypes.ts +6 -3
- package/src/generic/ResolverBase.ts +78 -119
- package/src/generic/RunViewResolver.ts +23 -27
- package/src/index.ts +48 -66
- package/src/resolvers/ActionResolver.ts +57 -46
- package/src/resolvers/AskSkipResolver.ts +533 -607
- package/src/resolvers/ComponentRegistryResolver.ts +562 -547
- package/src/resolvers/CreateQueryResolver.ts +655 -683
- package/src/resolvers/DatasetResolver.ts +6 -5
- package/src/resolvers/EntityCommunicationsResolver.ts +1 -1
- package/src/resolvers/EntityRecordNameResolver.ts +5 -9
- package/src/resolvers/EntityResolver.ts +7 -9
- package/src/resolvers/FileCategoryResolver.ts +2 -2
- package/src/resolvers/FileResolver.ts +4 -4
- package/src/resolvers/GetDataContextDataResolver.ts +118 -106
- package/src/resolvers/GetDataResolver.ts +205 -194
- package/src/resolvers/MergeRecordsResolver.ts +5 -5
- package/src/resolvers/PotentialDuplicateRecordResolver.ts +1 -1
- package/src/resolvers/QueryResolver.ts +78 -95
- package/src/resolvers/ReportResolver.ts +2 -2
- package/src/resolvers/RunAIAgentResolver.ts +828 -818
- package/src/resolvers/RunAIPromptResolver.ts +709 -693
- package/src/resolvers/RunTemplateResolver.ts +103 -105
- package/src/resolvers/SqlLoggingConfigResolver.ts +72 -69
- package/src/resolvers/SyncDataResolver.ts +352 -386
- package/src/resolvers/SyncRolesUsersResolver.ts +350 -387
- package/src/resolvers/TaskResolver.ts +115 -110
- package/src/resolvers/TransactionGroupResolver.ts +138 -143
- package/src/resolvers/UserFavoriteResolver.ts +8 -17
- package/src/resolvers/UserViewResolver.ts +12 -17
- package/src/rest/EntityCRUDHandler.ts +268 -291
- package/src/rest/RESTEndpointHandler.ts +776 -782
- package/src/rest/ViewOperationsHandler.ts +195 -191
- package/src/scheduler/LearningCycleScheduler.ts +52 -8
- package/src/services/ScheduledJobsService.ts +132 -129
- package/src/services/TaskOrchestrator.ts +776 -792
- package/src/types.ts +9 -15
- package/src/util.ts +109 -112
|
@@ -7,10 +7,10 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
8
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
9
|
};
|
|
10
|
-
import { BaseAgent } from
|
|
11
|
-
import { SkipSDK } from
|
|
12
|
-
import { LogStatus, LogError, RunView } from
|
|
13
|
-
import { RegisterClass } from
|
|
10
|
+
import { BaseAgent } from "@memberjunction/ai-agents";
|
|
11
|
+
import { SkipSDK } from "./skip-sdk.js";
|
|
12
|
+
import { LogStatus, LogError, RunView } from "@memberjunction/core";
|
|
13
|
+
import { RegisterClass } from "@memberjunction/global";
|
|
14
14
|
let SkipProxyAgent = class SkipProxyAgent extends BaseAgent {
|
|
15
15
|
skipSDK;
|
|
16
16
|
constructor() {
|
|
@@ -28,9 +28,9 @@ let SkipProxyAgent = class SkipProxyAgent extends BaseAgent {
|
|
|
28
28
|
terminate: true,
|
|
29
29
|
step: 'Failed',
|
|
30
30
|
message: 'Missing required contextUser',
|
|
31
|
-
errorMessage: 'Missing required contextUser'
|
|
31
|
+
errorMessage: 'Missing required contextUser'
|
|
32
32
|
},
|
|
33
|
-
stepCount: 1
|
|
33
|
+
stepCount: 1
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
let skipMessages;
|
|
@@ -61,11 +61,11 @@ let SkipProxyAgent = class SkipProxyAgent extends BaseAgent {
|
|
|
61
61
|
percentage: 0,
|
|
62
62
|
metadata: {
|
|
63
63
|
conversationId,
|
|
64
|
-
responsePhase
|
|
65
|
-
}
|
|
64
|
+
responsePhase
|
|
65
|
+
}
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
|
-
}
|
|
68
|
+
}
|
|
69
69
|
};
|
|
70
70
|
const result = await this.skipSDK.chat(skipOptions);
|
|
71
71
|
if (!result.success || !result.response) {
|
|
@@ -75,16 +75,16 @@ let SkipProxyAgent = class SkipProxyAgent extends BaseAgent {
|
|
|
75
75
|
terminate: true,
|
|
76
76
|
step: 'Failed',
|
|
77
77
|
message: 'Skip API call failed',
|
|
78
|
-
errorMessage: result.error
|
|
78
|
+
errorMessage: result.error
|
|
79
79
|
},
|
|
80
|
-
stepCount: 1
|
|
80
|
+
stepCount: 1
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
const nextStep = this.mapSkipResponseToNextStep(result.response, conversationId);
|
|
84
84
|
LogStatus(`[SkipProxyAgent] Skip execution completed with phase: ${result.responsePhase}`);
|
|
85
85
|
return {
|
|
86
86
|
finalStep: nextStep,
|
|
87
|
-
stepCount: 1
|
|
87
|
+
stepCount: 1
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
async loadMessagesFromDatabase(conversationId, contextUser) {
|
|
@@ -93,14 +93,14 @@ let SkipProxyAgent = class SkipProxyAgent extends BaseAgent {
|
|
|
93
93
|
const result = await rv.RunView({
|
|
94
94
|
EntityName: 'Conversation Details',
|
|
95
95
|
ExtraFilter: `ConversationID='${conversationId}'`,
|
|
96
|
-
OrderBy: '__mj_CreatedAt ASC'
|
|
96
|
+
OrderBy: '__mj_CreatedAt ASC'
|
|
97
97
|
}, contextUser);
|
|
98
98
|
if (!result.Success) {
|
|
99
99
|
throw new Error(`Failed to load conversation details: ${result.ErrorMessage}`);
|
|
100
100
|
}
|
|
101
101
|
const allMessages = (result.Results || []).map((r) => {
|
|
102
102
|
const dbRole = (r.Role || '').trim().toLowerCase();
|
|
103
|
-
const skipRole = dbRole === 'ai' || dbRole === 'system' || dbRole === 'assistant' ? 'system' : 'user';
|
|
103
|
+
const skipRole = (dbRole === 'ai' || dbRole === 'system' || dbRole === 'assistant') ? 'system' : 'user';
|
|
104
104
|
const content = r.Message;
|
|
105
105
|
const message = {
|
|
106
106
|
content: content,
|
|
@@ -142,7 +142,7 @@ let SkipProxyAgent = class SkipProxyAgent extends BaseAgent {
|
|
|
142
142
|
reflectionInsights: msg.metadata?.reflectionInsights,
|
|
143
143
|
summaryOfEarlierConveration: msg.metadata?.summaryOfEarlierConversation,
|
|
144
144
|
createdAt: msg.metadata?.createdAt,
|
|
145
|
-
updatedAt: msg.metadata?.updatedAt
|
|
145
|
+
updatedAt: msg.metadata?.updatedAt
|
|
146
146
|
};
|
|
147
147
|
});
|
|
148
148
|
}
|
|
@@ -155,7 +155,7 @@ let SkipProxyAgent = class SkipProxyAgent extends BaseAgent {
|
|
|
155
155
|
terminate: true,
|
|
156
156
|
step: 'Success',
|
|
157
157
|
message: completeResponse.title || 'Analysis complete',
|
|
158
|
-
newPayload: componentSpec
|
|
158
|
+
newPayload: componentSpec
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
161
|
case 'clarifying_question': {
|
|
@@ -164,7 +164,7 @@ let SkipProxyAgent = class SkipProxyAgent extends BaseAgent {
|
|
|
164
164
|
terminate: true,
|
|
165
165
|
step: 'Chat',
|
|
166
166
|
message: clarifyResponse.clarifyingQuestion,
|
|
167
|
-
newPayload: undefined
|
|
167
|
+
newPayload: undefined
|
|
168
168
|
};
|
|
169
169
|
}
|
|
170
170
|
default: {
|
|
@@ -174,7 +174,7 @@ let SkipProxyAgent = class SkipProxyAgent extends BaseAgent {
|
|
|
174
174
|
step: 'Failed',
|
|
175
175
|
message: `Unknown Skip response phase: ${apiResponse.responsePhase}`,
|
|
176
176
|
errorMessage: `Unknown Skip response phase: ${apiResponse.responsePhase}`,
|
|
177
|
-
newPayload: undefined
|
|
177
|
+
newPayload: undefined
|
|
178
178
|
};
|
|
179
179
|
}
|
|
180
180
|
}
|
|
@@ -183,8 +183,8 @@ let SkipProxyAgent = class SkipProxyAgent extends BaseAgent {
|
|
|
183
183
|
return {
|
|
184
184
|
terminate: true,
|
|
185
185
|
step: 'Success',
|
|
186
|
-
message:
|
|
187
|
-
newPayload: demoSpecJson
|
|
186
|
+
message: "Demo Report (not real)",
|
|
187
|
+
newPayload: demoSpecJson
|
|
188
188
|
};
|
|
189
189
|
}
|
|
190
190
|
};
|
|
@@ -194,1023 +194,1166 @@ SkipProxyAgent = __decorate([
|
|
|
194
194
|
], SkipProxyAgent);
|
|
195
195
|
export { SkipProxyAgent };
|
|
196
196
|
const demoSpecJson = {
|
|
197
|
-
name:
|
|
198
|
-
title:
|
|
199
|
-
description:
|
|
200
|
-
type:
|
|
201
|
-
functionalRequirements: "## Entity Browser Requirements\n\n### Core Functionality\n- Display entities in a responsive grid or card layout based on user preference\n- Allow users to select view mode (grid vs card)\n- Click on an entity to slide in a details panel from the right\n- Show entity metadata including fields and relationships in the details panel\n- Provide a collapsible filter panel on the left side\n- Support sorting by multiple fields with visual indicators\n- Include a search bar for quick entity filtering\n- Provide an 'Open' button to trigger the OpenEntityRecord callback\n- Remember user's last selected entity and view preferences\n\n### UX Considerations\n- Smooth animations for panel transitions\n- Responsive design that works on different screen sizes\n- Loading states while fetching data\n- Empty states with helpful messages\n- Keyboard navigation support (arrow keys, tab, enter)\n- Visual feedback for hover and selection states\n- Maintain scroll position when switching between entities",
|
|
202
|
-
dataRequirements: {
|
|
203
|
-
mode:
|
|
204
|
-
entities: [
|
|
197
|
+
"name": "EntityBrowser",
|
|
198
|
+
"title": "Entity Browser",
|
|
199
|
+
"description": "A comprehensive entity browser with multi-panel display showing entities in a grid or card view with a sliding details panel, collapsible filters, sorting, and entity record opening capability.",
|
|
200
|
+
"type": "dashboard",
|
|
201
|
+
"functionalRequirements": "## Entity Browser Requirements\n\n### Core Functionality\n- Display entities in a responsive grid or card layout based on user preference\n- Allow users to select view mode (grid vs card)\n- Click on an entity to slide in a details panel from the right\n- Show entity metadata including fields and relationships in the details panel\n- Provide a collapsible filter panel on the left side\n- Support sorting by multiple fields with visual indicators\n- Include a search bar for quick entity filtering\n- Provide an 'Open' button to trigger the OpenEntityRecord callback\n- Remember user's last selected entity and view preferences\n\n### UX Considerations\n- Smooth animations for panel transitions\n- Responsive design that works on different screen sizes\n- Loading states while fetching data\n- Empty states with helpful messages\n- Keyboard navigation support (arrow keys, tab, enter)\n- Visual feedback for hover and selection states\n- Maintain scroll position when switching between entities",
|
|
202
|
+
"dataRequirements": {
|
|
203
|
+
"mode": "views",
|
|
204
|
+
"entities": [
|
|
205
205
|
{
|
|
206
|
-
name:
|
|
207
|
-
description:
|
|
208
|
-
displayFields: [
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
206
|
+
"name": "Entities",
|
|
207
|
+
"description": "Metadata about all entities in the system",
|
|
208
|
+
"displayFields": [
|
|
209
|
+
"ID",
|
|
210
|
+
"Name",
|
|
211
|
+
"DisplayName",
|
|
212
|
+
"NameSuffix",
|
|
213
|
+
"Description",
|
|
214
|
+
"SchemaName",
|
|
215
|
+
"BaseTable",
|
|
216
|
+
"BaseView"
|
|
217
|
+
],
|
|
218
|
+
"filterFields": [
|
|
219
|
+
"SchemaName",
|
|
220
|
+
"BaseTable"
|
|
221
|
+
],
|
|
222
|
+
"sortFields": [
|
|
223
|
+
"Name",
|
|
224
|
+
"DisplayName"
|
|
225
|
+
],
|
|
226
|
+
"fieldMetadata": [
|
|
212
227
|
{
|
|
213
|
-
name:
|
|
214
|
-
sequence: 1,
|
|
215
|
-
defaultInView: false,
|
|
216
|
-
type:
|
|
217
|
-
allowsNull: false,
|
|
218
|
-
isPrimaryKey: true,
|
|
219
|
-
description:
|
|
228
|
+
"name": "ID",
|
|
229
|
+
"sequence": 1,
|
|
230
|
+
"defaultInView": false,
|
|
231
|
+
"type": "uniqueidentifier",
|
|
232
|
+
"allowsNull": false,
|
|
233
|
+
"isPrimaryKey": true,
|
|
234
|
+
"description": "Unique identifier for the entity"
|
|
220
235
|
},
|
|
221
236
|
{
|
|
222
|
-
name:
|
|
223
|
-
sequence: 2,
|
|
224
|
-
defaultInView: true,
|
|
225
|
-
type:
|
|
226
|
-
allowsNull: false,
|
|
227
|
-
isPrimaryKey: false,
|
|
228
|
-
description:
|
|
237
|
+
"name": "Name",
|
|
238
|
+
"sequence": 2,
|
|
239
|
+
"defaultInView": true,
|
|
240
|
+
"type": "nvarchar",
|
|
241
|
+
"allowsNull": false,
|
|
242
|
+
"isPrimaryKey": false,
|
|
243
|
+
"description": "System name of the entity"
|
|
229
244
|
},
|
|
230
245
|
{
|
|
231
|
-
name:
|
|
232
|
-
sequence: 3,
|
|
233
|
-
defaultInView: true,
|
|
234
|
-
type:
|
|
235
|
-
allowsNull: true,
|
|
236
|
-
isPrimaryKey: false,
|
|
237
|
-
description:
|
|
246
|
+
"name": "DisplayName",
|
|
247
|
+
"sequence": 3,
|
|
248
|
+
"defaultInView": true,
|
|
249
|
+
"type": "nvarchar",
|
|
250
|
+
"allowsNull": true,
|
|
251
|
+
"isPrimaryKey": false,
|
|
252
|
+
"description": "User-friendly display name for the entity"
|
|
238
253
|
},
|
|
239
254
|
{
|
|
240
|
-
name:
|
|
241
|
-
sequence: 4,
|
|
242
|
-
defaultInView: true,
|
|
243
|
-
type:
|
|
244
|
-
allowsNull: true,
|
|
245
|
-
isPrimaryKey: false,
|
|
246
|
-
description:
|
|
255
|
+
"name": "NameSuffix",
|
|
256
|
+
"sequence": 4,
|
|
257
|
+
"defaultInView": true,
|
|
258
|
+
"type": "nvarchar",
|
|
259
|
+
"allowsNull": true,
|
|
260
|
+
"isPrimaryKey": false,
|
|
261
|
+
"description": "Optional suffix appended to entity names for display purposes"
|
|
247
262
|
},
|
|
248
263
|
{
|
|
249
|
-
name:
|
|
250
|
-
sequence: 5,
|
|
251
|
-
defaultInView: true,
|
|
252
|
-
type:
|
|
253
|
-
allowsNull: true,
|
|
254
|
-
isPrimaryKey: false,
|
|
255
|
-
description:
|
|
264
|
+
"name": "Description",
|
|
265
|
+
"sequence": 5,
|
|
266
|
+
"defaultInView": true,
|
|
267
|
+
"type": "nvarchar",
|
|
268
|
+
"allowsNull": true,
|
|
269
|
+
"isPrimaryKey": false,
|
|
270
|
+
"description": "Description of the entity"
|
|
256
271
|
},
|
|
257
272
|
{
|
|
258
|
-
name:
|
|
259
|
-
sequence: 6,
|
|
260
|
-
defaultInView: true,
|
|
261
|
-
type:
|
|
262
|
-
allowsNull: true,
|
|
263
|
-
isPrimaryKey: false,
|
|
264
|
-
description:
|
|
273
|
+
"name": "SchemaName",
|
|
274
|
+
"sequence": 6,
|
|
275
|
+
"defaultInView": true,
|
|
276
|
+
"type": "nvarchar",
|
|
277
|
+
"allowsNull": true,
|
|
278
|
+
"isPrimaryKey": false,
|
|
279
|
+
"description": "Database schema name"
|
|
265
280
|
},
|
|
266
281
|
{
|
|
267
|
-
name:
|
|
268
|
-
sequence: 7,
|
|
269
|
-
defaultInView: true,
|
|
270
|
-
type:
|
|
271
|
-
allowsNull: true,
|
|
272
|
-
isPrimaryKey: false,
|
|
273
|
-
description:
|
|
282
|
+
"name": "BaseTable",
|
|
283
|
+
"sequence": 7,
|
|
284
|
+
"defaultInView": true,
|
|
285
|
+
"type": "nvarchar",
|
|
286
|
+
"allowsNull": true,
|
|
287
|
+
"isPrimaryKey": false,
|
|
288
|
+
"description": "Base table in the database"
|
|
274
289
|
},
|
|
275
290
|
{
|
|
276
|
-
name:
|
|
277
|
-
sequence: 8,
|
|
278
|
-
defaultInView: true,
|
|
279
|
-
type:
|
|
280
|
-
allowsNull: false,
|
|
281
|
-
isPrimaryKey: false,
|
|
282
|
-
description:
|
|
283
|
-
}
|
|
291
|
+
"name": "BaseView",
|
|
292
|
+
"sequence": 8,
|
|
293
|
+
"defaultInView": true,
|
|
294
|
+
"type": "nvarchar",
|
|
295
|
+
"allowsNull": false,
|
|
296
|
+
"isPrimaryKey": false,
|
|
297
|
+
"description": "Base view used for the entity"
|
|
298
|
+
}
|
|
299
|
+
],
|
|
300
|
+
"permissionLevelNeeded": [
|
|
301
|
+
"read"
|
|
284
302
|
],
|
|
285
|
-
|
|
286
|
-
usageContext: 'Main entity list display and filtering',
|
|
303
|
+
"usageContext": "Main entity list display and filtering"
|
|
287
304
|
},
|
|
288
305
|
{
|
|
289
|
-
name:
|
|
290
|
-
description:
|
|
291
|
-
displayFields: [
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
306
|
+
"name": "Entity Fields",
|
|
307
|
+
"description": "Fields belonging to each entity",
|
|
308
|
+
"displayFields": [
|
|
309
|
+
"Name",
|
|
310
|
+
"DisplayName",
|
|
311
|
+
"Type",
|
|
312
|
+
"Length",
|
|
313
|
+
"AllowsNull",
|
|
314
|
+
"IsPrimaryKey",
|
|
315
|
+
"IsUnique"
|
|
316
|
+
],
|
|
317
|
+
"filterFields": [
|
|
318
|
+
"EntityID"
|
|
319
|
+
],
|
|
320
|
+
"sortFields": [
|
|
321
|
+
"Sequence",
|
|
322
|
+
"Name"
|
|
323
|
+
],
|
|
324
|
+
"fieldMetadata": [
|
|
295
325
|
{
|
|
296
|
-
name:
|
|
297
|
-
sequence: 1,
|
|
298
|
-
defaultInView: false,
|
|
299
|
-
type:
|
|
300
|
-
allowsNull: false,
|
|
301
|
-
isPrimaryKey: false,
|
|
302
|
-
description:
|
|
326
|
+
"name": "EntityID",
|
|
327
|
+
"sequence": 1,
|
|
328
|
+
"defaultInView": false,
|
|
329
|
+
"type": "uniqueidentifier",
|
|
330
|
+
"allowsNull": false,
|
|
331
|
+
"isPrimaryKey": false,
|
|
332
|
+
"description": "Reference to parent entity"
|
|
303
333
|
},
|
|
304
334
|
{
|
|
305
|
-
name:
|
|
306
|
-
sequence: 2,
|
|
307
|
-
defaultInView: true,
|
|
308
|
-
type:
|
|
309
|
-
allowsNull: false,
|
|
310
|
-
isPrimaryKey: false,
|
|
311
|
-
description:
|
|
335
|
+
"name": "Name",
|
|
336
|
+
"sequence": 2,
|
|
337
|
+
"defaultInView": true,
|
|
338
|
+
"type": "nvarchar",
|
|
339
|
+
"allowsNull": false,
|
|
340
|
+
"isPrimaryKey": false,
|
|
341
|
+
"description": "Field name"
|
|
312
342
|
},
|
|
313
343
|
{
|
|
314
|
-
name:
|
|
315
|
-
sequence: 3,
|
|
316
|
-
defaultInView: true,
|
|
317
|
-
type:
|
|
318
|
-
allowsNull: true,
|
|
319
|
-
isPrimaryKey: false,
|
|
320
|
-
description:
|
|
344
|
+
"name": "DisplayName",
|
|
345
|
+
"sequence": 3,
|
|
346
|
+
"defaultInView": true,
|
|
347
|
+
"type": "nvarchar",
|
|
348
|
+
"allowsNull": true,
|
|
349
|
+
"isPrimaryKey": false,
|
|
350
|
+
"description": "User-friendly field name"
|
|
321
351
|
},
|
|
322
352
|
{
|
|
323
|
-
name:
|
|
324
|
-
sequence: 4,
|
|
325
|
-
defaultInView: true,
|
|
326
|
-
type:
|
|
327
|
-
allowsNull: false,
|
|
328
|
-
isPrimaryKey: false,
|
|
329
|
-
description:
|
|
353
|
+
"name": "Type",
|
|
354
|
+
"sequence": 4,
|
|
355
|
+
"defaultInView": true,
|
|
356
|
+
"type": "nvarchar",
|
|
357
|
+
"allowsNull": false,
|
|
358
|
+
"isPrimaryKey": false,
|
|
359
|
+
"description": "Data type of the field"
|
|
330
360
|
},
|
|
331
361
|
{
|
|
332
|
-
name:
|
|
333
|
-
sequence: 5,
|
|
334
|
-
defaultInView: true,
|
|
335
|
-
type:
|
|
336
|
-
allowsNull: true,
|
|
337
|
-
isPrimaryKey: false,
|
|
338
|
-
description:
|
|
362
|
+
"name": "Length",
|
|
363
|
+
"sequence": 5,
|
|
364
|
+
"defaultInView": true,
|
|
365
|
+
"type": "int",
|
|
366
|
+
"allowsNull": true,
|
|
367
|
+
"isPrimaryKey": false,
|
|
368
|
+
"description": "Maximum length for string fields"
|
|
339
369
|
},
|
|
340
370
|
{
|
|
341
|
-
name:
|
|
342
|
-
sequence: 6,
|
|
343
|
-
defaultInView: true,
|
|
344
|
-
type:
|
|
345
|
-
allowsNull: false,
|
|
346
|
-
isPrimaryKey: false,
|
|
347
|
-
description:
|
|
371
|
+
"name": "AllowsNull",
|
|
372
|
+
"sequence": 6,
|
|
373
|
+
"defaultInView": true,
|
|
374
|
+
"type": "bit",
|
|
375
|
+
"allowsNull": false,
|
|
376
|
+
"isPrimaryKey": false,
|
|
377
|
+
"description": "Whether field allows null values"
|
|
348
378
|
},
|
|
349
379
|
{
|
|
350
|
-
name:
|
|
351
|
-
sequence: 7,
|
|
352
|
-
defaultInView: true,
|
|
353
|
-
type:
|
|
354
|
-
allowsNull: false,
|
|
355
|
-
isPrimaryKey: false,
|
|
356
|
-
description:
|
|
380
|
+
"name": "IsPrimaryKey",
|
|
381
|
+
"sequence": 7,
|
|
382
|
+
"defaultInView": true,
|
|
383
|
+
"type": "bit",
|
|
384
|
+
"allowsNull": false,
|
|
385
|
+
"isPrimaryKey": false,
|
|
386
|
+
"description": "Whether field is part of primary key"
|
|
357
387
|
},
|
|
358
388
|
{
|
|
359
|
-
name:
|
|
360
|
-
sequence: 8,
|
|
361
|
-
defaultInView: true,
|
|
362
|
-
type:
|
|
363
|
-
allowsNull: false,
|
|
364
|
-
isPrimaryKey: false,
|
|
365
|
-
description:
|
|
389
|
+
"name": "IsUnique",
|
|
390
|
+
"sequence": 8,
|
|
391
|
+
"defaultInView": true,
|
|
392
|
+
"type": "bit",
|
|
393
|
+
"allowsNull": false,
|
|
394
|
+
"isPrimaryKey": false,
|
|
395
|
+
"description": "Whether field must be unique"
|
|
366
396
|
},
|
|
367
397
|
{
|
|
368
|
-
name:
|
|
369
|
-
sequence: 9,
|
|
370
|
-
defaultInView: false,
|
|
371
|
-
type:
|
|
372
|
-
allowsNull: false,
|
|
373
|
-
isPrimaryKey: false,
|
|
374
|
-
description:
|
|
375
|
-
}
|
|
398
|
+
"name": "Sequence",
|
|
399
|
+
"sequence": 9,
|
|
400
|
+
"defaultInView": false,
|
|
401
|
+
"type": "int",
|
|
402
|
+
"allowsNull": false,
|
|
403
|
+
"isPrimaryKey": false,
|
|
404
|
+
"description": "Display order of the field"
|
|
405
|
+
}
|
|
406
|
+
],
|
|
407
|
+
"permissionLevelNeeded": [
|
|
408
|
+
"read"
|
|
376
409
|
],
|
|
377
|
-
|
|
378
|
-
usageContext: 'Details panel to show entity fields',
|
|
410
|
+
"usageContext": "Details panel to show entity fields"
|
|
379
411
|
},
|
|
380
412
|
{
|
|
381
|
-
name:
|
|
382
|
-
description:
|
|
383
|
-
displayFields: [
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
413
|
+
"name": "Entity Relationships",
|
|
414
|
+
"description": "Relationships between entities",
|
|
415
|
+
"displayFields": [
|
|
416
|
+
"RelatedEntity",
|
|
417
|
+
"Type",
|
|
418
|
+
"DisplayName",
|
|
419
|
+
"RelatedEntityJoinField"
|
|
420
|
+
],
|
|
421
|
+
"filterFields": [
|
|
422
|
+
"EntityID"
|
|
423
|
+
],
|
|
424
|
+
"sortFields": [
|
|
425
|
+
"Sequence",
|
|
426
|
+
"RelatedEntity"
|
|
427
|
+
],
|
|
428
|
+
"fieldMetadata": [
|
|
387
429
|
{
|
|
388
|
-
name:
|
|
389
|
-
sequence: 1,
|
|
390
|
-
defaultInView: false,
|
|
391
|
-
type:
|
|
392
|
-
allowsNull: false,
|
|
393
|
-
isPrimaryKey: false,
|
|
394
|
-
description:
|
|
430
|
+
"name": "EntityID",
|
|
431
|
+
"sequence": 1,
|
|
432
|
+
"defaultInView": false,
|
|
433
|
+
"type": "uniqueidentifier",
|
|
434
|
+
"allowsNull": false,
|
|
435
|
+
"isPrimaryKey": false,
|
|
436
|
+
"description": "Reference to parent entity"
|
|
395
437
|
},
|
|
396
438
|
{
|
|
397
|
-
name:
|
|
398
|
-
sequence: 2,
|
|
399
|
-
defaultInView: true,
|
|
400
|
-
type:
|
|
401
|
-
allowsNull: false,
|
|
402
|
-
isPrimaryKey: false,
|
|
403
|
-
description:
|
|
439
|
+
"name": "RelatedEntity",
|
|
440
|
+
"sequence": 2,
|
|
441
|
+
"defaultInView": true,
|
|
442
|
+
"type": "nvarchar",
|
|
443
|
+
"allowsNull": false,
|
|
444
|
+
"isPrimaryKey": false,
|
|
445
|
+
"description": "The related entity in the relationship"
|
|
404
446
|
},
|
|
405
447
|
{
|
|
406
|
-
name:
|
|
407
|
-
sequence: 3,
|
|
408
|
-
defaultInView: true,
|
|
409
|
-
type:
|
|
410
|
-
allowsNull: false,
|
|
411
|
-
isPrimaryKey: false,
|
|
412
|
-
description:
|
|
448
|
+
"name": "Type",
|
|
449
|
+
"sequence": 3,
|
|
450
|
+
"defaultInView": true,
|
|
451
|
+
"type": "nvarchar",
|
|
452
|
+
"allowsNull": false,
|
|
453
|
+
"isPrimaryKey": false,
|
|
454
|
+
"description": "Type of relationship (One to Many, Many to One, etc.)"
|
|
413
455
|
},
|
|
414
456
|
{
|
|
415
|
-
name:
|
|
416
|
-
sequence: 4,
|
|
417
|
-
defaultInView: true,
|
|
418
|
-
type:
|
|
419
|
-
allowsNull: true,
|
|
420
|
-
isPrimaryKey: false,
|
|
421
|
-
description:
|
|
457
|
+
"name": "DisplayName",
|
|
458
|
+
"sequence": 4,
|
|
459
|
+
"defaultInView": true,
|
|
460
|
+
"type": "nvarchar",
|
|
461
|
+
"allowsNull": true,
|
|
462
|
+
"isPrimaryKey": false,
|
|
463
|
+
"description": "User-friendly name for the relationship"
|
|
422
464
|
},
|
|
423
465
|
{
|
|
424
|
-
name:
|
|
425
|
-
sequence: 5,
|
|
426
|
-
defaultInView: true,
|
|
427
|
-
type:
|
|
428
|
-
allowsNull: true,
|
|
429
|
-
isPrimaryKey: false,
|
|
430
|
-
description:
|
|
466
|
+
"name": "RelatedEntityJoinField",
|
|
467
|
+
"sequence": 5,
|
|
468
|
+
"defaultInView": true,
|
|
469
|
+
"type": "nvarchar",
|
|
470
|
+
"allowsNull": true,
|
|
471
|
+
"isPrimaryKey": false,
|
|
472
|
+
"description": "The field in the related entity that joins to this entity"
|
|
431
473
|
},
|
|
432
474
|
{
|
|
433
|
-
name:
|
|
434
|
-
sequence: 6,
|
|
435
|
-
defaultInView: false,
|
|
436
|
-
type:
|
|
437
|
-
allowsNull: false,
|
|
438
|
-
isPrimaryKey: false,
|
|
439
|
-
description:
|
|
440
|
-
}
|
|
475
|
+
"name": "Sequence",
|
|
476
|
+
"sequence": 6,
|
|
477
|
+
"defaultInView": false,
|
|
478
|
+
"type": "int",
|
|
479
|
+
"allowsNull": false,
|
|
480
|
+
"isPrimaryKey": false,
|
|
481
|
+
"description": "Display order"
|
|
482
|
+
}
|
|
441
483
|
],
|
|
442
|
-
permissionLevelNeeded: [
|
|
443
|
-
|
|
444
|
-
|
|
484
|
+
"permissionLevelNeeded": [
|
|
485
|
+
"read"
|
|
486
|
+
],
|
|
487
|
+
"usageContext": "Details panel to show entity relationships"
|
|
488
|
+
}
|
|
445
489
|
],
|
|
446
|
-
queries: [],
|
|
447
|
-
description:
|
|
490
|
+
"queries": [],
|
|
491
|
+
"description": "This component requires access to entity metadata including entities, their fields, and relationships to provide a comprehensive entity browsing experience"
|
|
448
492
|
},
|
|
449
|
-
technicalDesign: "## Technical Architecture\n\n### Component Structure\n- **Root Component (EntityBrowser)**: Manages overall layout and state coordination\n- **EntityList (Child)**: Displays entities in grid/card view with sorting\n- **EntityDetails (Child)**: Sliding panel showing entity fields and relationships\n- **EntityFilter (Child)**: Collapsible filter panel with dynamic filters\n\n### State Management\n- Selected entity ID (persisted in savedUserSettings)\n- View mode (grid/card) (persisted)\n- Active filters (persisted)\n- Sort configuration (persisted)\n- Panel visibility states (details open, filters collapsed)\n- Search query\n- Loading states for async operations\n\n### Layout\n```\n+------------------+------------------------+------------------+\n| | | |\n| Filter Panel | Entity Grid/Cards | Details Panel |\n| (Collapsible) | (Main Content) | (Sliding) |\n| | | |\n| [Schema Filter] | +-----+ +-----+ | Entity: Orders |\n| [Table Filter] | | Card | | Card | | |\n| [Search Box] | +-----+ +-----+ | Fields: |\n| | | - ID |\n| Sort By: | +-----+ +-----+ | - CustomerID |\n| [Name ↓] | | Card | | Card | | - OrderDate |\n| | +-----+ +-----+ | |\n| | | Relationships: |\n| | | → Customers |\n| | | → OrderItems |\n| | | |\n| | | [Open Record] |\n+------------------+------------------------+------------------+\n```\n\n### Data Flow\n1. Root component loads entities on mount\n2. Passes entity data to EntityList\n3. EntityList handles selection and passes selectedId up\n4. Root loads fields/relationships for selected entity\n5. Passes detailed data to EntityDetails\n6. Filter changes trigger data reload\n7. All user preferences saved via onSaveUserSettings\n\n### Interaction Patterns\n- Click entity card → Select and open details\n- Click filter → Apply and reload data\n- Click sort → Update sort and reload\n- Click 'Open' → Trigger OpenEntityRecord callback\n- Press Escape → Close details panel\n- Click outside → Close details panel",
|
|
450
|
-
properties: [],
|
|
451
|
-
events: [],
|
|
452
|
-
exampleUsage:
|
|
453
|
-
dependencies: [
|
|
493
|
+
"technicalDesign": "## Technical Architecture\n\n### Component Structure\n- **Root Component (EntityBrowser)**: Manages overall layout and state coordination\n- **EntityList (Child)**: Displays entities in grid/card view with sorting\n- **EntityDetails (Child)**: Sliding panel showing entity fields and relationships\n- **EntityFilter (Child)**: Collapsible filter panel with dynamic filters\n\n### State Management\n- Selected entity ID (persisted in savedUserSettings)\n- View mode (grid/card) (persisted)\n- Active filters (persisted)\n- Sort configuration (persisted)\n- Panel visibility states (details open, filters collapsed)\n- Search query\n- Loading states for async operations\n\n### Layout\n```\n+------------------+------------------------+------------------+\n| | | |\n| Filter Panel | Entity Grid/Cards | Details Panel |\n| (Collapsible) | (Main Content) | (Sliding) |\n| | | |\n| [Schema Filter] | +-----+ +-----+ | Entity: Orders |\n| [Table Filter] | | Card | | Card | | |\n| [Search Box] | +-----+ +-----+ | Fields: |\n| | | - ID |\n| Sort By: | +-----+ +-----+ | - CustomerID |\n| [Name ↓] | | Card | | Card | | - OrderDate |\n| | +-----+ +-----+ | |\n| | | Relationships: |\n| | | → Customers |\n| | | → OrderItems |\n| | | |\n| | | [Open Record] |\n+------------------+------------------------+------------------+\n```\n\n### Data Flow\n1. Root component loads entities on mount\n2. Passes entity data to EntityList\n3. EntityList handles selection and passes selectedId up\n4. Root loads fields/relationships for selected entity\n5. Passes detailed data to EntityDetails\n6. Filter changes trigger data reload\n7. All user preferences saved via onSaveUserSettings\n\n### Interaction Patterns\n- Click entity card → Select and open details\n- Click filter → Apply and reload data\n- Click sort → Update sort and reload\n- Click 'Open' → Trigger OpenEntityRecord callback\n- Press Escape → Close details panel\n- Click outside → Close details panel",
|
|
494
|
+
"properties": [],
|
|
495
|
+
"events": [],
|
|
496
|
+
"exampleUsage": "<EntityBrowser\n utilities={utilities}\n styles={styles}\n components={components}\n callbacks={callbacks}\n savedUserSettings={savedUserSettings}\n onSaveUserSettings={onSaveUserSettings}\n/>",
|
|
497
|
+
"dependencies": [
|
|
454
498
|
{
|
|
455
|
-
name:
|
|
456
|
-
title:
|
|
457
|
-
description:
|
|
458
|
-
type:
|
|
459
|
-
functionalRequirements:
|
|
460
|
-
dataRequirements: {
|
|
461
|
-
mode:
|
|
462
|
-
entities: [
|
|
499
|
+
"name": "EntityList",
|
|
500
|
+
"title": "Entity List",
|
|
501
|
+
"description": "Displays entities in a grid or card layout with sorting capabilities",
|
|
502
|
+
"type": "table",
|
|
503
|
+
"functionalRequirements": "## Entity List Requirements\n\n- Display entities in grid or card view based on viewMode prop\n- Support sorting by multiple fields\n- Handle entity selection and notify parent\n- Show loading state while data loads\n- Display record count badges\n- Highlight selected entity\n- Support keyboard navigation",
|
|
504
|
+
"dataRequirements": {
|
|
505
|
+
"mode": "views",
|
|
506
|
+
"entities": [
|
|
463
507
|
{
|
|
464
|
-
name:
|
|
465
|
-
description:
|
|
466
|
-
displayFields: [
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
508
|
+
"name": "Entities",
|
|
509
|
+
"description": "Metadata about all entities in the system",
|
|
510
|
+
"displayFields": [
|
|
511
|
+
"ID",
|
|
512
|
+
"Name",
|
|
513
|
+
"DisplayName",
|
|
514
|
+
"NameSuffix",
|
|
515
|
+
"Description",
|
|
516
|
+
"SchemaName",
|
|
517
|
+
"BaseTable",
|
|
518
|
+
"BaseView"
|
|
519
|
+
],
|
|
520
|
+
"filterFields": [
|
|
521
|
+
"SchemaName",
|
|
522
|
+
"BaseTable"
|
|
523
|
+
],
|
|
524
|
+
"sortFields": [
|
|
525
|
+
"Name",
|
|
526
|
+
"DisplayName"
|
|
527
|
+
],
|
|
528
|
+
"fieldMetadata": [
|
|
470
529
|
{
|
|
471
|
-
name:
|
|
472
|
-
sequence: 1,
|
|
473
|
-
defaultInView: false,
|
|
474
|
-
type:
|
|
475
|
-
allowsNull: false,
|
|
476
|
-
isPrimaryKey: true,
|
|
477
|
-
description:
|
|
530
|
+
"name": "ID",
|
|
531
|
+
"sequence": 1,
|
|
532
|
+
"defaultInView": false,
|
|
533
|
+
"type": "uniqueidentifier",
|
|
534
|
+
"allowsNull": false,
|
|
535
|
+
"isPrimaryKey": true,
|
|
536
|
+
"description": "Unique identifier for the entity"
|
|
478
537
|
},
|
|
479
538
|
{
|
|
480
|
-
name:
|
|
481
|
-
sequence: 2,
|
|
482
|
-
defaultInView: true,
|
|
483
|
-
type:
|
|
484
|
-
allowsNull: false,
|
|
485
|
-
isPrimaryKey: false,
|
|
486
|
-
description:
|
|
539
|
+
"name": "Name",
|
|
540
|
+
"sequence": 2,
|
|
541
|
+
"defaultInView": true,
|
|
542
|
+
"type": "nvarchar",
|
|
543
|
+
"allowsNull": false,
|
|
544
|
+
"isPrimaryKey": false,
|
|
545
|
+
"description": "System name of the entity"
|
|
487
546
|
},
|
|
488
547
|
{
|
|
489
|
-
name:
|
|
490
|
-
sequence: 3,
|
|
491
|
-
defaultInView: true,
|
|
492
|
-
type:
|
|
493
|
-
allowsNull: true,
|
|
494
|
-
isPrimaryKey: false,
|
|
495
|
-
description:
|
|
548
|
+
"name": "DisplayName",
|
|
549
|
+
"sequence": 3,
|
|
550
|
+
"defaultInView": true,
|
|
551
|
+
"type": "nvarchar",
|
|
552
|
+
"allowsNull": true,
|
|
553
|
+
"isPrimaryKey": false,
|
|
554
|
+
"description": "User-friendly display name for the entity"
|
|
496
555
|
},
|
|
497
556
|
{
|
|
498
|
-
name:
|
|
499
|
-
sequence: 4,
|
|
500
|
-
defaultInView: true,
|
|
501
|
-
type:
|
|
502
|
-
allowsNull: true,
|
|
503
|
-
isPrimaryKey: false,
|
|
504
|
-
description:
|
|
557
|
+
"name": "NameSuffix",
|
|
558
|
+
"sequence": 4,
|
|
559
|
+
"defaultInView": true,
|
|
560
|
+
"type": "nvarchar",
|
|
561
|
+
"allowsNull": true,
|
|
562
|
+
"isPrimaryKey": false,
|
|
563
|
+
"description": "Optional suffix appended to entity names for display purposes"
|
|
505
564
|
},
|
|
506
565
|
{
|
|
507
|
-
name:
|
|
508
|
-
sequence: 5,
|
|
509
|
-
defaultInView: true,
|
|
510
|
-
type:
|
|
511
|
-
allowsNull: true,
|
|
512
|
-
isPrimaryKey: false,
|
|
513
|
-
description:
|
|
566
|
+
"name": "Description",
|
|
567
|
+
"sequence": 5,
|
|
568
|
+
"defaultInView": true,
|
|
569
|
+
"type": "nvarchar",
|
|
570
|
+
"allowsNull": true,
|
|
571
|
+
"isPrimaryKey": false,
|
|
572
|
+
"description": "Description of the entity"
|
|
514
573
|
},
|
|
515
574
|
{
|
|
516
|
-
name:
|
|
517
|
-
sequence: 6,
|
|
518
|
-
defaultInView: true,
|
|
519
|
-
type:
|
|
520
|
-
allowsNull: true,
|
|
521
|
-
isPrimaryKey: false,
|
|
522
|
-
description:
|
|
575
|
+
"name": "SchemaName",
|
|
576
|
+
"sequence": 6,
|
|
577
|
+
"defaultInView": true,
|
|
578
|
+
"type": "nvarchar",
|
|
579
|
+
"allowsNull": true,
|
|
580
|
+
"isPrimaryKey": false,
|
|
581
|
+
"description": "Database schema name"
|
|
523
582
|
},
|
|
524
583
|
{
|
|
525
|
-
name:
|
|
526
|
-
sequence: 7,
|
|
527
|
-
defaultInView: true,
|
|
528
|
-
type:
|
|
529
|
-
allowsNull: true,
|
|
530
|
-
isPrimaryKey: false,
|
|
531
|
-
description:
|
|
584
|
+
"name": "BaseTable",
|
|
585
|
+
"sequence": 7,
|
|
586
|
+
"defaultInView": true,
|
|
587
|
+
"type": "nvarchar",
|
|
588
|
+
"allowsNull": true,
|
|
589
|
+
"isPrimaryKey": false,
|
|
590
|
+
"description": "Base table in the database"
|
|
532
591
|
},
|
|
533
592
|
{
|
|
534
|
-
name:
|
|
535
|
-
sequence: 8,
|
|
536
|
-
defaultInView: true,
|
|
537
|
-
type:
|
|
538
|
-
allowsNull: false,
|
|
539
|
-
isPrimaryKey: false,
|
|
540
|
-
description:
|
|
541
|
-
}
|
|
593
|
+
"name": "BaseView",
|
|
594
|
+
"sequence": 8,
|
|
595
|
+
"defaultInView": true,
|
|
596
|
+
"type": "nvarchar",
|
|
597
|
+
"allowsNull": false,
|
|
598
|
+
"isPrimaryKey": false,
|
|
599
|
+
"description": "Base view used for the entity"
|
|
600
|
+
}
|
|
601
|
+
],
|
|
602
|
+
"permissionLevelNeeded": [
|
|
603
|
+
"read"
|
|
542
604
|
],
|
|
543
|
-
|
|
544
|
-
usageContext: 'Main entity list display and filtering',
|
|
605
|
+
"usageContext": "Main entity list display and filtering"
|
|
545
606
|
},
|
|
546
607
|
{
|
|
547
|
-
name:
|
|
548
|
-
description:
|
|
549
|
-
displayFields: [
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
608
|
+
"name": "Entity Fields",
|
|
609
|
+
"description": "Fields belonging to each entity",
|
|
610
|
+
"displayFields": [
|
|
611
|
+
"Name",
|
|
612
|
+
"DisplayName",
|
|
613
|
+
"Type",
|
|
614
|
+
"Length",
|
|
615
|
+
"AllowsNull",
|
|
616
|
+
"IsPrimaryKey",
|
|
617
|
+
"IsUnique"
|
|
618
|
+
],
|
|
619
|
+
"filterFields": [
|
|
620
|
+
"EntityID"
|
|
621
|
+
],
|
|
622
|
+
"sortFields": [
|
|
623
|
+
"Sequence",
|
|
624
|
+
"Name"
|
|
625
|
+
],
|
|
626
|
+
"fieldMetadata": [
|
|
553
627
|
{
|
|
554
|
-
name:
|
|
555
|
-
sequence: 1,
|
|
556
|
-
defaultInView: false,
|
|
557
|
-
type:
|
|
558
|
-
allowsNull: false,
|
|
559
|
-
isPrimaryKey: false,
|
|
560
|
-
description:
|
|
628
|
+
"name": "EntityID",
|
|
629
|
+
"sequence": 1,
|
|
630
|
+
"defaultInView": false,
|
|
631
|
+
"type": "uniqueidentifier",
|
|
632
|
+
"allowsNull": false,
|
|
633
|
+
"isPrimaryKey": false,
|
|
634
|
+
"description": "Reference to parent entity"
|
|
561
635
|
},
|
|
562
636
|
{
|
|
563
|
-
name:
|
|
564
|
-
sequence: 2,
|
|
565
|
-
defaultInView: true,
|
|
566
|
-
type:
|
|
567
|
-
allowsNull: false,
|
|
568
|
-
isPrimaryKey: false,
|
|
569
|
-
description:
|
|
637
|
+
"name": "Name",
|
|
638
|
+
"sequence": 2,
|
|
639
|
+
"defaultInView": true,
|
|
640
|
+
"type": "nvarchar",
|
|
641
|
+
"allowsNull": false,
|
|
642
|
+
"isPrimaryKey": false,
|
|
643
|
+
"description": "Field name"
|
|
570
644
|
},
|
|
571
645
|
{
|
|
572
|
-
name:
|
|
573
|
-
sequence: 3,
|
|
574
|
-
defaultInView: true,
|
|
575
|
-
type:
|
|
576
|
-
allowsNull: true,
|
|
577
|
-
isPrimaryKey: false,
|
|
578
|
-
description:
|
|
646
|
+
"name": "DisplayName",
|
|
647
|
+
"sequence": 3,
|
|
648
|
+
"defaultInView": true,
|
|
649
|
+
"type": "nvarchar",
|
|
650
|
+
"allowsNull": true,
|
|
651
|
+
"isPrimaryKey": false,
|
|
652
|
+
"description": "User-friendly field name"
|
|
579
653
|
},
|
|
580
654
|
{
|
|
581
|
-
name:
|
|
582
|
-
sequence: 4,
|
|
583
|
-
defaultInView: true,
|
|
584
|
-
type:
|
|
585
|
-
allowsNull: false,
|
|
586
|
-
isPrimaryKey: false,
|
|
587
|
-
description:
|
|
655
|
+
"name": "Type",
|
|
656
|
+
"sequence": 4,
|
|
657
|
+
"defaultInView": true,
|
|
658
|
+
"type": "nvarchar",
|
|
659
|
+
"allowsNull": false,
|
|
660
|
+
"isPrimaryKey": false,
|
|
661
|
+
"description": "Data type of the field"
|
|
588
662
|
},
|
|
589
663
|
{
|
|
590
|
-
name:
|
|
591
|
-
sequence: 5,
|
|
592
|
-
defaultInView: true,
|
|
593
|
-
type:
|
|
594
|
-
allowsNull: true,
|
|
595
|
-
isPrimaryKey: false,
|
|
596
|
-
description:
|
|
664
|
+
"name": "Length",
|
|
665
|
+
"sequence": 5,
|
|
666
|
+
"defaultInView": true,
|
|
667
|
+
"type": "int",
|
|
668
|
+
"allowsNull": true,
|
|
669
|
+
"isPrimaryKey": false,
|
|
670
|
+
"description": "Maximum length for string fields"
|
|
597
671
|
},
|
|
598
672
|
{
|
|
599
|
-
name:
|
|
600
|
-
sequence: 6,
|
|
601
|
-
defaultInView: true,
|
|
602
|
-
type:
|
|
603
|
-
allowsNull: false,
|
|
604
|
-
isPrimaryKey: false,
|
|
605
|
-
description:
|
|
673
|
+
"name": "AllowsNull",
|
|
674
|
+
"sequence": 6,
|
|
675
|
+
"defaultInView": true,
|
|
676
|
+
"type": "bit",
|
|
677
|
+
"allowsNull": false,
|
|
678
|
+
"isPrimaryKey": false,
|
|
679
|
+
"description": "Whether field allows null values"
|
|
606
680
|
},
|
|
607
681
|
{
|
|
608
|
-
name:
|
|
609
|
-
sequence: 7,
|
|
610
|
-
defaultInView: true,
|
|
611
|
-
type:
|
|
612
|
-
allowsNull: false,
|
|
613
|
-
isPrimaryKey: false,
|
|
614
|
-
description:
|
|
682
|
+
"name": "IsPrimaryKey",
|
|
683
|
+
"sequence": 7,
|
|
684
|
+
"defaultInView": true,
|
|
685
|
+
"type": "bit",
|
|
686
|
+
"allowsNull": false,
|
|
687
|
+
"isPrimaryKey": false,
|
|
688
|
+
"description": "Whether field is part of primary key"
|
|
615
689
|
},
|
|
616
690
|
{
|
|
617
|
-
name:
|
|
618
|
-
sequence: 8,
|
|
619
|
-
defaultInView: true,
|
|
620
|
-
type:
|
|
621
|
-
allowsNull: false,
|
|
622
|
-
isPrimaryKey: false,
|
|
623
|
-
description:
|
|
691
|
+
"name": "IsUnique",
|
|
692
|
+
"sequence": 8,
|
|
693
|
+
"defaultInView": true,
|
|
694
|
+
"type": "bit",
|
|
695
|
+
"allowsNull": false,
|
|
696
|
+
"isPrimaryKey": false,
|
|
697
|
+
"description": "Whether field must be unique"
|
|
624
698
|
},
|
|
625
699
|
{
|
|
626
|
-
name:
|
|
627
|
-
sequence: 9,
|
|
628
|
-
defaultInView: false,
|
|
629
|
-
type:
|
|
630
|
-
allowsNull: false,
|
|
631
|
-
isPrimaryKey: false,
|
|
632
|
-
description:
|
|
633
|
-
}
|
|
700
|
+
"name": "Sequence",
|
|
701
|
+
"sequence": 9,
|
|
702
|
+
"defaultInView": false,
|
|
703
|
+
"type": "int",
|
|
704
|
+
"allowsNull": false,
|
|
705
|
+
"isPrimaryKey": false,
|
|
706
|
+
"description": "Display order of the field"
|
|
707
|
+
}
|
|
708
|
+
],
|
|
709
|
+
"permissionLevelNeeded": [
|
|
710
|
+
"read"
|
|
634
711
|
],
|
|
635
|
-
|
|
636
|
-
usageContext: 'Details panel to show entity fields',
|
|
712
|
+
"usageContext": "Details panel to show entity fields"
|
|
637
713
|
},
|
|
638
714
|
{
|
|
639
|
-
name:
|
|
640
|
-
description:
|
|
641
|
-
displayFields: [
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
715
|
+
"name": "Entity Relationships",
|
|
716
|
+
"description": "Relationships between entities",
|
|
717
|
+
"displayFields": [
|
|
718
|
+
"RelatedEntity",
|
|
719
|
+
"Type",
|
|
720
|
+
"DisplayName",
|
|
721
|
+
"RelatedEntityJoinField"
|
|
722
|
+
],
|
|
723
|
+
"filterFields": [
|
|
724
|
+
"EntityID"
|
|
725
|
+
],
|
|
726
|
+
"sortFields": [
|
|
727
|
+
"Sequence",
|
|
728
|
+
"RelatedEntity"
|
|
729
|
+
],
|
|
730
|
+
"fieldMetadata": [
|
|
645
731
|
{
|
|
646
|
-
name:
|
|
647
|
-
sequence: 1,
|
|
648
|
-
defaultInView: false,
|
|
649
|
-
type:
|
|
650
|
-
allowsNull: false,
|
|
651
|
-
isPrimaryKey: false,
|
|
652
|
-
description:
|
|
732
|
+
"name": "EntityID",
|
|
733
|
+
"sequence": 1,
|
|
734
|
+
"defaultInView": false,
|
|
735
|
+
"type": "uniqueidentifier",
|
|
736
|
+
"allowsNull": false,
|
|
737
|
+
"isPrimaryKey": false,
|
|
738
|
+
"description": "Reference to parent entity"
|
|
653
739
|
},
|
|
654
740
|
{
|
|
655
|
-
name:
|
|
656
|
-
sequence: 2,
|
|
657
|
-
defaultInView: true,
|
|
658
|
-
type:
|
|
659
|
-
allowsNull: false,
|
|
660
|
-
isPrimaryKey: false,
|
|
661
|
-
description:
|
|
741
|
+
"name": "RelatedEntity",
|
|
742
|
+
"sequence": 2,
|
|
743
|
+
"defaultInView": true,
|
|
744
|
+
"type": "nvarchar",
|
|
745
|
+
"allowsNull": false,
|
|
746
|
+
"isPrimaryKey": false,
|
|
747
|
+
"description": "The related entity in the relationship"
|
|
662
748
|
},
|
|
663
749
|
{
|
|
664
|
-
name:
|
|
665
|
-
sequence: 3,
|
|
666
|
-
defaultInView: true,
|
|
667
|
-
type:
|
|
668
|
-
allowsNull: false,
|
|
669
|
-
isPrimaryKey: false,
|
|
670
|
-
description:
|
|
750
|
+
"name": "Type",
|
|
751
|
+
"sequence": 3,
|
|
752
|
+
"defaultInView": true,
|
|
753
|
+
"type": "nvarchar",
|
|
754
|
+
"allowsNull": false,
|
|
755
|
+
"isPrimaryKey": false,
|
|
756
|
+
"description": "Type of relationship (One to Many, Many to One, etc.)"
|
|
671
757
|
},
|
|
672
758
|
{
|
|
673
|
-
name:
|
|
674
|
-
sequence: 4,
|
|
675
|
-
defaultInView: true,
|
|
676
|
-
type:
|
|
677
|
-
allowsNull: true,
|
|
678
|
-
isPrimaryKey: false,
|
|
679
|
-
description:
|
|
759
|
+
"name": "DisplayName",
|
|
760
|
+
"sequence": 4,
|
|
761
|
+
"defaultInView": true,
|
|
762
|
+
"type": "nvarchar",
|
|
763
|
+
"allowsNull": true,
|
|
764
|
+
"isPrimaryKey": false,
|
|
765
|
+
"description": "User-friendly name for the relationship"
|
|
680
766
|
},
|
|
681
767
|
{
|
|
682
|
-
name:
|
|
683
|
-
sequence: 5,
|
|
684
|
-
defaultInView: true,
|
|
685
|
-
type:
|
|
686
|
-
allowsNull: true,
|
|
687
|
-
isPrimaryKey: false,
|
|
688
|
-
description:
|
|
768
|
+
"name": "RelatedEntityJoinField",
|
|
769
|
+
"sequence": 5,
|
|
770
|
+
"defaultInView": true,
|
|
771
|
+
"type": "nvarchar",
|
|
772
|
+
"allowsNull": true,
|
|
773
|
+
"isPrimaryKey": false,
|
|
774
|
+
"description": "The field in the related entity that joins to this entity"
|
|
689
775
|
},
|
|
690
776
|
{
|
|
691
|
-
name:
|
|
692
|
-
sequence: 6,
|
|
693
|
-
defaultInView: false,
|
|
694
|
-
type:
|
|
695
|
-
allowsNull: false,
|
|
696
|
-
isPrimaryKey: false,
|
|
697
|
-
description:
|
|
698
|
-
}
|
|
777
|
+
"name": "Sequence",
|
|
778
|
+
"sequence": 6,
|
|
779
|
+
"defaultInView": false,
|
|
780
|
+
"type": "int",
|
|
781
|
+
"allowsNull": false,
|
|
782
|
+
"isPrimaryKey": false,
|
|
783
|
+
"description": "Display order"
|
|
784
|
+
}
|
|
699
785
|
],
|
|
700
|
-
permissionLevelNeeded: [
|
|
701
|
-
|
|
702
|
-
|
|
786
|
+
"permissionLevelNeeded": [
|
|
787
|
+
"read"
|
|
788
|
+
],
|
|
789
|
+
"usageContext": "Details panel to show entity relationships"
|
|
790
|
+
}
|
|
703
791
|
],
|
|
704
|
-
queries: [],
|
|
705
|
-
description:
|
|
792
|
+
"queries": [],
|
|
793
|
+
"description": "This component requires access to entity metadata including entities, their fields, and relationships to provide a comprehensive entity browsing experience"
|
|
706
794
|
},
|
|
707
|
-
technicalDesign: "## Technical Design\n\n### Props\n- entities: Array of entity objects\n- viewMode: 'grid' | 'card'\n- selectedEntityId: Currently selected entity\n- onSelectEntity: Callback when entity selected\n- sortBy: Current sort field\n- sortDirection: 'asc' | 'desc'\n- onSortChange: Callback for sort changes\n\n### Rendering\n- Grid mode: Compact table with columns\n- Card mode: Cards with entity info\n- Sort indicators in headers\n- Selection highlighting",
|
|
708
|
-
properties: [
|
|
795
|
+
"technicalDesign": "## Technical Design\n\n### Props\n- entities: Array of entity objects\n- viewMode: 'grid' | 'card'\n- selectedEntityId: Currently selected entity\n- onSelectEntity: Callback when entity selected\n- sortBy: Current sort field\n- sortDirection: 'asc' | 'desc'\n- onSortChange: Callback for sort changes\n\n### Rendering\n- Grid mode: Compact table with columns\n- Card mode: Cards with entity info\n- Sort indicators in headers\n- Selection highlighting",
|
|
796
|
+
"properties": [
|
|
709
797
|
{
|
|
710
|
-
name:
|
|
711
|
-
description:
|
|
712
|
-
type:
|
|
713
|
-
required: true
|
|
798
|
+
"name": "entities",
|
|
799
|
+
"description": "Array of entity objects to display",
|
|
800
|
+
"type": "array",
|
|
801
|
+
"required": true
|
|
714
802
|
},
|
|
715
803
|
{
|
|
716
|
-
name:
|
|
717
|
-
description:
|
|
718
|
-
type:
|
|
719
|
-
required: true,
|
|
720
|
-
possibleValues: [
|
|
804
|
+
"name": "viewMode",
|
|
805
|
+
"description": "Display mode - grid or card view",
|
|
806
|
+
"type": "string",
|
|
807
|
+
"required": true,
|
|
808
|
+
"possibleValues": [
|
|
809
|
+
"grid",
|
|
810
|
+
"card"
|
|
811
|
+
]
|
|
721
812
|
},
|
|
722
813
|
{
|
|
723
|
-
name:
|
|
724
|
-
description:
|
|
725
|
-
type:
|
|
726
|
-
required: true
|
|
814
|
+
"name": "selectedEntityId",
|
|
815
|
+
"description": "ID of the currently selected entity",
|
|
816
|
+
"type": "string",
|
|
817
|
+
"required": true
|
|
727
818
|
},
|
|
728
819
|
{
|
|
729
|
-
name:
|
|
730
|
-
description:
|
|
731
|
-
type:
|
|
732
|
-
required: true
|
|
820
|
+
"name": "onSelectEntity",
|
|
821
|
+
"description": "Callback when an entity is selected",
|
|
822
|
+
"type": "function",
|
|
823
|
+
"required": true
|
|
733
824
|
},
|
|
734
825
|
{
|
|
735
|
-
name:
|
|
736
|
-
description:
|
|
737
|
-
type:
|
|
738
|
-
required: true,
|
|
739
|
-
defaultValue:
|
|
826
|
+
"name": "sortBy",
|
|
827
|
+
"description": "Field to sort by",
|
|
828
|
+
"type": "string",
|
|
829
|
+
"required": true,
|
|
830
|
+
"defaultValue": "Name"
|
|
740
831
|
},
|
|
741
832
|
{
|
|
742
|
-
name:
|
|
743
|
-
description:
|
|
744
|
-
type:
|
|
745
|
-
required: true,
|
|
746
|
-
defaultValue:
|
|
747
|
-
possibleValues: [
|
|
833
|
+
"name": "sortDirection",
|
|
834
|
+
"description": "Sort direction",
|
|
835
|
+
"type": "string",
|
|
836
|
+
"required": true,
|
|
837
|
+
"defaultValue": "asc",
|
|
838
|
+
"possibleValues": [
|
|
839
|
+
"asc",
|
|
840
|
+
"desc"
|
|
841
|
+
]
|
|
748
842
|
},
|
|
749
843
|
{
|
|
750
|
-
name:
|
|
751
|
-
description:
|
|
752
|
-
type:
|
|
753
|
-
required: true
|
|
754
|
-
}
|
|
844
|
+
"name": "onSortChange",
|
|
845
|
+
"description": "Callback when sort changes",
|
|
846
|
+
"type": "function",
|
|
847
|
+
"required": true
|
|
848
|
+
}
|
|
755
849
|
],
|
|
756
|
-
events: [
|
|
850
|
+
"events": [
|
|
757
851
|
{
|
|
758
|
-
name:
|
|
759
|
-
description:
|
|
760
|
-
parameters: [
|
|
852
|
+
"name": "onSelectEntity",
|
|
853
|
+
"description": "Fired when an entity is selected",
|
|
854
|
+
"parameters": [
|
|
761
855
|
{
|
|
762
|
-
name:
|
|
763
|
-
description:
|
|
764
|
-
type:
|
|
765
|
-
}
|
|
766
|
-
]
|
|
856
|
+
"name": "entityId",
|
|
857
|
+
"description": "ID of the selected entity",
|
|
858
|
+
"type": "string"
|
|
859
|
+
}
|
|
860
|
+
]
|
|
767
861
|
},
|
|
768
862
|
{
|
|
769
|
-
name:
|
|
770
|
-
description:
|
|
771
|
-
parameters: [
|
|
863
|
+
"name": "onSortChange",
|
|
864
|
+
"description": "Fired when sort configuration changes",
|
|
865
|
+
"parameters": [
|
|
772
866
|
{
|
|
773
|
-
name:
|
|
774
|
-
description:
|
|
775
|
-
type:
|
|
867
|
+
"name": "sortBy",
|
|
868
|
+
"description": "Field to sort by",
|
|
869
|
+
"type": "string"
|
|
776
870
|
},
|
|
777
871
|
{
|
|
778
|
-
name:
|
|
779
|
-
description:
|
|
780
|
-
type:
|
|
781
|
-
}
|
|
782
|
-
]
|
|
783
|
-
}
|
|
872
|
+
"name": "sortDirection",
|
|
873
|
+
"description": "Sort direction",
|
|
874
|
+
"type": "string"
|
|
875
|
+
}
|
|
876
|
+
]
|
|
877
|
+
}
|
|
784
878
|
],
|
|
785
|
-
exampleUsage:
|
|
786
|
-
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}",
|
|
787
|
-
dependencies: [
|
|
879
|
+
"exampleUsage": "<EntityList\n entities={entities}\n viewMode={viewMode}\n selectedEntityId={selectedEntityId}\n onSelectEntity={handleSelectEntity}\n sortBy={sortBy}\n sortDirection={sortDirection}\n onSortChange={handleSortChange}\n utilities={utilities}\n styles={styles}\n components={components}\n callbacks={callbacks}\n/>",
|
|
880
|
+
"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}",
|
|
881
|
+
"dependencies": [
|
|
788
882
|
{
|
|
789
|
-
name:
|
|
790
|
-
location:
|
|
791
|
-
namespace:
|
|
792
|
-
version:
|
|
793
|
-
}
|
|
883
|
+
"name": "DataGrid",
|
|
884
|
+
"location": "registry",
|
|
885
|
+
"namespace": "Generic/UI/Table",
|
|
886
|
+
"version": "^1.0.0"
|
|
887
|
+
}
|
|
794
888
|
],
|
|
795
|
-
libraries: []
|
|
889
|
+
"libraries": []
|
|
796
890
|
},
|
|
797
891
|
{
|
|
798
|
-
name:
|
|
799
|
-
title:
|
|
800
|
-
description:
|
|
801
|
-
type:
|
|
802
|
-
functionalRequirements: "## Entity Details Requirements\n\n- Slide in from the right when an entity is selected\n- Display entity metadata at the top\n- Show fields in a formatted table\n- Display relationships with icons\n- Include 'Open Record' button\n- Support closing via X button or Escape key\n- Smooth slide animation\n- Scrollable content area",
|
|
803
|
-
technicalDesign: "## Technical Design\n\n### Props\n- entity: Selected entity object\n- fields: Array of entity fields\n- relationships: Array of entity relationships\n- isOpen: Whether panel is visible\n- onClose: Callback to close panel\n- onOpenRecord: Callback to open entity record\n\n### Layout\n- Fixed position overlay\n- Slide animation using transform\n- Header with entity name and close button\n- Sections for metadata, fields, relationships\n- Sticky 'Open Record' button at bottom",
|
|
804
|
-
dataRequirements: {
|
|
805
|
-
mode:
|
|
806
|
-
entities: [
|
|
892
|
+
"name": "EntityDetails",
|
|
893
|
+
"title": "Entity Details Panel",
|
|
894
|
+
"description": "Sliding panel that displays detailed information about a selected entity including fields and relationships",
|
|
895
|
+
"type": "form",
|
|
896
|
+
"functionalRequirements": "## Entity Details Requirements\n\n- Slide in from the right when an entity is selected\n- Display entity metadata at the top\n- Show fields in a formatted table\n- Display relationships with icons\n- Include 'Open Record' button\n- Support closing via X button or Escape key\n- Smooth slide animation\n- Scrollable content area",
|
|
897
|
+
"technicalDesign": "## Technical Design\n\n### Props\n- entity: Selected entity object\n- fields: Array of entity fields\n- relationships: Array of entity relationships\n- isOpen: Whether panel is visible\n- onClose: Callback to close panel\n- onOpenRecord: Callback to open entity record\n\n### Layout\n- Fixed position overlay\n- Slide animation using transform\n- Header with entity name and close button\n- Sections for metadata, fields, relationships\n- Sticky 'Open Record' button at bottom",
|
|
898
|
+
"dataRequirements": {
|
|
899
|
+
"mode": "views",
|
|
900
|
+
"entities": [
|
|
807
901
|
{
|
|
808
|
-
name:
|
|
809
|
-
description:
|
|
810
|
-
displayFields: [
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
902
|
+
"name": "Entities",
|
|
903
|
+
"description": "Metadata about all entities in the system",
|
|
904
|
+
"displayFields": [
|
|
905
|
+
"ID",
|
|
906
|
+
"Name",
|
|
907
|
+
"DisplayName",
|
|
908
|
+
"NameSuffix",
|
|
909
|
+
"Description",
|
|
910
|
+
"SchemaName",
|
|
911
|
+
"BaseTable",
|
|
912
|
+
"BaseView"
|
|
913
|
+
],
|
|
914
|
+
"filterFields": [
|
|
915
|
+
"SchemaName",
|
|
916
|
+
"BaseTable"
|
|
917
|
+
],
|
|
918
|
+
"sortFields": [
|
|
919
|
+
"Name",
|
|
920
|
+
"DisplayName"
|
|
921
|
+
],
|
|
922
|
+
"fieldMetadata": [
|
|
814
923
|
{
|
|
815
|
-
name:
|
|
816
|
-
sequence: 1,
|
|
817
|
-
defaultInView: false,
|
|
818
|
-
type:
|
|
819
|
-
allowsNull: false,
|
|
820
|
-
isPrimaryKey: true,
|
|
821
|
-
description:
|
|
924
|
+
"name": "ID",
|
|
925
|
+
"sequence": 1,
|
|
926
|
+
"defaultInView": false,
|
|
927
|
+
"type": "uniqueidentifier",
|
|
928
|
+
"allowsNull": false,
|
|
929
|
+
"isPrimaryKey": true,
|
|
930
|
+
"description": "Unique identifier for the entity"
|
|
822
931
|
},
|
|
823
932
|
{
|
|
824
|
-
name:
|
|
825
|
-
sequence: 2,
|
|
826
|
-
defaultInView: true,
|
|
827
|
-
type:
|
|
828
|
-
allowsNull: false,
|
|
829
|
-
isPrimaryKey: false,
|
|
830
|
-
description:
|
|
933
|
+
"name": "Name",
|
|
934
|
+
"sequence": 2,
|
|
935
|
+
"defaultInView": true,
|
|
936
|
+
"type": "nvarchar",
|
|
937
|
+
"allowsNull": false,
|
|
938
|
+
"isPrimaryKey": false,
|
|
939
|
+
"description": "System name of the entity"
|
|
831
940
|
},
|
|
832
941
|
{
|
|
833
|
-
name:
|
|
834
|
-
sequence: 3,
|
|
835
|
-
defaultInView: true,
|
|
836
|
-
type:
|
|
837
|
-
allowsNull: true,
|
|
838
|
-
isPrimaryKey: false,
|
|
839
|
-
description:
|
|
942
|
+
"name": "DisplayName",
|
|
943
|
+
"sequence": 3,
|
|
944
|
+
"defaultInView": true,
|
|
945
|
+
"type": "nvarchar",
|
|
946
|
+
"allowsNull": true,
|
|
947
|
+
"isPrimaryKey": false,
|
|
948
|
+
"description": "User-friendly display name for the entity"
|
|
840
949
|
},
|
|
841
950
|
{
|
|
842
|
-
name:
|
|
843
|
-
sequence: 4,
|
|
844
|
-
defaultInView: true,
|
|
845
|
-
type:
|
|
846
|
-
allowsNull: true,
|
|
847
|
-
isPrimaryKey: false,
|
|
848
|
-
description:
|
|
951
|
+
"name": "NameSuffix",
|
|
952
|
+
"sequence": 4,
|
|
953
|
+
"defaultInView": true,
|
|
954
|
+
"type": "nvarchar",
|
|
955
|
+
"allowsNull": true,
|
|
956
|
+
"isPrimaryKey": false,
|
|
957
|
+
"description": "Optional suffix appended to entity names for display purposes"
|
|
849
958
|
},
|
|
850
959
|
{
|
|
851
|
-
name:
|
|
852
|
-
sequence: 5,
|
|
853
|
-
defaultInView: true,
|
|
854
|
-
type:
|
|
855
|
-
allowsNull: true,
|
|
856
|
-
isPrimaryKey: false,
|
|
857
|
-
description:
|
|
960
|
+
"name": "Description",
|
|
961
|
+
"sequence": 5,
|
|
962
|
+
"defaultInView": true,
|
|
963
|
+
"type": "nvarchar",
|
|
964
|
+
"allowsNull": true,
|
|
965
|
+
"isPrimaryKey": false,
|
|
966
|
+
"description": "Description of the entity"
|
|
858
967
|
},
|
|
859
968
|
{
|
|
860
|
-
name:
|
|
861
|
-
sequence: 6,
|
|
862
|
-
defaultInView: true,
|
|
863
|
-
type:
|
|
864
|
-
allowsNull: true,
|
|
865
|
-
isPrimaryKey: false,
|
|
866
|
-
description:
|
|
969
|
+
"name": "SchemaName",
|
|
970
|
+
"sequence": 6,
|
|
971
|
+
"defaultInView": true,
|
|
972
|
+
"type": "nvarchar",
|
|
973
|
+
"allowsNull": true,
|
|
974
|
+
"isPrimaryKey": false,
|
|
975
|
+
"description": "Database schema name"
|
|
867
976
|
},
|
|
868
977
|
{
|
|
869
|
-
name:
|
|
870
|
-
sequence: 7,
|
|
871
|
-
defaultInView: true,
|
|
872
|
-
type:
|
|
873
|
-
allowsNull: true,
|
|
874
|
-
isPrimaryKey: false,
|
|
875
|
-
description:
|
|
978
|
+
"name": "BaseTable",
|
|
979
|
+
"sequence": 7,
|
|
980
|
+
"defaultInView": true,
|
|
981
|
+
"type": "nvarchar",
|
|
982
|
+
"allowsNull": true,
|
|
983
|
+
"isPrimaryKey": false,
|
|
984
|
+
"description": "Base table in the database"
|
|
876
985
|
},
|
|
877
986
|
{
|
|
878
|
-
name:
|
|
879
|
-
sequence: 8,
|
|
880
|
-
defaultInView: true,
|
|
881
|
-
type:
|
|
882
|
-
allowsNull: false,
|
|
883
|
-
isPrimaryKey: false,
|
|
884
|
-
description:
|
|
885
|
-
}
|
|
987
|
+
"name": "BaseView",
|
|
988
|
+
"sequence": 8,
|
|
989
|
+
"defaultInView": true,
|
|
990
|
+
"type": "nvarchar",
|
|
991
|
+
"allowsNull": false,
|
|
992
|
+
"isPrimaryKey": false,
|
|
993
|
+
"description": "Base view used for the entity"
|
|
994
|
+
}
|
|
995
|
+
],
|
|
996
|
+
"permissionLevelNeeded": [
|
|
997
|
+
"read"
|
|
886
998
|
],
|
|
887
|
-
|
|
888
|
-
usageContext: 'Main entity list display and filtering',
|
|
999
|
+
"usageContext": "Main entity list display and filtering"
|
|
889
1000
|
},
|
|
890
1001
|
{
|
|
891
|
-
name:
|
|
892
|
-
description:
|
|
893
|
-
displayFields: [
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1002
|
+
"name": "Entity Fields",
|
|
1003
|
+
"description": "Fields belonging to each entity",
|
|
1004
|
+
"displayFields": [
|
|
1005
|
+
"Name",
|
|
1006
|
+
"DisplayName",
|
|
1007
|
+
"Type",
|
|
1008
|
+
"Length",
|
|
1009
|
+
"AllowsNull",
|
|
1010
|
+
"IsPrimaryKey",
|
|
1011
|
+
"IsUnique"
|
|
1012
|
+
],
|
|
1013
|
+
"filterFields": [
|
|
1014
|
+
"EntityID"
|
|
1015
|
+
],
|
|
1016
|
+
"sortFields": [
|
|
1017
|
+
"Sequence",
|
|
1018
|
+
"Name"
|
|
1019
|
+
],
|
|
1020
|
+
"fieldMetadata": [
|
|
897
1021
|
{
|
|
898
|
-
name:
|
|
899
|
-
sequence: 1,
|
|
900
|
-
defaultInView: false,
|
|
901
|
-
type:
|
|
902
|
-
allowsNull: false,
|
|
903
|
-
isPrimaryKey: false,
|
|
904
|
-
description:
|
|
1022
|
+
"name": "EntityID",
|
|
1023
|
+
"sequence": 1,
|
|
1024
|
+
"defaultInView": false,
|
|
1025
|
+
"type": "uniqueidentifier",
|
|
1026
|
+
"allowsNull": false,
|
|
1027
|
+
"isPrimaryKey": false,
|
|
1028
|
+
"description": "Reference to parent entity"
|
|
905
1029
|
},
|
|
906
1030
|
{
|
|
907
|
-
name:
|
|
908
|
-
sequence: 2,
|
|
909
|
-
defaultInView: true,
|
|
910
|
-
type:
|
|
911
|
-
allowsNull: false,
|
|
912
|
-
isPrimaryKey: false,
|
|
913
|
-
description:
|
|
1031
|
+
"name": "Name",
|
|
1032
|
+
"sequence": 2,
|
|
1033
|
+
"defaultInView": true,
|
|
1034
|
+
"type": "nvarchar",
|
|
1035
|
+
"allowsNull": false,
|
|
1036
|
+
"isPrimaryKey": false,
|
|
1037
|
+
"description": "Field name"
|
|
914
1038
|
},
|
|
915
1039
|
{
|
|
916
|
-
name:
|
|
917
|
-
sequence: 3,
|
|
918
|
-
defaultInView: true,
|
|
919
|
-
type:
|
|
920
|
-
allowsNull: true,
|
|
921
|
-
isPrimaryKey: false,
|
|
922
|
-
description:
|
|
1040
|
+
"name": "DisplayName",
|
|
1041
|
+
"sequence": 3,
|
|
1042
|
+
"defaultInView": true,
|
|
1043
|
+
"type": "nvarchar",
|
|
1044
|
+
"allowsNull": true,
|
|
1045
|
+
"isPrimaryKey": false,
|
|
1046
|
+
"description": "User-friendly field name"
|
|
923
1047
|
},
|
|
924
1048
|
{
|
|
925
|
-
name:
|
|
926
|
-
sequence: 4,
|
|
927
|
-
defaultInView: true,
|
|
928
|
-
type:
|
|
929
|
-
allowsNull: false,
|
|
930
|
-
isPrimaryKey: false,
|
|
931
|
-
description:
|
|
1049
|
+
"name": "Type",
|
|
1050
|
+
"sequence": 4,
|
|
1051
|
+
"defaultInView": true,
|
|
1052
|
+
"type": "nvarchar",
|
|
1053
|
+
"allowsNull": false,
|
|
1054
|
+
"isPrimaryKey": false,
|
|
1055
|
+
"description": "Data type of the field"
|
|
932
1056
|
},
|
|
933
1057
|
{
|
|
934
|
-
name:
|
|
935
|
-
sequence: 5,
|
|
936
|
-
defaultInView: true,
|
|
937
|
-
type:
|
|
938
|
-
allowsNull: true,
|
|
939
|
-
isPrimaryKey: false,
|
|
940
|
-
description:
|
|
1058
|
+
"name": "Length",
|
|
1059
|
+
"sequence": 5,
|
|
1060
|
+
"defaultInView": true,
|
|
1061
|
+
"type": "int",
|
|
1062
|
+
"allowsNull": true,
|
|
1063
|
+
"isPrimaryKey": false,
|
|
1064
|
+
"description": "Maximum length for string fields"
|
|
941
1065
|
},
|
|
942
1066
|
{
|
|
943
|
-
name:
|
|
944
|
-
sequence: 6,
|
|
945
|
-
defaultInView: true,
|
|
946
|
-
type:
|
|
947
|
-
allowsNull: false,
|
|
948
|
-
isPrimaryKey: false,
|
|
949
|
-
description:
|
|
1067
|
+
"name": "AllowsNull",
|
|
1068
|
+
"sequence": 6,
|
|
1069
|
+
"defaultInView": true,
|
|
1070
|
+
"type": "bit",
|
|
1071
|
+
"allowsNull": false,
|
|
1072
|
+
"isPrimaryKey": false,
|
|
1073
|
+
"description": "Whether field allows null values"
|
|
950
1074
|
},
|
|
951
1075
|
{
|
|
952
|
-
name:
|
|
953
|
-
sequence: 7,
|
|
954
|
-
defaultInView: true,
|
|
955
|
-
type:
|
|
956
|
-
allowsNull: false,
|
|
957
|
-
isPrimaryKey: false,
|
|
958
|
-
description:
|
|
1076
|
+
"name": "IsPrimaryKey",
|
|
1077
|
+
"sequence": 7,
|
|
1078
|
+
"defaultInView": true,
|
|
1079
|
+
"type": "bit",
|
|
1080
|
+
"allowsNull": false,
|
|
1081
|
+
"isPrimaryKey": false,
|
|
1082
|
+
"description": "Whether field is part of primary key"
|
|
959
1083
|
},
|
|
960
1084
|
{
|
|
961
|
-
name:
|
|
962
|
-
sequence: 8,
|
|
963
|
-
defaultInView: true,
|
|
964
|
-
type:
|
|
965
|
-
allowsNull: false,
|
|
966
|
-
isPrimaryKey: false,
|
|
967
|
-
description:
|
|
1085
|
+
"name": "IsUnique",
|
|
1086
|
+
"sequence": 8,
|
|
1087
|
+
"defaultInView": true,
|
|
1088
|
+
"type": "bit",
|
|
1089
|
+
"allowsNull": false,
|
|
1090
|
+
"isPrimaryKey": false,
|
|
1091
|
+
"description": "Whether field must be unique"
|
|
968
1092
|
},
|
|
969
1093
|
{
|
|
970
|
-
name:
|
|
971
|
-
sequence: 9,
|
|
972
|
-
defaultInView: false,
|
|
973
|
-
type:
|
|
974
|
-
allowsNull: false,
|
|
975
|
-
isPrimaryKey: false,
|
|
976
|
-
description:
|
|
977
|
-
}
|
|
1094
|
+
"name": "Sequence",
|
|
1095
|
+
"sequence": 9,
|
|
1096
|
+
"defaultInView": false,
|
|
1097
|
+
"type": "int",
|
|
1098
|
+
"allowsNull": false,
|
|
1099
|
+
"isPrimaryKey": false,
|
|
1100
|
+
"description": "Display order of the field"
|
|
1101
|
+
}
|
|
1102
|
+
],
|
|
1103
|
+
"permissionLevelNeeded": [
|
|
1104
|
+
"read"
|
|
978
1105
|
],
|
|
979
|
-
|
|
980
|
-
usageContext: 'Details panel to show entity fields',
|
|
1106
|
+
"usageContext": "Details panel to show entity fields"
|
|
981
1107
|
},
|
|
982
1108
|
{
|
|
983
|
-
name:
|
|
984
|
-
description:
|
|
985
|
-
displayFields: [
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1109
|
+
"name": "Entity Relationships",
|
|
1110
|
+
"description": "Relationships between entities",
|
|
1111
|
+
"displayFields": [
|
|
1112
|
+
"RelatedEntity",
|
|
1113
|
+
"Type",
|
|
1114
|
+
"DisplayName",
|
|
1115
|
+
"RelatedEntityJoinField"
|
|
1116
|
+
],
|
|
1117
|
+
"filterFields": [
|
|
1118
|
+
"EntityID"
|
|
1119
|
+
],
|
|
1120
|
+
"sortFields": [
|
|
1121
|
+
"Sequence",
|
|
1122
|
+
"RelatedEntity"
|
|
1123
|
+
],
|
|
1124
|
+
"fieldMetadata": [
|
|
989
1125
|
{
|
|
990
|
-
name:
|
|
991
|
-
sequence: 1,
|
|
992
|
-
defaultInView: false,
|
|
993
|
-
type:
|
|
994
|
-
allowsNull: false,
|
|
995
|
-
isPrimaryKey: false,
|
|
996
|
-
description:
|
|
1126
|
+
"name": "EntityID",
|
|
1127
|
+
"sequence": 1,
|
|
1128
|
+
"defaultInView": false,
|
|
1129
|
+
"type": "uniqueidentifier",
|
|
1130
|
+
"allowsNull": false,
|
|
1131
|
+
"isPrimaryKey": false,
|
|
1132
|
+
"description": "Reference to parent entity"
|
|
997
1133
|
},
|
|
998
1134
|
{
|
|
999
|
-
name:
|
|
1000
|
-
sequence: 2,
|
|
1001
|
-
defaultInView: true,
|
|
1002
|
-
type:
|
|
1003
|
-
allowsNull: false,
|
|
1004
|
-
isPrimaryKey: false,
|
|
1005
|
-
description:
|
|
1135
|
+
"name": "RelatedEntity",
|
|
1136
|
+
"sequence": 2,
|
|
1137
|
+
"defaultInView": true,
|
|
1138
|
+
"type": "nvarchar",
|
|
1139
|
+
"allowsNull": false,
|
|
1140
|
+
"isPrimaryKey": false,
|
|
1141
|
+
"description": "The related entity in the relationship"
|
|
1006
1142
|
},
|
|
1007
1143
|
{
|
|
1008
|
-
name:
|
|
1009
|
-
sequence: 3,
|
|
1010
|
-
defaultInView: true,
|
|
1011
|
-
type:
|
|
1012
|
-
allowsNull: false,
|
|
1013
|
-
isPrimaryKey: false,
|
|
1014
|
-
description:
|
|
1144
|
+
"name": "Type",
|
|
1145
|
+
"sequence": 3,
|
|
1146
|
+
"defaultInView": true,
|
|
1147
|
+
"type": "nvarchar",
|
|
1148
|
+
"allowsNull": false,
|
|
1149
|
+
"isPrimaryKey": false,
|
|
1150
|
+
"description": "Type of relationship (One to Many, Many to One, etc.)"
|
|
1015
1151
|
},
|
|
1016
1152
|
{
|
|
1017
|
-
name:
|
|
1018
|
-
sequence: 4,
|
|
1019
|
-
defaultInView: true,
|
|
1020
|
-
type:
|
|
1021
|
-
allowsNull: true,
|
|
1022
|
-
isPrimaryKey: false,
|
|
1023
|
-
description:
|
|
1153
|
+
"name": "DisplayName",
|
|
1154
|
+
"sequence": 4,
|
|
1155
|
+
"defaultInView": true,
|
|
1156
|
+
"type": "nvarchar",
|
|
1157
|
+
"allowsNull": true,
|
|
1158
|
+
"isPrimaryKey": false,
|
|
1159
|
+
"description": "User-friendly name for the relationship"
|
|
1024
1160
|
},
|
|
1025
1161
|
{
|
|
1026
|
-
name:
|
|
1027
|
-
sequence: 5,
|
|
1028
|
-
defaultInView: true,
|
|
1029
|
-
type:
|
|
1030
|
-
allowsNull: true,
|
|
1031
|
-
isPrimaryKey: false,
|
|
1032
|
-
description:
|
|
1162
|
+
"name": "RelatedEntityJoinField",
|
|
1163
|
+
"sequence": 5,
|
|
1164
|
+
"defaultInView": true,
|
|
1165
|
+
"type": "nvarchar",
|
|
1166
|
+
"allowsNull": true,
|
|
1167
|
+
"isPrimaryKey": false,
|
|
1168
|
+
"description": "The field in the related entity that joins to this entity"
|
|
1033
1169
|
},
|
|
1034
1170
|
{
|
|
1035
|
-
name:
|
|
1036
|
-
sequence: 6,
|
|
1037
|
-
defaultInView: false,
|
|
1038
|
-
type:
|
|
1039
|
-
allowsNull: false,
|
|
1040
|
-
isPrimaryKey: false,
|
|
1041
|
-
description:
|
|
1042
|
-
}
|
|
1171
|
+
"name": "Sequence",
|
|
1172
|
+
"sequence": 6,
|
|
1173
|
+
"defaultInView": false,
|
|
1174
|
+
"type": "int",
|
|
1175
|
+
"allowsNull": false,
|
|
1176
|
+
"isPrimaryKey": false,
|
|
1177
|
+
"description": "Display order"
|
|
1178
|
+
}
|
|
1043
1179
|
],
|
|
1044
|
-
permissionLevelNeeded: [
|
|
1045
|
-
|
|
1046
|
-
|
|
1180
|
+
"permissionLevelNeeded": [
|
|
1181
|
+
"read"
|
|
1182
|
+
],
|
|
1183
|
+
"usageContext": "Details panel to show entity relationships"
|
|
1184
|
+
}
|
|
1047
1185
|
],
|
|
1048
|
-
queries: [],
|
|
1049
|
-
description:
|
|
1186
|
+
"queries": [],
|
|
1187
|
+
"description": "This component requires access to entity metadata including entities, their fields, and relationships to provide a comprehensive entity browsing experience"
|
|
1050
1188
|
},
|
|
1051
|
-
properties: [
|
|
1189
|
+
"properties": [
|
|
1052
1190
|
{
|
|
1053
|
-
name:
|
|
1054
|
-
description:
|
|
1055
|
-
type:
|
|
1056
|
-
required: true
|
|
1191
|
+
"name": "entity",
|
|
1192
|
+
"description": "The selected entity object",
|
|
1193
|
+
"type": "object",
|
|
1194
|
+
"required": true
|
|
1057
1195
|
},
|
|
1058
1196
|
{
|
|
1059
|
-
name:
|
|
1060
|
-
description:
|
|
1061
|
-
type:
|
|
1062
|
-
required: true,
|
|
1063
|
-
defaultValue: []
|
|
1197
|
+
"name": "fields",
|
|
1198
|
+
"description": "Array of fields for the entity",
|
|
1199
|
+
"type": "array",
|
|
1200
|
+
"required": true,
|
|
1201
|
+
"defaultValue": []
|
|
1064
1202
|
},
|
|
1065
1203
|
{
|
|
1066
|
-
name:
|
|
1067
|
-
description:
|
|
1068
|
-
type:
|
|
1069
|
-
required: true,
|
|
1070
|
-
defaultValue: []
|
|
1204
|
+
"name": "relationships",
|
|
1205
|
+
"description": "Array of relationships for the entity",
|
|
1206
|
+
"type": "array",
|
|
1207
|
+
"required": true,
|
|
1208
|
+
"defaultValue": []
|
|
1071
1209
|
},
|
|
1072
1210
|
{
|
|
1073
|
-
name:
|
|
1074
|
-
description:
|
|
1075
|
-
type:
|
|
1076
|
-
required: true
|
|
1211
|
+
"name": "isOpen",
|
|
1212
|
+
"description": "Whether the panel is open",
|
|
1213
|
+
"type": "boolean",
|
|
1214
|
+
"required": true
|
|
1077
1215
|
},
|
|
1078
1216
|
{
|
|
1079
|
-
name:
|
|
1080
|
-
description:
|
|
1081
|
-
type:
|
|
1082
|
-
required: true
|
|
1217
|
+
"name": "onClose",
|
|
1218
|
+
"description": "Callback to close the panel",
|
|
1219
|
+
"type": "function",
|
|
1220
|
+
"required": true
|
|
1083
1221
|
},
|
|
1084
1222
|
{
|
|
1085
|
-
name:
|
|
1086
|
-
description:
|
|
1087
|
-
type:
|
|
1088
|
-
required: true
|
|
1089
|
-
}
|
|
1223
|
+
"name": "onOpenRecord",
|
|
1224
|
+
"description": "Callback to open the entity record",
|
|
1225
|
+
"type": "function",
|
|
1226
|
+
"required": true
|
|
1227
|
+
}
|
|
1090
1228
|
],
|
|
1091
|
-
events: [
|
|
1229
|
+
"events": [
|
|
1092
1230
|
{
|
|
1093
|
-
name:
|
|
1094
|
-
description:
|
|
1095
|
-
parameters: []
|
|
1231
|
+
"name": "onClose",
|
|
1232
|
+
"description": "Fired when the panel should close",
|
|
1233
|
+
"parameters": []
|
|
1096
1234
|
},
|
|
1097
1235
|
{
|
|
1098
|
-
name:
|
|
1099
|
-
description:
|
|
1100
|
-
parameters: [
|
|
1236
|
+
"name": "onOpenRecord",
|
|
1237
|
+
"description": "Fired when the open record button is clicked",
|
|
1238
|
+
"parameters": [
|
|
1101
1239
|
{
|
|
1102
|
-
name:
|
|
1103
|
-
description:
|
|
1104
|
-
type:
|
|
1105
|
-
}
|
|
1106
|
-
]
|
|
1107
|
-
}
|
|
1240
|
+
"name": "entityName",
|
|
1241
|
+
"description": "Name of the entity to open",
|
|
1242
|
+
"type": "string"
|
|
1243
|
+
}
|
|
1244
|
+
]
|
|
1245
|
+
}
|
|
1108
1246
|
],
|
|
1109
|
-
exampleUsage:
|
|
1110
|
-
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}",
|
|
1111
|
-
dependencies: [
|
|
1247
|
+
"exampleUsage": "<EntityDetails\n entity={selectedEntity}\n fields={entityFields}\n relationships={entityRelationships}\n isOpen={detailsPanelOpen}\n onClose={handleCloseDetails}\n onOpenRecord={handleOpenRecord}\n utilities={utilities}\n styles={styles}\n components={components}\n callbacks={callbacks}\n/>",
|
|
1248
|
+
"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}",
|
|
1249
|
+
"dependencies": [
|
|
1112
1250
|
{
|
|
1113
|
-
name:
|
|
1114
|
-
location:
|
|
1115
|
-
namespace:
|
|
1116
|
-
version:
|
|
1117
|
-
}
|
|
1251
|
+
"name": "OpenRecordButton",
|
|
1252
|
+
"location": "registry",
|
|
1253
|
+
"namespace": "Generic/Navigation",
|
|
1254
|
+
"version": "^1.0.0"
|
|
1255
|
+
}
|
|
1118
1256
|
],
|
|
1119
|
-
libraries: []
|
|
1257
|
+
"libraries": []
|
|
1120
1258
|
},
|
|
1121
1259
|
{
|
|
1122
|
-
name:
|
|
1123
|
-
title:
|
|
1124
|
-
description:
|
|
1125
|
-
type:
|
|
1126
|
-
functionalRequirements:
|
|
1127
|
-
dataRequirements: {
|
|
1128
|
-
mode:
|
|
1129
|
-
description:
|
|
1130
|
-
entities: [
|
|
1260
|
+
"name": "EntityFilter",
|
|
1261
|
+
"title": "Entity Filter Panel",
|
|
1262
|
+
"description": "Collapsible filter panel for filtering entities by various criteria",
|
|
1263
|
+
"type": "form",
|
|
1264
|
+
"functionalRequirements": "## Entity Filter Requirements\n\n- Collapsible panel on the left side\n- Filter by schema name (dropdown)\n- Filter by base table (dropdown)\n- Search box for text search\n- Clear all filters button\n- Show active filter count\n- Smooth collapse/expand animation\n- Remember collapsed state",
|
|
1265
|
+
"dataRequirements": {
|
|
1266
|
+
"mode": "views",
|
|
1267
|
+
"description": "Receives filter options derived from Entities metadata",
|
|
1268
|
+
"entities": [
|
|
1131
1269
|
{
|
|
1132
|
-
name:
|
|
1133
|
-
description:
|
|
1134
|
-
displayFields: [
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1270
|
+
"name": "Entities",
|
|
1271
|
+
"description": "Source of schema and table filter options",
|
|
1272
|
+
"displayFields": [
|
|
1273
|
+
"SchemaName",
|
|
1274
|
+
"BaseTable"
|
|
1275
|
+
],
|
|
1276
|
+
"filterFields": [],
|
|
1277
|
+
"sortFields": [],
|
|
1278
|
+
"fieldMetadata": [],
|
|
1279
|
+
"permissionLevelNeeded": [
|
|
1280
|
+
"read"
|
|
1281
|
+
],
|
|
1282
|
+
"usageContext": "Extracts unique schema names and base tables for filter dropdowns"
|
|
1283
|
+
}
|
|
1141
1284
|
],
|
|
1142
|
-
queries: []
|
|
1285
|
+
"queries": []
|
|
1143
1286
|
},
|
|
1144
|
-
technicalDesign:
|
|
1145
|
-
properties: [
|
|
1287
|
+
"technicalDesign": "## Technical Design\n\n### Props\n- filters: Current filter values\n- onFilterChange: Callback when filters change\n- schemas: Available schema options\n- tables: Available table options\n- isCollapsed: Whether panel is collapsed\n- onToggleCollapse: Callback to toggle collapse\n\n### Components\n- Collapse toggle button\n- Schema dropdown\n- Table dropdown\n- Search input\n- Clear filters button\n- Active filter badges",
|
|
1288
|
+
"properties": [
|
|
1146
1289
|
{
|
|
1147
|
-
name:
|
|
1148
|
-
description:
|
|
1149
|
-
type:
|
|
1150
|
-
required: true
|
|
1290
|
+
"name": "filters",
|
|
1291
|
+
"description": "Current filter values",
|
|
1292
|
+
"type": "{schema?: string, table?: string, search?: string}",
|
|
1293
|
+
"required": true
|
|
1151
1294
|
},
|
|
1152
1295
|
{
|
|
1153
|
-
name:
|
|
1154
|
-
description:
|
|
1155
|
-
type:
|
|
1156
|
-
required: true
|
|
1296
|
+
"name": "onFilterChange",
|
|
1297
|
+
"description": "Callback when filters change",
|
|
1298
|
+
"type": "(filters: {schema?: string, table?: string, search?: string}) => void",
|
|
1299
|
+
"required": true
|
|
1157
1300
|
},
|
|
1158
1301
|
{
|
|
1159
|
-
name:
|
|
1160
|
-
description:
|
|
1161
|
-
type:
|
|
1162
|
-
required: true
|
|
1302
|
+
"name": "schemas",
|
|
1303
|
+
"description": "Available schema options",
|
|
1304
|
+
"type": "Array<string>",
|
|
1305
|
+
"required": true
|
|
1163
1306
|
},
|
|
1164
1307
|
{
|
|
1165
|
-
name:
|
|
1166
|
-
description:
|
|
1167
|
-
type:
|
|
1168
|
-
required: true
|
|
1308
|
+
"name": "tables",
|
|
1309
|
+
"description": "Available table options",
|
|
1310
|
+
"type": "Array<string>",
|
|
1311
|
+
"required": true
|
|
1169
1312
|
},
|
|
1170
1313
|
{
|
|
1171
|
-
name:
|
|
1172
|
-
description:
|
|
1173
|
-
type:
|
|
1174
|
-
required: true
|
|
1314
|
+
"name": "isCollapsed",
|
|
1315
|
+
"description": "Whether the panel is collapsed",
|
|
1316
|
+
"type": "boolean",
|
|
1317
|
+
"required": true
|
|
1175
1318
|
},
|
|
1176
1319
|
{
|
|
1177
|
-
name:
|
|
1178
|
-
description:
|
|
1179
|
-
type:
|
|
1180
|
-
required: true
|
|
1181
|
-
}
|
|
1320
|
+
"name": "onToggleCollapse",
|
|
1321
|
+
"description": "Callback to toggle collapse state",
|
|
1322
|
+
"type": "() => void",
|
|
1323
|
+
"required": true
|
|
1324
|
+
}
|
|
1182
1325
|
],
|
|
1183
|
-
events: [
|
|
1326
|
+
"events": [
|
|
1184
1327
|
{
|
|
1185
|
-
name:
|
|
1186
|
-
description:
|
|
1187
|
-
parameters: [
|
|
1328
|
+
"name": "onFilterChange",
|
|
1329
|
+
"description": "Fired when filter values change",
|
|
1330
|
+
"parameters": [
|
|
1188
1331
|
{
|
|
1189
|
-
name:
|
|
1190
|
-
description:
|
|
1191
|
-
type:
|
|
1192
|
-
}
|
|
1193
|
-
]
|
|
1332
|
+
"name": "filters",
|
|
1333
|
+
"description": "Updated filter object",
|
|
1334
|
+
"type": "object"
|
|
1335
|
+
}
|
|
1336
|
+
]
|
|
1194
1337
|
},
|
|
1195
1338
|
{
|
|
1196
|
-
name:
|
|
1197
|
-
description:
|
|
1198
|
-
parameters: []
|
|
1199
|
-
}
|
|
1339
|
+
"name": "onToggleCollapse",
|
|
1340
|
+
"description": "Fired when collapse state should toggle",
|
|
1341
|
+
"parameters": []
|
|
1342
|
+
}
|
|
1200
1343
|
],
|
|
1201
|
-
exampleUsage:
|
|
1202
|
-
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}",
|
|
1203
|
-
dependencies: [],
|
|
1204
|
-
libraries: []
|
|
1344
|
+
"exampleUsage": "<EntityFilter\n filters={filters}\n onFilterChange={handleFilterChange}\n schemas={uniqueSchemas}\n tables={uniqueTables}\n isCollapsed={filterPanelCollapsed}\n onToggleCollapse={handleToggleFilter}\n utilities={utilities}\n styles={styles}\n components={components}\n callbacks={callbacks}\n/>",
|
|
1345
|
+
"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}",
|
|
1346
|
+
"dependencies": [],
|
|
1347
|
+
"libraries": []
|
|
1205
1348
|
},
|
|
1206
1349
|
{
|
|
1207
|
-
name:
|
|
1208
|
-
location:
|
|
1209
|
-
namespace:
|
|
1210
|
-
version:
|
|
1211
|
-
}
|
|
1350
|
+
"name": "OpenRecordButton",
|
|
1351
|
+
"location": "registry",
|
|
1352
|
+
"namespace": "Generic/Navigation",
|
|
1353
|
+
"version": "^1.0.0"
|
|
1354
|
+
}
|
|
1212
1355
|
],
|
|
1213
|
-
libraries: [],
|
|
1214
|
-
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}",
|
|
1356
|
+
"libraries": [],
|
|
1357
|
+
"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}"
|
|
1215
1358
|
};
|
|
1216
1359
|
//# sourceMappingURL=skip-agent.js.map
|