From a65c1851ad59109e1eb1b6d799e7d978954ca321 Mon Sep 17 00:00:00 2001 From: Hamza Alqurneh Date: Wed, 19 Aug 2026 12:56:41 +0300 Subject: [PATCH 1/2] Create retry policies from the subscription page Replaces the inline custom-rules editor with a named-policy selector and a create dialog, so retry rules always live somewhere reusable. --- .../RetryPolicies/NewRetryPolicyModal.tsx | 66 +++++++++++++++ src/components/Subscription.tsx | 82 +++++++++---------- 2 files changed, 106 insertions(+), 42 deletions(-) create mode 100644 src/components/RetryPolicies/NewRetryPolicyModal.tsx diff --git a/src/components/RetryPolicies/NewRetryPolicyModal.tsx b/src/components/RetryPolicies/NewRetryPolicyModal.tsx new file mode 100644 index 0000000..f2b4ff7 --- /dev/null +++ b/src/components/RetryPolicies/NewRetryPolicyModal.tsx @@ -0,0 +1,66 @@ +import React, {useState} from "react"; +import Modal from "src/components/common/Modal"; +import FormField from "src/components/common/forms/FormField"; +import TextEditor from "src/components/common/forms/TextEditor"; +import RetryGroupsEditor from "src/components/RetryPolicies/RetryGroupsEditor"; +import {useCreateRetryPolicyMutation} from "src/client/apis/retryPoliciesApi"; +import {RetryGroup} from "src/types/retryPolicies"; +import {apiErrorMessage} from "src/client/apis/apiError"; + +interface Props { + /** Suggested name, so a policy made for one subscription is recognisable in the list later. */ + suggestedName?: string + onCreated: (id: number) => void + onClose: () => void +} + +// Creates a real, named policy from wherever one is being picked, rather than sending the reader to +// the policies page and back — a subscription being edited would lose what has been typed so far. +// +// It is a named policy on purpose. The alternative used to be an inline policy stored on the +// subscription, which no page listed and no reset could reach: once its budget ran out that +// subscription stopped retrying for good. +const NewRetryPolicyModal: React.FC = ({suggestedName, onCreated, onClose}) => { + + const [name, setName] = useState(suggestedName ?? "") + const [groups, setGroups] = useState([]) + const [error, setError] = useState() + const [create] = useCreateRetryPolicyMutation() + + const onSubmit = async () => { + try { + const result = await create({name, groups}).unwrap() + onCreated(result.id) + } catch (e) { + // Kept open with the groups intact: the server rejects a group that could never fire, and + // that is worth fixing here rather than starting again. + setError(apiErrorMessage(e, "Could not create this policy.")) + } + } + + return ( + + {error && +

+ {error} +

} + + + + + +

+ Creating it here selects it for this subscription. It can be reused by others, and + edited later on the retry policies page. +

