Skip to content

Commit 97c5aaa

Browse files
committed
feat: undefined value if no nodes selected + query validation error
1 parent f42be94 commit 97c5aaa

File tree

7 files changed

+119
-98
lines changed

7 files changed

+119
-98
lines changed

packages/app-builder/src/components/Scenario/Sanction/FieldNode.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,19 @@ export const FieldNode = ({
1111
onBlur,
1212
}: {
1313
value?: AstNode;
14-
onChange?: (value?: AstNode) => void;
14+
onChange?: (value: AstNode | null) => void;
1515
onBlur?: () => void;
1616
placeholder?: string;
1717
viewOnly?: boolean;
1818
}) => (
1919
<div onBlur={onBlur}>
2020
<MatchOperand
2121
viewOnly={viewOnly}
22-
node={value ?? NewUndefinedAstNode()}
22+
node={value}
2323
placeholder={placeholder}
2424
onSave={(node) => {
2525
onChange?.(
26-
hasSubObject(NewUndefinedAstNode() as AstNode, node)
27-
? undefined
28-
: node,
26+
hasSubObject(NewUndefinedAstNode() as AstNode, node) ? null : node,
2927
);
3028
}}
3129
/>

packages/app-builder/src/components/Scenario/Sanction/FieldNodeConcat.tsx

Lines changed: 28 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export function FieldNodeConcat({
3737
value?: AstNode;
3838
limit?: number;
3939
placeholder?: string;
40-
onChange?: (node?: AstNode) => void;
40+
onChange?: (node: AstNode | null) => void;
4141
onBlur?: () => void;
4242
viewOnly?: boolean;
4343
}) {
@@ -48,21 +48,18 @@ export function FieldNodeConcat({
4848
);
4949

5050
useEffect(() => {
51-
if (nodes.length) {
52-
const finalNodes = nodes.filter(
53-
(n) => !hasSubObject(NewUndefinedAstNode() as AstNode, omit(n, ['id'])),
54-
);
51+
const finalNodes = nodes.filter(
52+
(n) => !hasSubObject(NewUndefinedAstNode() as AstNode, omit(n, ['id'])),
53+
);
5554

56-
onChange?.(
57-
finalNodes.length !== 0
58-
? NewStringConcatAstNode(finalNodes.map(omit(['id'])), {
59-
withSeparator: true,
60-
})
61-
: undefined,
62-
);
63-
} else {
64-
onChange?.(undefined);
65-
}
55+
const result =
56+
finalNodes.length !== 0
57+
? NewStringConcatAstNode(finalNodes.map(omit(['id'])), {
58+
withSeparator: true,
59+
})
60+
: null;
61+
62+
onChange?.(result);
6663
}, [nodes, onChange]);
6764

6865
const onDragEnd: OnDragEndResponder<string> = (result): void => {
@@ -107,27 +104,23 @@ export function FieldNodeConcat({
107104
>
108105
{!viewOnly ? (
109106
<div className="flex flex-row">
107+
<div
108+
key={node.id}
109+
className="hover:bg-grey-95 flex size-6 items-center justify-center rounded"
110+
{...dragProvided.dragHandleProps}
111+
>
112+
<Icon icon="drag" className="text-grey-80 size-3" />
113+
</div>
110114
{nodes.length > 1 ? (
111-
<>
112-
<div
113-
className="hover:bg-grey-95 flex size-6 items-center justify-center rounded"
114-
{...dragProvided.dragHandleProps}
115-
>
116-
<Icon
117-
icon="drag"
118-
className="text-grey-80 size-3"
119-
/>
120-
</div>
121-
<Button
122-
size="icon"
123-
variant="tertiary"
124-
onClick={() =>
125-
setNodes((prev) => splice(prev, index, 1, []))
126-
}
127-
>
128-
<Icon icon="cross" className="size-4" />
129-
</Button>
130-
</>
115+
<Button
116+
size="icon"
117+
variant="tertiary"
118+
onClick={() =>
119+
setNodes((prev) => splice(prev, index, 1, []))
120+
}
121+
>
122+
<Icon icon="cross" className="size-4" />
123+
</Button>
131124
) : null}
132125
{!limit || nodes.length < limit ? (
133126
<Button

packages/app-builder/src/components/Scenario/Sanction/MatchOperand.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type AstNode } from '@app-builder/models';
1+
import { type AstNode, NewUndefinedAstNode } from '@app-builder/models';
22
import {
33
useGetAstNodeOperandProps,
44
useOperandOptions,
@@ -13,7 +13,7 @@ export const MatchOperand = memo(function MatchOperand({
1313
placeholder,
1414
viewOnly,
1515
}: {
16-
node: AstNode;
16+
node?: AstNode;
1717
onSave?: (astNode: AstNode) => void;
1818
placeholder?: string;
1919
viewOnly?: boolean;
@@ -23,7 +23,7 @@ export const MatchOperand = memo(function MatchOperand({
2323

2424
return (
2525
<Operand
26-
{...getOperandAstNodeOperandProps(node)}
26+
{...getOperandAstNodeOperandProps(node ?? NewUndefinedAstNode())}
2727
placeholder={placeholder}
2828
options={options.filter((o) => o.dataType === 'String')}
2929
validationStatus="valid"

packages/app-builder/src/locales/ar/scenarios.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,5 +423,6 @@
423423
"edit_aggregation.filters_in": "المرشحات في <strong>{{tableName}}</strong>",
424424
"sanction_counterparty_id.tooltip": "اختر قيمة معرف فريدة من نوعها (مثل IBAN ، رقم الهاتف) ، والتي يمكن أن تكون قائمة بيضاء في حالة وجود تطابق كاذب.",
425425
"sanction_counterparty_name.tooltip": "حدد الاسم الأول الاسم الأخير أو الاسم الكامل.",
426-
"sanction_counterparty_id_placeholder": "حدد معرف طرف مقابل فريد"
426+
"sanction_counterparty_id_placeholder": "حدد معرف طرف مقابل فريد",
427+
"sanction.match_settings.no_empty": "تحتاج إلى تحديد اسم أو تسمية واحدة على الأقل"
427428
}

packages/app-builder/src/locales/en/scenarios.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@
196196
"sanction.nudge": "Improve your rules with a sanction check based on OpenSanction API",
197197
"sanction.match_settings.title": "Matching settings",
198198
"sanction.match_settings.callout": "Choose information that should be checked.",
199+
"sanction.match_settings.no_empty": "You need to select at least one name or label",
199200
"sanction.lists.title": "Sanction lists",
200201
"sanction.lists.callout": "Select lists that are relevant to your business",
201202
"sanction.lists.nb_selected_one": "({{count}} selected)",

packages/app-builder/src/locales/fr/scenarios.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,5 +423,6 @@
423423
"edit_aggregation.filter_field_label": "",
424424
"sanction_counterparty_id.tooltip": "Choisissez une valeur d'identifiant unique (comme Iban, numéro de téléphone), qui peut être liste blanche en cas de faux match.",
425425
"sanction_counterparty_name.tooltip": "Sélectionnez le nom de famille du prénom ou le nom complet.",
426-
"sanction_counterparty_id_placeholder": "Sélectionnez un identifiant de contrepartie unique"
426+
"sanction_counterparty_id_placeholder": "Sélectionnez un identifiant de contrepartie unique",
427+
"sanction.match_settings.no_empty": "Vous devez sélectionner au moins un nom ou un étiquette"
427428
}

packages/app-builder/src/routes/_builder+/scenarios+/$scenarioId+/i+/$iterationId+/sanction.tsx

Lines changed: 80 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import {
3636
import { useFetcher, useLoaderData } from '@remix-run/react';
3737
import { useForm } from '@tanstack/react-form';
3838
import { decode as formDataToObject } from 'decode-formdata';
39-
import { type Namespace } from 'i18next';
39+
import { type Namespace, t as rawT } from 'i18next';
4040
import { serialize as objectToFormData } from 'object-to-formdata';
4141
import { Trans, useTranslation } from 'react-i18next';
4242
import { difference } from 'remeda';
@@ -133,8 +133,22 @@ const editSanctionFormSchema = z.object({
133133
z.literal('block_and_review'),
134134
]),
135135
triggerRule: z.any().nullish(),
136-
counterPartyName: z.any().nullish(),
137-
counterPartyLabel: z.any().nullish(),
136+
query: z
137+
.object({
138+
name: z.any().nullish(),
139+
label: z.any().nullish(),
140+
})
141+
.superRefine((arg, ctx) => {
142+
if (!arg.name && !arg.label) {
143+
ctx.addIssue({
144+
code: 'invalid_arguments',
145+
path: ['label'],
146+
message: rawT('scenarios:sanction.match_settings.no_empty'),
147+
argumentsError: rawT('scenarios:sanction.match_settings.no_empty'),
148+
});
149+
}
150+
return true;
151+
}),
138152
counterPartyId: z.any().nullish(),
139153
datasets: z.array(z.string()),
140154
});
@@ -161,22 +175,27 @@ export async function action({ request, params }: ActionFunctionArgs) {
161175
arrays: ['datasets'],
162176
});
163177

164-
try {
165-
console.log('Form Data', formDataDecoded);
166-
167-
const data = editSanctionFormSchema.parse(formDataDecoded);
178+
const result = editSanctionFormSchema.safeParse(formDataDecoded);
168179

169-
console.log('Data', formDataDecoded);
180+
if (!result.success) {
181+
return json(
182+
{ status: 'error', errors: result.error.flatten() },
183+
{
184+
headers: { 'Set-Cookie': await commitSession(session) },
185+
},
186+
);
187+
}
170188

189+
try {
171190
await scenarioIterationSanctionRepository.upsertSanctionCheckConfig({
172191
iterationId,
173192
changes: {
174-
...data,
175-
counterPartyId: data.counterPartyId as AstNode | undefined,
176-
triggerRule: data.triggerRule as AstNode | undefined,
193+
...result.data,
194+
counterPartyId: result.data.counterPartyId as AstNode | undefined,
195+
triggerRule: result.data.triggerRule as AstNode | undefined,
177196
query: {
178-
name: data.counterPartyName as AstNode | undefined,
179-
label: data.counterPartyLabel as AstNode | undefined,
197+
name: result.data.query?.name as AstNode | undefined,
198+
label: result.data.query?.label as AstNode | undefined,
180199
},
181200
},
182201
});
@@ -187,21 +206,19 @@ export async function action({ request, params }: ActionFunctionArgs) {
187206
});
188207

189208
return json(
190-
{ status: 'success' },
209+
{ status: 'success', errors: [] },
191210
{
192211
headers: { 'Set-Cookie': await commitSession(session) },
193212
},
194213
);
195214
} catch (error) {
196-
console.log('Error', error);
197-
198215
setToastMessage(session, {
199216
type: 'error',
200217
messageKey: 'common:errors.unknown',
201218
});
202219

203220
return json(
204-
{ status: 'error' },
221+
{ status: 'error', errors: [] },
205222
{
206223
headers: { 'Set-Cookie': await commitSession(session) },
207224
},
@@ -251,8 +268,10 @@ export default function SanctionDetail() {
251268
(sanctionCheckConfig?.forcedOutcome as SanctionOutcome) ??
252269
'block_and_review',
253270
triggerRule: sanctionCheckConfig?.triggerRule,
254-
counterPartyName: sanctionCheckConfig?.query?.name,
255-
counterPartyLabel: sanctionCheckConfig?.query?.label,
271+
query: {
272+
name: sanctionCheckConfig?.query?.name,
273+
label: sanctionCheckConfig?.query?.label,
274+
},
256275
counterPartyId: sanctionCheckConfig?.counterPartyId,
257276
},
258277
});
@@ -307,8 +326,8 @@ export default function SanctionDetail() {
307326
type="text"
308327
name={field.name}
309328
onBlur={field.handleBlur}
310-
onChange={(e) =>
311-
field.handleChange(e.currentTarget.value)
329+
onChange={({ currentTarget: { value } }) =>
330+
field.handleChange(value)
312331
}
313332
placeholder={t(
314333
'scenarios:edit_rule.name_placeholder',
@@ -439,6 +458,43 @@ export default function SanctionDetail() {
439458
</Collapsible.Content>
440459
</Collapsible.Container>
441460

461+
<Collapsible.Container className="bg-grey-100 max-w-3xl">
462+
<Collapsible.Title className="mb-2">
463+
{t('scenarios:sanction_counterparty_id')}
464+
</Collapsible.Title>
465+
<Collapsible.Content>
466+
<form.Field name="counterPartyId">
467+
{(field) => (
468+
<div className="flex flex-col gap-4">
469+
<FormLabel
470+
className="inline-flex items-center gap-1"
471+
name={field.name}
472+
>
473+
{t('scenarios:sanction_counterparty_id')}
474+
<FieldToolTip>
475+
{t('scenarios:sanction_counterparty_id.tooltip')}
476+
</FieldToolTip>
477+
</FormLabel>
478+
<OptionsProvider {...options}>
479+
<FieldNode
480+
viewOnly={editor === 'view'}
481+
value={field.state.value}
482+
onChange={field.handleChange}
483+
onBlur={field.handleBlur}
484+
placeholder={t(
485+
'scenarios:sanction_counterparty_id_placeholder',
486+
)}
487+
/>
488+
</OptionsProvider>
489+
<FormErrorOrDescription
490+
errors={field.state.meta.errors}
491+
/>
492+
</div>
493+
)}
494+
</form.Field>
495+
</Collapsible.Content>
496+
</Collapsible.Container>
497+
442498
<Collapsible.Container className="bg-grey-100 max-w-3xl">
443499
<Collapsible.Title>
444500
{t('scenarios:sanction.match_settings.title')}
@@ -450,36 +506,7 @@ export default function SanctionDetail() {
450506
</p>
451507
</Callout>
452508
<div className="flex flex-col gap-6">
453-
<form.Field name="counterPartyId">
454-
{(field) => (
455-
<div className="flex flex-col gap-4">
456-
<FormLabel
457-
className="inline-flex items-center gap-1"
458-
name={field.name}
459-
>
460-
{t('scenarios:sanction_counterparty_id')}
461-
<FieldToolTip>
462-
{t('scenarios:sanction_counterparty_id.tooltip')}
463-
</FieldToolTip>
464-
</FormLabel>
465-
<OptionsProvider {...options}>
466-
<FieldNode
467-
viewOnly={editor === 'view'}
468-
value={field.state.value}
469-
onChange={field.handleChange}
470-
onBlur={field.handleBlur}
471-
placeholder={t(
472-
'scenarios:sanction_counterparty_id_placeholder',
473-
)}
474-
/>
475-
</OptionsProvider>
476-
<FormErrorOrDescription
477-
errors={field.state.meta.errors}
478-
/>
479-
</div>
480-
)}
481-
</form.Field>
482-
<form.Field name="counterPartyName">
509+
<form.Field name="query.name">
483510
{(field) => (
484511
<div className="flex flex-col gap-4">
485512
<FormLabel
@@ -494,7 +521,7 @@ export default function SanctionDetail() {
494521
<OptionsProvider {...options}>
495522
<FieldNodeConcat
496523
viewOnly={editor === 'view'}
497-
value={field.state.value}
524+
value={sanctionCheckConfig?.query?.name}
498525
onChange={field.handleChange}
499526
onBlur={field.handleBlur}
500527
placeholder="Select the First name or Full Name"
@@ -507,7 +534,7 @@ export default function SanctionDetail() {
507534
</div>
508535
)}
509536
</form.Field>
510-
<form.Field name="counterPartyLabel">
537+
<form.Field name="query.label">
511538
{(field) => (
512539
<div className="flex flex-col gap-4">
513540
<FormLabel name={field.name}>

0 commit comments

Comments
 (0)