Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Generator/DTO/Attributes/ChoiceAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public class ChoiceAttribute : Attribute

public int? DefaultValue { get; }

public string? GlobalOptionSetName { get; set; }

public ChoiceAttribute(PicklistAttributeMetadata metadata) : base(metadata)
{
Options = metadata.OptionSet.Options.Select(x => new Option(
Expand All @@ -20,6 +22,7 @@ public ChoiceAttribute(PicklistAttributeMetadata metadata) : base(metadata)
x.Description.ToLabelString().PrettyDescription()));
Type = "Single";
DefaultValue = metadata.DefaultFormValue;
GlobalOptionSetName = metadata.OptionSet.IsGlobal == true ? metadata.OptionSet.Name : null;
}

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

public ChoiceAttribute(MultiSelectPicklistAttributeMetadata metadata) : base(metadata)
Expand All @@ -42,5 +46,6 @@ public ChoiceAttribute(MultiSelectPicklistAttributeMetadata metadata) : base(met
x.Description.ToLabelString().PrettyDescription()));
Type = "Multi";
DefaultValue = metadata.DefaultFormValue;
GlobalOptionSetName = metadata.OptionSet.IsGlobal == true ? metadata.OptionSet.Name : null;
}
}
12 changes: 12 additions & 0 deletions Generator/DTO/GlobalOptionSetUsage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Generator.DTO;

internal record GlobalOptionSetUsageReference(
string EntitySchemaName,
string EntityDisplayName,
string AttributeSchemaName,
string AttributeDisplayName);

internal record GlobalOptionSetUsage(
string Name,
string DisplayName,
List<GlobalOptionSetUsageReference> Usages);
44 changes: 42 additions & 2 deletions Generator/DataverseService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public DataverseService(
this.solutionComponentService = solutionComponentService;
}

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

/// BUILD GLOBAL OPTION SET USAGE MAP
var globalOptionSetUsages = new Dictionary<string, GlobalOptionSetUsage>();
foreach (var entMeta in entitiesInSolutionMetadata)
{
var relevantAttributes = entMeta.Attributes.Where(attr => attributesInSolution.Contains(attr.MetadataId!.Value));
foreach (var attr in relevantAttributes)
{
string? globalOptionSetName = null;
string? globalOptionSetDisplayName = null;

if (attr is PicklistAttributeMetadata picklist && picklist.OptionSet?.IsGlobal == true)
{
globalOptionSetName = picklist.OptionSet.Name;
globalOptionSetDisplayName = picklist.OptionSet.DisplayName.ToLabelString();
}
else if (attr is MultiSelectPicklistAttributeMetadata multiSelect && multiSelect.OptionSet?.IsGlobal == true)
{
globalOptionSetName = multiSelect.OptionSet.Name;
globalOptionSetDisplayName = multiSelect.OptionSet.DisplayName.ToLabelString();
}

if (globalOptionSetName != null)
{
if (!globalOptionSetUsages.ContainsKey(globalOptionSetName))
{
globalOptionSetUsages[globalOptionSetName] = new GlobalOptionSetUsage(
globalOptionSetName,
globalOptionSetDisplayName ?? globalOptionSetName,
new List<GlobalOptionSetUsageReference>());
}

globalOptionSetUsages[globalOptionSetName].Usages.Add(new GlobalOptionSetUsageReference(
entMeta.SchemaName,
entMeta.DisplayName.ToLabelString(),
attr.SchemaName,
attr.DisplayName.ToLabelString()));
}
}
}

