@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.
Files changed (77) hide show
  1. package/LICENSE +661 -0
  2. package/dist/assets/index-DD9fzkbz.js +232 -0
  3. package/dist/index.html +70 -0
  4. package/index.html +0 -1
  5. package/npm-shrinkwrap.json +6329 -0
  6. package/package.json +95 -51
  7. package/CHANGELOG.md +0 -120
  8. package/eslint.config.js +0 -23
  9. package/public/vite.svg +0 -1
  10. package/src/app/AppShell.tsx +0 -1733
  11. package/src/assets/20px-Microsoft_Office_Excel_(2025/342/200/223present).svg.png +0 -0
  12. package/src/assets/react.svg +0 -1
  13. package/src/features/fetchxml/api/dataverseMetadata.ts +0 -192
  14. package/src/features/fetchxml/api/excelExport.ts +0 -318
  15. package/src/features/fetchxml/api/formattedValues.ts +0 -107
  16. package/src/features/fetchxml/api/pptbClient.ts +0 -2635
  17. package/src/features/fetchxml/model/displaySettings.ts +0 -18
  18. package/src/features/fetchxml/model/fetchxml.ts +0 -346
  19. package/src/features/fetchxml/model/fetchxmlIntellisense.ts +0 -583
  20. package/src/features/fetchxml/model/fetchxmlParser.ts +0 -708
  21. package/src/features/fetchxml/model/layoutxml.ts +0 -372
  22. package/src/features/fetchxml/model/nodes.ts +0 -255
  23. package/src/features/fetchxml/model/operators.ts +0 -624
  24. package/src/features/fetchxml/model/treeUtils.ts +0 -174
  25. package/src/features/fetchxml/state/builderStore.tsx +0 -1160
  26. package/src/features/fetchxml/state/cache.ts +0 -393
  27. package/src/features/fetchxml/ui/Dialogs/BulkDeleteDialog.tsx +0 -352
  28. package/src/features/fetchxml/ui/Dialogs/DeleteConfirmDialog.tsx +0 -396
  29. package/src/features/fetchxml/ui/Dialogs/SaveViewDialog.tsx +0 -612
  30. package/src/features/fetchxml/ui/Dialogs/SolutionPicker.tsx +0 -114
  31. package/src/features/fetchxml/ui/Dialogs/WorkflowPickerDialog.tsx +0 -437
  32. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/AttributeEditor.tsx +0 -214
  33. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ConditionEditor.tsx +0 -533
  34. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FetchOptionsEditor.tsx +0 -296
  35. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FilterEditor.tsx +0 -106
  36. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/LinkEntityEditor.tsx +0 -433
  37. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/OrderEditor.tsx +0 -161
  38. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ValueOfPicker.tsx +0 -194
  39. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel.tsx +0 -225
  40. package/src/features/fetchxml/ui/LeftPane/TreeView.tsx +0 -604
  41. package/src/features/fetchxml/ui/RightPane/AddColumnsPanel.tsx +0 -800
  42. package/src/features/fetchxml/ui/RightPane/ColumnConfigDialog.tsx +0 -170
  43. package/src/features/fetchxml/ui/RightPane/DataGridCellRenderers.tsx +0 -356
  44. package/src/features/fetchxml/ui/RightPane/EditColumnsPanel.tsx +0 -372
  45. package/src/features/fetchxml/ui/RightPane/FetchXmlEditor.tsx +0 -399
  46. package/src/features/fetchxml/ui/RightPane/FormattedValueUtils.ts +0 -189
  47. package/src/features/fetchxml/ui/RightPane/LayoutXmlViewer.tsx +0 -171
  48. package/src/features/fetchxml/ui/RightPane/PreviewTabs.tsx +0 -471
  49. package/src/features/fetchxml/ui/RightPane/ResultsCommandBar.tsx +0 -430
  50. package/src/features/fetchxml/ui/RightPane/ResultsGrid.tsx +0 -983
  51. package/src/features/fetchxml/ui/RightPane/XmlTextArea.tsx +0 -65
  52. package/src/features/fetchxml/ui/Settings/SettingsDrawer.tsx +0 -175
  53. package/src/features/fetchxml/ui/Toolbar/EntitySelector.tsx +0 -788
  54. package/src/features/fetchxml/ui/Toolbar/LoadViewPicker.tsx +0 -274
  55. package/src/features/fetchxml/ui/Toolbar/SaveViewButton.tsx +0 -174
  56. package/src/main.tsx +0 -11
  57. package/src/shared/components/AttributePicker.tsx +0 -138
  58. package/src/shared/components/BetweenInputPicker.tsx +0 -120
  59. package/src/shared/components/BooleanValuePicker.tsx +0 -165
  60. package/src/shared/components/DateTimeInputPicker.tsx +0 -170
  61. package/src/shared/components/DynamicValueList.tsx +0 -128
  62. package/src/shared/components/ExcelIcon.tsx +0 -30
  63. package/src/shared/components/MultiValuePicker.tsx +0 -178
  64. package/src/shared/components/NumericInputPicker.tsx +0 -156
  65. package/src/shared/components/OptionSetValuePicker.tsx +0 -150
  66. package/src/shared/components/RelationshipPicker.tsx +0 -268
  67. package/src/shared/contexts/ThemeContext.tsx +0 -19
  68. package/src/shared/hooks/useAccessMode.ts +0 -77
  69. package/src/shared/hooks/useLazyMetadata.ts +0 -132
  70. package/src/shared/hooks/usePptbContext.ts +0 -170
  71. package/src/shared/hooks/usePublisherFilter.ts +0 -214
  72. package/src/shared/hooks/useSolutionFilter.ts +0 -162
  73. package/src/shared/utils/debug.ts +0 -87
  74. package/tsconfig.app.json +0 -28
  75. package/tsconfig.json +0 -7
  76. package/tsconfig.node.json +0 -26
  77. 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
- }