Skip to content

Commit 96a0ca4

Browse files
authored
Merge pull request #82 from delegateas/features/global-option-set
Global vs. Local Choice indication
2 parents 6c53230 + fa27ea9 commit 96a0ca4

File tree

11 files changed

+165
-8
lines changed

11 files changed

+165
-8
lines changed

Generator/DTO/Attributes/ChoiceAttribute.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public class ChoiceAttribute : Attribute
1111

1212
public int? DefaultValue { get; }
1313

14+
public string? GlobalOptionSetName { get; set; }
15+
1416
public ChoiceAttribute(PicklistAttributeMetadata metadata) : base(metadata)
1517
{
1618
Options = metadata.OptionSet.Options.Select(x => new Option(
@@ -20,6 +22,7 @@ public ChoiceAttribute(PicklistAttributeMetadata metadata) : base(metadata)
2022
x.Description.ToLabelString().PrettyDescription()));
2123
Type = "Single";
2224
DefaultValue = metadata.DefaultFormValue;
25+
GlobalOptionSetName = metadata.OptionSet.IsGlobal == true ? metadata.OptionSet.Name : null;
2326
}
2427

2528
public ChoiceAttribute(StateAttributeMetadata metadata) : base(metadata)
@@ -31,6 +34,7 @@ public ChoiceAttribute(StateAttributeMetadata metadata) : base(metadata)
3134
x.Description.ToLabelString().PrettyDescription()));
3235
Type = "Single";
3336
DefaultValue = metadata.DefaultFormValue;
37+
GlobalOptionSetName = null; // State attributes are always local
3438
}
3539

3640
public ChoiceAttribute(MultiSelectPicklistAttributeMetadata metadata) : base(metadata)
@@ -42,5 +46,6 @@ public ChoiceAttribute(MultiSelectPicklistAttributeMetadata metadata) : base(met
4246
x.Description.ToLabelString().PrettyDescription()));
4347
Type = "Multi";
4448
DefaultValue = metadata.DefaultFormValue;
49+
GlobalOptionSetName = metadata.OptionSet.IsGlobal == true ? metadata.OptionSet.Name : null;
4550
}
4651
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Generator.DTO;
2+
3+
internal record GlobalOptionSetUsageReference(
4+
string EntitySchemaName,
5+
string EntityDisplayName,
6+
string AttributeSchemaName,
7+
string AttributeDisplayName);
8+
9+
internal record GlobalOptionSetUsage(
10+
string Name,
11+
string DisplayName,
12+
List<GlobalOptionSetUsageReference> Usages);

Generator/DataverseService.cs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public DataverseService(
7272
this.solutionComponentService = solutionComponentService;
7373
}
7474

