@mohsinonxrm/pptb-fetchxml-studio 1.0.0 → 1.0.2
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/README.md +1 -1
- 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,120 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Input picker for operators that require TWO values (e.g., between, not-between, in-fiscal-period-and-year)
|
|
3
|
-
* Displays two input fields side-by-side with an "and" label between them
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { Label, makeStyles, tokens } from "@fluentui/react-components";
|
|
7
|
-
import { NumericInputPicker } from "./NumericInputPicker";
|
|
8
|
-
import { DateTimeInputPicker } from "./DateTimeInputPicker";
|
|
9
|
-
import type { AttributeMetadata } from "../../features/fetchxml/api/pptbClient";
|
|
10
|
-
|
|
11
|
-
const useStyles = makeStyles({
|
|
12
|
-
container: {
|
|
13
|
-
display: "flex",
|
|
14
|
-
alignItems: "center",
|
|
15
|
-
gap: tokens.spacingHorizontalM,
|
|
16
|
-
width: "100%",
|
|
17
|
-
},
|
|
18
|
-
inputWrapper: {
|
|
19
|
-
flex: 1,
|
|
20
|
-
minWidth: "120px",
|
|
21
|
-
},
|
|
22
|
-
separator: {
|
|
23
|
-
color: tokens.colorNeutralForeground3,
|
|
24
|
-
fontWeight: tokens.fontWeightSemibold,
|
|
25
|
-
paddingInline: tokens.spacingHorizontalXS,
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
interface BetweenInputPickerProps {
|
|
30
|
-
attribute: AttributeMetadata;
|
|
31
|
-
value?: [number | string | undefined, number | string | undefined]; // Tuple [value1, value2]
|
|
32
|
-
onChange: (value: [number | string | undefined, number | string | undefined]) => void;
|
|
33
|
-
placeholder1?: string;
|
|
34
|
-
placeholder2?: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* BetweenInputPicker component for dual-value operators like 'between', 'not-between'
|
|
39
|
-
*/
|
|
40
|
-
export function BetweenInputPicker({
|
|
41
|
-
attribute,
|
|
42
|
-
value = [undefined, undefined],
|
|
43
|
-
onChange,
|
|
44
|
-
placeholder1 = "Start value",
|
|
45
|
-
placeholder2 = "End value",
|
|
46
|
-
}: BetweenInputPickerProps) {
|
|
47
|
-
const styles = useStyles();
|
|
48
|
-
|
|
49
|
-
const [value1, value2] = value;
|
|
50
|
-
|
|
51
|
-
const handleValue1Change = (newValue: number | string | Date | undefined) => {
|
|
52
|
-
const val1 =
|
|
53
|
-
newValue instanceof Date
|
|
54
|
-
? newValue.toISOString()
|
|
55
|
-
: typeof newValue === "number" || typeof newValue === "string"
|
|
56
|
-
? newValue
|
|
57
|
-
: undefined;
|
|
58
|
-
onChange([val1, value2]);
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const handleValue2Change = (newValue: number | string | Date | undefined) => {
|
|
62
|
-
const val2 =
|
|
63
|
-
newValue instanceof Date
|
|
64
|
-
? newValue.toISOString()
|
|
65
|
-
: typeof newValue === "number" || typeof newValue === "string"
|
|
66
|
-
? newValue
|
|
67
|
-
: undefined;
|
|
68
|
-
onChange([value1, val2]);
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const isNumeric =
|
|
72
|
-
attribute.AttributeType === "Integer" ||
|
|
73
|
-
attribute.AttributeType === "BigInt" ||
|
|
74
|
-
attribute.AttributeType === "Decimal" ||
|
|
75
|
-
attribute.AttributeType === "Double";
|
|
76
|
-
|
|
77
|
-
const isDateTime = attribute.AttributeType === "DateTime";
|
|
78
|
-
|
|
79
|
-
return (
|
|
80
|
-
<div className={styles.container}>
|
|
81
|
-
<div className={styles.inputWrapper}>
|
|
82
|
-
{isNumeric ? (
|
|
83
|
-
<NumericInputPicker
|
|
84
|
-
attribute={attribute}
|
|
85
|
-
value={typeof value1 === "number" ? value1 : undefined}
|
|
86
|
-
onChange={handleValue1Change}
|
|
87
|
-
placeholder={placeholder1}
|
|
88
|
-
/>
|
|
89
|
-
) : isDateTime ? (
|
|
90
|
-
<DateTimeInputPicker
|
|
91
|
-
attribute={attribute}
|
|
92
|
-
value={typeof value1 === "string" ? value1 : undefined}
|
|
93
|
-
onChange={handleValue1Change}
|
|
94
|
-
placeholder={placeholder1}
|
|
95
|
-
/>
|
|
96
|
-
) : null}
|
|
97
|
-
</div>
|
|
98
|
-
|
|
99
|
-
<Label className={styles.separator}>and</Label>
|
|
100
|
-
|
|
101
|
-
<div className={styles.inputWrapper}>
|
|
102
|
-
{isNumeric ? (
|
|
103
|
-
<NumericInputPicker
|
|
104
|
-
attribute={attribute}
|
|
105
|
-
value={typeof value2 === "number" ? value2 : undefined}
|
|
106
|
-
onChange={handleValue2Change}
|
|
107
|
-
placeholder={placeholder2}
|
|
108
|
-
/>
|
|
109
|
-
) : isDateTime ? (
|
|
110
|
-
<DateTimeInputPicker
|
|
111
|
-
attribute={attribute}
|
|
112
|
-
value={typeof value2 === "string" ? value2 : undefined}
|
|
113
|
-
onChange={handleValue2Change}
|
|
114
|
-
placeholder={placeholder2}
|
|
115
|
-
/>
|
|
116
|
-
) : null}
|
|
117
|
-
</div>
|
|
118
|
-
</div>
|
|
119
|
-
);
|
|
120
|
-
}
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared Boolean Value Picker component using Fluent UI v9 Combobox
|
|
3
|
-
* Loads true/false labels from TwoOptions attribute metadata
|
|
4
|
-
* Displays custom labels like "Yes - true" / "No - false" or "On - true" / "Off - false"
|
|
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 BooleanOption {
|
|
13
|
-
value: boolean;
|
|
14
|
-
label: string;
|
|
15
|
-
displayText: string; // "Label - value" format
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface BooleanValuePickerProps {
|
|
19
|
-
entityLogicalName: string;
|
|
20
|
-
attributeLogicalName: string;
|
|
21
|
-
value?: boolean | string; // Can be boolean, "true"/"false", or "1"/"0"
|
|
22
|
-
onChange: (value: boolean | undefined) => void;
|
|
23
|
-
placeholder?: string;
|
|
24
|
-
disabled?: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function BooleanValuePicker({
|
|
28
|
-
entityLogicalName,
|
|
29
|
-
attributeLogicalName,
|
|
30
|
-
value,
|
|
31
|
-
onChange,
|
|
32
|
-
placeholder = "Select true or false",
|
|
33
|
-
disabled = false,
|
|
34
|
-
}: BooleanValuePickerProps) {
|
|
35
|
-
const comboId = useId("boolean-combobox");
|
|
36
|
-
const [options, setOptions] = useState<BooleanOption[]>([]);
|
|
37
|
-
const [loading, setLoading] = useState(false);
|
|
38
|
-
const [error, setError] = useState<string | null>(null);
|
|
39
|
-
const [query, setQuery] = useState<string>("");
|
|
40
|
-
|
|
41
|
-
// Load boolean labels from attribute metadata
|
|
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) {
|
|
54
|
-
// Fallback to default labels if metadata doesn't have OptionSet
|
|
55
|
-
console.warn("No OptionSet found for boolean attribute, using defaults");
|
|
56
|
-
setOptions([
|
|
57
|
-
{ value: true, label: "Yes", displayText: "Yes - true" },
|
|
58
|
-
{ value: false, label: "No", displayText: "No - false" },
|
|
59
|
-
]);
|
|
60
|
-
setLoading(false);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// TwoOptions attributes have TrueOption and FalseOption
|
|
65
|
-
const trueLabel = attr.OptionSet.TrueOption?.Label?.UserLocalizedLabel?.Label || "Yes";
|
|
66
|
-
const falseLabel = attr.OptionSet.FalseOption?.Label?.UserLocalizedLabel?.Label || "No";
|
|
67
|
-
|
|
68
|
-
setOptions([
|
|
69
|
-
{ value: true, label: trueLabel, displayText: `${trueLabel} - true` },
|
|
70
|
-
{ value: false, label: falseLabel, displayText: `${falseLabel} - false` },
|
|
71
|
-
]);
|
|
72
|
-
setLoading(false);
|
|
73
|
-
})
|
|
74
|
-
.catch((err: unknown) => {
|
|
75
|
-
console.error("Failed to load boolean attribute metadata:", err);
|
|
76
|
-
// Fallback to defaults on error
|
|
77
|
-
setOptions([
|
|
78
|
-
{ value: true, label: "Yes", displayText: "Yes - true" },
|
|
79
|
-
{ value: false, label: "No", displayText: "No - false" },
|
|
80
|
-
]);
|
|
81
|
-
setLoading(false);
|
|
82
|
-
});
|
|
83
|
-
}, [entityLogicalName, attributeLogicalName]);
|
|
84
|
-
|
|
85
|
-
// Sync query with value when value changes or options load
|
|
86
|
-
useEffect(() => {
|
|
87
|
-
if (value !== undefined && value !== null && value !== "") {
|
|
88
|
-
// Normalize value to boolean
|
|
89
|
-
let boolValue: boolean;
|
|
90
|
-
if (typeof value === "boolean") {
|
|
91
|
-
boolValue = value;
|
|
92
|
-
} else if (typeof value === "string") {
|
|
93
|
-
boolValue = value === "true" || value === "1";
|
|
94
|
-
} else {
|
|
95
|
-
boolValue = Boolean(value);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const opt = options.find((o) => o.value === boolValue);
|
|
99
|
-
if (opt) {
|
|
100
|
-
setQuery(opt.displayText);
|
|
101
|
-
} else {
|
|
102
|
-
setQuery(String(value));
|
|
103
|
-
}
|
|
104
|
-
} else {
|
|
105
|
-
setQuery("");
|
|
106
|
-
}
|
|
107
|
-
}, [value, options]);
|
|
108
|
-
|
|
109
|
-
// Convert options to combobox format and filter by both label and boolean value
|
|
110
|
-
const comboboxOptions = options
|
|
111
|
-
.filter((opt) => {
|
|
112
|
-
if (!query) return true;
|
|
113
|
-
const searchLower = query.toLowerCase();
|
|
114
|
-
return (
|
|
115
|
-
opt.label.toLowerCase().includes(searchLower) ||
|
|
116
|
-
String(opt.value).toLowerCase().includes(searchLower)
|
|
117
|
-
);
|
|
118
|
-
})
|
|
119
|
-
.map((opt) => ({
|
|
120
|
-
children: opt.displayText,
|
|
121
|
-
value: String(opt.value), // Combobox uses string values
|
|
122
|
-
}));
|
|
123
|
-
|
|
124
|
-
// Apply Fluent UI's visual filtering only (we've already filtered the data)
|
|
125
|
-
const filteredChildren = useComboboxFilter(query, comboboxOptions, {
|
|
126
|
-
noOptionsMessage: loading ? "Loading..." : error || "No options",
|
|
127
|
-
filter: () => true, // Always return true since we pre-filtered
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (ev) => {
|
|
131
|
-
setQuery(ev.target.value);
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const handleOptionSelect: NonNullable<React.ComponentProps<typeof Combobox>["onOptionSelect"]> = (
|
|
135
|
-
_ev,
|
|
136
|
-
data
|
|
137
|
-
) => {
|
|
138
|
-
if (!data.optionValue) {
|
|
139
|
-
// Clear button clicked
|
|
140
|
-
setQuery("");
|
|
141
|
-
onChange(undefined);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const selectedOption = options.find((opt) => String(opt.value) === data.optionValue);
|
|
146
|
-
if (selectedOption) {
|
|
147
|
-
setQuery(selectedOption.displayText);
|
|
148
|
-
onChange(selectedOption.value);
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
return (
|
|
153
|
-
<Combobox
|
|
154
|
-
aria-labelledby={comboId}
|
|
155
|
-
placeholder={placeholder}
|
|
156
|
-
value={query}
|
|
157
|
-
onChange={handleChange}
|
|
158
|
-
onOptionSelect={handleOptionSelect}
|
|
159
|
-
disabled={disabled || loading}
|
|
160
|
-
clearable
|
|
161
|
-
>
|
|
162
|
-
{filteredChildren}
|
|
163
|
-
</Combobox>
|
|
164
|
-
);
|
|
165
|
-
}
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared DateTime Input Picker component using Fluent UI v9 DatePicker and TimePicker
|
|
3
|
-
* Handles DateTime attribute types
|
|
4
|
-
* Renders DatePicker for DateOnly format, DatePicker + TimePicker for DateAndTime format
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { useState, useEffect } from "react";
|
|
8
|
-
import { makeStyles } from "@fluentui/react-components";
|
|
9
|
-
import { DatePicker } from "@fluentui/react-datepicker-compat";
|
|
10
|
-
import { TimePicker, formatDateToTimeString } from "@fluentui/react-timepicker-compat";
|
|
11
|
-
import type { AttributeMetadata } from "../../features/fetchxml/api/pptbClient";
|
|
12
|
-
|
|
13
|
-
const useStyles = makeStyles({
|
|
14
|
-
root: {
|
|
15
|
-
display: "flex",
|
|
16
|
-
flexDirection: "row",
|
|
17
|
-
gap: "8px",
|
|
18
|
-
},
|
|
19
|
-
datePicker: {
|
|
20
|
-
flexGrow: 1,
|
|
21
|
-
},
|
|
22
|
-
timePicker: {
|
|
23
|
-
flexGrow: 1,
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
interface DateTimeInputPickerProps {
|
|
28
|
-
attribute: AttributeMetadata;
|
|
29
|
-
value?: Date | string;
|
|
30
|
-
onChange: (value: Date | undefined) => void;
|
|
31
|
-
placeholder?: string;
|
|
32
|
-
disabled?: boolean;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function DateTimeInputPicker({
|
|
36
|
-
attribute,
|
|
37
|
-
value,
|
|
38
|
-
onChange,
|
|
39
|
-
placeholder = "Select a date",
|
|
40
|
-
disabled = false,
|
|
41
|
-
}: DateTimeInputPickerProps) {
|
|
42
|
-
const styles = useStyles();
|
|
43
|
-
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
|
|
44
|
-
const [selectedTime, setSelectedTime] = useState<Date | null>(null);
|
|
45
|
-
const [timePickerValue, setTimePickerValue] = useState<string>("");
|
|
46
|
-
|
|
47
|
-
const isDateOnly = attribute.Format === "DateOnly";
|
|
48
|
-
|
|
49
|
-
// Sync selected date/time with prop value
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
if (value) {
|
|
52
|
-
const dateValue = typeof value === "string" ? new Date(value) : value;
|
|
53
|
-
if (!isNaN(dateValue.getTime())) {
|
|
54
|
-
setSelectedDate(dateValue);
|
|
55
|
-
if (!isDateOnly) {
|
|
56
|
-
setSelectedTime(dateValue);
|
|
57
|
-
setTimePickerValue(formatDateToTimeString(dateValue));
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
} else {
|
|
61
|
-
setSelectedDate(null);
|
|
62
|
-
setSelectedTime(null);
|
|
63
|
-
setTimePickerValue("");
|
|
64
|
-
}
|
|
65
|
-
}, [value, isDateOnly]);
|
|
66
|
-
|
|
67
|
-
// Parse min/max dates
|
|
68
|
-
const minDate = attribute.MinSupportedValue ? new Date(attribute.MinSupportedValue) : undefined;
|
|
69
|
-
const maxDate = attribute.MaxSupportedValue ? new Date(attribute.MaxSupportedValue) : undefined;
|
|
70
|
-
|
|
71
|
-
// Handle date selection
|
|
72
|
-
const handleDateChange = (date: Date | null | undefined) => {
|
|
73
|
-
setSelectedDate(date ?? null);
|
|
74
|
-
|
|
75
|
-
if (!date) {
|
|
76
|
-
onChange(undefined);
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (isDateOnly) {
|
|
81
|
-
// For DateOnly, just use the date at midnight UTC
|
|
82
|
-
const dateOnly = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
|
83
|
-
onChange(dateOnly);
|
|
84
|
-
} else {
|
|
85
|
-
// For DateAndTime, combine with selected time or use midnight
|
|
86
|
-
if (selectedTime) {
|
|
87
|
-
const combined = new Date(
|
|
88
|
-
date.getFullYear(),
|
|
89
|
-
date.getMonth(),
|
|
90
|
-
date.getDate(),
|
|
91
|
-
selectedTime.getHours(),
|
|
92
|
-
selectedTime.getMinutes(),
|
|
93
|
-
selectedTime.getSeconds()
|
|
94
|
-
);
|
|
95
|
-
onChange(combined);
|
|
96
|
-
} else {
|
|
97
|
-
onChange(date);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
// Handle time selection (only for DateAndTime format)
|
|
103
|
-
const handleTimeChange = (
|
|
104
|
-
_ev: unknown,
|
|
105
|
-
data: { selectedTime: Date | null; selectedTimeText?: string }
|
|
106
|
-
) => {
|
|
107
|
-
setSelectedTime(data.selectedTime);
|
|
108
|
-
setTimePickerValue(data.selectedTimeText ?? "");
|
|
109
|
-
|
|
110
|
-
if (selectedDate && data.selectedTime) {
|
|
111
|
-
const combined = new Date(
|
|
112
|
-
selectedDate.getFullYear(),
|
|
113
|
-
selectedDate.getMonth(),
|
|
114
|
-
selectedDate.getDate(),
|
|
115
|
-
data.selectedTime.getHours(),
|
|
116
|
-
data.selectedTime.getMinutes(),
|
|
117
|
-
data.selectedTime.getSeconds()
|
|
118
|
-
);
|
|
119
|
-
onChange(combined);
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
// Handle time input (freeform)
|
|
124
|
-
const handleTimeInput = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
|
125
|
-
setTimePickerValue(ev.target.value);
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
if (isDateOnly) {
|
|
129
|
-
// DateOnly: just show DatePicker
|
|
130
|
-
return (
|
|
131
|
-
<DatePicker
|
|
132
|
-
value={selectedDate}
|
|
133
|
-
onSelectDate={handleDateChange}
|
|
134
|
-
placeholder={placeholder}
|
|
135
|
-
disabled={disabled}
|
|
136
|
-
minDate={minDate}
|
|
137
|
-
maxDate={maxDate}
|
|
138
|
-
allowTextInput
|
|
139
|
-
className={styles.datePicker}
|
|
140
|
-
/>
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// DateAndTime: show DatePicker + TimePicker
|
|
145
|
-
return (
|
|
146
|
-
<div className={styles.root}>
|
|
147
|
-
<DatePicker
|
|
148
|
-
value={selectedDate}
|
|
149
|
-
onSelectDate={handleDateChange}
|
|
150
|
-
placeholder="Select date"
|
|
151
|
-
disabled={disabled}
|
|
152
|
-
minDate={minDate}
|
|
153
|
-
maxDate={maxDate}
|
|
154
|
-
allowTextInput
|
|
155
|
-
className={styles.datePicker}
|
|
156
|
-
/>
|
|
157
|
-
<TimePicker
|
|
158
|
-
placeholder="Select time"
|
|
159
|
-
freeform
|
|
160
|
-
dateAnchor={selectedDate ?? undefined}
|
|
161
|
-
selectedTime={selectedTime}
|
|
162
|
-
onTimeChange={handleTimeChange}
|
|
163
|
-
value={timePickerValue}
|
|
164
|
-
onInput={handleTimeInput}
|
|
165
|
-
disabled={disabled || !selectedDate}
|
|
166
|
-
className={styles.timePicker}
|
|
167
|
-
/>
|
|
168
|
-
</div>
|
|
169
|
-
);
|
|
170
|
-
}
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dynamic list of input fields with add/remove buttons for multi-value operators
|
|
3
|
-
* Each value gets its own input field to handle values with commas, spaces, etc.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { useState, useEffect } from "react";
|
|
7
|
-
import { Input, Button, makeStyles, tokens } from "@fluentui/react-components";
|
|
8
|
-
import { Add20Regular, Dismiss20Regular } from "@fluentui/react-icons";
|
|
9
|
-
|
|
10
|
-
const useStyles = makeStyles({
|
|
11
|
-
container: {
|
|
12
|
-
display: "flex",
|
|
13
|
-
flexDirection: "column",
|
|
14
|
-
gap: tokens.spacingVerticalS,
|
|
15
|
-
width: "100%",
|
|
16
|
-
},
|
|
17
|
-
row: {
|
|
18
|
-
display: "flex",
|
|
19
|
-
alignItems: "center",
|
|
20
|
-
gap: tokens.spacingHorizontalS,
|
|
21
|
-
},
|
|
22
|
-
input: {
|
|
23
|
-
flexGrow: 1,
|
|
24
|
-
},
|
|
25
|
-
addButton: {
|
|
26
|
-
marginTop: tokens.spacingVerticalXS,
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
interface DynamicValueListProps {
|
|
31
|
-
values: string[];
|
|
32
|
-
onChange: (values: string[]) => void;
|
|
33
|
-
placeholder?: string;
|
|
34
|
-
inputType?: "text" | "number";
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Component that displays a dynamic list of input fields
|
|
39
|
-
* User can add/remove fields as needed
|
|
40
|
-
*/
|
|
41
|
-
export function DynamicValueList({
|
|
42
|
-
values,
|
|
43
|
-
onChange,
|
|
44
|
-
placeholder = "Enter value",
|
|
45
|
-
inputType = "text",
|
|
46
|
-
}: DynamicValueListProps) {
|
|
47
|
-
const styles = useStyles();
|
|
48
|
-
const [internalValues, setInternalValues] = useState<string[]>(values.length > 0 ? values : [""]);
|
|
49
|
-
|
|
50
|
-
// Sync external changes
|
|
51
|
-
useEffect(() => {
|
|
52
|
-
if (values.length === 0 && internalValues.length === 1 && internalValues[0] === "") {
|
|
53
|
-
// Don't update if external is empty and we just have one empty field
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Only update if values actually changed
|
|
58
|
-
const currentNonEmpty = internalValues.filter((v) => v.trim() !== "");
|
|
59
|
-
const valuesChanged =
|
|
60
|
-
values.length !== currentNonEmpty.length || values.some((v, i) => v !== currentNonEmpty[i]);
|
|
61
|
-
|
|
62
|
-
if (valuesChanged) {
|
|
63
|
-
setInternalValues(values.length > 0 ? values : [""]);
|
|
64
|
-
}
|
|
65
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
66
|
-
}, [values]); // internalValues intentionally excluded to prevent infinite loop
|
|
67
|
-
|
|
68
|
-
const handleValueChange = (index: number, newValue: string) => {
|
|
69
|
-
const updated = [...internalValues];
|
|
70
|
-
updated[index] = newValue;
|
|
71
|
-
setInternalValues(updated);
|
|
72
|
-
|
|
73
|
-
// Filter out empty values before notifying parent
|
|
74
|
-
const nonEmpty = updated.filter((v) => v.trim() !== "");
|
|
75
|
-
onChange(nonEmpty);
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const handleAddField = () => {
|
|
79
|
-
const updated = [...internalValues, ""];
|
|
80
|
-
setInternalValues(updated);
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const handleRemoveField = (index: number) => {
|
|
84
|
-
if (internalValues.length === 1) {
|
|
85
|
-
// Don't remove the last field, just clear it
|
|
86
|
-
const updated = [""];
|
|
87
|
-
setInternalValues(updated);
|
|
88
|
-
onChange([]);
|
|
89
|
-
} else {
|
|
90
|
-
const updated = internalValues.filter((_, i) => i !== index);
|
|
91
|
-
setInternalValues(updated);
|
|
92
|
-
const nonEmpty = updated.filter((v) => v.trim() !== "");
|
|
93
|
-
onChange(nonEmpty);
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
return (
|
|
98
|
-
<div className={styles.container}>
|
|
99
|
-
{internalValues.map((value, index) => (
|
|
100
|
-
<div key={index} className={styles.row}>
|
|
101
|
-
<Input
|
|
102
|
-
className={styles.input}
|
|
103
|
-
value={value}
|
|
104
|
-
onChange={(_, data) => handleValueChange(index, data.value)}
|
|
105
|
-
placeholder={`${placeholder} ${index + 1}`}
|
|
106
|
-
type={inputType}
|
|
107
|
-
/>
|
|
108
|
-
{internalValues.length > 1 && (
|
|
109
|
-
<Button
|
|
110
|
-
appearance="subtle"
|
|
111
|
-
icon={<Dismiss20Regular />}
|
|
112
|
-
onClick={() => handleRemoveField(index)}
|
|
113
|
-
aria-label={`Remove value ${index + 1}`}
|
|
114
|
-
/>
|
|
115
|
-
)}
|
|
116
|
-
</div>
|
|
117
|
-
))}
|
|
118
|
-
<Button
|
|
119
|
-
className={styles.addButton}
|
|
120
|
-
appearance="secondary"
|
|
121
|
-
icon={<Add20Regular />}
|
|
122
|
-
onClick={handleAddField}
|
|
123
|
-
>
|
|
124
|
-
Add Value
|
|
125
|
-
</Button>
|
|
126
|
-
</div>
|
|
127
|
-
);
|
|
128
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Excel icon component using the Microsoft Office Excel logo
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import excelIconUrl from "../../assets/20px-Microsoft_Office_Excel_(2025–present).svg.png";
|
|
6
|
-
|
|
7
|
-
interface ExcelIconProps {
|
|
8
|
-
/** Size in pixels (default: 20) */
|
|
9
|
-
size?: number;
|
|
10
|
-
/** Additional CSS class */
|
|
11
|
-
className?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Excel icon for export buttons
|
|
16
|
-
* Uses the official Microsoft Office Excel 2025 logo
|
|
17
|
-
*/
|
|
18
|
-
export function ExcelIcon({ size = 20, className }: ExcelIconProps) {
|
|
19
|
-
return (
|
|
20
|
-
<img
|
|
21
|
-
src={excelIconUrl}
|
|
22
|
-
alt=""
|
|
23
|
-
width={size}
|
|
24
|
-
height={size}
|
|
25
|
-
className={className}
|
|
26
|
-
style={{ display: "block" }}
|
|
27
|
-
aria-hidden="true"
|
|
28
|
-
/>
|
|
29
|
-
);
|
|
30
|
-
}
|