Skip to content

Commit 34433b7

Browse files
authored
fix(novu,dashboard): step resolver generated defaults; stale preview fixes NV-7236 (novuhq#10319)
1 parent ac049e2 commit 34433b7

7 files changed

Lines changed: 296 additions & 99 deletions

File tree

apps/dashboard/src/components/workflow-editor/steps/controls/custom-step-controls.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const CustomStepControls = (props: CustomStepControlsProps) => {
3434
const [isRestoreDefaultModalOpen, setIsRestoreDefaultModalOpen] = useState(false);
3535
const { step, workflow, update } = useWorkflow();
3636
const { saveForm } = useSaveForm();
37-
const { control } = useFormContext();
37+
const { control, reset } = useFormContext();
3838
const watchedValues = useWatch({ control });
3939

4040
const dataSchemaDefaults = buildDefaultValuesOfDataSchema(step?.controls.dataSchema ?? {});
@@ -117,6 +117,7 @@ export const CustomStepControls = (props: CustomStepControlsProps) => {
117117
if (!workflow || !step) return;
118118

119119
update(updateStepInWorkflow(workflow, step.stepId, { controlValues: null }));
120+
reset(dataSchemaDefaults, { keepErrors: true });
120121
setIsRestoreDefaultModalOpen(false);
121122
setIsOverridden(false);
122123
}}
@@ -171,7 +172,12 @@ export const CustomStepControls = (props: CustomStepControlsProps) => {
171172
!isOverridden && 'opacity-60 pointer-events-none'
172173
)}
173174
>
174-
<JsonForm schema={(dataSchema as RJSFSchema) || {}} formData={watchedValues} disabled={!isOverridden} />
175+
<JsonForm
176+
key={String(isOverridden)}
177+
schema={(dataSchema as RJSFSchema) || {}}
178+
formData={isOverridden ? watchedValues : dataSchemaDefaults}
179+
disabled={!isOverridden}
180+
/>
175181
</div>
176182
</AccordionContent>
177183
</AccordionItem>