75-
public async Task<(IEnumerable<Record>, IEnumerable<SolutionWarning>, IEnumerable<SolutionComponentCollection>)> GetFilteredMetadata()
75+
public async Task<(IEnumerable<Record>, IEnumerable<SolutionWarning>, IEnumerable<SolutionComponentCollection>, Dictionary<string, GlobalOptionSetUsage>)> GetFilteredMetadata()
7676
{
7777
// used to collect warnings for the insights dashboard
7878
var warnings = new List<SolutionWarning>();
@@ -249,6 +249,46 @@ public DataverseService(
249249
workflowDependencies = new Dictionary<Guid, List<WorkflowInfo>>();
250250
}
251251

252+
/// BUILD GLOBAL OPTION SET USAGE MAP
253+
var globalOptionSetUsages = new Dictionary<string, GlobalOptionSetUsage>();
254+
foreach (var entMeta in entitiesInSolutionMetadata)
255+
{
256+
var relevantAttributes = entMeta.Attributes.Where(attr => attributesInSolution.Contains(attr.MetadataId!.Value));
257+
foreach (var attr in relevantAttributes)
258+
{
259+
string? globalOptionSetName = null;
260+
string? globalOptionSetDisplayName = null;
261+
262+
if (attr is PicklistAttributeMetadata picklist && picklist.OptionSet?.IsGlobal == true)
263+
{
264+
globalOptionSetName = picklist.OptionSet.Name;
265+
globalOptionSetDisplayName = picklist.OptionSet.DisplayName.ToLabelString();
266+
}
267+
else if (attr is MultiSelectPicklistAttributeMetadata multiSelect && multiSelect.OptionSet?.IsGlobal == true)
268+
{
269+
globalOptionSetName = multiSelect.OptionSet.Name;
270+
globalOptionSetDisplayName = multiSelect.OptionSet.DisplayName.ToLabelString();
271+
}
272+
273+
if (globalOptionSetName != null)
274+
{
275+
if (!globalOptionSetUsages.ContainsKey(globalOptionSetName))
276+
{
277+
globalOptionSetUsages[globalOptionSetName] = new GlobalOptionSetUsage(
278+
globalOptionSetName,
279+
globalOptionSetDisplayName ?? globalOptionSetName,
280+
new List<GlobalOptionSetUsageReference>());
281+
}
282+
283+
globalOptionSetUsages[globalOptionSetName].Usages.Add(new GlobalOptionSetUsageReference(
284+
entMeta.SchemaName,
285+
entMeta.DisplayName.ToLabelString(),
286+
attr.SchemaName,
287+
attr.DisplayName.ToLabelString()));
288+
}
289+
}
290+
}
291+
252292
var records =
253293
entitiesInSolutionMetadata
254294
.Select(entMeta =>
@@ -350,7 +390,7 @@ public DataverseService(
350390
}
351391

352392
logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] GetFilteredMetadata completed");
353-
return (records, warnings, solutionComponentCollections);
393+
return (records, warnings, solutionComponentCollections, globalOptionSetUsages);
354394
}
355395
}
356396

Generator/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@
6666

6767
// Resolve and use DataverseService
6868
var dataverseService = serviceProvider.GetRequiredService<DataverseService>();
69-
var (entities, warnings, solutionComponents) = await dataverseService.GetFilteredMetadata();
69+
var (entities, warnings, solutionComponents, globalOptionSetUsages) = await dataverseService.GetFilteredMetadata();
7070

71-
var websiteBuilder = new WebsiteBuilder(configuration, entities, warnings, solutionComponents);
71+
var websiteBuilder = new WebsiteBuilder(configuration, entities, warnings, globalOptionSetUsages, solutionComponents);
7272
websiteBuilder.AddData();
7373

7474
// Token provider function

Generator/WebsiteBuilder.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Generator.DTO.Warnings;
33
using Microsoft.Extensions.Configuration;
44
using Newtonsoft.Json;
5+
using System.Collections.Generic;
56
using System.Text;
67

78
namespace Generator;
@@ -12,18 +13,21 @@ internal class WebsiteBuilder
1213
private readonly IEnumerable<Record> records;
1314
private readonly IEnumerable<SolutionWarning> warnings;
1415
private readonly IEnumerable<SolutionComponentCollection> solutionComponents;
16+
private readonly Dictionary<string, GlobalOptionSetUsage> globalOptionSetUsages;
1517
private readonly string OutputFolder;
1618

1719
public WebsiteBuilder(
1820
IConfiguration configuration,
1921
IEnumerable<Record> records,
2022
IEnumerable<SolutionWarning> warnings,
23+
Dictionary<string, GlobalOptionSetUsage> globalOptionSetUsages,
2124
IEnumerable<SolutionComponentCollection>? solutionComponents = null)
2225
{
2326
this.configuration = configuration;
2427
this.records = records;
2528
this.warnings = warnings;
2629
this.solutionComponents = solutionComponents ?? Enumerable.Empty<SolutionComponentCollection>();
30+
this.globalOptionSetUsages = globalOptionSetUsages;
2731

2832
// Assuming execution in bin/xxx/net8.0
2933
OutputFolder = configuration["OutputFolder"] ?? Path.Combine(System.Reflection.Assembly.GetExecutingAssembly().Location, "../../../../../Website/generated");
@@ -80,6 +84,15 @@ internal void AddData()
8084
}
8185
sb.AppendLine("]");
8286

