Skip to content

Commit 2d47be0

Browse files
authored
fix(splits): Make split assignment idempotent (#10252)
1 parent af14425 commit 2d47be0

File tree

5 files changed

+177
-397
lines changed

5 files changed

+177
-397
lines changed

app/schema.graphql

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,6 @@ type AddAnnotationConfigToProjectPayload {
1010
project: Project!
1111
}
1212

13-
input AddDatasetExamplesToDatasetSplitsInput {
14-
datasetSplitIds: [ID!]!
15-
exampleIds: [ID!]!
16-
}
17-
18-
type AddDatasetExamplesToDatasetSplitsMutationPayload {
19-
query: Query!
20-
examples: [DatasetExample!]!
21-
}
22-
2313
input AddExamplesToDatasetInput {
2414
datasetId: ID!
2515
examples: [DatasetExampleInput!]!
@@ -1834,8 +1824,7 @@ type Mutation {
18341824
createDatasetSplit(input: CreateDatasetSplitInput!): DatasetSplitMutationPayload!
18351825
patchDatasetSplit(input: PatchDatasetSplitInput!): DatasetSplitMutationPayload!
18361826
deleteDatasetSplits(input: DeleteDatasetSplitInput!): DeleteDatasetSplitsMutationPayload!
1837-
addDatasetExamplesToDatasetSplits(input: AddDatasetExamplesToDatasetSplitsInput!): AddDatasetExamplesToDatasetSplitsMutationPayload!
1838-
removeDatasetExamplesFromDatasetSplits(input: RemoveDatasetExamplesFromDatasetSplitsInput!): RemoveDatasetExamplesFromDatasetSplitsMutationPayload!
1827+
setDatasetExampleSplits(input: SetDatasetExampleSplitsInput!): SetDatasetExampleSplitsMutationPayload!
18391828
createDatasetSplitWithExamples(input: CreateDatasetSplitWithExamplesInput!): DatasetSplitMutationPayloadWithExamples!
18401829
deleteExperiments(input: DeleteExperimentsInput!): ExperimentMutationPayload!
18411830

@@ -2555,16 +2544,6 @@ type RemoveAnnotationConfigFromProjectPayload {
25552544
project: Project!
25562545
}
25572546

2558-
input RemoveDatasetExamplesFromDatasetSplitsInput {
2559-
datasetSplitIds: [ID!]!
2560-
exampleIds: [ID!]!
2561-
}
2562-
2563-
type RemoveDatasetExamplesFromDatasetSplitsMutationPayload {
2564-
query: Query!
2565-
examples: [DatasetExample!]!
2566-
}
2567-
25682547
type ResponseFormat {
25692548
definition: JSON!
25702549
}
@@ -2605,6 +2584,16 @@ type ServerStatus {
26052584
insufficientStorage: Boolean!
26062585
}
26072586

2587+
input SetDatasetExampleSplitsInput {
2588+
exampleId: ID!
2589+
datasetSplitIds: [ID!]!
2590+
}
2591+
2592+
type SetDatasetExampleSplitsMutationPayload {
2593+
query: Query!
2594+
example: DatasetExample!
2595+
}
2596+
26082597
input SetDatasetLabelsInput {
26092598
datasetId: ID!
26102599
datasetLabelIds: [ID!]!

app/src/pages/examples/ExamplesSplitMenu.tsx

Lines changed: 75 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
1+
import {
2+
Suspense,
3+
useCallback,
4+
useEffect,
5+
useMemo,
6+
useRef,
7+
useState,
8+
} from "react";
29
import { graphql, useLazyLoadQuery, useMutation } from "react-relay";
310

411
import {
@@ -75,6 +82,8 @@ export const ExamplesSplitMenu = ({
7582
const [mode, setMode] = useState<"filter" | "apply" | "create">(() =>
7683
getInitialMode(selectedExampleIds)
7784
);
85+
const [isMenuOpen, setIsMenuOpen] = useState(false); // used to keep the menu open when a split is applied
86+
const shouldKeepMenuOpenRef = useRef(false); // required as menu is no longer multi-select
7887
useEffect(() => {
7988
setMode(getInitialMode(selectedExampleIds));
8089
}, [selectedExampleIds]);
@@ -92,10 +101,21 @@ export const ExamplesSplitMenu = ({
92101
const selectedPartialExamples = useMemo(() => {
93102
return selectedExampleIds.map((id) => examplesCache[id]).filter(Boolean);
94103
}, [selectedExampleIds, examplesCache]);
104+
const requestKeepMenuOpen = useCallback(() => {
105+
shouldKeepMenuOpenRef.current = true;
106+
}, []);
95107

96108
return (
97109
<MenuTrigger
110+
isOpen={isMenuOpen}
98111
onOpenChange={(open) => {
112+
if (!open && shouldKeepMenuOpenRef.current) {
113+
shouldKeepMenuOpenRef.current = false;
114+
setIsMenuOpen(true);
115+
return;
116+
}
117+
shouldKeepMenuOpenRef.current = false;
118+
setIsMenuOpen(open);
99119
if (!open) {
100120
setMode(getInitialMode(selectedExampleIds));
101121
}
@@ -118,6 +138,7 @@ export const ExamplesSplitMenu = ({
118138
onExampleSelectionChange={onExampleSelectionChange}
119139
selectedPartialExamples={selectedPartialExamples}
120140
setMode={setMode}
141+
onRequestKeepMenuOpen={requestKeepMenuOpen}
121142
/>
122143
)}
123144
{mode === "create" && (
@@ -144,6 +165,7 @@ const SplitMenu = ({
144165
onSelectionChange,
145166
selectedPartialExamples,
146167
setMode,
168+
onRequestKeepMenuOpen,
147169
}: {
148170
selectedSplitIds: string[];
149171
selectedExampleIds: string[];
@@ -154,6 +176,7 @@ const SplitMenu = ({
154176
datasetSplits: { id: string; name: string }[];
155177
}[];
156178
setMode: (mode: "filter" | "apply" | "create") => void;
179+
onRequestKeepMenuOpen: () => void;
157180
}) => {
158181
const { contains } = useFilter({ sensitivity: "base" });
159182
const data = useLazyLoadQuery<ExamplesSplitMenuQuery>(
@@ -174,28 +197,12 @@ const SplitMenu = ({
174197
// fetch when menu is opened, but show cache data first to prevent flickering
175198
{ fetchPolicy: "store-and-network" }
176199
);
177-
const [addExamplesToSplits] = useMutation(graphql`
178-
mutation ExamplesSplitMenuAddDatasetExamplesToDatasetSplitsMutation(
179-
$input: AddDatasetExamplesToDatasetSplitsInput!
200+
const [setExampleSplits] = useMutation(graphql`
201+
mutation ExamplesSplitMenuSetDatasetExampleSplitsMutation(
202+
$input: SetDatasetExampleSplitsInput!
180203
) {
181-
addDatasetExamplesToDatasetSplits(input: $input) {
182-
examples {
183-
id
184-
datasetSplits {
185-
id
186-
name
187-
color
188-
}
189-
}
190-
}
191-
}
192-
`);
193-
const [removeExamplesFromSplit] = useMutation(graphql`
194-
mutation ExamplesSplitMenuRemoveDatasetExamplesFromDatasetSplitMutation(
195-
$input: RemoveDatasetExamplesFromDatasetSplitsInput!
196-
) {
197-
removeDatasetExamplesFromDatasetSplits(input: $input) {
198-
examples {
204+
setDatasetExampleSplits(input: $input) {
205+
example {
199206
id
200207
datasetSplits {
201208
id
@@ -207,33 +214,17 @@ const SplitMenu = ({
207214
}
208215
`);
209216
const onUpdateSplits = useCallback(
210-
(changes: {
211-
selectedExampleIds: string[];
212-
addSplitIds?: string[];
213-
removeSplitIds?: string[];
214-
}) => {
215-
if (changes.addSplitIds) {
216-
addExamplesToSplits({
217-
variables: {
218-
input: {
219-
exampleIds: changes.selectedExampleIds,
220-
datasetSplitIds: changes.addSplitIds,
221-
},
217+
(changes: { exampleId: string; splitIds: string[] }) => {
218+
setExampleSplits({
219+
variables: {
220+
input: {
221+
exampleId: changes.exampleId,
222+
datasetSplitIds: changes.splitIds,
222223
},
223-
});
224-
}
225-
if (changes.removeSplitIds) {
226-
removeExamplesFromSplit({
227-
variables: {
228-
input: {
229-
exampleIds: changes.selectedExampleIds,
230-
datasetSplitIds: changes.removeSplitIds,
231-
},
232-
},
233-
});
234-
}
224+
},
225+
});
235226
},
236-
[addExamplesToSplits, removeExamplesFromSplit]
227+
[setExampleSplits]
237228
);
238229
const splits = useMemo(() => {
239230
return data.datasetSplits.edges.map((edge) => edge.split);
@@ -274,6 +265,7 @@ const SplitMenu = ({
274265
onSelectionChange={onUpdateSplits}
275266
splits={splits as Mutable<typeof splits>}
276267
selectedPartialExamples={selectedPartialExamples}
268+
onRequestKeepMenuOpen={onRequestKeepMenuOpen}
277269
/>
278270
)}
279271
</Autocomplete>
@@ -331,17 +323,18 @@ const SplitMenuApplyContent = ({
331323
onSelectionChange,
332324
splits,
333325
selectedPartialExamples,
326+
onRequestKeepMenuOpen,
334327
}: {
335328
onSelectionChange: (changes: {
336-
selectedExampleIds: string[];
337-
addSplitIds?: string[];
338-
removeSplitIds?: string[];
329+
exampleId: string;
330+
splitIds: string[];
339331
}) => void;
340332
splits: { id: string; name: string; color: string }[];
341333
selectedPartialExamples: {
342334
id: string;
343335
datasetSplits: { id: string; name: string }[];
344336
}[];
337+
onRequestKeepMenuOpen: () => void;
345338
}) => {
346339
// derive checkbox states for each split based on the selectedPartialExamples
347340
type SplitState = Record<string, "checked" | "indeterminate" | "unchecked">;
@@ -368,32 +361,44 @@ const SplitMenuApplyContent = ({
368361
return acc;
369362
}, {} as SplitState);
370363
}, [splits, selectedPartialExamples]);
364+
const handleSplitToggle = useCallback(
365+
(selectedId: string) => {
366+
// because the menu is no longer multi-select, we need to keep the menu open when a split is applied
367+
onRequestKeepMenuOpen();
368+
for (const example of selectedPartialExamples) {
369+
const currentSplitIds = example.datasetSplits.map((s) => s.id);
370+
let newSplitIds: string[];
371+
372+
if (splitStates[selectedId] === "checked") {
373+
newSplitIds = currentSplitIds.filter((id) => id !== selectedId);
374+
} else {
375+
newSplitIds = currentSplitIds.includes(selectedId)
376+
? currentSplitIds
377+
: [...currentSplitIds, selectedId];
378+
}
379+
380+
onSelectionChange({
381+
exampleId: example.id,
382+
splitIds: newSplitIds,
383+
});
384+
}
385+
},
386+
[
387+
onRequestKeepMenuOpen,
388+
onSelectionChange,
389+
selectedPartialExamples,
390+
splitStates,
391+
]
392+
);
371393
return (
372394
<Menu
373395
items={splits}
374396
renderEmptyState={() => "No splits found"}
375-
// hack to keep the menu open when splits are changed
376-
selectedKeys={[]}
377-
selectionMode="multiple"
397+
// NOTE: Menu is no longer multi-select, so we track the menu open state manually
398+
selectionMode="none"
378399
// ensure that menu items are re-rendered when splitStates changes
379400
dependencies={[splitStates]}
380-
// update selection state externally, the menu does not actually know what is selected
381-
onSelectionChange={(keys) => {
382-
const selectedId = Array.from(keys as Set<string>)[0];
383-
if (splitStates[selectedId] === "checked") {
384-
// remove split from all selected examples
385-
onSelectionChange({
386-
selectedExampleIds: selectedPartialExamples.map((e) => e.id),
387-
removeSplitIds: [selectedId],
388-
});
389-
} else {
390-
// state is indeterminate or unchecked, add split to all selected examples
391-
onSelectionChange({
392-
selectedExampleIds: selectedPartialExamples.map((e) => e.id),
393-
addSplitIds: [selectedId],
394-
});
395-
}
396-
}}
401+
onAction={(key) => handleSplitToggle(key as string)}
397402
>
398403
{({ id, name, color }) => (
399404
<MenuItem id={id} textValue={name}>

0 commit comments

Comments
 (0)