Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions backend/internal/flow/common/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,11 +272,12 @@ const (
ActionTypeSubmit ActionType = "SUBMIT"
// ActionTypeReject represents a reject/deny action
ActionTypeReject ActionType = "REJECT"
// ActionTypeSignOutConfirm marks the confirmation prompt's action edge in a sign-out flow. When the
// End-User confirms, the prompt node forwards this type to the session sign-out node (as the action
// type in ForwardedData), which reads it to tell a confirmed re-run apart from the initial request,
// so no runtime flag has to be persisted.
ActionTypeSignOutConfirm ActionType = "SIGN_OUT_CONFIRM"
// ActionTypeConfirm marks a confirmation prompt's action edge. When the End-User confirms, the
// prompt node forwards this type to the next node (as the action type in ForwardedData), where the
// executor that routed to the prompt reads it to tell a confirmed re-run apart from the initial
// request, so no runtime flag has to be persisted. It is deliberately not tied to one use case:
// the session sign-out executor is its first consumer, not its only possible one.
ActionTypeConfirm ActionType = "CONFIRM"
)

// ForwardedData key constants define keys used in the ForwardedData map.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func (e *sessionSignOutExecutor) decide(ctx *providers.NodeContext) signOutOutco

actionType, _ := ctx.ForwardedData[common.ForwardedDataKeyActionType].(string)
switch common.ActionType(actionType) {
case common.ActionTypeSignOutConfirm:
case common.ActionTypeConfirm:
return signOutTerminate
default:
// The initial request forwards no action type at all, and a type this executor does not
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func (suite *SessionSignOutExecutorTestSuite) TestPromptsWhenConfirmationRequire
}

