Skip to content

Commit 2b6e57a

Browse files
committed
🐛 Fix custom target order handling
Resolves: https://issues.redhat.com/browse/MTA-6287 Resolves: #2708 All targets will new be displayed, even if they are not in the target order setting. The target query hook now will return the server ordering of the targets, and ordering by the "ui.target.order" setting. All targets are included in both lists. Any unordered targets is added to the end of the ordered list. By putting this logic in the target query hook, target order can be used the same way everywhere it is needed. On the migration-targets page, the targets are now displayed in the order specified by the "ui.target.order" setting. Any targets not in the setting are added to the end of the list. On the custom-target-form, updated the onSubmit handler to both create a new target and add it to the target order. This is done awaiting async mutations to help ensure the operations finish before the create modal is closed. Signed-off-by: Scott J Dickerson <[email protected]>
1 parent a2921ea commit 2b6e57a

File tree

5 files changed

+134
-77
lines changed

5 files changed

+134
-77
lines changed

client/src/app/pages/applications/analysis-wizard/set-targets.tsx

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ interface SetTargetsProps {
3737
const useEnhancedTargets = (applications: Application[]) => {
3838
const {
3939
targets,
40+
targetsInOrder,
4041
isFetching: isTargetsLoading,
4142
fetchError: isTargetsError,
4243
} = useFetchTargets();
@@ -69,31 +70,14 @@ const useEnhancedTargets = (applications: Application[]) => {
6970
[applications, languageTags, languageProviders]
7071
);
7172

72-
// 1. missing target order setting is not a blocker (only lowers user experience)
73-
// 2. targets without assigned position (if any) are put at the end
74-
const targetsWithOrder = useMemo(
75-
() =>
76-
targets
77-
.map((target) => {
78-
const index = targetOrder.findIndex((id) => id === target.id);
79-
return {
80-
target,
81-
order: index === -1 ? targets.length : index,
82-
};
83-
})
84-
.sort((a, b) => a.order - b.order)
85-
.map(({ target }) => target),
86-
[targets, targetOrder]
87-
);
88-
8973
return {
9074
// true if some queries are still fetching data for the first time (initial load)
9175
// note that the default re-try count (3) is used
9276
isLoading:
9377
isTagCategoriesLoading || isTargetsLoading || isTargetOrderLoading,
9478
// missing targets are the only blocker
9579
isError: !!isTargetsError,
96-
targets: targetsWithOrder,
80+
targets: targetsInOrder,
9781
applicationProviders,
9882
languageProviders,
9983
};

client/src/app/pages/migration-targets/components/custom-target-form.tsx

Lines changed: 72 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { NotificationsContext } from "@app/components/NotificationsContext";
2727
import { OptionWithValue, SimpleSelect } from "@app/components/SimpleSelect";
2828
import { UploadFileSchema } from "@app/pages/applications/analysis-wizard/schema";
2929
import { useFetchIdentities } from "@app/queries/identities";
30+
import { useSettingMutation } from "@app/queries/settings";
3031
import {
3132
useCreateTargetMutation,
3233
useFetchTargets,
@@ -47,6 +48,7 @@ import defaultImage from "./default.png";
4748

4849
export interface CustomTargetFormProps {
4950
target?: Target | null;
51+
targetOrder: number[];
5052
onSaved: (response: AxiosResponse<Target>) => void;
5153
onCancel: () => void;
5254
}
@@ -77,12 +79,58 @@ const targetRulesetsToReadFiles = (target?: Target) =>
7779
};
7880
}) || [];
7981

