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