+ +
+ +
+
+ ); +} + +export default NewRetryPolicyModal; diff --git a/src/components/Subscription.tsx b/src/components/Subscription.tsx index 42bbd8a..64f9515 100644 --- a/src/components/Subscription.tsx +++ b/src/components/Subscription.tsx @@ -20,7 +20,7 @@ import { NATIVE_JSON_MAPPER_ID } from "src/types/mapping"; import SubscriptionSelector from "./Subscriptions/SubscriptionSelector"; import ScheduleEditor from "./Subscriptions/ScheduleEditor"; import RetryPolicySelector from "src/components/RetryPolicies/RetryPolicySelector"; -import RetryGroupsEditor from "src/components/RetryPolicies/RetryGroupsEditor"; +import NewRetryPolicyModal from "src/components/RetryPolicies/NewRetryPolicyModal"; import SubscriptionFilter from "src/components/Subscriptions/SubscriptionFilter"; import {TrailBaseModel} from "src/types/trail"; import TrialsViewModal from "src/components/common/trails/trialsViewModal"; @@ -56,7 +56,6 @@ const Component = () => { const [openModal, setOpenModal] = useState<"NONE" | "TRAIL" | "CREATE_DRAFT">("NONE"); const [subscriptionTrail, setSubscriptionTrail] = useState([]); const [updateSubscriptionData, setUpdateSubscriptionData] = useState({}) - const [retryPolicyMode, setRetryPolicyMode] = useState<"NONE" | "NAMED" | "CUSTOM">("NONE"); const savedDataRef = useRef('{}'); const { workGroupsAvailable } = useTypedSelector(state => state.features); const subscriptionCategories = useSubscriptionCategoriesQuery({limit: 1000, offset: 0}) @@ -70,6 +69,7 @@ const Component = () => { const [publishDraft] = usePublishDraftSubscriptionMutation() const [receiveNow] = useReceiveSubscriptionMutation() const [mode, setMode] = useState("PUBLISHED") + const [creatingPolicy, setCreatingPolicy] = useState(false) const mapperMetadata = useAdapterMetadataQuery(updateSubscriptionData.mapperId, {skip: !updateSubscriptionData.mapperId}) const handlerMetadata = useAdapterMetadataQuery(updateSubscriptionData.handlerId, {skip: !updateSubscriptionData.handlerId}) const receiverMetadata = useAdapterMetadataQuery(updateSubscriptionData.receiverId, {skip: !updateSubscriptionData.receiverId}) @@ -89,11 +89,9 @@ const Component = () => { }); setUpdateSubscriptionData(normalized); savedDataRef.current = JSON.stringify(normalized); - setRetryPolicyMode(normalized.customRetryPolicy ? "CUSTOM" : normalized.retryPolicyId ? "NAMED" : "NONE"); } else { setUpdateSubscriptionData({}); savedDataRef.current = '{}'; - setRetryPolicyMode("NONE"); } }, [subscriptionData, id]); @@ -153,21 +151,21 @@ const Component = () => { if (!updateSubscriptionData) return <> const subscriptionType = normalizeSubscriptionType(updateSubscriptionData?.type); - const onChangeRetryPolicyMode = (mode: string) => { - setRetryPolicyMode(mode as typeof retryPolicyMode) - if (mode === "NAMED") { - onChangeSubscriptionData("customRetryPolicy", null) - } else if (mode === "CUSTOM") { - onChangeSubscriptionData("retryPolicyId", null) - onChangeSubscriptionData("customRetryPolicy", updateSubscriptionData.customRetryPolicy ?? {groups: []}) - } else { - onChangeSubscriptionData("retryPolicyId", null) - onChangeSubscriptionData("customRetryPolicy", null) - } - } return (
+ {creatingPolicy && + setCreatingPolicy(false)} + onCreated={(policyId) => { + setCreatingPolicy(false) + // Selected straight away, so the reader is left where they were with the thing + // they just made already chosen. Inline rules, if any, give way to it. + onChangeSubscriptionData("retryPolicyId", policyId) + onChangeSubscriptionData("customRetryPolicy", null) + }}/>} + { openModal === "CREATE_DRAFT" && {
- - item.title} - optionValue={(item: OptionType) => item.id} - isClearable={false} - options={[ - {id: "NONE", title: "None"}, - {id: "NAMED", title: "Named Policy"}, - {id: "CUSTOM", title: "Custom"}, - ]}/> - - - {retryPolicyMode === "NAMED" && -
- onChangeSubscriptionData("retryPolicyId", v ? Number(v) : null)}/> + {/* Clearing the selector is what "no retries" means, so there is no separate mode + to choose first. Policies are always named ones: the inline alternative was + listed nowhere and its spent budget could not be reset, so a subscription that + ran out stopped retrying for good. */} + +
+
+ onChangeSubscriptionData("retryPolicyId", v ? Number(v) : null)}/> +
+ + +
- } +
- {retryPolicyMode === "CUSTOM" && - onChangeSubscriptionData("customRetryPolicy", {groups: g})}/> - } + {/* Only reachable for a subscription whose inline policy was set through the API. + Saying so beats a page that shows an empty selector while retries are in fact + governed by rules it does not display. */} + {updateSubscriptionData.customRetryPolicy && +

+ This subscription carries its own retry rules, set outside this page. They + still apply. Selecting a policy above replaces them. +

}
From e767b5a8c90bfd94481ef98fe561a1a1fd7829f2 Mon Sep 17 00:00:00 2001 From: Hamza Alqurneh Date: Wed, 19 Aug 2026 15:47:39 +0300 Subject: [PATCH 2/2] Make selecting a named policy actually replace inline rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The selector set only retryPolicyId, so rules set through the API stayed and kept winning — the notice saying they were replaced was wrong. Create is also disabled while the request is in flight, so a double click cannot make two policies. --- src/components/RetryPolicies/NewRetryPolicyModal.tsx | 4 ++-- src/components/Subscription.tsx | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/components/RetryPolicies/NewRetryPolicyModal.tsx b/src/components/RetryPolicies/NewRetryPolicyModal.tsx index f2b4ff7..ff163d9 100644 --- a/src/components/RetryPolicies/NewRetryPolicyModal.tsx +++ b/src/components/RetryPolicies/NewRetryPolicyModal.tsx @@ -25,7 +25,7 @@ const NewRetryPolicyModal: React.FC = ({suggestedName, onCreated, onClose const [name, setName] = useState(suggestedName ?? "") const [groups, setGroups] = useState([]) const [error, setError] = useState() - const [create] = useCreateRetryPolicyMutation() + const [create, {isLoading}] = useCreateRetryPolicyMutation() const onSubmit = async () => { try { @@ -40,7 +40,7 @@ const NewRetryPolicyModal: React.FC = ({suggestedName, onCreated, onClose return ( + submitDisabled={!name.trim() || isLoading}> {error &&

{error} diff --git a/src/components/Subscription.tsx b/src/components/Subscription.tsx index 64f9515..d68cd8f 100644 --- a/src/components/Subscription.tsx +++ b/src/components/Subscription.tsx @@ -440,7 +440,16 @@ const Component = () => {

onChangeSubscriptionData("retryPolicyId", v ? Number(v) : null)}/> + onChange={(v) => { + onChangeSubscriptionData("retryPolicyId", v ? Number(v) : null) + // A named policy replaces inline rules rather than sitting + // beside them: the evaluator prefers a stored + // customRetryPolicy over the named one, so leaving it behind + // would keep invisible rules in charge of a subscription + // whose page says they were replaced. Cleared when the + // selector is emptied too, which is what "no retries" means. + onChangeSubscriptionData("customRetryPolicy", null) + }}/>