apps/dashboard/src/components/workflow-editor/steps/controls/text-widget.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ export function TextWidget(props: WidgetProps) {
2626
render={({ field, fieldState }) => {
2727
let stringValue = '';
2828

29-
if (typeof field.value === 'string') {
29+
if (disabled) {
30+
stringValue = typeof rjsfValue === 'string' ? rjsfValue : '';
31+
} else if (typeof field.value === 'string') {
3032
stringValue = field.value;
3133
} else if (typeof rjsfValue === 'string') {
3234
stringValue = rjsfValue;

apps/dashboard/src/hooks/use-form-autosave.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,15 @@ export function useFormAutosave<U extends Record<string, unknown>, T extends Fie
5656
}
5757

5858
const values = { ...previousData, ...data };
59-
// form.reset(values, { keepErrors: true });
6059
lastSavedDataRef.current = serializedData;
61-
save(values, { onSuccess: options?.onSuccess });
60+
save(values, {
61+
onSuccess: () => {
62+
// Reset dirty state after successful save so that polling hooks (e.g. useStepResolverPolling)
63+
// are not permanently blocked. keepValues: true avoids regenerating useFieldArray field IDs.
64+
formRef.current.reset(values, { keepErrors: true, keepValues: true });
65+
options?.onSuccess?.();
66+
},
67+
});
6268
},
6369
[formRef, savePropsRef]
6470
);

apps/dashboard/src/pages/edit-step-template-v2.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,15 @@ export function EditStepTemplateV2Page() {
2424
// on every render where `values` has a new reference — which regenerates all
2525
// useFieldArray field IDs and causes visible row flicker on every save round-trip.
2626
const hasInitializedRef = useRef(false);
27+
const prevHashRef = useRef(step?.stepResolverHash);
2728
useEffect(() => {
28-
if (hasInitializedRef.current || !step) return;
29-
hasInitializedRef.current = true;
30-
form.reset(getControlsDefaultValues(step), { keepErrors: true });
29+
if (!step) return;
30+
const hashChanged = step.stepResolverHash !== prevHashRef.current;
31+
prevHashRef.current = step.stepResolverHash;
32+
if (!hasInitializedRef.current || hashChanged) {
33+
hasInitializedRef.current = true;
34+
form.reset(getControlsDefaultValues(step), { keepErrors: true });
35+
}
3136
}, [form, step]);
3237

3338
const { onBlur, saveForm, saveFormDebounced } = useFormAutosave({

packages/novu/src/commands/step/templates/__snapshots__/step-file.spec.ts.snap

Lines changed: 154 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2,108 +2,203 @@
22

33
exports[`generateChatStepFile > should match snapshot 1`] = `
44
"import { step } from '@novu/framework/step-resolver';
5-
6-
export default step.chat('send-chat', async (controls, { payload, subscriber, context, steps }) => ({
7-
body: \`Hello \${subscriber.firstName ?? 'there'}, a message for you.\`,
8-
}));
5+
import { z } from 'zod';
6+
7+
export default step.chat(
8+
'send-chat',
9+
async (controls, { payload, subscriber }) => ({
10+
body: \`Hi \${subscriber.firstName ?? 'there'}, \${controls.message}\`,
11+
}),
12+
{
13+
controlSchema: z.object({
14+
message: z.string().default('You have a new message.'),
15+
}),
16+
// skip: (_controls, { subscriber }) => !subscriber.channels?.chat,
17+
}
18+
);
919
"
1020
`;
1121
1222
exports[`generateEmailStepFile > should match snapshot 1`] = `
1323
"import { step } from '@novu/framework/step-resolver';
14-
15-
export default step.email('plain-email', async (controls, { payload, subscriber, context, steps }) => ({
16-
subject: 'No Subject',
17-
body: \`<html><body><p>Hello \${subscriber.firstName ?? 'there'},</p><p>Your message here.</p></body></html>\`,
18-
}));
24+
import { z } from 'zod';
25+
26+
export default step.email(
27+
'plain-email',
28+
async (controls, { payload, subscriber }) => ({
29+
subject: controls.subject,
30+
body: \`
31+
<html>
32+
<body>
33+
<h1>\${controls.heading}</h1>
34+
<p>Hi \${subscriber.firstName ?? 'there'},</p>
35+
<p>\${controls.body}</p>
36+
<p><a href="\${controls.ctaUrl}">View details</a></p>
37+
</body>
38+
</html>
39+
\`,
40+
// Optionally override the sender for this step:
41+
// from: { email: 'noreply@example.com', name: 'My App' },
42+
}),
43+
{
44+
controlSchema: z.object({
45+
subject: z.string().default('You have a new notification'),
46+
heading: z.string().default('New activity'),
47+
body: z.string().default('You have a new message.'),
48+
ctaUrl: z.string().default('/'),
49+
}),
50+
// skip: (_controls, { subscriber }) => !subscriber.email,
51+
}
52+
);
1953
"
2054
`;
2155
2256
exports[`generateInAppStepFile > should match snapshot 1`] = `
2357
"import { step } from '@novu/framework/step-resolver';
24-
25-
export default step.inApp('in-app-notify', async (controls, { payload, subscriber, context, steps }) => ({
26-
subject: 'New notification',
27-
body: \`Hello \${subscriber.firstName ?? 'there'}, you have a new in-app notification.\`,
28-
}));
58+
import { z } from 'zod';
59+
60+
export default step.inApp(
61+
'in-app-notify',
62+
async (controls, { payload, subscriber }) => ({
63+
subject: controls.subject,
64+
body: controls.body,
65+
// avatar: subscriber.avatar,
66+
primaryAction: {
67+
label: controls.ctaLabel,
68+
redirect: { url: controls.ctaUrl, target: '_blank' },
69+
},
70+
// secondaryAction: { label: 'Dismiss' },
71+
}),
72+
{
73+
controlSchema: z.object({
74+
subject: z.string().default('New activity'),
75+
body: z.string().default('You have a new notification.'),
76+
ctaLabel: z.string().default('View details'),
77+
ctaUrl: z.string().default('/'),
78+
}),
79+
// skip: (_controls, { subscriber }) => !subscriber.channels?.in_app,
80+
}
81+
);
2982
"
3083
`;
3184
3285
exports[`generatePushStepFile > should match snapshot 1`] = `
3386
"import { step } from '@novu/framework/step-resolver';
34-
35-
export default step.push('send-push', async (controls, { payload, subscriber, context, steps }) => ({
36-
subject: 'New notification',
37-
body: \`Hello \${subscriber.firstName ?? 'there'}, you have a new notification.\`,
38-
}));
87+
import { z } from 'zod';
88+
89+
export default step.push(
90+
'send-push',
91+
async (controls, { payload, subscriber }) => ({
92+
subject: controls.title,
93+
body: controls.body,
94+
}),
95+
{
96+
controlSchema: z.object({
97+
title: z.string().default('New activity'),
98+
body: z.string().default('You have a new notification.'),
99+
}),
100+
// skip: (_controls, { subscriber }) => !subscriber.channels?.push,
101+
}
102+
);
39103
"
40104
`;
41105
42106
exports[`generateReactEmailStepFile > should match snapshot 1`] = `
43107
"import { step } from '@novu/framework/step-resolver';
44108
import { render } from '@react-email/components';
109+
import { z } from 'zod';
45110
import EmailTemplate from '../emails/welcome';
46111
47-
export default step.email('welcome-email', async (controls, { payload, subscriber, context, steps }) => ({
48-
subject: 'No Subject',
49-
body: await render(
50-
<EmailTemplate
51-
{...payload}
52-
subscriber={subscriber}
53-
context={context}
54-
steps={steps}
55-
controls={controls}
56-
/>
57-
),
58-
}));
112+
export default step.email(
113+
'welcome-email',
114+
async (controls, { payload, subscriber, steps }) => ({
115+
subject: controls.subject,
116+
body: await render(
117+
<EmailTemplate
118+
controls={controls}
119+
subscriber={subscriber}
120+
steps={steps}
121+
/>
122+
),
123+
}),
124+
{
125+
controlSchema: z.object({
126+
subject: z.string().default('You have a new notification'),
127+
}),
128+
}
129+
);
59130
"
60131
`;
61132
62133
exports[`generateReactEmailStepFile > should match snapshot with different import paths > nested-import 1`] = `
63134
"import { step } from '@novu/framework/step-resolver';
64135
import { render } from '@react-email/components';
136+
import { z } from 'zod';
65137
import EmailTemplate from '../../src/emails/welcome';
66138
67-
export default step.email('welcome-email', async (controls, { payload, subscriber, context, steps }) => ({
68-
subject: 'No Subject',
69-
body: await render(
70-
<EmailTemplate
71-
{...payload}
72-
subscriber={subscriber}
73-
context={context}
74-
steps={steps}
75-
controls={controls}
76-
/>
77-
),
78-
}));
139+
export default step.email(
140+
'welcome-email',
141+
async (controls, { payload, subscriber, steps }) => ({
142+
subject: controls.subject,
143+
body: await render(
144+
<EmailTemplate
145+
controls={controls}
146+
subscriber={subscriber}
147+
steps={steps}
148+
/>
149+
),
150+
}),
151+
{
152+
controlSchema: z.object({
153+
subject: z.string().default('You have a new notification'),
154+
}),
155+
}
156+
);
79157
"
80158
`;
81159
82160
exports[`generateReactEmailStepFile > should match snapshot with different import paths > relative-import 1`] = `
83161
"import { step } from '@novu/framework/step-resolver';
84162
import { render } from '@react-email/components';
163+
import { z } from 'zod';
85164
import EmailTemplate from './emails/welcome';
86165
87-
export default step.email('welcome-email', async (controls, { payload, subscriber, context, steps }) => ({
88-
subject: 'No Subject',
89-
body: await render(
90-
<EmailTemplate
91-
{...payload}
92-
subscriber={subscriber}
93-
context={context}
94-
steps={steps}
95-
controls={controls}
96-
/>
97-
),
98-
}));
166+
export default step.email(
167+
'welcome-email',
168+
async (controls, { payload, subscriber, steps }) => ({
169+
subject: controls.subject,
170+
body: await render(
171+
<EmailTemplate
172+
controls={controls}
173+
subscriber={subscriber}
174+
steps={steps}
175+
/>
176+
),
177+
}),
178+
{
179+
controlSchema: z.object({
180+
subject: z.string().default('You have a new notification'),
181+
}),
182+
}
183+
);
99184
"
100185
`;
101186
102187
exports[`generateSmsStepFile > should match snapshot 1`] = `
103188
"import { step } from '@novu/framework/step-resolver';
104-
105-
export default step.sms('send-sms', async (controls, { payload, subscriber, context, steps }) => ({
106-
body: \`Hello \${subscriber.firstName ?? 'there'}, your message here.\`,
107-
}));
189+
import { z } from 'zod';
190+
191+
export default step.sms(
192+
'send-sms',
193+
async (controls, { payload, subscriber }) => ({
194+
body: \`Hi \${subscriber.firstName ?? 'there'}, \${controls.message}\`,
195+
}),
196+
{
197+
controlSchema: z.object({
198+
message: z.string().default('You have a new notification. Reply STOP to unsubscribe.'),
199+
}),
200+
// skip: (_controls, { subscriber }) => !subscriber.phone,
201+
}
202+
);
108203
"
109204
`;

packages/novu/src/commands/step/templates/step-file.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ describe('generateReactEmailStepFile', () => {
2323

2424
it('imports render from @react-email/components and calls it', () => {
2525
const result = generateReactEmailStepFile(stepId, '../emails/welcome');
26-
expect(result).toContain("step.email('welcome-email'");
26+
expect(result).toContain('step.email(');
27+
expect(result).toContain("'welcome-email'");
2728
expect(result).toContain("from '@react-email/components'");
2829
expect(result).toContain('await render(');
2930
});
@@ -42,7 +43,8 @@ describe('generateEmailStepFile', () => {
4243

4344
it('does not use React Email', () => {
4445
const result = generateEmailStepFile('plain-email');
45-
expect(result).toContain("step.email('plain-email'");
46+
expect(result).toContain('step.email(');
47+
expect(result).toContain("'plain-email'");
4648
expect(result).not.toContain('@react-email');
4749
expect(result).not.toContain('await render(');
4850
});

0 commit comments

Comments
 (0)