82+
const useTargetQueryHooks = (
83+
onSaved: (response: AxiosResponse<Target>) => void,
84+
reset: () => void
85+
) => {
86+
const { pushNotification } = useContext(NotificationsContext);
87+
88+
const { mutateAsync: createImageFileAsync } = useCreateFileMutation();
89+
90+
const { mutateAsync: createTargetAsync } = useCreateTargetMutation(
91+
(response: AxiosResponse<Target>) => {
92+
// TODO: Consider any removed files and delete them from hub if necessary
93+
onSaved(response);
94+
reset();
95+
},
96+
(error: AxiosError) => {
97+
pushNotification({
98+
title: getAxiosErrorMessage(error),
99+
variant: "danger",
100+
});
101+
}
102+
);
103+
104+
const onUpdateTargetSuccess = (response: AxiosResponse<Target>) => {
105+
// TODO: Consider any removed files and delete them from hub if necessary
106+
onSaved(response);
107+
reset();
108+
};
109+
110+
const onUpdateTargetFailure = (_error: AxiosError) => {};
111+
112+
const { mutate: updateTarget } = useUpdateTargetMutation(
113+
onUpdateTargetSuccess,
114+
onUpdateTargetFailure
115+
);
116+
117+
const { mutateAsync: targetOrderMutateAsync } =
118+
useSettingMutation("ui.target.order");
119+
120+
return {
121+
createTargetAsync,
122+
updateTarget,
123+
targetOrderMutateAsync,
124+
createImageFileAsync,
125+
};
126+
};
127+
80128
export const CustomTargetForm: React.FC<CustomTargetFormProps> = ({
81129
target: initialTarget,
130+
targetOrder,
82131
onSaved,
83132
onCancel,
84133
}) => {
85-
const { pushNotification } = useContext(NotificationsContext);
86134
const baseProviderList = useMigrationProviderList();
87135
const providerList = useMemo(
88136
() =>
@@ -261,6 +309,13 @@ export const CustomTargetForm: React.FC<CustomTargetFormProps> = ({
261309

262310
const values = getValues();
263311

312+
const {
313+
createTargetAsync,
314+
updateTarget,
315+
targetOrderMutateAsync,
316+
createImageFileAsync,
317+
} = useTargetQueryHooks(onSaved, reset);
318+
264319
const onSubmit = async (formValues: CustomTargetFormValues) => {
265320
const rules: Rule[] = formValues.customRulesFiles
266321
.map((file) => {
@@ -339,7 +394,22 @@ export const CustomTargetForm: React.FC<CustomTargetFormProps> = ({
339394
if (target) {
340395
updateTarget({ id: target.id, ...payload });
341396
} else {
342-
createTarget(payload);
397+
let newTargetId: number | undefined;
398+
try {
399+
const response = await createTargetAsync(payload);
400+
newTargetId = response.data.id;
401+
} catch {
402+
/* ignore */
403+
}
404+
405+
// Add the new target to the target order and wait for it to complete before continuing.
406+
if (newTargetId !== undefined) {
407+
try {
408+
await targetOrderMutateAsync([...targetOrder, newTargetId]);
409+
} catch {
410+
/* ignore */
411+
}
412+
}
343413
}
344414
};
345415

@@ -348,39 +418,6 @@ export const CustomTargetForm: React.FC<CustomTargetFormProps> = ({
348418
onCancel();
349419
};
350420

351-
const { mutateAsync: createImageFileAsync } = useCreateFileMutation();
352-
353-
const onCreateTargetSuccess = (response: AxiosResponse<Target>) => {
354-
// TODO: Consider any removed files and delete them from hub if necessary
355-
onSaved(response);
356-
reset();
357-
};
358-
359-
const onCreateTargetFailure = (error: AxiosError) => {
360-
pushNotification({
361-
title: getAxiosErrorMessage(error),
362-
variant: "danger",
363-
});
364-
};
365-
366-
const { mutate: createTarget } = useCreateTargetMutation(
367-
onCreateTargetSuccess,
368-
onCreateTargetFailure
369-
);
370-
371-
const onUpdateTargetSuccess = (response: AxiosResponse<Target>) => {
372-
// TODO: Consider any removed files and delete them from hub if necessary
373-
onSaved(response);
374-
reset();
375-
};
376-
377-
const onUpdateTargetFailure = (_error: AxiosError) => {};
378-
379-
const { mutate: updateTarget } = useUpdateTargetMutation(
380-
onUpdateTargetSuccess,
381-
onUpdateTargetFailure
382-
);
383-
384421
const handleImageFileUpload = async (file: File) => {
385422
setImageFilename(file.name);
386423
return createImageFileAsync({ file });

client/src/app/pages/migration-targets/migration-targets.tsx

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export const MigrationTargets: React.FC = () => {
5050
const { t } = useTranslation();
5151
const { pushNotification } = React.useContext(NotificationsContext);
5252

53-
const { targets, refetch: refetchTargets } = useFetchTargets();
53+
const { targetsInOrder } = useFetchTargets();
5454

5555
const targetOrderSetting = useSetting("ui.target.order");
5656
const targetOrderSettingMutation = useSettingMutation("ui.target.order");
@@ -94,7 +94,7 @@ export const MigrationTargets: React.FC = () => {
9494
onDeleteTargetError
9595
);
9696

97-
const onCustomTargetModalSaved = (response: AxiosResponse<Target>) => {
97+
const onCustomTargetModalSaved = async (response: AxiosResponse<Target>) => {
9898
if (targetToUpdate) {
9999
pushNotification({
100100
title: t("toastr.success.saveWhat", {
@@ -121,18 +121,6 @@ export const MigrationTargets: React.FC = () => {
121121
),
122122
});
123123

124-
// update target order
125-
if (
126-
targetOrderSetting.isSuccess &&
127-
response.data.id &&
128-
targetOrderSetting.data
129-
) {
130-
targetOrderSettingMutation.mutate([
131-
...targetOrderSetting.data,
132-
response.data.id,
133-
]);
134-
}
135-
136124
// Make sure the new target's provider is part of the providers filter so it can be seen
137125
if (filterState.filterValues["provider"]) {
138126
const targetProvider = response.data.provider;
@@ -149,8 +137,8 @@ export const MigrationTargets: React.FC = () => {
149137
});
150138
}
151139
}
140+
152141
setCreateUpdateModalState(null);
153-
refetchTargets();
154142
};
155143

156144
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor));
@@ -180,13 +168,15 @@ export const MigrationTargets: React.FC = () => {
180168

181169
const handleDragStart = ({ active }: DragStartEvent) => {
182170
const activeId = active.id as number;
183-
const activeTarget = targets.find((target) => target.id === activeId);
171+
const activeTarget = targetsInOrder.find(
172+
(target) => target.id === activeId
173+
);
184174
setActiveTarget(activeTarget ?? null);
185175
};
186176

187177
const tableControls = useLocalTableControls({
188178
tableName: "target-cards",
189-
items: targets,
179+
items: targetsInOrder,
190180
idProperty: "name",
191181
initialFilterValues: { provider: [DEFAULT_PROVIDER] },
192182
columnNames: {
@@ -200,7 +190,7 @@ export const MigrationTargets: React.FC = () => {
200190
filterCategories: [
201191
{
202192
selectOptions: unique(
203-
targets.map((target) => target.provider).filter(Boolean)
193+
targetsInOrder.map((target) => target.provider).filter(Boolean)
204194
).map((target) => ({ value: target })),
205195
placeholderText: "Filter by language...",
206196
categoryKey: "provider",
@@ -230,9 +220,19 @@ export const MigrationTargets: React.FC = () => {
230220
return [];
231221
}
232222

233-
return targetOrderSetting.data
223+
// Get targets in the order specified by the setting
224+
const orderedTargets = targetOrderSetting.data
234225
.map((id) => filteredTargets.find((target) => target.id === id))
235226
.filter(Boolean);
227+
228+
// Find targets that are in filteredTargets but not in the ordered list
229+
const orderedTargetIds = new Set(orderedTargets.map((target) => target.id));
230+
const unorderedTargets = filteredTargets.filter(
231+
(target) => !orderedTargetIds.has(target.id)
232+
);
233+
234+
// Combine ordered targets with unordered ones at the end
235+
return [...orderedTargets, ...unorderedTargets];
236236
}, [filteredTargets, targetOrderSetting.data, targetOrderSetting.isSuccess]);
237237

238238
return (
@@ -315,6 +315,7 @@ export const MigrationTargets: React.FC = () => {
315315
>
316316
<CustomTargetForm
317317
target={targetToUpdate}
318+
targetOrder={targetOrderSetting.data ?? []}
318319
onSaved={onCustomTargetModalSaved}
319320
onCancel={() => setCreateUpdateModalState(null)}
320321
/>

client/src/app/queries/settings.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ export const useSettingMutation = <K extends keyof SettingTypes>(key: K) => {
2323
return useMutation({
2424
mutationFn: (value: SettingTypes[K]) => updateSetting({ key, value }),
2525
onSuccess: () => {
26-
queryClient.invalidateQueries({ queryKey: [SettingQueryKey] });
26+
queryClient.invalidateQueries({ queryKey: [SettingQueryKey, key] });
2727
},
2828
onError: () => {
29-
queryClient.invalidateQueries({ queryKey: [SettingQueryKey] });
29+
queryClient.invalidateQueries({ queryKey: [SettingQueryKey, key] });
3030
},
3131
});
3232
};

client/src/app/queries/targets.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useMemo } from "react";
12
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
23
import { AxiosError, AxiosResponse } from "axios";
34

@@ -11,20 +12,53 @@ import {
1112
updateTarget,
1213
} from "@app/api/rest";
1314

15+
import { useSetting } from "./settings";
16+
1417
export const TargetsQueryKey = "targets";
1518

1619
export const useFetchTargets = (
1720
refetchInterval: number | false = DEFAULT_REFETCH_INTERVAL
1821
) => {
19-
const { data, isLoading, isSuccess, error, refetch } = useQuery<Target[]>({
22+
const {
23+
data: targets,
24+
isLoading,
25+
isSuccess,
26+
error,
27+
refetch,
28+
} = useQuery<Target[]>({
2029
queryKey: [TargetsQueryKey],
2130
queryFn: async () => await getTargets(),
2231
onError: (err) => console.log(err),
2332
refetchInterval,
2433
});
2534

35+
const { data: targetOrder = [] } = useSetting("ui.target.order");
36+
37+
const targetsInOrder = useMemo(
38+
() =>
39+
!targets
40+
? []
41+
: targets
42+
.map((target, targetIndex) => {
43+
const index = targetOrder.findIndex((id) => id === target.id);
44+
return {
45+
target,
46+
order: index === -1 ? targets.length + targetIndex : index,
47+
};
48+
})
49+
.sort((a, b) => a.order - b.order)
50+
.map(({ target }) => target),
51+
[targets, targetOrder]
52+
);
53+
2654
return {
27-
targets: data || [],
55+
/** Targets in their original order */
56+
targets: targets || [],
57+
/**
58+
* Targets in the order specified by the setting "ui.target.order" including targets
59+
* that are not in the setting at the end of the list.
60+
*/
61+
targetsInOrder,
2862
isFetching: isLoading,
2963
isSuccess,
3064
fetchError: error,
@@ -83,7 +117,7 @@ export const useCreateTargetMutation = (
83117
onError: (err: AxiosError) => void
84118
) => {
85119
const queryClient = useQueryClient();
86-
const { isPending, mutate, error } = useMutation({
120+
const { isPending, mutate, mutateAsync, error } = useMutation({
87121
mutationFn: createTarget,
88122
onSuccess: (res) => {
89123
onSuccess(res);
@@ -95,6 +129,7 @@ export const useCreateTargetMutation = (
95129
});
96130
return {
97131
mutate,
132+
mutateAsync,
98133
isPending,
99134
error,
100135
};

0 commit comments

Comments
 (0)