Skip to content

Commit edf9415

Browse files
committed
feat: global choice indication
1 parent 31fcf73 commit edf9415

11 files changed

Lines changed: 164 additions & 10 deletions

File tree

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: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public DataverseService(
6969
this.solutionComponentService = solutionComponentService;
7070
}
7171

72-
public async Task<(IEnumerable<Record>, IEnumerable<SolutionWarning>)> GetFilteredMetadata()
72+
public async Task<(IEnumerable<Record>, IEnumerable<SolutionWarning>, Dictionary<string, GlobalOptionSetUsage>)> GetFilteredMetadata()
7373
{
7474
// used to collect warnings for the insights dashboard
7575
var warnings = new List<SolutionWarning>();
@@ -246,6 +246,46 @@ public DataverseService(
246246
workflowDependencies = new Dictionary<Guid, List<WorkflowInfo>>();
247247
}
248248

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

278-
logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] GetFilteredMetadata completed - returning empty results");
279-
return (records, warnings);
318+
logger.LogInformation($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] GetFilteredMetadata completed - returning {records.Count} records with {globalOptionSetUsages.Count} global option sets");
319+
return (records, warnings, globalOptionSetUsages);
280320
}
281321
}
282322

Generator/Program.cs

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

6666
// Resolve and use DataverseService
6767
var dataverseService = serviceProvider.GetRequiredService<DataverseService>();
68-
var (entities, warnings) = await dataverseService.GetFilteredMetadata();
68+
var (entities, warnings, globalOptionSetUsages) = await dataverseService.GetFilteredMetadata();
6969

70-
var websiteBuilder = new WebsiteBuilder(configuration, entities, warnings);
70+
var websiteBuilder = new WebsiteBuilder(configuration, entities, warnings, globalOptionSetUsages);
7171
websiteBuilder.AddData();
7272

7373
// Token provider function

Generator/WebsiteBuilder.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ internal class WebsiteBuilder
1212
private readonly IEnumerable<Record> records;
1313
private readonly IEnumerable<SolutionWarning> warnings;
1414
private readonly IEnumerable<Solution> solutions;
15+
private readonly Dictionary<string, GlobalOptionSetUsage> globalOptionSetUsages;
1516
private readonly string OutputFolder;
1617

17-
public WebsiteBuilder(IConfiguration configuration, IEnumerable<Record> records, IEnumerable<SolutionWarning> warnings)
18+
public WebsiteBuilder(IConfiguration configuration, IEnumerable<Record> records, IEnumerable<SolutionWarning> warnings, Dictionary<string, GlobalOptionSetUsage> globalOptionSetUsages)
1819
{
1920
this.configuration = configuration;
2021
this.records = records;
2122
this.warnings = warnings;
23+
this.globalOptionSetUsages = globalOptionSetUsages;
2224

2325
// Assuming execution in bin/xxx/net8.0
2426
OutputFolder = configuration["OutputFolder"] ?? Path.Combine(System.Reflection.Assembly.GetExecutingAssembly().Location, "../../../../../Website/generated");
@@ -66,6 +68,15 @@ internal void AddData()
6668
}
6769
sb.AppendLine("]");
6870

71+
// GLOBAL OPTION SETS
72+
sb.AppendLine("");
73+
sb.AppendLine("export const GlobalOptionSets: Record<string, { Name: string; DisplayName: string; Usages: { EntitySchemaName: string; EntityDisplayName: string; AttributeSchemaName: string; AttributeDisplayName: string }[] }> = {");
74+
foreach (var (key, usage) in globalOptionSetUsages)
75+
{
76+
sb.AppendLine($" \"{key}\": {JsonConvert.SerializeObject(usage)},");
77+
}
78+
sb.AppendLine("};");
79+
6980
File.WriteAllText(Path.Combine(OutputFolder, "Data.ts"), sb.ToString());
7081
}
7182
}

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+
}
Lines changed: 2 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 } from '../../generated/Data';
2+
import { Groups, SolutionWarnings, SolutionCount, GlobalOptionSets } from '../../generated/Data';
33

44
self.onmessage = function () {
55
const entityMap = new Map<string, EntityType>();
@@ -8,5 +8,5 @@ self.onmessage = function () {
88
entityMap.set(entity.SchemaName, entity);
99
});
1010
});
11-
self.postMessage({ groups: Groups, entityMap: entityMap, warnings: SolutionWarnings, solutionCount: SolutionCount });
11+
self.postMessage({ groups: Groups, entityMap: entityMap, warnings: SolutionWarnings, solutionCount: SolutionCount, globalOptionSets: GlobalOptionSets });
1212
};

Website/contexts/DatamodelDataContext.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,23 @@ 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;
28+
globalOptionSets: Record<string, GlobalOptionSetUsage>;
1729
search: string;
1830
searchScope: SearchScope;
1931
filtered: Array<
@@ -28,6 +40,7 @@ const initialState: DatamodelDataState = {
2840
groups: [],
2941
warnings: [],
3042
solutionCount: 0,
43+
globalOptionSets: {},
3144
search: "",
3245
searchScope: {
3346
columnNames: true,
@@ -54,6 +67,8 @@ const datamodelDataReducer = (state: DatamodelDataState, action: any): Datamodel
5467
return { ...state, warnings: action.payload };
5568
case "SET_SOLUTION_COUNT":
5669
return { ...state, solutionCount: action.payload };
70+
case "SET_GLOBAL_OPTION_SETS":
71+
return { ...state, globalOptionSets: action.payload };
5772
case "SET_SEARCH":
5873
return { ...state, search: action.payload };
5974
case "SET_SEARCH_SCOPE":
@@ -83,6 +98,7 @@ export const DatamodelDataProvider = ({ children }: { children: ReactNode }) =>
8398
dispatch({ type: "SET_ENTITIES", payload: e.data.entityMap || new Map() });
8499
dispatch({ type: "SET_WARNINGS", payload: e.data.warnings || [] });
85100
dispatch({ type: "SET_SOLUTION_COUNT", payload: e.data.solutionCount || 0 });
101+
dispatch({ type: "SET_GLOBAL_OPTION_SETS", payload: e.data.globalOptionSets || {} });
86102
worker.terminate();
87103
};
88104
worker.postMessage({});

Website/lib/Types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export type ChoiceAttributeType = BaseAttribute & {
117117
AttributeType: "ChoiceAttribute",
118118
Type: "Single" | "Multi",
119119
DefaultValue: number | null,
120+
GlobalOptionSetName: string | null,
120121
Options: {
121122
Name: string,
122123
Value: number,

0 commit comments

Comments
 (0)