diff --git a/packages/app-builder/src/components/Scenario/Sanction/FieldNode.tsx b/packages/app-builder/src/components/Scenario/Sanction/FieldNode.tsx index 517afe6a1..c0cb0fc75 100644 --- a/packages/app-builder/src/components/Scenario/Sanction/FieldNode.tsx +++ b/packages/app-builder/src/components/Scenario/Sanction/FieldNode.tsx @@ -3,20 +3,17 @@ import { type AstNode, NewUndefinedAstNode } from '@app-builder/models'; import { MatchOperand } from './MatchOperand'; export const FieldNode = ({ - name, value, placeholder, onChange, onBlur, }: { value?: AstNode; - name?: string; onChange?: (value: AstNode) => void; onBlur?: () => void; placeholder?: string; }) => ( - <> - +
- +
); diff --git a/packages/app-builder/src/components/Scenario/Sanction/FieldNodeConcat.tsx b/packages/app-builder/src/components/Scenario/Sanction/FieldNodeConcat.tsx index 1d5bc3fa0..7f3f0793d 100644 --- a/packages/app-builder/src/components/Scenario/Sanction/FieldNodeConcat.tsx +++ b/packages/app-builder/src/components/Scenario/Sanction/FieldNodeConcat.tsx @@ -7,8 +7,7 @@ import { Icon } from 'ui-icons'; import { MatchOperand } from './MatchOperand'; -export function FieldMatches({ - name, +export function FieldNodeConcat({ value, limit, onBlur, @@ -18,7 +17,6 @@ export function FieldMatches({ value?: AstNode; limit?: number; placeholder?: string; - name?: string; onChange?: (nodes: AstNode) => void; onBlur?: () => void; }) { @@ -41,14 +39,15 @@ export function FieldMatches({ onChange?.({ name: 'StringConcat', children: values(nodes), - namedChildren: {}, + namedChildren: { + withSeparator: { constant: true, namedChildren: {}, children: [] }, + }, }); } }, [nodes, matches.length, onChange]); return ( -
- +
{(matches.length === 0 ? defaultMatches : matches).map( ([id, match], i) => (
diff --git a/packages/app-builder/src/components/Scenario/Sanction/FieldSanction.tsx b/packages/app-builder/src/components/Scenario/Sanction/FieldSanction.tsx index 6cfbc6d87..877a5154e 100644 --- a/packages/app-builder/src/components/Scenario/Sanction/FieldSanction.tsx +++ b/packages/app-builder/src/components/Scenario/Sanction/FieldSanction.tsx @@ -8,11 +8,12 @@ import { useMemo, useState, } from 'react'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Checkbox, CollapsibleV2 } from 'ui-design-system'; import { Icon } from 'ui-icons'; -const FieldCategory = ({ +const FieldCategory = memo(function FieldCategory({ section, selectedIds, updateSelectedIds, @@ -20,8 +21,8 @@ const FieldCategory = ({ section: OpenSanctionsCatalogSection; selectedIds: string[]; updateSelectedIds: Dispatch>; -}) => { - const { t } = useTranslation(['common']); +}) { + const { t } = useTranslation(['common', 'scenarios']); const [open, setOpen] = useState(false); const defaultListIds = useMemo( @@ -34,6 +35,11 @@ const FieldCategory = ({ [selectedIds, defaultListIds], ); + const nbSelected = useMemo( + () => selectedIds.filter((id) => defaultListIds.includes(id)).length, + [selectedIds, defaultListIds], + ); + return (
@@ -49,6 +55,13 @@ const FieldCategory = ({ })} /> {section.title} + {!isAllSelected && nbSelected ? ( + + {t('scenarios:sanction.lists.nb_selected', { + count: nbSelected, + })} + + ) : null}
@@ -89,10 +102,9 @@ const FieldCategory = ({
); -}; +}); export const FieldSanction = ({ - name, onChange, onBlur, sections, @@ -100,7 +112,6 @@ export const FieldSanction = ({ }: { defaultValue?: string[]; sections: OpenSanctionsCatalogSection[]; - name?: string; onChange?: (value: string[]) => void; onBlur?: () => void; }) => { @@ -113,8 +124,7 @@ export const FieldSanction = ({ }, [selectedIds, onChange]); return ( -
- +
{sections.map((section) => ( void; onBlur?: () => void; @@ -44,9 +42,8 @@ export const FieldTrigger = ({ }, [astNode, onChange]); return ( - <> - +
- +
); }; diff --git a/packages/app-builder/src/components/Scenario/Sanction/MatchOperand.tsx b/packages/app-builder/src/components/Scenario/Sanction/MatchOperand.tsx index f3c8f659d..583ddc271 100644 --- a/packages/app-builder/src/components/Scenario/Sanction/MatchOperand.tsx +++ b/packages/app-builder/src/components/Scenario/Sanction/MatchOperand.tsx @@ -3,10 +3,11 @@ import { useGetAstNodeOperandProps, useOperandOptions, } from '@app-builder/services/editor/options'; +import { memo } from 'react'; import { Operand } from '../AstBuilder/AstBuilderNode/Operand'; -export const MatchOperand = ({ +export const MatchOperand = memo(function MatchOperand({ node, onSave, placeholder, @@ -14,7 +15,7 @@ export const MatchOperand = ({ node: AstNode; onSave?: (astNode: AstNode) => void; placeholder?: string; -}) => { +}) { const getOperandAstNodeOperandProps = useGetAstNodeOperandProps(); const options = useOperandOptions(); @@ -27,4 +28,4 @@ export const MatchOperand = ({ onSave={onSave} /> ); -}; +}); diff --git a/packages/app-builder/src/locales/ar/scenarios.json b/packages/app-builder/src/locales/ar/scenarios.json index b17c18c43..552064075 100644 --- a/packages/app-builder/src/locales/ar/scenarios.json +++ b/packages/app-builder/src/locales/ar/scenarios.json @@ -393,5 +393,7 @@ "sanction.nudge": "تحسين قواعدك من خلال فحص العقوبات بناءً على واجهة برمجة تطبيقات OpenSacntion", "sanction_counterparty_id": "معرف الطرف المقابل", "sanction_counterparty_name": "اسم الطرف المقابل", - "sanction_transaction_label": "تسمية المعاملة" + "sanction_transaction_label": "تسمية المعاملة", + "sanction.lists.nb_selected_one": "({{count}} المحدد)", + "sanction.lists.nb_selected_other": "({{count}} المحدد)" } diff --git a/packages/app-builder/src/locales/en/scenarios.json b/packages/app-builder/src/locales/en/scenarios.json index c0c932f09..9bba4f029 100644 --- a/packages/app-builder/src/locales/en/scenarios.json +++ b/packages/app-builder/src/locales/en/scenarios.json @@ -182,6 +182,8 @@ "sanction.match_settings.callout": "Choose information that should be checked.", "sanction.lists.title": "Sanction lists", "sanction.lists.callout": "Select lists that are relevant to your business", + "sanction.lists.nb_selected_one": "({{count}} selected)", + "sanction.lists.nb_selected_other": "({{count}} selected)", "sanction_transaction_label": "Transaction Label", "sanction_counterparty_id": "Counterparty ID", "sanction_counterparty_name": "Counterparty name", diff --git a/packages/app-builder/src/locales/fr/scenarios.json b/packages/app-builder/src/locales/fr/scenarios.json index 76c8d954b..7d935dad7 100644 --- a/packages/app-builder/src/locales/fr/scenarios.json +++ b/packages/app-builder/src/locales/fr/scenarios.json @@ -393,5 +393,7 @@ "sanction.nudge": "Améliorez vos règles avec un chèque de sanction basé sur l'OpenSacntion API", "sanction_counterparty_id": "Identifiant de contrepartie", "sanction_counterparty_name": "Nom de contrepartie", - "sanction_transaction_label": "Étiquette de transaction" + "sanction_transaction_label": "Étiquette de transaction", + "sanction.lists.nb_selected_one": "({{count}} sélectionnée)", + "sanction.lists.nb_selected_other": "({{count}} sélectionnées)" } diff --git a/packages/app-builder/src/routes/_builder+/scenarios+/$scenarioId+/i+/$iterationId+/sanction.tsx b/packages/app-builder/src/routes/_builder+/scenarios+/$scenarioId+/i+/$iterationId+/sanction.tsx index 83816097f..7be322bfd 100644 --- a/packages/app-builder/src/routes/_builder+/scenarios+/$scenarioId+/i+/$iterationId+/sanction.tsx +++ b/packages/app-builder/src/routes/_builder+/scenarios+/$scenarioId+/i+/$iterationId+/sanction.tsx @@ -11,7 +11,7 @@ import { FormLabel } from '@app-builder/components/Form/Tanstack/FormLabel'; import { setToastMessage } from '@app-builder/components/MarbleToaster'; import { type AstBuilderProps } from '@app-builder/components/Scenario/AstBuilder'; import { FieldNode } from '@app-builder/components/Scenario/Sanction/FieldNode'; -import { FieldMatches } from '@app-builder/components/Scenario/Sanction/FieldNodeConcat'; +import { FieldNodeConcat } from '@app-builder/components/Scenario/Sanction/FieldNodeConcat'; import { FieldOutcomes } from '@app-builder/components/Scenario/Sanction/FieldOutcomes'; import { FieldRuleGroup } from '@app-builder/components/Scenario/Sanction/FieldRuleGroup'; import { FieldSanction } from '@app-builder/components/Scenario/Sanction/FieldSanction'; @@ -27,7 +27,6 @@ import { OptionsProvider } from '@app-builder/services/editor/options'; import { serverServices } from '@app-builder/services/init.server'; import { getRoute } from '@app-builder/utils/routes'; import { fromParams, fromUUID, useParam } from '@app-builder/utils/short-uuid'; -import { parseWithZod } from '@conform-to/zod'; import { type ActionFunctionArgs, json, @@ -35,6 +34,7 @@ import { } from '@remix-run/node'; import { useFetcher, useLoaderData } from '@remix-run/react'; import { useForm } from '@tanstack/react-form'; +import { decode as formDataToObject } from 'decode-formdata'; import { type Namespace } from 'i18next'; import { serialize as objectToFormData } from 'object-to-formdata'; import { useMemo } from 'react'; @@ -126,7 +126,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const editSanctionFormSchema = z.object({ name: z.string().nonempty(), description: z.string().optional(), - scoreModifier: z.number(), + scoreModifier: z.coerce.number(), ruleGroup: z.string().nonempty(), forceOutcome: z.union([ z.literal('review'), @@ -150,31 +150,32 @@ export async function action({ request, params }: ActionFunctionArgs) { toastSessionService: { getSession, commitSession }, } = serverServices; - const session = await getSession(request); - const formData = await request.formData(); - const iterationId = fromParams(params, 'iterationId'); - - const submission = parseWithZod(formData, { schema: editSanctionFormSchema }); + const [session, formData, { scenarioIterationSanctionRepository }] = + await Promise.all([ + getSession(request), + request.formData(), + authService.isAuthenticated(request, { + failureRedirect: getRoute('/sign-in'), + }), + ]); - if (submission.status !== 'success') { - return json(submission.reply()); - } - - const { scenarioIterationSanctionRepository } = - await authService.isAuthenticated(request, { - failureRedirect: getRoute('/sign-in'), - }); + const iterationId = fromParams(params, 'iterationId'); + const formDataDecoded = formDataToObject(formData, { + arrays: ['datasets'], + }); try { + const data = editSanctionFormSchema.parse(formDataDecoded); + await scenarioIterationSanctionRepository.upsertSanctionCheckConfig({ iterationId, changes: { - ...submission.value, - counterPartyId: submission.value.counterPartyId as AstNode | undefined, - triggerRule: submission.value.triggerRule as AstNode | undefined, + ...data, + counterPartyId: data.counterPartyId as AstNode | undefined, + triggerRule: data.triggerRule as AstNode | undefined, query: { - name: submission.value.query.name as AstNode, - label: submission.value.query.label as AstNode | undefined, + name: data.query.name as AstNode, + label: data.query.label as AstNode | undefined, }, }, }); @@ -455,7 +456,6 @@ export default function SanctionDetail() { options={options} onBlur={field.handleBlur} onChange={field.handleChange} - name={field.name} trigger={field.state.value} /> )} @@ -482,7 +482,6 @@ export default function SanctionDetail() { {t('scenarios:sanction_counterparty_id')} {t('scenarios:sanction_counterparty_name')} - (
; @@ -44,24 +44,22 @@ export async function action({ request }: LoaderFunctionArgs) { toastSessionService: { getSession, commitSession }, } = serverServices; - const session = await getSession(request); - const formData = await request.formData(); + const [session, formData, { organization: repository }] = await Promise.all([ + getSession(request), + request.formData(), + authService.isAuthenticated(request, { + failureRedirect: getRoute('/sign-in'), + }), + ]); - const submission = parseWithZod(formData, { schema: editOrganizationSchema }); - - if (submission.status !== 'success') { - return json(submission.reply()); - } - - const { organization: repository } = await authService.isAuthenticated( - request, - { failureRedirect: getRoute('/sign-in') }, - ); + const formDataDecoded = formDataToObject(formData); try { + const data = editOrganizationSchema.parse(formDataDecoded); + await repository.updateOrganization({ - organizationId: submission.value.organizationId, - changes: submission.value, + organizationId: data.organizationId, + changes: data, }); setToastMessage(session, {