@mohsinonxrm/pptb-fetchxml-studio 1.0.0 → 1.0.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/LICENSE +661 -0
- package/dist/assets/index-DD9fzkbz.js +232 -0
- package/dist/index.html +70 -0
- package/index.html +0 -1
- package/npm-shrinkwrap.json +6329 -0
- package/package.json +95 -51
- package/CHANGELOG.md +0 -120
- package/eslint.config.js +0 -23
- package/public/vite.svg +0 -1
- package/src/app/AppShell.tsx +0 -1733
- package/src/assets/20px-Microsoft_Office_Excel_(2025/342/200/223present).svg.png +0 -0
- package/src/assets/react.svg +0 -1
- package/src/features/fetchxml/api/dataverseMetadata.ts +0 -192
- package/src/features/fetchxml/api/excelExport.ts +0 -318
- package/src/features/fetchxml/api/formattedValues.ts +0 -107
- package/src/features/fetchxml/api/pptbClient.ts +0 -2635
- package/src/features/fetchxml/model/displaySettings.ts +0 -18
- package/src/features/fetchxml/model/fetchxml.ts +0 -346
- package/src/features/fetchxml/model/fetchxmlIntellisense.ts +0 -583
- package/src/features/fetchxml/model/fetchxmlParser.ts +0 -708
- package/src/features/fetchxml/model/layoutxml.ts +0 -372
- package/src/features/fetchxml/model/nodes.ts +0 -255
- package/src/features/fetchxml/model/operators.ts +0 -624
- package/src/features/fetchxml/model/treeUtils.ts +0 -174
- package/src/features/fetchxml/state/builderStore.tsx +0 -1160
- package/src/features/fetchxml/state/cache.ts +0 -393
- package/src/features/fetchxml/ui/Dialogs/BulkDeleteDialog.tsx +0 -352
- package/src/features/fetchxml/ui/Dialogs/DeleteConfirmDialog.tsx +0 -396
- package/src/features/fetchxml/ui/Dialogs/SaveViewDialog.tsx +0 -612
- package/src/features/fetchxml/ui/Dialogs/SolutionPicker.tsx +0 -114
- package/src/features/fetchxml/ui/Dialogs/WorkflowPickerDialog.tsx +0 -437
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/AttributeEditor.tsx +0 -214
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ConditionEditor.tsx +0 -533
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FetchOptionsEditor.tsx +0 -296
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FilterEditor.tsx +0 -106
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/LinkEntityEditor.tsx +0 -433
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/OrderEditor.tsx +0 -161
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ValueOfPicker.tsx +0 -194
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel.tsx +0 -225
- package/src/features/fetchxml/ui/LeftPane/TreeView.tsx +0 -604
- package/src/features/fetchxml/ui/RightPane/AddColumnsPanel.tsx +0 -800
- package/src/features/fetchxml/ui/RightPane/ColumnConfigDialog.tsx +0 -170
- package/src/features/fetchxml/ui/RightPane/DataGridCellRenderers.tsx +0 -356
- package/src/features/fetchxml/ui/RightPane/EditColumnsPanel.tsx +0 -372
- package/src/features/fetchxml/ui/RightPane/FetchXmlEditor.tsx +0 -399
- package/src/features/fetchxml/ui/RightPane/FormattedValueUtils.ts +0 -189
- package/src/features/fetchxml/ui/RightPane/LayoutXmlViewer.tsx +0 -171
- package/src/features/fetchxml/ui/RightPane/PreviewTabs.tsx +0 -471
- package/src/features/fetchxml/ui/RightPane/ResultsCommandBar.tsx +0 -430
- package/src/features/fetchxml/ui/RightPane/ResultsGrid.tsx +0 -983
- package/src/features/fetchxml/ui/RightPane/XmlTextArea.tsx +0 -65
- package/src/features/fetchxml/ui/Settings/SettingsDrawer.tsx +0 -175
- package/src/features/fetchxml/ui/Toolbar/EntitySelector.tsx +0 -788
- package/src/features/fetchxml/ui/Toolbar/LoadViewPicker.tsx +0 -274
- package/src/features/fetchxml/ui/Toolbar/SaveViewButton.tsx +0 -174
- package/src/main.tsx +0 -11
- package/src/shared/components/AttributePicker.tsx +0 -138
- package/src/shared/components/BetweenInputPicker.tsx +0 -120
- package/src/shared/components/BooleanValuePicker.tsx +0 -165
- package/src/shared/components/DateTimeInputPicker.tsx +0 -170
- package/src/shared/components/DynamicValueList.tsx +0 -128
- package/src/shared/components/ExcelIcon.tsx +0 -30
- package/src/shared/components/MultiValuePicker.tsx +0 -178
- package/src/shared/components/NumericInputPicker.tsx +0 -156
- package/src/shared/components/OptionSetValuePicker.tsx +0 -150
- package/src/shared/components/RelationshipPicker.tsx +0 -268
- package/src/shared/contexts/ThemeContext.tsx +0 -19
- package/src/shared/hooks/useAccessMode.ts +0 -77
- package/src/shared/hooks/useLazyMetadata.ts +0 -132
- package/src/shared/hooks/usePptbContext.ts +0 -170
- package/src/shared/hooks/usePublisherFilter.ts +0 -214
- package/src/shared/hooks/useSolutionFilter.ts +0 -162
- package/src/shared/utils/debug.ts +0 -87
- package/tsconfig.app.json +0 -28
- package/tsconfig.json +0 -7
- package/tsconfig.node.json +0 -26
- package/vite.config.ts +0 -7
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Input picker for operators that require MULTIPLE values (e.g., in, not-in, contain-values)
|
|
3
|
-
* Uses Fluent UI v9 TagPicker for multi-select picklists and DynamicValueList for other types
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { useState, useEffect } from "react";
|
|
7
|
-
import {
|
|
8
|
-
TagPicker,
|
|
9
|
-
TagPickerControl,
|
|
10
|
-
TagPickerGroup,
|
|
11
|
-
TagPickerInput,
|
|
12
|
-
TagPickerList,
|
|
13
|
-
TagPickerOption,
|
|
14
|
-
Tag,
|
|
15
|
-
makeStyles,
|
|
16
|
-
} from "@fluentui/react-components";
|
|
17
|
-
import type { TagPickerProps } from "@fluentui/react-components";
|
|
18
|
-
import { loadAttributeWithOptionSet } from "../../features/fetchxml/api/dataverseMetadata";
|
|
19
|
-
import type { AttributeMetadata } from "../../features/fetchxml/api/pptbClient";
|
|
20
|
-
import { DynamicValueList } from "./DynamicValueList";
|
|
21
|
-
|
|
22
|
-
const useStyles = makeStyles({
|
|
23
|
-
container: {
|
|
24
|
-
width: "100%",
|
|
25
|
-
},
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
interface MultiValuePickerProps {
|
|
29
|
-
entityLogicalName: string;
|
|
30
|
-
attributeLogicalName: string;
|
|
31
|
-
attribute: AttributeMetadata;
|
|
32
|
-
value?: (string | number)[]; // Array of values (mixed types supported from parser)
|
|
33
|
-
onChange: (value: (string | number)[] | undefined) => void;
|
|
34
|
-
placeholder?: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* MultiValuePicker component for multi-value operators like 'in', 'not-in', 'contain-values'
|
|
39
|
-
*/
|
|
40
|
-
export function MultiValuePicker({
|
|
41
|
-
entityLogicalName,
|
|
42
|
-
attributeLogicalName,
|
|
43
|
-
attribute,
|
|
44
|
-
value = [],
|
|
45
|
-
onChange,
|
|
46
|
-
placeholder = "Enter value",
|
|
47
|
-
}: MultiValuePickerProps) {
|
|
48
|
-
const styles = useStyles();
|
|
49
|
-
const [options, setOptions] = useState<Array<{ label: string; value: number }>>([]);
|
|
50
|
-
const [selectedValues, setSelectedValues] = useState<string[]>((value ?? []).map(String));
|
|
51
|
-
|
|
52
|
-
const isPicklist =
|
|
53
|
-
attribute.AttributeType === "Picklist" ||
|
|
54
|
-
attribute.AttributeType === "State" ||
|
|
55
|
-
attribute.AttributeType === "Status";
|
|
56
|
-
|
|
57
|
-
// Load picklist options when needed
|
|
58
|
-
useEffect(() => {
|
|
59
|
-
if (isPicklist) {
|
|
60
|
-
loadAttributeWithOptionSet(entityLogicalName, attributeLogicalName)
|
|
61
|
-
.then((metadata: AttributeMetadata) => {
|
|
62
|
-
if (metadata.OptionSet?.Options) {
|
|
63
|
-
const opts = metadata.OptionSet.Options.map((opt) => ({
|
|
64
|
-
label: opt.Label?.UserLocalizedLabel?.Label || String(opt.Value),
|
|
65
|
-
value: opt.Value,
|
|
66
|
-
}));
|
|
67
|
-
setOptions(opts);
|
|
68
|
-
}
|
|
69
|
-
})
|
|
70
|
-
.catch((error: unknown) => {
|
|
71
|
-
console.error("Failed to load OptionSet metadata:", error);
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
}, [entityLogicalName, attributeLogicalName, isPicklist]);
|
|
75
|
-
|
|
76
|
-
// Sync external value changes to internal state (for picklist only)
|
|
77
|
-
useEffect(() => {
|
|
78
|
-
if (!isPicklist) return;
|
|
79
|
-
|
|
80
|
-
const newSelectedValues = (value ?? []).map(String);
|
|
81
|
-
// Only update if the values actually changed (avoid infinite loop)
|
|
82
|
-
const hasChanged =
|
|
83
|
-
newSelectedValues.length !== selectedValues.length ||
|
|
84
|
-
newSelectedValues.some((v, i) => v !== selectedValues[i]);
|
|
85
|
-
|
|
86
|
-
if (hasChanged) {
|
|
87
|
-
setSelectedValues(newSelectedValues);
|
|
88
|
-
}
|
|
89
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
90
|
-
}, [value, isPicklist]); // selectedValues intentionally excluded to prevent infinite loop
|
|
91
|
-
|
|
92
|
-
const handleDynamicListChange = (values: string[]) => {
|
|
93
|
-
// Determine if we need to convert to numbers
|
|
94
|
-
const isNumeric =
|
|
95
|
-
attribute.AttributeType === "Integer" ||
|
|
96
|
-
attribute.AttributeType === "BigInt" ||
|
|
97
|
-
attribute.AttributeType === "Decimal" ||
|
|
98
|
-
attribute.AttributeType === "Double" ||
|
|
99
|
-
attribute.AttributeType === "Money";
|
|
100
|
-
|
|
101
|
-
if (isNumeric) {
|
|
102
|
-
const numbers = values.map((v) => parseFloat(v)).filter((n) => !isNaN(n));
|
|
103
|
-
onChange(numbers.length > 0 ? numbers : undefined);
|
|
104
|
-
} else {
|
|
105
|
-
onChange(values.length > 0 ? values : undefined);
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const handlePicklistChange: TagPickerProps["onOptionSelect"] = (_, data) => {
|
|
110
|
-
const selected = data.selectedOptions.map((opt) => parseInt(opt, 10)).filter((n) => !isNaN(n));
|
|
111
|
-
|
|
112
|
-
// Update local state immediately for controlled component behavior
|
|
113
|
-
setSelectedValues(data.selectedOptions);
|
|
114
|
-
|
|
115
|
-
// Notify parent of the change (parent will update value prop, but useEffect will detect no change)
|
|
116
|
-
onChange(selected.length > 0 ? selected : undefined);
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
if (isPicklist) {
|
|
120
|
-
const availableOptions = options.filter((opt) => !selectedValues.includes(String(opt.value)));
|
|
121
|
-
|
|
122
|
-
return (
|
|
123
|
-
<div className={styles.container}>
|
|
124
|
-
<TagPicker onOptionSelect={handlePicklistChange} selectedOptions={selectedValues}>
|
|
125
|
-
<TagPickerControl>
|
|
126
|
-
<TagPickerGroup aria-label="Selected values">
|
|
127
|
-
{selectedValues.map((val) => {
|
|
128
|
-
const opt = options.find((o) => String(o.value) === val);
|
|
129
|
-
return (
|
|
130
|
-
<Tag key={val} shape="rounded" value={val}>
|
|
131
|
-
{opt ? `${opt.label} - ${opt.value}` : val}
|
|
132
|
-
</Tag>
|
|
133
|
-
);
|
|
134
|
-
})}
|
|
135
|
-
</TagPickerGroup>
|
|
136
|
-
<TagPickerInput aria-label={placeholder} placeholder={placeholder} />
|
|
137
|
-
</TagPickerControl>
|
|
138
|
-
<TagPickerList>
|
|
139
|
-
{availableOptions.length > 0 ? (
|
|
140
|
-
availableOptions.map((opt) => (
|
|
141
|
-
<TagPickerOption
|
|
142
|
-
key={opt.value}
|
|
143
|
-
value={String(opt.value)}
|
|
144
|
-
text={`${opt.label} - ${opt.value}`}
|
|
145
|
-
>
|
|
146
|
-
{opt.label} - {opt.value}
|
|
147
|
-
</TagPickerOption>
|
|
148
|
-
))
|
|
149
|
-
) : (
|
|
150
|
-
<TagPickerOption value="no-options" text="No options available">
|
|
151
|
-
No options available
|
|
152
|
-
</TagPickerOption>
|
|
153
|
-
)}
|
|
154
|
-
</TagPickerList>
|
|
155
|
-
</TagPicker>
|
|
156
|
-
</div>
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// For non-picklist attributes, use dynamic value list with add/remove buttons
|
|
161
|
-
const isNumeric =
|
|
162
|
-
attribute.AttributeType === "Integer" ||
|
|
163
|
-
attribute.AttributeType === "BigInt" ||
|
|
164
|
-
attribute.AttributeType === "Decimal" ||
|
|
165
|
-
attribute.AttributeType === "Double" ||
|
|
166
|
-
attribute.AttributeType === "Money";
|
|
167
|
-
|
|
168
|
-
return (
|
|
169
|
-
<div className={styles.container}>
|
|
170
|
-
<DynamicValueList
|
|
171
|
-
values={(value ?? []).map(String)}
|
|
172
|
-
onChange={handleDynamicListChange}
|
|
173
|
-
placeholder={placeholder}
|
|
174
|
-
inputType={isNumeric ? "number" : "text"}
|
|
175
|
-
/>
|
|
176
|
-
</div>
|
|
177
|
-
);
|
|
178
|
-
}
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared Numeric Input Picker component using Fluent UI v9 Input
|
|
3
|
-
* Handles Integer, BigInt, Decimal, and Double attribute types
|
|
4
|
-
* Sets min/max values and precision based on metadata
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { useState, useEffect } from "react";
|
|
8
|
-
import { Input, useId } from "@fluentui/react-components";
|
|
9
|
-
import type { AttributeMetadata } from "../../features/fetchxml/api/pptbClient";
|
|
10
|
-
|
|
11
|
-
interface NumericInputPickerProps {
|
|
12
|
-
attribute: AttributeMetadata;
|
|
13
|
-
value?: number | string;
|
|
14
|
-
onChange: (value: number | undefined) => void;
|
|
15
|
-
placeholder?: string;
|
|
16
|
-
disabled?: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function NumericInputPicker({
|
|
20
|
-
attribute,
|
|
21
|
-
value,
|
|
22
|
-
onChange,
|
|
23
|
-
placeholder = "Enter a number",
|
|
24
|
-
disabled = false,
|
|
25
|
-
}: NumericInputPickerProps) {
|
|
26
|
-
const inputId = useId("numeric-input");
|
|
27
|
-
const [inputValue, setInputValue] = useState<string>("");
|
|
28
|
-
|
|
29
|
-
// Format number to avoid scientific notation
|
|
30
|
-
const formatNumber = (num: number): string => {
|
|
31
|
-
if (Math.abs(num) < 1e-10 && num !== 0) {
|
|
32
|
-
// Very small numbers - use high precision
|
|
33
|
-
return num.toFixed(20).replace(/\.?0+$/, "");
|
|
34
|
-
} else if (Number.isInteger(num)) {
|
|
35
|
-
// Integer - no decimal point needed
|
|
36
|
-
return num.toString();
|
|
37
|
-
} else {
|
|
38
|
-
// Decimal - preserve precision but remove trailing zeros
|
|
39
|
-
return num.toFixed(10).replace(/\.?0+$/, "");
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// Sync input value with prop value
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
if (value !== undefined && value !== null && value !== "") {
|
|
46
|
-
const numValue = typeof value === "number" ? value : parseFloat(String(value));
|
|
47
|
-
setInputValue(isNaN(numValue) ? String(value) : formatNumber(numValue));
|
|
48
|
-
} else {
|
|
49
|
-
setInputValue("");
|
|
50
|
-
}
|
|
51
|
-
}, [value]);
|
|
52
|
-
|
|
53
|
-
// Determine input step based on attribute type and precision
|
|
54
|
-
const getStep = (): number | undefined => {
|
|
55
|
-
const attrType = attribute.AttributeType;
|
|
56
|
-
|
|
57
|
-
// Integer and BigInt: whole numbers only
|
|
58
|
-
if (attrType === "Integer" || attrType === "BigInt") {
|
|
59
|
-
return 1;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Decimal and Double: use precision if available
|
|
63
|
-
if ((attrType === "Decimal" || attrType === "Double") && attribute.Precision !== undefined) {
|
|
64
|
-
// step = 10^(-precision), e.g., precision 2 → step 0.01
|
|
65
|
-
return Math.pow(10, -attribute.Precision);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return undefined;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
// Round value to the attribute's precision
|
|
72
|
-
const roundToPrecision = (val: number): number => {
|
|
73
|
-
const attrType = attribute.AttributeType;
|
|
74
|
-
|
|
75
|
-
// Integer and BigInt: round to whole number
|
|
76
|
-
if (attrType === "Integer" || attrType === "BigInt") {
|
|
77
|
-
return Math.round(val);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Decimal and Double: round to precision
|
|
81
|
-
if ((attrType === "Decimal" || attrType === "Double") && attribute.Precision !== undefined) {
|
|
82
|
-
const multiplier = Math.pow(10, attribute.Precision);
|
|
83
|
-
return Math.round(val * multiplier) / multiplier;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return val;
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
// Handle input change
|
|
90
|
-
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (ev) => {
|
|
91
|
-
const newValue = ev.target.value;
|
|
92
|
-
setInputValue(newValue);
|
|
93
|
-
|
|
94
|
-
// Parse and validate
|
|
95
|
-
if (newValue === "") {
|
|
96
|
-
onChange(undefined);
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const parsed = parseFloat(newValue);
|
|
101
|
-
if (!isNaN(parsed)) {
|
|
102
|
-
// Round to precision to avoid floating-point errors
|
|
103
|
-
const rounded = roundToPrecision(parsed);
|
|
104
|
-
onChange(rounded);
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
// Handle blur to validate min/max
|
|
109
|
-
const handleBlur = () => {
|
|
110
|
-
if (inputValue === "") {
|
|
111
|
-
onChange(undefined);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const parsed = parseFloat(inputValue);
|
|
116
|
-
if (isNaN(parsed)) {
|
|
117
|
-
// Invalid number, clear
|
|
118
|
-
setInputValue("");
|
|
119
|
-
onChange(undefined);
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Round to precision first to avoid floating-point errors
|
|
124
|
-
let constrainedValue = roundToPrecision(parsed);
|
|
125
|
-
|
|
126
|
-
// Enforce min/max constraints
|
|
127
|
-
if (attribute.MinValue !== undefined && constrainedValue < attribute.MinValue) {
|
|
128
|
-
constrainedValue = attribute.MinValue;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (attribute.MaxValue !== undefined && constrainedValue > attribute.MaxValue) {
|
|
132
|
-
constrainedValue = attribute.MaxValue;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Update if constrained or rounded
|
|
136
|
-
if (constrainedValue !== parsed) {
|
|
137
|
-
setInputValue(String(constrainedValue));
|
|
138
|
-
onChange(constrainedValue);
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
return (
|
|
143
|
-
<Input
|
|
144
|
-
id={inputId}
|
|
145
|
-
type="number"
|
|
146
|
-
value={inputValue}
|
|
147
|
-
onChange={handleChange}
|
|
148
|
-
onBlur={handleBlur}
|
|
149
|
-
placeholder={placeholder}
|
|
150
|
-
disabled={disabled}
|
|
151
|
-
min={attribute.MinValue}
|
|
152
|
-
max={attribute.MaxValue}
|
|
153
|
-
step={getStep()}
|
|
154
|
-
/>
|
|
155
|
-
);
|
|
156
|
-
}
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared OptionSet Value Picker component using Fluent UI v9 Combobox
|
|
3
|
-
* Loads option values from attribute metadata and displays as searchable dropdown
|
|
4
|
-
* Supports Picklist, State, and Status attribute types
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { useState, useEffect } from "react";
|
|
8
|
-
import { Combobox, useComboboxFilter, useId } from "@fluentui/react-components";
|
|
9
|
-
import { loadAttributeWithOptionSet } from "../../features/fetchxml/api/dataverseMetadata";
|
|
10
|
-
import type { AttributeMetadata } from "../../features/fetchxml/api/pptbClient";
|
|
11
|
-
|
|
12
|
-
interface OptionSetOption {
|
|
13
|
-
value: number;
|
|
14
|
-
label: string;
|
|
15
|
-
displayText: string; // "Label - Value" format
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface OptionSetValuePickerProps {
|
|
19
|
-
entityLogicalName: string;
|
|
20
|
-
attributeLogicalName: string;
|
|
21
|
-
value?: number | string; // Can be number or string representation
|
|
22
|
-
onChange: (value: number | undefined) => void;
|
|
23
|
-
placeholder?: string;
|
|
24
|
-
disabled?: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function OptionSetValuePicker({
|
|
28
|
-
entityLogicalName,
|
|
29
|
-
attributeLogicalName,
|
|
30
|
-
value,
|
|
31
|
-
onChange,
|
|
32
|
-
placeholder = "Select a value",
|
|
33
|
-
disabled = false,
|
|
34
|
-
}: OptionSetValuePickerProps) {
|
|
35
|
-
const comboId = useId("optionset-combobox");
|
|
36
|
-
const [options, setOptions] = useState<OptionSetOption[]>([]);
|
|
37
|
-
const [loading, setLoading] = useState(false);
|
|
38
|
-
const [error, setError] = useState<string | null>(null);
|
|
39
|
-
const [query, setQuery] = useState<string>("");
|
|
40
|
-
|
|
41
|
-
// Load option set values when entity/attribute changes
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
if (!entityLogicalName || !attributeLogicalName) {
|
|
44
|
-
setOptions([]);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
setLoading(true);
|
|
49
|
-
setError(null);
|
|
50
|
-
|
|
51
|
-
loadAttributeWithOptionSet(entityLogicalName, attributeLogicalName)
|
|
52
|
-
.then((attr: AttributeMetadata) => {
|
|
53
|
-
if (!attr.OptionSet?.Options) {
|
|
54
|
-
setError("No options found for this attribute");
|
|
55
|
-
setOptions([]);
|
|
56
|
-
setLoading(false);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Convert metadata options to our format
|
|
61
|
-
const optionsList: OptionSetOption[] = attr.OptionSet.Options.map((opt) => {
|
|
62
|
-
const label = opt.Label?.UserLocalizedLabel?.Label || `Option ${opt.Value}`;
|
|
63
|
-
return {
|
|
64
|
-
value: opt.Value,
|
|
65
|
-
label,
|
|
66
|
-
displayText: `${label} - ${opt.Value}`,
|
|
67
|
-
};
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
setOptions(optionsList);
|
|
71
|
-
setLoading(false);
|
|
72
|
-
})
|
|
73
|
-
.catch((err: unknown) => {
|
|
74
|
-
console.error("Failed to load option set values:", err);
|
|
75
|
-
setError("Failed to load options");
|
|
76
|
-
setLoading(false);
|
|
77
|
-
});
|
|
78
|
-
}, [entityLogicalName, attributeLogicalName]);
|
|
79
|
-
|
|
80
|
-
// Sync query with value when value changes or options load
|
|
81
|
-
useEffect(() => {
|
|
82
|
-
if (value !== undefined && value !== null && value !== "") {
|
|
83
|
-
const numValue = typeof value === "string" ? parseInt(value, 10) : value;
|
|
84
|
-
const opt = options.find((o) => o.value === numValue);
|
|
85
|
-
if (opt) {
|
|
86
|
-
setQuery(opt.displayText);
|
|
87
|
-
} else {
|
|
88
|
-
setQuery(String(value));
|
|
89
|
-
}
|
|
90
|
-
} else {
|
|
91
|
-
setQuery("");
|
|
92
|
-
}
|
|
93
|
-
}, [value, options]);
|
|
94
|
-
|
|
95
|
-
// Convert options to combobox format and filter by both label and value
|
|
96
|
-
const comboboxOptions = options
|
|
97
|
-
.filter((opt) => {
|
|
98
|
-
if (!query) return true;
|
|
99
|
-
const searchLower = query.toLowerCase();
|
|
100
|
-
return (
|
|
101
|
-
opt.label.toLowerCase().includes(searchLower) || String(opt.value).includes(searchLower)
|
|
102
|
-
);
|
|
103
|
-
})
|
|
104
|
-
.map((opt) => ({
|
|
105
|
-
children: opt.displayText,
|
|
106
|
-
value: String(opt.value), // Combobox uses string values
|
|
107
|
-
}));
|
|
108
|
-
|
|
109
|
-
// Apply Fluent UI's visual filtering only (we've already filtered the data)
|
|
110
|
-
const filteredChildren = useComboboxFilter(query, comboboxOptions, {
|
|
111
|
-
noOptionsMessage: loading ? "Loading options..." : error || "No options found",
|
|
112
|
-
filter: () => true, // Always return true since we pre-filtered
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (ev) => {
|
|
116
|
-
setQuery(ev.target.value);
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
const handleOptionSelect: NonNullable<React.ComponentProps<typeof Combobox>["onOptionSelect"]> = (
|
|
120
|
-
_ev,
|
|
121
|
-
data
|
|
122
|
-
) => {
|
|
123
|
-
if (!data.optionValue) {
|
|
124
|
-
// Clear button clicked
|
|
125
|
-
setQuery("");
|
|
126
|
-
onChange(undefined);
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const selectedOption = options.find((opt) => String(opt.value) === data.optionValue);
|
|
131
|
-
if (selectedOption) {
|
|
132
|
-
setQuery(selectedOption.displayText);
|
|
133
|
-
onChange(selectedOption.value);
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
return (
|
|
138
|
-
<Combobox
|
|
139
|
-
aria-labelledby={comboId}
|
|
140
|
-
placeholder={placeholder}
|
|
141
|
-
value={query}
|
|
142
|
-
onChange={handleChange}
|
|
143
|
-
onOptionSelect={handleOptionSelect}
|
|
144
|
-
disabled={disabled || loading}
|
|
145
|
-
clearable
|
|
146
|
-
>
|
|
147
|
-
{filteredChildren}
|
|
148
|
-
</Combobox>
|
|
149
|
-
);
|
|
150
|
-
}
|