From 00ce89a3798d362f71d1bcbb209069795dcad1b8 Mon Sep 17 00:00:00 2001 From: Yuwen Memon Date: Sun, 25 Oct 2026 15:22:09 -0700 Subject: [PATCH 1/5] feat(rules): make Try this instead the suggested button on a refusal --- src/components/Form/FormWrapper.tsx | 2 ++ src/components/Form/types.ts | 3 ++ src/components/FormAlertWithSubmitButton.tsx | 9 ++++-- .../workspace/rules/DescribeRuleForm.tsx | 32 +++++++++++-------- tests/ui/DescribeRuleModalTest.tsx | 27 ++++++++++++++-- 5 files changed, 56 insertions(+), 17 deletions(-) diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index b6c63efe437e..51e33fc460b0 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -90,6 +90,7 @@ function FormWrapper({ submitFlexEnabled = true, enabledWhenOffline, buttonVariant, + isSubmitButtonNeutral = false, formID, shouldUseScrollView = true, scrollContextEnabled = false, @@ -217,6 +218,7 @@ function FormWrapper({ ]} enabledWhenOffline={enabledWhenOffline} buttonVariant={buttonVariant} + isButtonNeutral={isSubmitButtonNeutral} disablePressOnEnter={disablePressOnEnter} enterKeyEventListenerPriority={enterKeyEventListenerPriority} shouldRenderFooterAboveSubmit={shouldRenderFooterAboveSubmit} diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 9f8687cdfdc3..7a9691271551 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -178,6 +178,9 @@ type FormProps = ForwardedFSClassProp /** The visual variant of the submit button, which controls its color scheme */ buttonVariant?: ButtonVariant; + /** Whether the submit button has the neutral color in place of `buttonVariant`, so that a button in `footerContent` can be the suggested action */ + isSubmitButtonNeutral?: boolean; + /** Should fix the errors alert be displayed when there is an error in the form */ shouldHideFixErrorsAlert?: boolean; diff --git a/src/components/FormAlertWithSubmitButton.tsx b/src/components/FormAlertWithSubmitButton.tsx index ef7c9afabe65..bebe455d288f 100644 --- a/src/components/FormAlertWithSubmitButton.tsx +++ b/src/components/FormAlertWithSubmitButton.tsx @@ -52,6 +52,9 @@ type FormAlertWithSubmitButtonProps = WithSentryLabel & { /** The visual variant of the submit button, which controls its color scheme */ buttonVariant?: ButtonVariant; + /** Whether the submit button has the neutral color in place of `buttonVariant` */ + isButtonNeutral?: boolean; + /** Custom content to display in the footer after submit button */ footerContent?: React.ReactNode; @@ -98,6 +101,7 @@ function FormAlertWithSubmitButton({ enabledWhenOffline = false, disablePressOnEnter = false, buttonVariant = CONST.BUTTON_VARIANT.SUCCESS, + isButtonNeutral = false, footerContent, buttonRef, buttonStyles, @@ -116,6 +120,7 @@ function FormAlertWithSubmitButton({ }: FormAlertWithSubmitButtonProps) { const styles = useThemeStyles(); const style = [!shouldRenderFooterAboveSubmit && footerContent && addButtonBottomPadding ? styles.mb3 : undefined, buttonStyles]; + const variant = isButtonNeutral ? undefined : buttonVariant; const {isLoading, startWithLoading} = usePressLoading({isLoading: isOnyxLoading}); @@ -147,7 +152,7 @@ function FormAlertWithSubmitButton({ {shouldRenderFooterAboveSubmit && footerContent} {isOffline && !enabledWhenOffline ? ( + ) + } // An empty description sends no request, so nothing would stop a loading state that starts on the press. shouldShowLoadingImmediatelyOnPress={!canSkipDescription} keyboardSubmitBehavior={CONST.KEYBOARD_SUBMIT_BEHAVIOR.SUBMIT_ONLY} @@ -338,19 +356,7 @@ function DescribeRuleForm({ {translate(IMPOSSIBLE_TITLE_BY_REASON[refusal.reason ?? ''] ?? 'workspace.rules.newRule.impossible.noCapability')} {!!refusal.explanation && {refusal.explanation}} - {!!refusalSuggestedPrompt && ( - <> - {refusalSuggestedPrompt} - - - )} + {!!refusalSuggestedPrompt && {refusalSuggestedPrompt}} )} diff --git a/tests/ui/DescribeRuleModalTest.tsx b/tests/ui/DescribeRuleModalTest.tsx index da2ad7202fec..dc61eb555d76 100644 --- a/tests/ui/DescribeRuleModalTest.tsx +++ b/tests/ui/DescribeRuleModalTest.tsx @@ -22,6 +22,7 @@ import {translateLocal} from '../utils/TestHelper'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; let mockPrompt = ''; +let mockIsSubmitButtonNeutral: boolean | undefined; jest.mock('@react-navigation/native', () => { const actualNavigation = jest.requireActual('@react-navigation/native'); @@ -77,17 +78,32 @@ jest.mock('@components/Form/FormProvider', () => { Pressable: React.ComponentType<{accessibilityLabel?: string; onPress?: () => void; children?: React.ReactNode}>; Text: React.ComponentType<{children?: React.ReactNode}>; }>('react-native'); - return ({children, onSubmit, submitButtonText}: {children: React.ReactNode; onSubmit: (values: {prompt: string}) => void; submitButtonText: string}) => - ReactModule.createElement( + return ({ + children, + onSubmit, + submitButtonText, + footerContent, + isSubmitButtonNeutral, + }: { + children: React.ReactNode; + onSubmit: (values: {prompt: string}) => void; + submitButtonText: string; + footerContent?: React.ReactNode; + isSubmitButtonNeutral?: boolean; + }) => { + mockIsSubmitButtonNeutral = isSubmitButtonNeutral; + return ReactModule.createElement( ReactModule.Fragment, null, children, + footerContent, ReactModule.createElement( Pressable, {accessibilityLabel: submitButtonText, onPress: () => onSubmit({prompt: mockPrompt})}, ReactModule.createElement(Text, null, submitButtonText), ), ); + }; }); const POLICY_ID = 'policy1'; @@ -263,6 +279,10 @@ describe('DescribeRuleModal', () => { expect(screen.getByText(IMPOSSIBLE_ANSWER.explanation)).toBeOnTheScreen(); expect(screen.getByText(IMPOSSIBLE_ANSWER.suggestedPrompt)).toBeOnTheScreen(); + // And Try this instead is the suggested action, so Next turns neutral + expect(screen.getByText('workspace.rules.newRule.impossible.tryThisInstead')).toBeOnTheScreen(); + expect(mockIsSubmitButtonNeutral).toBe(true); + // And the form stops loading without an error, because the answer is shown in the form const form = await getOnyxValue(ONYXKEYS.FORMS.NEW_RULE_PROMPT_FORM); expect(form?.isLoading).toBe(false); @@ -308,5 +328,8 @@ describe('DescribeRuleModal', () => { // Then the modal shows the general title and no Try this instead button, because there is nothing to try expect(screen.getByText('workspace.rules.newRule.impossible.noCapability')).toBeOnTheScreen(); expect(screen.queryByText('workspace.rules.newRule.impossible.tryThisInstead')).toBeNull(); + + // And Next stays the suggested action + expect(mockIsSubmitButtonNeutral).toBe(false); }); }); From 97068b76d342700b40910e8bd05503ef45cbcfaa Mon Sep 17 00:00:00 2001 From: Yuwen Memon Date: Sun, 25 Oct 2026 15:34:47 -0700 Subject: [PATCH 2/5] style(rules): put Next and Try this instead side by side --- src/pages/workspace/rules/DescribeRuleForm.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/rules/DescribeRuleForm.tsx b/src/pages/workspace/rules/DescribeRuleForm.tsx index 8e98bb2ce020..749ccf7b73cf 100644 --- a/src/pages/workspace/rules/DescribeRuleForm.tsx +++ b/src/pages/workspace/rules/DescribeRuleForm.tsx @@ -305,7 +305,9 @@ function DescribeRuleForm({ submitFlexEnabled={submitFlexEnabled} shouldHideFixErrorsAlert isSubmitButtonNeutral={!!refusalSuggestedPrompt} - shouldRenderFooterAboveSubmit + submitButtonAndFooterContainerStyles={refusalSuggestedPrompt ? [styles.flexRow, styles.gap2] : undefined} + // The form puts a bottom margin on the submit button when the footer follows it. Side by side, the buttons must line up. + submitButtonInnerStyles={refusalSuggestedPrompt ? [styles.flex1, styles.mb0] : undefined} footerContent={ !!refusalSuggestedPrompt && ( ) } @@ -365,8 +372,8 @@ function DescribeRuleForm({ {!!refusal && ( {translate(IMPOSSIBLE_TITLE_BY_REASON[refusal.reason ?? ''] ?? 'workspace.rules.newRule.impossible.noCapability')} - {!!refusal.explanation && {refusal.explanation}} - {!!refusalSuggestedPrompt && {refusalSuggestedPrompt}} + {!!refusalDetail && {refusalDetail}} + {!!refusalSuggestedPrompt && {translate('workspace.rules.newRule.impossible.quotedSuggestion', refusalSuggestedPrompt)}} )} diff --git a/tests/ui/DescribeRuleModalTest.tsx b/tests/ui/DescribeRuleModalTest.tsx index fdc5532e1683..8e2d2705b425 100644 --- a/tests/ui/DescribeRuleModalTest.tsx +++ b/tests/ui/DescribeRuleModalTest.tsx @@ -35,7 +35,7 @@ jest.mock('@react-navigation/native', () => { }); jest.mock('@hooks/useLocalize', () => jest.fn(() => ({ - translate: (key: string) => key, + translate: (key: string, ...params: unknown[]) => [key, ...params].join(' '), })), ); jest.mock('@hooks/useResponsiveLayout', () => jest.fn(() => ({isSmallScreenWidth: false, shouldUseNarrowLayout: false}))); @@ -276,11 +276,11 @@ describe('DescribeRuleModal', () => { expect(onRuleGenerated).not.toHaveBeenCalled(); expect(onClose).not.toHaveBeenCalled(); expect(screen.getByText('workspace.rules.newRule.impossible.noCapability')).toBeOnTheScreen(); - expect(screen.getByText(IMPOSSIBLE_ANSWER.explanation)).toBeOnTheScreen(); - expect(screen.getByText(IMPOSSIBLE_ANSWER.suggestedPrompt)).toBeOnTheScreen(); + expect(screen.getByText(`workspace.rules.newRule.impossible.explanationWithSuggestion ${IMPOSSIBLE_ANSWER.explanation}`)).toBeOnTheScreen(); + expect(screen.getByText(`workspace.rules.newRule.impossible.quotedSuggestion ${IMPOSSIBLE_ANSWER.suggestedPrompt}`)).toBeOnTheScreen(); - // And Try this instead is the suggested action, so Next turns neutral - expect(screen.getByText('workspace.rules.newRule.impossible.tryThisInstead')).toBeOnTheScreen(); + // And Use suggestion is the suggested action, so Next turns neutral + expect(screen.getByText('workspace.rules.newRule.impossible.useSuggestion')).toBeOnTheScreen(); expect(mockIsSubmitButtonNeutral).toBe(true); // And the submit button keeps Next, because an editor has no agent rule to create anyway @@ -292,15 +292,15 @@ describe('DescribeRuleModal', () => { expect(await getPromptErrors()).toEqual([]); }); - it('describes the suggested rule when the admin taps Try this instead', async () => { + it('describes the suggested rule when the admin taps Use suggestion', async () => { // Given Concierge answered that the description cannot be done and suggested another one const logSpy = jest.spyOn(Log, 'info'); await renderFlagForReviewModal(jest.fn(), jest.fn()); const generationID = describeRule(writeSpy, 'Text the approver when a report is submitted'); await answerGeneration({...IMPOSSIBLE_ANSWER, generationID}); - // When the admin taps Try this instead - fireEvent.press(screen.getByText('workspace.rules.newRule.impossible.tryThisInstead')); + // When the admin taps Use suggestion + fireEvent.press(screen.getByText('workspace.rules.newRule.impossible.useSuggestion')); await waitForBatchedUpdatesWithAct(); // Then Concierge gets the suggested description for the same editor, so the admin does not retype it @@ -310,7 +310,7 @@ describe('DescribeRuleModal', () => { expect(logSpy).toHaveBeenCalledWith('[GenerateRule] Tried the suggested description', false, {policyID: POLICY_ID, generationID, reason: IMPOSSIBLE_ANSWER.reason}); // And the refusal goes away, because it belongs to the earlier description - expect(screen.queryByText(IMPOSSIBLE_ANSWER.explanation)).toBeNull(); + expect(screen.queryByText(`workspace.rules.newRule.impossible.explanationWithSuggestion ${IMPOSSIBLE_ANSWER.explanation}`)).toBeNull(); logSpy.mockRestore(); }); @@ -328,9 +328,9 @@ describe('DescribeRuleModal', () => { suggestedPrompt: '', }); - // Then the modal shows the general title and no Try this instead button, because there is nothing to try + // Then the modal shows the general title and no Use suggestion button, because there is nothing to try expect(screen.getByText('workspace.rules.newRule.impossible.noCapability')).toBeOnTheScreen(); - expect(screen.queryByText('workspace.rules.newRule.impossible.tryThisInstead')).toBeNull(); + expect(screen.queryByText('workspace.rules.newRule.impossible.useSuggestion')).toBeNull(); // And Next stays the suggested action expect(mockIsSubmitButtonNeutral).toBe(false); diff --git a/tests/ui/RulesNewPageDescribeRuleTest.tsx b/tests/ui/RulesNewPageDescribeRuleTest.tsx index 611db5ee6580..006d60ea1bc6 100644 --- a/tests/ui/RulesNewPageDescribeRuleTest.tsx +++ b/tests/ui/RulesNewPageDescribeRuleTest.tsx @@ -50,7 +50,7 @@ jest.mock('@hooks/useLazyAsset', () => ({ })); jest.mock('@hooks/useLocalize', () => jest.fn(() => ({ - translate: (key: string) => key, + translate: (key: string, ...params: unknown[]) => [key, ...params].join(' '), })), ); jest.mock('@hooks/usePermissions', () => @@ -499,7 +499,7 @@ describe('New rule describe section', () => { // Then New rule shows the reason and the suggestion, and nothing opens, because no rule can be drafted expect(screen.getByText('workspace.rules.newRule.impossible.noCapability')).toBeOnTheScreen(); - expect(screen.getByText('Comment on the report when it is submitted')).toBeOnTheScreen(); + expect(screen.getByText('workspace.rules.newRule.impossible.quotedSuggestion Comment on the report when it is submitted')).toBeOnTheScreen(); expect(Navigation.navigate).not.toHaveBeenCalled(); expect(await getOnyxValue(ONYXKEYS.FORMS.ADD_AGENT_RULE_FORM_DRAFT)).toBeUndefined(); }); @@ -519,7 +519,7 @@ describe('New rule describe section', () => { }); const generateRuleRequestCount = writeSpy.mock.calls.filter(([command]) => command === WRITE_COMMANDS.GENERATE_RULE).length; - // When the admin keeps their own description and taps Create anyway, the button that replaces Next next to Try this instead + // When the admin keeps their own description and taps Create anyway, the button that replaces Next next to Use suggestion fireEvent.press(screen.getByLabelText('workspace.rules.newRule.impossible.createAnyway')); await waitForBatchedUpdatesWithAct(); From fa47a52555968dca310ae3c18e99c2165ec8a3f4 Mon Sep 17 00:00:00 2001 From: Yuwen Memon Date: Tue, 27 Oct 2026 23:18:08 -0700 Subject: [PATCH 5/5] style(rules): drop the refusal header and lead with the explanation --- src/languages/de.ts | 1 - src/languages/el.ts | 1 - src/languages/en.ts | 1 - src/languages/es.ts | 1 - src/languages/fr.ts | 1 - src/languages/it.ts | 1 - src/languages/ja.ts | 1 - src/languages/nl.ts | 1 - src/languages/pl.ts | 1 - src/languages/pt-BR.ts | 1 - src/languages/zh-hans.ts | 1 - src/pages/workspace/rules/DescribeRuleForm.tsx | 13 +++++-------- tests/ui/DescribeRuleModalTest.tsx | 9 +++++---- tests/ui/RulesNewPageDescribeRuleTest.tsx | 2 +- 14 files changed, 11 insertions(+), 24 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 610470618198..2c58e1d74938 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -8953,7 +8953,6 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc retroactive: 'Regeln gelten nur für neue Aktivitäten.', historyLimit: 'Regeln können nicht so weit zurückblicken.', useSuggestion: 'Vorschlag verwenden', - trySuggestionBelow: 'Probieren Sie stattdessen den Vorschlag unten aus:', explanationWithSuggestion: (explanation: string) => `${explanation} Probieren Sie stattdessen den Vorschlag unten aus:`, quotedSuggestion: (suggestedPrompt: string) => `„${suggestedPrompt}“`, createAnyway: 'Trotzdem erstellen', diff --git a/src/languages/el.ts b/src/languages/el.ts index 996faec5a5ee..c031aef06872 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -9042,7 +9042,6 @@ ${reportName}`, retroactive: 'Οι κανόνες ισχύουν μόνο για νέα δραστηριότητα.', historyLimit: 'Οι κανόνες δεν μπορούν να ανατρέξουν τόσο πίσω.', useSuggestion: 'Χρήση πρότασης', - trySuggestionBelow: 'Δοκιμάστε αντ’ αυτού την παρακάτω πρόταση:', explanationWithSuggestion: (explanation: string) => `${explanation} Δοκιμάστε αντ’ αυτού την παρακάτω πρόταση:`, quotedSuggestion: (suggestedPrompt: string) => `«${suggestedPrompt}»`, createAnyway: 'Δημιουργία ούτως ή άλλως', diff --git a/src/languages/en.ts b/src/languages/en.ts index 58ef53ba53be..48f11315ff9e 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -9317,7 +9317,6 @@ const translations = { retroactive: 'Rules only apply to new activity.', historyLimit: "Rules can't look back that far.", useSuggestion: 'Use suggestion', - trySuggestionBelow: 'Try the suggestion below instead:', explanationWithSuggestion: (explanation: string) => `${explanation} Try the suggestion below instead:`, quotedSuggestion: (suggestedPrompt: string) => `"${suggestedPrompt}"`, createAnyway: 'Create anyway', diff --git a/src/languages/es.ts b/src/languages/es.ts index 9bb36eead68c..d4934ea205ad 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -8961,7 +8961,6 @@ ${reportName}`, retroactive: 'Las reglas solo se aplican a la actividad nueva.', historyLimit: 'Las reglas no pueden mirar tan atrás.', useSuggestion: 'Usar sugerencia', - trySuggestionBelow: 'Prueba en su lugar la sugerencia de abajo:', explanationWithSuggestion: (explanation: string) => `${explanation} Prueba en su lugar la sugerencia de abajo:`, quotedSuggestion: (suggestedPrompt: string) => `"${suggestedPrompt}"`, createAnyway: 'Crear de todos modos', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 39bd355bfc0d..e21bc36b89c1 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -8976,7 +8976,6 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e retroactive: 'Les règles ne s’appliquent qu’aux nouvelles activités.', historyLimit: 'Les règles ne peuvent pas remonter aussi loin dans le temps.', useSuggestion: 'Utiliser la suggestion', - trySuggestionBelow: 'Essayez plutôt la suggestion ci-dessous :', explanationWithSuggestion: (explanation: string) => `${explanation} Essayez plutôt la suggestion ci-dessous :`, quotedSuggestion: (suggestedPrompt: string) => `« ${suggestedPrompt} »`, createAnyway: 'Créer quand même', diff --git a/src/languages/it.ts b/src/languages/it.ts index b067dd5cbf77..968b664dd48c 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -8907,7 +8907,6 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, retroactive: 'Le regole si applicano solo alla nuova attività.', historyLimit: 'Le regole non possono risalire così indietro.', useSuggestion: 'Usa il suggerimento', - trySuggestionBelow: 'Prova invece il suggerimento qui sotto:', explanationWithSuggestion: (explanation: string) => `${explanation} Prova invece il suggerimento qui sotto:`, quotedSuggestion: (suggestedPrompt: string) => `"${suggestedPrompt}"`, createAnyway: 'Crea comunque', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 5ce2651a5105..3c5d4a5eee73 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -8790,7 +8790,6 @@ ${reportName}`, retroactive: 'ルールは新しいアクティビティにのみ適用されます。', historyLimit: 'ルールはそれほど前までさかのぼることができません。', useSuggestion: '提案を使用', - trySuggestionBelow: '代わりに以下の提案をお試しください:', explanationWithSuggestion: (explanation: string) => `${explanation}代わりに以下の提案をお試しください:`, quotedSuggestion: (suggestedPrompt: string) => `「${suggestedPrompt}」`, createAnyway: 'このまま作成', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index da32de21baa4..edffbf3652e5 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -8879,7 +8879,6 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, retroactive: 'Regels zijn alleen van toepassing op nieuwe activiteit.', historyLimit: 'Regels kunnen niet zo ver terugkijken.', useSuggestion: 'Suggestie gebruiken', - trySuggestionBelow: 'Probeer in plaats daarvan de suggestie hieronder:', explanationWithSuggestion: (explanation: string) => `${explanation} Probeer in plaats daarvan de suggestie hieronder:`, quotedSuggestion: (suggestedPrompt: string) => `‘${suggestedPrompt}’`, createAnyway: 'Toch maken', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 36a817967593..16142fbe3451 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -8906,7 +8906,6 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, retroactive: 'Zasady mają zastosowanie tylko do nowej aktywności.', historyLimit: 'Reguły nie mogą sięgać tak daleko wstecz.', useSuggestion: 'Użyj sugestii', - trySuggestionBelow: 'Zamiast tego wypróbuj poniższą sugestię:', explanationWithSuggestion: (explanation: string) => `${explanation} Zamiast tego wypróbuj poniższą sugestię:`, quotedSuggestion: (suggestedPrompt: string) => `„${suggestedPrompt}”`, createAnyway: 'Utwórz mimo to', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 21817aa0c1ce..366882fefb12 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -8885,7 +8885,6 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, retroactive: 'As regras se aplicam apenas à nova atividade.', historyLimit: 'As regras não conseguem olhar tão para trás.', useSuggestion: 'Usar sugestão', - trySuggestionBelow: 'Experimente a sugestão abaixo:', explanationWithSuggestion: (explanation: string) => `${explanation} Experimente a sugestão abaixo:`, quotedSuggestion: (suggestedPrompt: string) => `"${suggestedPrompt}"`, createAnyway: 'Criar mesmo assim', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 690755dae095..0ab8aed62f12 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -8570,7 +8570,6 @@ ${reportName}`, retroactive: '规则仅适用于新活动。', historyLimit: '规则无法追溯到那么久以前。', useSuggestion: '使用建议', - trySuggestionBelow: '请改为尝试下面的建议:', explanationWithSuggestion: (explanation: string) => `${explanation}请改为尝试下面的建议:`, quotedSuggestion: (suggestedPrompt: string) => `“${suggestedPrompt}”`, createAnyway: '仍然创建', diff --git a/src/pages/workspace/rules/DescribeRuleForm.tsx b/src/pages/workspace/rules/DescribeRuleForm.tsx index a9048380ef20..0e1fd6b6eb44 100644 --- a/src/pages/workspace/rules/DescribeRuleForm.tsx +++ b/src/pages/workspace/rules/DescribeRuleForm.tsx @@ -166,12 +166,10 @@ function DescribeRuleForm({ const refusal = generatedRuleState === CONST.GENERATED_RULE.STATE.IMPOSSIBLE && !!generatedRuleID && generatedRuleID === submittedGenerationID ? generatedRule : undefined; const refusalSuggestedPrompt = refusal?.suggestedPrompt; const canCreateAnyway = !!refusalSuggestedPrompt && !!onAgentRuleFallback; - let refusalDetail = refusal?.explanation; - if (refusalSuggestedPrompt) { - refusalDetail = refusal?.explanation - ? translate('workspace.rules.newRule.impossible.explanationWithSuggestion', refusal.explanation) - : translate('workspace.rules.newRule.impossible.trySuggestionBelow'); - } + // An answer without an explanation still says why, with the general text for its reason + const refusalExplanation = + refusal && (refusal.explanation ? refusal.explanation : translate(IMPOSSIBLE_TITLE_BY_REASON[refusal.reason ?? ''] ?? 'workspace.rules.newRule.impossible.noCapability')); + const refusalDetail = refusalExplanation && refusalSuggestedPrompt ? translate('workspace.rules.newRule.impossible.explanationWithSuggestion', refusalExplanation) : refusalExplanation; const inputLabel = translate('workspace.rules.newRule.describeRuleInputLabel'); const finish = () => { @@ -372,8 +370,7 @@ function DescribeRuleForm({ {!!refusal && ( - {translate(IMPOSSIBLE_TITLE_BY_REASON[refusal.reason ?? ''] ?? 'workspace.rules.newRule.impossible.noCapability')} - {!!refusalDetail && {refusalDetail}} + {!!refusalDetail && {refusalDetail}} {!!refusalSuggestedPrompt && {translate('workspace.rules.newRule.impossible.quotedSuggestion', refusalSuggestedPrompt)}} )} diff --git a/tests/ui/DescribeRuleModalTest.tsx b/tests/ui/DescribeRuleModalTest.tsx index d3a3f8195d04..781e8a7817ad 100644 --- a/tests/ui/DescribeRuleModalTest.tsx +++ b/tests/ui/DescribeRuleModalTest.tsx @@ -282,8 +282,8 @@ describe('DescribeRuleModal', () => { // Then the modal stays open with the reason, the explanation and the suggestion, and nothing reaches the editor form expect(onRuleGenerated).not.toHaveBeenCalled(); expect(onClose).not.toHaveBeenCalled(); - expect(screen.getByText('workspace.rules.newRule.impossible.noCapability')).toBeOnTheScreen(); expect(screen.getByText(`workspace.rules.newRule.impossible.explanationWithSuggestion ${IMPOSSIBLE_ANSWER.explanation}`)).toBeOnTheScreen(); + expect(screen.queryByText('workspace.rules.newRule.impossible.noCapability')).toBeNull(); expect(screen.getByText(`workspace.rules.newRule.impossible.quotedSuggestion ${IMPOSSIBLE_ANSWER.suggestedPrompt}`)).toBeOnTheScreen(); // And Use suggestion is the suggested action, so Next turns neutral @@ -347,21 +347,22 @@ describe('DescribeRuleModal', () => { expect(screen.getByText(`workspace.rules.newRule.impossible.explanationWithSuggestion ${IMPOSSIBLE_ANSWER.explanation}`)).toBeOnTheScreen(); }); - it('shows the general title and no suggestion when the reason is unknown and nothing is suggested', async () => { + it('shows the general text and no suggestion when the reason is unknown and nothing is explained or suggested', async () => { // Given the flag for review editor asked for a rule await renderFlagForReviewModal(jest.fn(), jest.fn()); const generationID = describeRule(writeSpy, 'Text the approver when a report is submitted'); - // When Concierge answers with a reason that this app version does not know, and with no suggestion + // When Concierge answers with a reason that this app version does not know, with no explanation and no suggestion await answerGeneration({ ...IMPOSSIBLE_ANSWER, generationID, + explanation: '', // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- simulates an answer from a server that knows more reasons than this app version reason: 'futureReason' as GeneratedRuleImpossibleReason, suggestedPrompt: '', }); - // Then the modal shows the general title and no Use suggestion button, because there is nothing to try + // Then the modal still says why with the general text, and shows no Use suggestion button, because there is nothing to try expect(screen.getByText('workspace.rules.newRule.impossible.noCapability')).toBeOnTheScreen(); expect(screen.queryByText('workspace.rules.newRule.impossible.useSuggestion')).toBeNull(); diff --git a/tests/ui/RulesNewPageDescribeRuleTest.tsx b/tests/ui/RulesNewPageDescribeRuleTest.tsx index e451dc6edb27..2408fecddde1 100644 --- a/tests/ui/RulesNewPageDescribeRuleTest.tsx +++ b/tests/ui/RulesNewPageDescribeRuleTest.tsx @@ -463,7 +463,7 @@ describe('New rule describe section', () => { }); // Then New rule shows the reason and the suggestion, and nothing opens, because no rule can be drafted - expect(screen.getByText('workspace.rules.newRule.impossible.noCapability')).toBeOnTheScreen(); + expect(screen.getByText('workspace.rules.newRule.impossible.explanationWithSuggestion Rules cannot send text messages.')).toBeOnTheScreen(); expect(screen.getByText('workspace.rules.newRule.impossible.quotedSuggestion Comment on the report when it is submitted')).toBeOnTheScreen(); expect(Navigation.navigate).not.toHaveBeenCalled(); expect(await getOnyxValue(ONYXKEYS.FORMS.ADD_AGENT_RULE_FORM_DRAFT)).toBeUndefined();