// TestTerminatesAfterConfirmation covers the re-run after the End-User confirms: the confirmation
// prompt forwards its sign-out confirm action type, so the executor terminates the session instead of
// prompt forwards its confirm action type, so the executor terminates the session instead of
// prompting again.
func (suite *SessionSignOutExecutorTestSuite) TestTerminatesAfterConfirmation() {
sso := sessionmock.NewServiceMock(suite.T())
Expand All @@ -127,7 +127,7 @@ func (suite *SessionSignOutExecutorTestSuite) TestTerminatesAfterConfirmation()
ctx.NodeProperties = map[string]interface{}{propertyKeyPromptOnSignOut: true}
ctx.RuntimeData = map[string]string{common.RuntimeKeyLogoutPromptRequired: dataValueTrue}
ctx.ForwardedData = map[string]interface{}{
common.ForwardedDataKeyActionType: string(common.ActionTypeSignOutConfirm),
common.ForwardedDataKeyActionType: string(common.ActionTypeConfirm),
}

resp, err := exec.Execute(ctx)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ describe('CommonResourceProperties', () => {
<button type="button" onClick={() => onChange('label', 'New Label', resource)}>
Change Label
</button>
<button type="button" onClick={() => onChange('actionType', 'SIGN_OUT_CONFIRM', resource)}>
<button type="button" onClick={() => onChange('actionType', 'CONFIRM', resource)}>
Change Action Type
</button>
<button
Expand Down Expand Up @@ -245,7 +245,7 @@ describe('CommonResourceProperties', () => {
// generic field.
const resourceWithActionType: Base = {
...mockBaseResource,
actionType: 'SIGN_OUT_CONFIRM',
actionType: 'CONFIRM',
} as Base & {actionType: string};

render(<CommonResourceProperties />, {
Expand Down Expand Up @@ -409,7 +409,7 @@ describe('CommonResourceProperties', () => {
const updated = mockSetLastInteractedResource.mock.calls.at(-1)?.[0] as Record<string, unknown> & {
data?: Record<string, unknown>;
};
expect(updated.actionType).toBe('SIGN_OUT_CONFIRM');
expect(updated.actionType).toBe('CONFIRM');
expect(updated.data?.actionType).toBeUndefined();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,22 @@ import {ActionEventTypes, PromptActionTypes} from '@/features/flows/models/eleme

/**
* The options offered by the Action selector. `Submit` and `Trigger` are the
* button's `eventType`; `SignOut` is a submit button that additionally raises
* the `SIGN_OUT_CONFIRM` prompt action, so one selection maps onto two fields.
* button's `eventType`; `Confirm` is a submit button that additionally raises
* the `CONFIRM` prompt action, so one selection maps onto two fields.
*
* `SignOut` deliberately carries the same literal that is persisted as
* `Confirm` deliberately carries the same literal that is persisted as
* `prompts[].action.type`, so the value selected here and the value in the flow
* definition are one vocabulary rather than a UI-only alias.
* definition are one vocabulary rather than a UI-only alias. It is not specific
* to signing out: the session sign-out executor reads it today, but any executor
* that routes to a confirmation prompt can.
*
* The remaining `ActionEventTypes` (navigate, cancel, reset, back) are handled
* by the SDK renderers but deliberately not offered here.
*/
const ACTION_OPTIONS = {
Submit: 'SUBMIT',
Trigger: 'TRIGGER',
SignOut: PromptActionTypes.SignOutConfirm,
Confirm: PromptActionTypes.Confirm,
} as const;

type ActionOption = (typeof ACTION_OPTIONS)[keyof typeof ACTION_OPTIONS];
Expand All @@ -46,28 +48,28 @@ function ButtonExtendedProperties({resource, onChange}: ButtonExtendedProperties
const element = resource as Element & {eventType?: string};
const eventTypeValue = element?.eventType ?? ActionEventTypes.Trigger;

// Sign out is a submit button carrying an extra prompt action type, so it
// Confirm is a submit button carrying an extra prompt action type, so it
// takes precedence over the plain event type when deriving the selection.
const actionValue: ActionOption =
element?.actionType === PromptActionTypes.SignOutConfirm
? ACTION_OPTIONS.SignOut
element?.actionType === PromptActionTypes.Confirm
? ACTION_OPTIONS.Confirm
: eventTypeValue === ActionEventTypes.Submit
? ACTION_OPTIONS.Submit
: ACTION_OPTIONS.Trigger;

const handleActionChange = (nextAction: ActionOption): void => {
if (nextAction === ACTION_OPTIONS.SignOut) {
if (nextAction === ACTION_OPTIONS.Confirm) {
onChange('eventType', ActionEventTypes.Submit, resource);
onChange('actionType', PromptActionTypes.SignOutConfirm, resource);
onChange('actionType', PromptActionTypes.Confirm, resource);
return;
}

onChange('eventType', nextAction, resource);
// Clearing keeps the button from silently staying a sign-out confirmation
// Clearing keeps the button from silently staying a confirmation action
// after the author picks a plain action. Only the type this selector owns is
// cleared, so an action type it does not model (e.g. REJECT, authored in the
// flow definition directly) is left untouched rather than discarded.
if (element?.actionType === PromptActionTypes.SignOutConfirm) {
if (element?.actionType === PromptActionTypes.Confirm) {
onChange('actionType', '', resource);
}
};
Expand Down Expand Up @@ -125,8 +127,8 @@ function ButtonExtendedProperties({resource, onChange}: ButtonExtendedProperties
<MenuItem value={ACTION_OPTIONS.Trigger}>
{t('flows:core.buttonExtendedProperties.action.trigger', 'Trigger Action')}
</MenuItem>
<MenuItem value={ACTION_OPTIONS.SignOut}>
{t('flows:core.buttonExtendedProperties.action.signOut', 'Sign Out Action')}
<MenuItem value={ACTION_OPTIONS.Confirm}>
{t('flows:core.buttonExtendedProperties.action.confirm', 'Confirm Action')}
</MenuItem>
</Select>
<FormHelperText>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,37 +218,37 @@ describe('ButtonExtendedProperties', () => {
expect(select).toHaveTextContent('flows:core.buttonExtendedProperties.action.submit');
});

it('should display Sign out when the button carries the sign-out confirm action', () => {
// A sign-out button is a submit button plus the prompt action type, so
it('should display Confirm when the button carries the confirm action', () => {
// A confirmation button is a submit button plus the prompt action type, so
// the action type has to win over the plain event type.
const resource = createMockResource({
actionType: 'SIGN_OUT_CONFIRM',
actionType: 'CONFIRM',
eventType: 'SUBMIT',
} as Partial<Resource>);

const {container} = render(<ButtonExtendedProperties resource={resource} onChange={mockOnChange} />);

const select = container.querySelector('#event-type-select');
expect(select).toHaveTextContent('flows:core.buttonExtendedProperties.action.signOut');
expect(select).toHaveTextContent('flows:core.buttonExtendedProperties.action.confirm');
});

it('should write both the event type and the action type when Sign out is picked', async () => {
it('should write both the event type and the action type when Confirm is picked', async () => {
const user = userEvent.setup();
const resource = createMockResource({eventType: 'TRIGGER'} as Partial<Resource>);

render(<ButtonExtendedProperties resource={resource} onChange={mockOnChange} />);

await user.click(screen.getByRole('combobox'));
await user.click(screen.getByRole('option', {name: 'flows:core.buttonExtendedProperties.action.signOut'}));
await user.click(screen.getByRole('option', {name: 'flows:core.buttonExtendedProperties.action.confirm'}));

expect(mockOnChange).toHaveBeenCalledWith('eventType', 'SUBMIT', resource);
expect(mockOnChange).toHaveBeenCalledWith('actionType', 'SIGN_OUT_CONFIRM', resource);
expect(mockOnChange).toHaveBeenCalledWith('actionType', 'CONFIRM', resource);
});

it('should clear the action type when moving from Sign out back to a plain action', async () => {
it('should clear the action type when moving from Confirm back to a plain action', async () => {
const user = userEvent.setup();
const resource = createMockResource({
actionType: 'SIGN_OUT_CONFIRM',
actionType: 'CONFIRM',
eventType: 'SUBMIT',
} as Partial<Resource>);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13177,7 +13177,7 @@
"category": "BLOCK",
"components": [
{
"actionType": "SIGN_OUT_CONFIRM",
"actionType": "CONFIRM",
"category": "ACTION",
"eventType": "SUBMIT",
"id": "action_confirm",
Expand All @@ -13197,7 +13197,7 @@
{
"action": {
"ref": "action_confirm",
"type": "SIGN_OUT_CONFIRM",
"type": "CONFIRM",
"nextNode": "session_signout"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,13 @@ export type ActionEventTypes = (typeof ActionEventTypes)[keyof typeof ActionEven
* executor as edge metadata. Distinct from {@link ActionEventTypes}, which
* describes how the button behaves in the rendered form, and from
* `action.type` on the element, which carries canvas navigation semantics.
*
* The values are deliberately not tied to a single use case: `Confirm` is read
* by the session sign-out executor today, but any executor that routes to a
* confirmation prompt can consume it.
*/
export const PromptActionTypes = {
SignOutConfirm: 'SIGN_OUT_CONFIRM',
Confirm: 'CONFIRM',
Comment on lines +138 to +144

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Expect old references only in migration or compatibility code.
rg -n 'SIGN_OUT_CONFIRM|SignOutConfirm' .

Repository: thunder-id/thunderid

Length of output: 1903


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files matching elements/validation tests =="
git ls-files | grep -E 'frontend/apps/console/src/features/flows/(elements\.ts|validation/(validation-rules\.ts|__tests__/computeValidationNotifications\.test\.ts))$' || true

echo "== outline elements =="
ast-grep outline frontend/apps/console/src/features/flows/models/elements.ts --view compact || true

echo "== relevant elements.ts =="
sed -n '120,165p' frontend/apps/console/src/features/flows/models/elements.ts

echo "== validation rule relevant sections =="
sed -n '320,405p' frontend/apps/console/src/features/flows/validation/validation-rules.ts

echo "== test relevant sections =="
sed -n '580,670p' frontend/apps/console/src/features/flows/validation/__tests__/computeValidationNotifications.test.ts

echo "== all PromptActionTypes references (excluding generated/build-looking if any) =="
rg -n 'PromptActionTypes|Confirm:|actionType|SIGN_OUT_CONFIRM|SignOutConfirm' frontend/apps/console/src/features/flows -g '*.ts' -g '*.tsx'

Repository: thunder-id/thunderid

Length of output: 17953


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== flow transformers relevant sections =="
sed -n '340,385p' frontend/apps/console/src/features/flows/utils/reactFlowTransformer.ts
sed -n '100,125p' frontend/apps/console/src/features/flows/utils/flowToCanvasTransformer.ts

echo "== button extended properties relevant section =="
sed -n '1,90p' frontend/apps/console/src/features/flows/components/resource-property-panel/extended-properties/ButtonExtendedProperties.tsx

echo "== response/action model relevant section =="
sed -n '130,150p' frontend/apps/console/src/features/flows/models/responses.ts

echo "== broader FlowAction/SessionSignOutExecutor references =="
rg -n 'FlowAction|HandlePayload|action.type|NextExecutor|SessionSignOutExecutor|signOut|SignOut' frontend backend -g '*.ts' -g '*.tsx' -g '*.go' -g '*.java' -g '*.js' -g '*.jsx' 2>/dev/null | head -n 200

Repository: thunder-id/thunderid

Length of output: 33789


Preserve compatibility for existing SIGN_OUT_CONFIRM flow data.

PromptActionTypes now only exports CONFIRM, the button selector writes CONFIRM, extractActionFromComponent forwards component.actionType as prompts[].action.type, and signOutConfirmActionRule reads PromptActionTypes.Confirm. Existing definitions can still contain the old SIGN_OUT_CONFIRM value, so they can fail sign-out validation or stop sign-out validation from running. Keep a read-time alias, normalize saved definitions, or migrate stored flows before writing only CONFIRM.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/apps/console/src/features/flows/models/elements.ts` around lines 138
- 144, Preserve backward compatibility for existing SIGN_OUT_CONFIRM values in
the prompt action flow. Update the PromptActionTypes/extraction and
signOutConfirmActionRule path so saved definitions using SIGN_OUT_CONFIRM are
normalized or treated as equivalent to PromptActionTypes.Confirm at read time,
while new writes continue using CONFIRM.

} as const;

export type PromptActionTypes = (typeof PromptActionTypes)[keyof typeof PromptActionTypes];
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ export interface FlowNodeAction {
nextNode: string;
/**
* Semantic action type forwarded to the next node's executor (e.g. `SUBMIT`, `REJECT`,
* `SIGN_OUT_CONFIRM`). Restored onto the element as `actionType` on load so a definition
* authored outside the builder keeps its type when the flow is saved again.
* `CONFIRM`). Restored onto the element as `actionType` on load so a definition authored
* outside the builder keeps its type when the flow is saved again.
*/
type?: string;
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -458,15 +458,15 @@ describe('flowToCanvasTransformer', () => {
meta: {
components: [{id: 'action_confirm', type: 'ACTION', label: 'Sign out'}],
},
prompts: [{action: {ref: 'action_confirm', type: 'SIGN_OUT_CONFIRM', nextNode: 'next-node'}}],
prompts: [{action: {ref: 'action_confirm', type: 'CONFIRM', nextNode: 'next-node'}}],
layout: {position: {x: 0, y: 0}, size: {width: 300, height: 200}},
},
]);

const result = transformFlowToCanvas(flowData);

const component = result.nodes[0].data.components?.[0] as Record<string, unknown> | undefined;
expect(component?.actionType).toBe('SIGN_OUT_CONFIRM');
expect(component?.actionType).toBe('CONFIRM');
});

it('should restore the type when the element carries a cleared action type', () => {
Expand All @@ -480,15 +480,15 @@ describe('flowToCanvasTransformer', () => {
meta: {
components: [{id: 'action_confirm', type: 'ACTION', actionType: '', label: 'Sign out'}],
},
prompts: [{action: {ref: 'action_confirm', type: 'SIGN_OUT_CONFIRM', nextNode: 'next-node'}}],
prompts: [{action: {ref: 'action_confirm', type: 'CONFIRM', nextNode: 'next-node'}}],
layout: {position: {x: 0, y: 0}, size: {width: 300, height: 200}},
},
]);

const result = transformFlowToCanvas(flowData);

const component = result.nodes[0].data.components?.[0] as Record<string, unknown> | undefined;
expect(component?.actionType).toBe('SIGN_OUT_CONFIRM');
expect(component?.actionType).toBe('CONFIRM');
});

it('should keep an action type already on the element over the prompt action', () => {
Expand All @@ -501,7 +501,7 @@ describe('flowToCanvasTransformer', () => {
meta: {
components: [{id: 'action_confirm', type: 'ACTION', actionType: 'REJECT', label: 'No'}],
},
prompts: [{action: {ref: 'action_confirm', type: 'SIGN_OUT_CONFIRM', nextNode: 'next-node'}}],
prompts: [{action: {ref: 'action_confirm', type: 'CONFIRM', nextNode: 'next-node'}}],
layout: {position: {x: 0, y: 0}, size: {width: 300, height: 200}},
},
]);
Expand Down Expand Up @@ -758,7 +758,7 @@ describe('flowToCanvasTransformer', () => {
meta: {
components: [{id: 'action_confirm', type: 'ACTION', label: 'Sign out'}],
},
prompts: [{action: {ref: 'action_confirm', type: 'SIGN_OUT_CONFIRM', nextNode: 'session_signout'}}],
prompts: [{action: {ref: 'action_confirm', type: 'CONFIRM', nextNode: 'session_signout'}}],
layout: {position: {x: 0, y: 0}, size: {width: 300, height: 200}},
},
{
Expand All @@ -781,7 +781,7 @@ describe('flowToCanvasTransformer', () => {
expect(prompt?.prompts?.[0].action).toMatchObject({
ref: 'action_confirm',
nextNode: 'session_signout',
type: 'SIGN_OUT_CONFIRM',
type: 'CONFIRM',
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ describe('reactFlowTransformer', () => {
type: ElementTypes.Action,
category: ElementCategories.Action,
eventType: 'SUBMIT',
actionType: 'SIGN_OUT_CONFIRM',
actionType: 'CONFIRM',
} as Element & {eventType: string},
];

Expand All @@ -262,7 +262,7 @@ describe('reactFlowTransformer', () => {
expect(result.nodes[0].prompts?.[0].action).toEqual({
ref: 'button-1',
nextNode: 'session_signout',
type: 'SIGN_OUT_CONFIRM',
type: 'CONFIRM',
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ interface FlowAction {
nextNode: string;
/**
* Semantic action type forwarded by the prompt node to the next executor
* (e.g. `SIGN_OUT_CONFIRM`, which tells the session sign-out executor the
* End-User has confirmed).
* (e.g. `CONFIRM`, which tells the session sign-out executor the End-User
* has confirmed).
*/
type?: string;
executor?: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ describe('computeValidationNotifications', () => {
type: 'BLOCK',
category: 'BLOCK',
components: [
{id: buttonId, type: 'ACTION', category: 'ACTION', eventType: 'SUBMIT', actionType: 'SIGN_OUT_CONFIRM'},
{id: buttonId, type: 'ACTION', category: 'ACTION', eventType: 'SUBMIT', actionType: 'CONFIRM'},
],
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,9 +346,7 @@ function collectSignOutConfirmElements(elements: FlowElement[] | undefined): Flo
}

return elements.flatMap((element) => [
...((element as FlowElement & {actionType?: string}).actionType === PromptActionTypes.SignOutConfirm
? [element]
: []),
...((element as FlowElement & {actionType?: string}).actionType === PromptActionTypes.Confirm ? [element] : []),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Scope this rule to sign-out confirmation flows.

Line 349 now collects every PromptActionTypes.Confirm element, but signOutConfirmActionRule still requires each element to connect to a SessionSignOut node. When another executor uses CONFIRM, valid buttons will receive sign-out-specific confirmNotConnected or confirmInvalidTarget notifications.

Scope collection to session sign-out context, or split validation by target executor. Add a test for a non-sign-out CONFIRM action.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/apps/console/src/features/flows/validation/validation-rules.ts` at
line 349, Update signOutConfirmActionRule’s collection of
PromptActionTypes.Confirm elements to include only confirmations belonging to
session sign-out flows, or separate validation by target executor so
non-sign-out confirmations bypass sign-out checks. Preserve the existing
SessionSignOut connection and target validation, and add coverage for a
non-sign-out CONFIRM action.

...collectSignOutConfirmElements(element.components),
]);
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/packages/i18n/src/locales/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3846,7 +3846,7 @@ const translations = {
'core.buttonExtendedProperties.action.label': 'Action',
'core.buttonExtendedProperties.action.submit': 'Submit Form',
'core.buttonExtendedProperties.action.trigger': 'Trigger Action',
'core.buttonExtendedProperties.action.signOut': 'Sign Out Action',
'core.buttonExtendedProperties.action.confirm': 'Confirm Action',
'core.buttonExtendedProperties.action.hint': 'What happens when the button is activated',
'core.buttonExtendedProperties.startIcon.label': 'Start Icon',
'core.buttonExtendedProperties.startIcon.placeholder': 'Enter icon path (e.g., assets/images/icons/icon.svg)',
Expand Down
Loading