87+
// GLOBAL OPTION SETS
88+
sb.AppendLine("");
89+
sb.AppendLine("export const GlobalOptionSets: Record<string, { Name: string; DisplayName: string; Usages: { EntitySchemaName: string; EntityDisplayName: string; AttributeSchemaName: string; AttributeDisplayName: string }[] }> = {");
90+
foreach (var (key, usage) in globalOptionSetUsages)
91+
{
92+
sb.AppendLine($" \"{key}\": {JsonConvert.SerializeObject(usage)},");
93+
}
94+
sb.AppendLine("};");
95+
8396
File.WriteAllText(Path.Combine(OutputFolder, "Data.ts"), sb.ToString());
8497
}
8598
}

Website/components/datamodelview/attributes/ChoiceAttribute.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ChoiceAttributeType } from "@/lib/Types"
33
import { formatNumberSeperator } from "@/lib/utils"
44
import { Box, Typography, Chip } from "@mui/material"
55
import { CheckBoxOutlineBlankRounded, CheckBoxRounded, CheckRounded, RadioButtonCheckedRounded, RadioButtonUncheckedRounded } from "@mui/icons-material"
6+
import OptionSetScopeIndicator from "./OptionSetScopeIndicator"
67

78
export default function ChoiceAttribute({ attribute, highlightMatch, highlightTerm }: { attribute: ChoiceAttributeType, highlightMatch: (text: string, term: string) => string | React.JSX.Element, highlightTerm: string }) {
89

@@ -11,9 +12,10 @@ export default function ChoiceAttribute({ attribute, highlightMatch, highlightTe
1112
return (
1213
<Box className="flex flex-col gap-1">
1314
<Box className="flex items-center gap-2">
15+
<OptionSetScopeIndicator globalOptionSetName={attribute.GlobalOptionSetName} />
1416
<Typography className="font-semibold text-xs md:text-sm md:font-bold">{attribute.Type}-select</Typography>
1517
{attribute.DefaultValue !== null && attribute.DefaultValue !== -1 && !isMobile && (
16-
<Chip
18+
<Chip
1719
icon={<CheckRounded className="w-2 h-2 md:w-3 md:h-3" />}
1820
label={`Default: ${attribute.Options.find(o => o.Value === attribute.DefaultValue)?.Name}`}
1921
size="small"
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { Box, Tooltip, Typography } from "@mui/material";
2+
import { PublicRounded, HomeRounded } from "@mui/icons-material";
3+
import { useDatamodelData } from "@/contexts/DatamodelDataContext";
4+
5+
interface OptionSetScopeIndicatorProps {
6+
globalOptionSetName: string | null;
7+
}
8+
9+
export default function OptionSetScopeIndicator({ globalOptionSetName }: OptionSetScopeIndicatorProps) {
10+
const { globalOptionSets } = useDatamodelData();
11+
12+
if (!globalOptionSetName) {
13+
// Local option set
14+
return (
15+
<Tooltip title="Local choice" placement="top">
16+
<HomeRounded
17+
className="w-3 h-3 md:w-4 md:h-4"
18+
sx={{ color: 'text.secondary' }}
19+
/>
20+
</Tooltip>
21+
);
22+
}
23+
24+
// Global option set - show usages in tooltip
25+
const usage = globalOptionSets[globalOptionSetName];
26+
27+
if (!usage) {
28+
// Fallback if usage data not found
29+
return (
30+
<Tooltip title={`Global choice: ${globalOptionSetName}`} placement="top">
31+
<PublicRounded
32+
className="w-3 h-3 md:w-4 md:h-4"
33+
sx={{ color: 'primary.main' }}
34+
/>
35+
</Tooltip>
36+
);
37+
}
38+
39+
const tooltipContent = (
40+
<Box>
41+
<Typography className="font-semibold text-xs mb-1">
42+
Global choice: {usage.DisplayName}
43+
</Typography>
44+
<Typography className="text-xs mb-1">
45+
Used by {usage.Usages.length} field{usage.Usages.length !== 1 ? 's' : ''}:
46+
</Typography>
47+
<Box className="max-h-48 overflow-y-auto">
48+
{usage.Usages.map((u, idx) => (
49+
<Typography key={idx} className="text-xs pl-2">
50+
{u.EntityDisplayName} - {u.AttributeDisplayName}
51+
</Typography>
52+
))}
53+
</Box>
54+
</Box>
55+
);
56+
57+
return (
58+
<Tooltip title={tooltipContent} placement="top">
59+
<PublicRounded
60+
className="w-3 h-3 md:w-4 md:h-4"
61+
sx={{ color: 'primary.main' }}
62+
/>
63+
</Tooltip>
64+
);
65+
}

Website/components/datamodelview/dataLoaderWorker.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { EntityType } from '@/lib/Types';
2-
import { Groups, SolutionWarnings, SolutionCount, SolutionComponents } from '../../generated/Data';
2+
import { Groups, SolutionWarnings, SolutionCount, SolutionComponents, GlobalOptionSets } from '../../generated/Data';
33

44
self.onmessage = function () {
55
const entityMap = new Map<string, EntityType>();
@@ -13,6 +13,7 @@ self.onmessage = function () {
1313
entityMap: entityMap,
1414
warnings: SolutionWarnings,
1515
solutionCount: SolutionCount,
16-
solutionComponents: SolutionComponents
16+
solutionComponents: SolutionComponents,
17+
globalOptionSets: GlobalOptionSets
1718
});
1819
};

Website/contexts/DatamodelDataContext.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,24 @@ interface DataModelAction {
99
getEntityDataBySchemaName: (schemaName: string) => EntityType | undefined;
1010
}
1111

12+
export interface GlobalOptionSetUsage {
13+
Name: string;
14+
DisplayName: string;
15+
Usages: {
16+
EntitySchemaName: string;
17+
EntityDisplayName: string;
18+
AttributeSchemaName: string;
19+
AttributeDisplayName: string;
20+
}[];
21+
}
22+
1223
interface DatamodelDataState extends DataModelAction {
1324
groups: GroupType[];
1425
entityMap?: Map<string, EntityType>;
1526
warnings: SolutionWarningType[];
1627
solutionCount: number;
1728
solutionComponents: SolutionComponentCollectionType[];
29+
globalOptionSets: Record<string, GlobalOptionSetUsage>;
1830
search: string;
1931
searchScope: SearchScope;
2032
filtered: Array<
@@ -30,6 +42,7 @@ const initialState: DatamodelDataState = {
3042
warnings: [],
3143
solutionCount: 0,
3244
solutionComponents: [],
45+
globalOptionSets: {},
3346
search: "",
3447
searchScope: {
3548
columnNames: true,
@@ -58,6 +71,8 @@ const datamodelDataReducer = (state: DatamodelDataState, action: any): Datamodel
5871
return { ...state, solutionCount: action.payload };
5972
case "SET_SOLUTION_COMPONENTS":
6073
return { ...state, solutionComponents: action.payload };
74+
case "SET_GLOBAL_OPTION_SETS":
75+
return { ...state, globalOptionSets: action.payload };
6176
case "SET_SEARCH":
6277
return { ...state, search: action.payload };
6378
case "SET_SEARCH_SCOPE":
@@ -88,6 +103,7 @@ export const DatamodelDataProvider = ({ children }: { children: ReactNode }) =>
88103
dispatch({ type: "SET_WARNINGS", payload: e.data.warnings || [] });
89104
dispatch({ type: "SET_SOLUTION_COUNT", payload: e.data.solutionCount || 0 });
90105
dispatch({ type: "SET_SOLUTION_COMPONENTS", payload: e.data.solutionComponents || [] });
106+
dispatch({ type: "SET_GLOBAL_OPTION_SETS", payload: e.data.globalOptionSets || {} });
91107
worker.terminate();
92108
};
93109
worker.postMessage({});

Website/lib/Types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ export type ChoiceAttributeType = BaseAttribute & {
278278
AttributeType: "ChoiceAttribute",
279279
Type: "Single" | "Multi",
280280
DefaultValue: number | null,
281+
GlobalOptionSetName: string | null,
281282
Options: {
282283
Name: string,
283284
Value: number,

0 commit comments

Comments
 (0)