var records =
entitiesInSolutionMetadata
.Select(entMeta =>
Expand Down Expand Up @@ -350,7 +390,7 @@ public DataverseService(
}

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

Expand Down
4 changes: 2 additions & 2 deletions Generator/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@

// Resolve and use DataverseService
var dataverseService = serviceProvider.GetRequiredService<DataverseService>();
var (entities, warnings, solutionComponents) = await dataverseService.GetFilteredMetadata();
var (entities, warnings, solutionComponents, globalOptionSetUsages) = await dataverseService.GetFilteredMetadata();

var websiteBuilder = new WebsiteBuilder(configuration, entities, warnings, solutionComponents);
var websiteBuilder = new WebsiteBuilder(configuration, entities, warnings, globalOptionSetUsages, solutionComponents);
websiteBuilder.AddData();

// Token provider function
Expand Down
13 changes: 13 additions & 0 deletions Generator/WebsiteBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Generator.DTO.Warnings;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Text;

namespace Generator;
Expand All @@ -12,18 +13,21 @@ internal class WebsiteBuilder
private readonly IEnumerable<Record> records;
private readonly IEnumerable<SolutionWarning> warnings;
private readonly IEnumerable<SolutionComponentCollection> solutionComponents;
private readonly Dictionary<string, GlobalOptionSetUsage> globalOptionSetUsages;
private readonly string OutputFolder;

public WebsiteBuilder(
IConfiguration configuration,
IEnumerable<Record> records,
IEnumerable<SolutionWarning> warnings,
Dictionary<string, GlobalOptionSetUsage> globalOptionSetUsages,
IEnumerable<SolutionComponentCollection>? solutionComponents = null)
{
this.configuration = configuration;
this.records = records;
this.warnings = warnings;
this.solutionComponents = solutionComponents ?? Enumerable.Empty<SolutionComponentCollection>();
this.globalOptionSetUsages = globalOptionSetUsages;

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

// GLOBAL OPTION SETS
sb.AppendLine("");
sb.AppendLine("export const GlobalOptionSets: Record<string, { Name: string; DisplayName: string; Usages: { EntitySchemaName: string; EntityDisplayName: string; AttributeSchemaName: string; AttributeDisplayName: string }[] }> = {");
foreach (var (key, usage) in globalOptionSetUsages)
{
sb.AppendLine($" \"{key}\": {JsonConvert.SerializeObject(usage)},");
}
sb.AppendLine("};");

File.WriteAllText(Path.Combine(OutputFolder, "Data.ts"), sb.ToString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ChoiceAttributeType } from "@/lib/Types"
import { formatNumberSeperator } from "@/lib/utils"
import { Box, Typography, Chip } from "@mui/material"
import { CheckBoxOutlineBlankRounded, CheckBoxRounded, CheckRounded, RadioButtonCheckedRounded, RadioButtonUncheckedRounded } from "@mui/icons-material"
import OptionSetScopeIndicator from "./OptionSetScopeIndicator"

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

Expand All @@ -11,9 +12,10 @@ export default function ChoiceAttribute({ attribute, highlightMatch, highlightTe
return (
<Box className="flex flex-col gap-1">
<Box className="flex items-center gap-2">
<OptionSetScopeIndicator globalOptionSetName={attribute.GlobalOptionSetName} />
<Typography className="font-semibold text-xs md:text-sm md:font-bold">{attribute.Type}-select</Typography>
{attribute.DefaultValue !== null && attribute.DefaultValue !== -1 && !isMobile && (
<Chip
<Chip
icon={<CheckRounded className="w-2 h-2 md:w-3 md:h-3" />}
label={`Default: ${attribute.Options.find(o => o.Value === attribute.DefaultValue)?.Name}`}
size="small"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Box, Tooltip, Typography } from "@mui/material";
import { PublicRounded, HomeRounded } from "@mui/icons-material";
import { useDatamodelData } from "@/contexts/DatamodelDataContext";

interface OptionSetScopeIndicatorProps {
globalOptionSetName: string | null;
}

export default function OptionSetScopeIndicator({ globalOptionSetName }: OptionSetScopeIndicatorProps) {
const { globalOptionSets } = useDatamodelData();

if (!globalOptionSetName) {
// Local option set
return (
<Tooltip title="Local choice" placement="top">
<HomeRounded
className="w-3 h-3 md:w-4 md:h-4"
sx={{ color: 'text.secondary' }}
/>
</Tooltip>
);
}

// Global option set - show usages in tooltip
const usage = globalOptionSets[globalOptionSetName];

if (!usage) {
// Fallback if usage data not found
return (
<Tooltip title={`Global choice: ${globalOptionSetName}`} placement="top">
<PublicRounded
className="w-3 h-3 md:w-4 md:h-4"
sx={{ color: 'primary.main' }}
/>
</Tooltip>
);
}

const tooltipContent = (
<Box>
<Typography className="font-semibold text-xs mb-1">
Global choice: {usage.DisplayName}
</Typography>
<Typography className="text-xs mb-1">
Used by {usage.Usages.length} field{usage.Usages.length !== 1 ? 's' : ''}:
</Typography>
<Box className="max-h-48 overflow-y-auto">
{usage.Usages.map((u, idx) => (
<Typography key={idx} className="text-xs pl-2">
• {u.EntityDisplayName} - {u.AttributeDisplayName}
</Typography>
))}
</Box>
</Box>
);

return (
<Tooltip title={tooltipContent} placement="top">
<PublicRounded
className="w-3 h-3 md:w-4 md:h-4"
sx={{ color: 'primary.main' }}
/>
</Tooltip>
);
}
5 changes: 3 additions & 2 deletions Website/components/datamodelview/dataLoaderWorker.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { EntityType } from '@/lib/Types';
import { Groups, SolutionWarnings, SolutionCount, SolutionComponents } from '../../generated/Data';
import { Groups, SolutionWarnings, SolutionCount, SolutionComponents, GlobalOptionSets } from '../../generated/Data';

self.onmessage = function () {
const entityMap = new Map<string, EntityType>();
Expand All @@ -13,6 +13,7 @@ self.onmessage = function () {
entityMap: entityMap,
warnings: SolutionWarnings,
solutionCount: SolutionCount,
solutionComponents: SolutionComponents
solutionComponents: SolutionComponents,
globalOptionSets: GlobalOptionSets
});
};
16 changes: 16 additions & 0 deletions Website/contexts/DatamodelDataContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,24 @@ interface DataModelAction {
getEntityDataBySchemaName: (schemaName: string) => EntityType | undefined;
}

export interface GlobalOptionSetUsage {
Name: string;
DisplayName: string;
Usages: {
EntitySchemaName: string;
EntityDisplayName: string;
AttributeSchemaName: string;
AttributeDisplayName: string;
}[];
}

interface DatamodelDataState extends DataModelAction {
groups: GroupType[];
entityMap?: Map<string, EntityType>;
warnings: SolutionWarningType[];
solutionCount: number;
solutionComponents: SolutionComponentCollectionType[];
globalOptionSets: Record<string, GlobalOptionSetUsage>;
search: string;
searchScope: SearchScope;
filtered: Array<
Expand All @@ -30,6 +42,7 @@ const initialState: DatamodelDataState = {
warnings: [],
solutionCount: 0,
solutionComponents: [],
globalOptionSets: {},
search: "",
searchScope: {
columnNames: true,
Expand Down Expand Up @@ -58,6 +71,8 @@ const datamodelDataReducer = (state: DatamodelDataState, action: any): Datamodel
return { ...state, solutionCount: action.payload };
case "SET_SOLUTION_COMPONENTS":
return { ...state, solutionComponents: action.payload };
case "SET_GLOBAL_OPTION_SETS":
return { ...state, globalOptionSets: action.payload };
case "SET_SEARCH":
return { ...state, search: action.payload };
case "SET_SEARCH_SCOPE":
Expand Down Expand Up @@ -88,6 +103,7 @@ export const DatamodelDataProvider = ({ children }: { children: ReactNode }) =>
dispatch({ type: "SET_WARNINGS", payload: e.data.warnings || [] });
dispatch({ type: "SET_SOLUTION_COUNT", payload: e.data.solutionCount || 0 });
dispatch({ type: "SET_SOLUTION_COMPONENTS", payload: e.data.solutionComponents || [] });
dispatch({ type: "SET_GLOBAL_OPTION_SETS", payload: e.data.globalOptionSets || {} });
worker.terminate();
};
worker.postMessage({});
Expand Down
1 change: 1 addition & 0 deletions Website/lib/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ export type ChoiceAttributeType = BaseAttribute & {
AttributeType: "ChoiceAttribute",
Type: "Single" | "Multi",
DefaultValue: number | null,
GlobalOptionSetName: string | null,
Options: {
Name: string,
Value: number,
Expand Down
4 changes: 3 additions & 1 deletion Website/stubs/Data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,6 @@ export let Groups: GroupType[] = [

export let SolutionWarnings: SolutionWarningType[] = [];

export let SolutionComponents: SolutionComponentCollectionType[] = [];
export let SolutionComponents: SolutionComponentCollectionType[] = [];

export const GlobalOptionSets: Record<string, { Name: string; DisplayName: string; Usages: { EntitySchemaName: string; EntityDisplayName: string; AttributeSchemaName: string; AttributeDisplayName: string }[] }> = {};
Loading