Skip to content

Conversation

@kevinkim-ogp
Copy link
Contributor

  • feat: for-each BETA
  • chore: convert import to js for plumber admin

### TL;DR

Added a new "Find Multiple Rows" action to the Tiles app that allows users to retrieve multiple rows from a tile based on specified filters.

This is a prerequisite for for-each.

### What changed?

- Created a new `findMultipleRows` action that retrieves multiple rows from a tile based on filter conditions
- Implemented permission checks to ensure only owners and editors can access the data
- Added support for sorting results in ascending or descending order
- Implemented scan limit functionality to control the number of rows returned
- Created comprehensive tests to verify the action's functionality
- Added appropriate error handling for invalid filters and deleted tiles
- Integrated the new action with the existing Tiles app

### How to test?

1. Create a new action and select the "Find multiple rows" event from the Tiles app
2. Select a tile and configure filter conditions to match the rows you want to retrieve
3. Choose the order of results (ascending or descending)
4. Test the step and verify that the correct rows are returned


### Note to reviewers
This is meant to be the backend implementation of the findMultipleRows action, the data-out metadata will be updated again.

Feature flag has been added on LaunchDarkly to allow for progressive rollout.
### TL;DR
* Added a new "Get Multiple Rows" action to the M365 Excel app that allows users to retrieve multiple rows from a excel table based on the specified lookup column and value.
* Cell values for each column will be combined into a single variable under that column

This is a prerequisite for for-each.

#### Note to reviewers
* This is meant to be the backend implementation of getting rows, the data-out metadata will be updated again.
* Feature flag has been added on LaunchDarkly to allow for progressive rollout.
* There will be a PR created below this to display the find multiple row results in a table

### What changed?
* Created a new getTableRows action that retrieves multiple rows from a M365 Excel table based on a specified lookup column and value
*  Only the first 500 rows that meet the condition will be returned

### How to test?
1. Create a new step with M365 Excel app and select the 'Get table rows' event.
2. Select an excel file, table, and set the lookup column and value
3. Test the step and verify that the correct rows are returned

### Screenshots

![Screenshot 2025-04-15 at 2.55.18 PM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/Wrwhm8Mmhv1Z2GwlSb7V/568e961d-cb5d-4c20-9a9d-314af6320f56.png)

![Screenshot 2025-04-15 at 2.55.27 PM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/Wrwhm8Mmhv1Z2GwlSb7V/4e89363b-bd52-4c9d-ab85-477c8bb2989c.png)
Adds a table view for displaying multiple rows in test steps for both Tiles and M365 Excel apps.

The find multiple row action is hidden by a LaunchDarkly flag. It will be made available together with the for-each release.

- Added a modal with a table view for displaying multiple rows in test steps
- Test step displays "Number of rows found" and "List of row(s) found"
- Preserved column order from when displaying results
- Improved styling to match the Tiles table design

NA

[Screen Recording 2025-04-15 at 2.53.00 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/Wrwhm8Mmhv1Z2GwlSb7V/31e259a1-9416-4664-808a-74103865e704.mov" />](https://app.graphite.dev/media/video/Wrwhm8Mmhv1Z2GwlSb7V/31e259a1-9416-4664-808a-74103865e704.mov)

[Screen Recording 2025-04-15 at 2.53.33 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/Wrwhm8Mmhv1Z2GwlSb7V/bd0edfa3-2a95-4679-875e-17c47b66f787.mov" />](https://app.graphite.dev/media/video/Wrwhm8Mmhv1Z2GwlSb7V/bd0edfa3-2a95-4679-875e-17c47b66f787.mov)

^ Conflicts:
^	packages/frontend/src/components/FlowStepTestController/index.tsx
…1023)

## TL;DR
Refactored to dynamically obtain column values for the `findMultipleRow` action instead of storing them in the execution step’s `dataOut`, to avoid duplicate data — since the values are already stored in the row data.

**Note to reviewers**
The modal that displays the list of rows in table format will be updated in a subsequent PR. As of now, the test result can still be verified in the output after testing the step.

## What changed?
* Restructured the output format from `rows.rowData` to `data.rows` for better semantic clarity
* Removed unnecessary consolidated column metadata as it is already available within the `data`
* Added explicit value paths (`data.rows.*.data.<columnId>`) to column definitions for dynamic data access
* Changed the type in metadata from 'array' to 'multiple-row-object' to better represent the data structure

## How to test?
Set up the find multiple row action and test the following:
- [ ] Number of rows returned is correct
- [ ] Test result shows the correct number of columns with:
  - `id`: Tile column id (uuid)
  - `name`: Tile column name
  - `value`: step variable-like string in this format `data.rows.*.data.<columnId>`


## Screenshots

![Screenshot 2025-06-02 at 9.23.58 AM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/Wrwhm8Mmhv1Z2GwlSb7V/01985352-322b-4bf7-ad38-ccc8c48be2b1.png)

![Screenshot 2025-06-02 at 9.25.16 AM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/Wrwhm8Mmhv1Z2GwlSb7V/87fc45fe-9956-4d6e-91b2-ea4829ef69dc.png)

![Screenshot 2025-06-02 at 9.25.30 AM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/Wrwhm8Mmhv1Z2GwlSb7V/20135570-2cc1-453e-b21c-e2e25338ab49.png)
…1025)

## TL;DR
Refactored to dynamically obtain column values for the `getTableRows` action instead of storing them in the execution step’s `dataOut`, to avoid duplicate data — since the values are already stored in the row data.

**Note to reviewers**
The modal that displays the list of rows in table format will be updated in a subsequent PR. As of now, the test result can still be verified in the output after testing the step.

## What changed?
* Restructured output to align with Tiles multiple row output:
  * `rows.rowData` to `data.rows` for better semantic clarity\
  * Removed unnecessary consolidated column metadata as it is already available within the `data`
  * Removed unnecessary `tableRowIndex` and `sheetRowNumber`
* Added explicit value paths (`data.rows.*.data.<hex-encoded-column-name>`) to column definitions for dynamic data access
* Changed the type in metadata from 'array' to 'multiple-row-object' to better represent the data structure

## How to test?
Set up the find table rows action and test the following:
- [ ] Number of rows returned is correct
- [ ] Test result shows the correct number of columns with:
  - `id`: Tile column id (uuid)
  - `name`: Tile column name
  - `value`: step variable-like string in this format `data.rows.*.data.<hex-encoded-column-name>`


## Screenshots

![Screenshot 2025-06-02 at 9.47.25 AM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/Wrwhm8Mmhv1Z2GwlSb7V/03295d11-07dc-47dd-8e91-2f764d210330.png)

![Screenshot 2025-06-02 at 9.47.36 AM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/Wrwhm8Mmhv1Z2GwlSb7V/45d33307-5712-4da2-af52-ee77c789b5b9.png)

![Screenshot 2025-06-02 at 9.47.43 AM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/Wrwhm8Mmhv1Z2GwlSb7V/0b208bd5-5ad4-4359-a67c-e7337acb2c12.png)
…#1026)

Refactored frontend to handle multi-row result variables and handle dynamic column values.

- Changed the variable structure from `rows` to `data` in `MultiRowResultVariables.tsx`
- Modified the variable processing in `variables.ts` to support the new "multiple-row-object" type
- Updated the data processing logic in `utils.ts` to handle the new structure format
- Updated the variable item key in the component to use both id and name for uniqueness
  - Added an optional `id` field to the Variable interface for unique key identification

- [ ] Modal opens with test results when executing Tiles find multiple rows
  - [ ] Column order is preserved in the result modal
  - [ ] Correct variables are shown in the test result:
    * List of row(s) found, Number of rows found
  - [ ] Variables are still displayed when 0 rows are found
    * Modal should not open automatically
    * Should not be able to open modal from 'Preview 0 row(s)'

- [ ] Modal opens with test results when executing Excel get table rows
  - [ ] Column order is preserved in the result modal
  - [ ] Correct variables are shown in the test result:
    * List of row(s) found, Number of rows found
  - [ ] Variables are still displayed when 0 rows are found
    * Modal should not open automatically
    * Should not be able to open modal from 'Preview 0 row(s)'

With the standardisation of data structure for multi-row results in the previous PRs, the frontend has to be updated to extract and display the column values dynamically.

**Tiles**

[Screen Recording 2025-06-02 at 10.11.06 AM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/Wrwhm8Mmhv1Z2GwlSb7V/110f6b54-de50-438f-b996-8bba49592a98.mov" />](https://app.graphite.dev/media/video/Wrwhm8Mmhv1Z2GwlSb7V/110f6b54-de50-438f-b996-8bba49592a98.mov)

**Excel**

[Screen Recording 2025-06-02 at 10.10.34 AM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/Wrwhm8Mmhv1Z2GwlSb7V/67075356-2446-4341-8b89-5e9b2d22a5c8.mov" />](https://app.graphite.dev/media/video/Wrwhm8Mmhv1Z2GwlSb7V/67075356-2446-4341-8b89-5e9b2d22a5c8.mov)
### TL;DR
Refactor Tiles multiple row lookup conditions to 'multirow-multicol'.

### What changed?
- Changed `find-multiple-rows` from `multirow` to `multirow-multicol`
- Created a new constant `LOOKUP_CONDITIONS_SUBFIELDS` in `constants.ts` that contains the shared filter condition configuration
- Updated the `find-multiple-rows` action to use the new constant instead of inline subfields
- Updated the `find-single-row` action to use the same shared constant


### How to test?

1. Verify that the "Find Multiple Rows" action still displays the correct filter conditions
2. Verify that the "Find Single Row" action still displays the correct filter conditions
3. Confirm that all filter operations (equals, greater than, contains, etc.) work as expected in both actions
### TL;DR

Added a new "For each" action to the Toolbox app that allows users to repeat actions for each item in a list.
Refactored parameter computation to return array (FormSG checkboxes) and objects (table object with rows and columns) as is instead of stringifying.

**Note to reviewer**:
* this just shows the app in the list, creating this app will throw an error step in the editor.
* this also does not process the input to get variables for use in subsequent steps (this will come in subsequent PRs).

### What changed?

- Created a new `forEach` action in the Toolbox app that enables users to iterate through items in a list
- Implemented the action's core functionality in `index.ts` with support for arrays and multiple-row objects
- Added metadata handling via `get-data-out-metadata.ts` to display the number of iterations
- Updated the Toolbox actions export to include the new forEach action
- Added the SlLoop icon for the forEach action in the frontend UI
- Refactored `compute-parameters` to not stringify arrays and objects when computing parameters for the for-each step

### How to test?
- [ ] Add step modal shows the For-each action under the Toolbox options

### Screenshots

![Screenshot 2025-06-04 at 12.00.15 PM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/Wrwhm8Mmhv1Z2GwlSb7V/9e8ab7c0-e422-43bc-a745-4e621ec63773.png)
### TL;DR
This PR adds functionality to add and display the for-each action in the editor.

**Note to reviewer**: deleting the for-each action and steps within the for-each will come in the next PR

### What changed?
- Implemented a new ForEach component that allows users to create loops in their flows
- Added ForEach initialisation logic to create an empty step when a new for-each action is created
- Created custom `useIsAppSelectable` to check if apps are selectable in the add step modal

### How to test?
1. Create a new flow or open an existing one
2. Add a step at the end of the flow
3. Select the "For Each" action from the toolbox
4. Verify that a new ForEach loop is created with an empty step inside
5. Try adding steps inside the ForEach loop
  * Note that hover add step button should only appear after an action has been created
6. Verify that IfThen can only be added as the last step inside ForEach
7. Verify that ForEach cannot be added inside another ForEach or IfThen
8. Verify that Delay cannot be added inside a ForEach action


### Screenshots
**New pipe**

[Screen Recording 2025-06-04 at 2.22.55 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/Wrwhm8Mmhv1Z2GwlSb7V/9a71c727-0600-47d2-a25e-d247fba30c5b.mov" />](https://app.graphite.dev/media/video/Wrwhm8Mmhv1Z2GwlSb7V/9a71c727-0600-47d2-a25e-d247fba30c5b.mov)

**Existing pipe**

[Screen Recording 2025-06-04 at 2.24.48 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/Wrwhm8Mmhv1Z2GwlSb7V/4ab018c7-2ad3-47df-b9ac-834da21f0c68.mov" />](https://app.graphite.dev/media/video/Wrwhm8Mmhv1Z2GwlSb7V/4ab018c7-2ad3-47df-b9ac-834da21f0c68.mov)
### TL;DR

Added `singleVariableSelection` property to restrict variable selection to a single variable in `string` and `multiline` RTE.

### What changed?

- Added a new `singleVariableSelection` property to the schema for string and multiline field types
- Implemented the property in the RichTextEditor component to prevent adding more than one variable when enabled
- Added keyboard event prevention to block typing when a variable is already selected
- Applied this property to specific actions:
  - "Update Row" action in the Tiles app
  - "For Each" action in the Toolbox app

### How to test?

- [ ] Open the "Update Row" action in the Tiles app and verify that the "Row ID" field only allows selecting a single variable
  - [ ] Verify that variable can be deleted using backspace
  - [ ] Verify that Row ID can be pasted in the input when copied from the Tiles page
- [ ] Open the "For Each" action in the Toolbox app and verify that the input field only allows selecting a single variable
  - [ ] Verify that variable can be deleted using backspace
- [ ] Verify that other fields without this property continue to allow multiple variables as before

### Why make this change?
Some action fields are designed to accept a single variable as input rather than a combination of text and variables. This change enforces that constraint in the UI, preventing users from creating invalid configurations that could lead to runtime errors or unexpected behaviour.

### Screenshots
**Tiles row ID**

[Screen Recording 2025-06-16 at 8.49.47 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/Wrwhm8Mmhv1Z2GwlSb7V/cd6b88f6-6aa9-4802-92d9-53272d448154.mov" />](https://app.graphite.dev/media/video/Wrwhm8Mmhv1Z2GwlSb7V/cd6b88f6-6aa9-4802-92d9-53272d448154.mov)

**For-each**

[Screen Recording 2025-06-16 at 8.50.23 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/Wrwhm8Mmhv1Z2GwlSb7V/098e4251-fd65-474f-8701-68ae461882ab.mov" />](https://app.graphite.dev/media/video/Wrwhm8Mmhv1Z2GwlSb7V/098e4251-fd65-474f-8701-68ae461882ab.mov)
### TL;DR
* Enable the deletion of the entire for-each action and steps within the loop
* Fix deletion of steps within the for-each action by creating an empty step when the last action is being deleted

### What changed?

- Enhanced `shouldCreateEmptyStep` function to handle For-Each steps, ensuring an empty step is created when the last step in a For-Each group is deleted
- Added a DeleteConfirmationDialog component for confirming deletion actions
- Implemented `useDeleteConfirmation` hook to manage the deletion flow
- Added logic to handle the edge case when deleting the last branch in a For-Each action
- Added For-Each deletion functionality with confirmation dialog
- Added `hasForEach` flag to the EditorContext to track For-Each steps in the flow

### How to test?
- [ ] Delete the entire For-each group
- [ ] Delete the last step in the For-each group and verify an empty step is created
  - [ ] If the last step is an If-then, an empty step should also be created

### Screenshots
**Deleting entire for-each group**

[Screen Recording 2025-06-04 at 2.47.04 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/Wrwhm8Mmhv1Z2GwlSb7V/68aa0b0d-f7b0-4b98-a52d-e60803a78d40.mov" />](https://app.graphite.dev/media/video/Wrwhm8Mmhv1Z2GwlSb7V/68aa0b0d-f7b0-4b98-a52d-e60803a78d40.mov)

**Deleting steps within the for-each group**

[Screen Recording 2025-06-04 at 2.50.53 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/Wrwhm8Mmhv1Z2GwlSb7V/84ae77f3-1c39-4597-898a-ad03678ef692.mov" />](https://app.graphite.dev/media/video/Wrwhm8Mmhv1Z2GwlSb7V/84ae77f3-1c39-4597-898a-ad03678ef692.mov)
## TL;DR
Not related to for-each, just a refactor of branch deletion since the previous PR created a generic component and hook to perform flow step group deletion.

## What changed?
Use the new generic component and hook to handle group deletion.

## How to test?
- [ ] Delete If-then branch
- [ ] Delete if-then branch within a for-each group

## Screenshots
**If-then**

[Screen Recording 2025-06-04 at 4.05.03 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/Wrwhm8Mmhv1Z2GwlSb7V/358acdc9-95c1-4bc9-9695-b71e492ad961.mov" />](https://app.graphite.dev/media/video/Wrwhm8Mmhv1Z2GwlSb7V/358acdc9-95c1-4bc9-9695-b71e492ad961.mov)

**If-then inside for-each**

[Screen Recording 2025-06-04 at 4.04.33 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/Wrwhm8Mmhv1Z2GwlSb7V/64a2f167-b039-4d61-b513-565eb05f19c3.mov" />](https://app.graphite.dev/media/video/Wrwhm8Mmhv1Z2GwlSb7V/64a2f167-b039-4d61-b513-565eb05f19c3.mov)
This PR processes the for-each input (checkbox or table data) to generate output in the data out and create variables for use in the subsequent steps.

**Note to reviewer**:
* this PR only generates the variable, which can be selected in subsequent steps. However, executing those steps will not pull the correct data yet, as the computation of the parameters will be implemented in the next PR.
* this PR does not check that the test execution result matches the form values, so it will always show 'previous result', this will be addressed in a later PR.

- Process different input sources in the For Each action: checkbox lists, tiles, and M365 Excel data
- Created utility functions to process and validate different input formats
- Implemented proper data output metadata to display relevant information based on input type
- Added constants for iteration keys and input source types
- Created comprehensive test suite for the new utility functions

1. Test the For Each action with different input types:
   - FormSG checkboxes (comma-separated values)
   - Tiles data (JSON with rows containing rowId)
   - M365 Excel data (JSON with rows without rowId)
2. Verify that the action correctly processes each input type
3. Check that the test result displays appropriate information:
   - For checkbox lists: shows the first item value
   - For table data: shows column names

**Checkbox**

[Screen Recording 2025-06-06 at 9.01.52 AM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/Wrwhm8Mmhv1Z2GwlSb7V/bcc73567-a3d7-44ef-bb01-5f1f92c045bd.mov" />](https://app.graphite.dev/media/video/Wrwhm8Mmhv1Z2GwlSb7V/bcc73567-a3d7-44ef-bb01-5f1f92c045bd.mov)

**Tiles**

[Screen Recording 2025-06-06 at 9.04.00 AM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/Wrwhm8Mmhv1Z2GwlSb7V/8ba51f14-a8b7-4cf9-be16-dc34aaf4df35.mov" />](https://app.graphite.dev/media/video/Wrwhm8Mmhv1Z2GwlSb7V/8ba51f14-a8b7-4cf9-be16-dc34aaf4df35.mov)

**Excel**

[Screen Recording 2025-06-06 at 9.06.52 AM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/Wrwhm8Mmhv1Z2GwlSb7V/4956ef80-321a-4f5c-96e4-a7d4580514aa.mov" />](https://app.graphite.dev/media/video/Wrwhm8Mmhv1Z2GwlSb7V/4956ef80-321a-4f5c-96e4-a7d4580514aa.mov)
Implements functionality to execute steps within the for-each action with variables:
* Variable from the for-each step
* Variable from before the for-each step
* Variable from within the for-each step (note this refers to iteration-specific variable)

- Add `key` column to `execution_steps` table to track step types
- Add the following to execution steps `metadata` to track progress of the for-each action
  - the for-each step:
    - `iterations`: number of iterations
    - `iterationStatus`: object where key is iteration (`iteration_x`) and value is the status (`success`, `failure`, `null`). this is introduced to enable easy checking of execution state and for retrieving iteration-specific execution steps in the executions page.
   - steps within the for-each:
     - `iteration`: iteration number
     - `isLastStep`: whether its the last step of the iteration. this is mainly used during retries to check whether to update the status of the entire execution
     - `isLastIteration`: whether this is the last iteration. this is mainly used during the execution itself to determine whether to update teh status of the entire execution
- Add validation to ensure flows have at most one For-Each step before allowing user to publish the pipe
- Add constants for For-Each configuration (max iterations, delay between iterations)
- Add tests for For-Each parameter computation

- [ ] Only one for-each step allowed
  - Manually modify `steps` in the DB to have more than 1 for-each action in an unpublished pipe
  - Attempt to publish via frontend, should see error message
- [ ] Check for-each action steps
  - Create actions within the for-each, verify that check step only tests with the first checkbox item or first row of data
- [ ] Execute for-each actions by publishing pipe
  - Create actions within the for-each that use a mix of variables from the for-each step and steps within the for-each
  - Verify that each iteration uses the correct data:
    - [ ] using for-each variable, it should use the correct checkbox item or row data
    - [ ] using variable from step within the action, it should use the data from the correct iteration
  - Verify that status is updated correctly:
    - [ ] `success`: all iterations succeeded
    - [ ] `failure`: any iteration failed, regardless of whether all iterations have run
    - [ ] `waiting`: execution has not completed, may include successful iterations
  - [ ] Pipe still succeeds if no checkbox items are selected
  - [ ] Pipe still succeeds if no rows are found by find multiple rows

![Screenshot 2025-06-09 at 10.43.40 AM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/Wrwhm8Mmhv1Z2GwlSb7V/33630896-5088-4377-b464-2822339c06a4.png)

![Screenshot 2025-06-09 at 10.43.58 AM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/Wrwhm8Mmhv1Z2GwlSb7V/1f18cce5-5e25-4492-8db5-b473e24356a7.png)

![Screenshot 2025-06-09 at 10.44.14 AM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/Wrwhm8Mmhv1Z2GwlSb7V/d1fac486-ce73-4d80-b4e6-0eed2f218fec.png)

![Screenshot 2025-06-09 at 10.44.33 AM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/Wrwhm8Mmhv1Z2GwlSb7V/8792a49b-f0a0-4373-a7b7-f47a65bb831f.png)
This PR enhances the execution steps display to properly handle iterations from for-each loops by introducing a grouped view for  execution steps from for-each iterations.

- Created new components for displaying grouped execution steps:
  - `ExecutionGroup`: Main component for displaying for-each iterations
  - `GroupStatusFilter`: Filter to show iterations by status (success/failure/waiting)
  - `IterationSelector`: Dropdown to navigate between different iterations
- Added `useExecutionStepStatus` hook to handle execution step status logic
- Implemented `processExecutionSteps` helper to organise steps into iterations
- Iteration-specific execution steps are fetched individually

- Create a Pipe with for-each step and actions
- Execute the Pipe and go to the Executions page
  - [ ] Dropdown shows the correct number of items
  - [ ] Status of each iteration is correct
  - [ ] Status filter applies correctly and shows the correct iteration
    - [ ] 'All' resets to the first iteration
    - [ ] 'Success' goes to the first successful iteration
    - [ ] 'Failure' goes to the first failed iteration
- Pagination should work correctly
  - [ ] Normal pipe without for-each and > 100 steps shows executions as usual
  - [ ] For-each within the first 100 steps
    - [ ] Should not show pagination
  - [ ] For-each as the 101st step or beyond
    - [ ] Should show pagination up to the page where the for-each step is on
  - [ ] For-each with more than 100 steps
    - [ ] Should show pagination within the execution group itself

**Pipe with < 100 steps and < 100 steps within the for-each**

[Screen Recording 2025-06-22 at 3.50.30 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/Wrwhm8Mmhv1Z2GwlSb7V/fb8027a1-3303-44b5-af11-632eb0026c13.mov" />](https://app.graphite.dev/media/video/Wrwhm8Mmhv1Z2GwlSb7V/fb8027a1-3303-44b5-af11-632eb0026c13.mov)

**Pipe with > 100 steps and for-each is is beyond the 101st step**

[Screen Recording 2025-06-22 at 3.50.59 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/Wrwhm8Mmhv1Z2GwlSb7V/15b762f8-ab56-40de-84b6-975632910e3c.mov" />](https://app.graphite.dev/media/video/Wrwhm8Mmhv1Z2GwlSb7V/15b762f8-ab56-40de-84b6-975632910e3c.mov)

**Pipe with > 100 steps within the for-each**

[Screen Recording 2025-06-22 at 3.51.36 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/Wrwhm8Mmhv1Z2GwlSb7V/118b053d-ea8a-4682-9a42-b02b3ea8b34f.mov" />](https://app.graphite.dev/media/video/Wrwhm8Mmhv1Z2GwlSb7V/118b053d-ea8a-4682-9a42-b02b3ea8b34f.mov)

**With items to run**

[Screen Recording 2025-06-18 at 9.08.11 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/Wrwhm8Mmhv1Z2GwlSb7V/3c06c226-5e41-468c-9e41-38d1cae84ab4.mov" />](https://app.graphite.dev/media/video/Wrwhm8Mmhv1Z2GwlSb7V/3c06c226-5e41-468c-9e41-38d1cae84ab4.mov)

**No items to run**

![Screenshot 2025-06-18 at 9.07.27 PM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/Wrwhm8Mmhv1Z2GwlSb7V/db0ed5eb-5ca8-4957-a144-7b3fe9284901.png)
### TL;DR

Added functionality to retry all failed iterations within a single execution.
This enhances user experience by making it easy to retry failed iterations, instead of opening each failed iteration and clicking the 'retry' button.

### What changed?

- Created a new GraphQL mutation `bulkRetryIterations` that allows retrying all failed iterations within a specific execution
- Added backend implementation to identify and retry failed iterations in chunks
- Enhanced the frontend UI to support retrying all failed iterations from the execution view
- Added a new retry button in the iteration selector component

### How to test?
- Create Pipe with for-each that will throw errors and cause failures
  - [ ] Should see 'Retry all failed items' button
  - [ ] Click on the button and should see toast indicating that Plumber is attempting to retry all failed items
  - [ ] Status on Executions page should reflect correct status
    - Success if all retries passed
    - Failed if any retries failed

### Screenshots

[Screen Recording 2025-06-22 at 4.17.46 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/Wrwhm8Mmhv1Z2GwlSb7V/a782a66c-d79f-42b2-9ae1-c72a132be29d.mov" />](https://app.graphite.dev/media/video/Wrwhm8Mmhv1Z2GwlSb7V/a782a66c-d79f-42b2-9ae1-c72a132be29d.mov)
Added a new "For Each Reminder" template to replace the existing "Schedule Reminders" template, with a new tag system to highlight new templates.

- Created a new template file `for-each-reminder.ts` that implements a workflow for scheduling reminders to multiple email recipients
- Updated the template index to use the new template instead of the old `SCHEDULE_REMINDERS_TEMPLATE`
- Added a new template tag type `new` to the GraphQL schema and TypeScript types
- Enhanced the `TemplateTile` component to display a "New" badge for templates with the "new" tag

1. Navigate to the templates page
2. Verify that the "Schedule reminders to a list of emails" template appears with a "New" badge
3. Create a flow using this template and confirm that it correctly:
   - Finds rows in a Tiles table where "RSVPed" is "Yes" and "Reminder sent" is empty
   - Iterates through each row using the forEach toolbox function
   - Sends an email reminder to each person
   - Updates the "Reminder sent" column to "Yes" after sending

This change aims to introduce and guide users to set up their for-each actions.

[Screen Recording 2025-06-17 at 1.22.43 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/Wrwhm8Mmhv1Z2GwlSb7V/bca30412-2437-4ad5-ba93-1dde672049e0.mov" />](https://app.graphite.dev/media/video/Wrwhm8Mmhv1Z2GwlSb7V/bca30412-2437-4ad5-ba93-1dde672049e0.mov)
Use the custom step name or app display name in the suggestions popover.
This only works with steps that have been tested after for-each was introduced and have `appKey` and `key` in the ExecutionStep.

**Note**: this PR exists within the for-each stack as it uses both `appKey` and `key` from the ExecutionStep, and `key` is only added to ExecutionStep as part of for-each.

Added a `getStepName` function that retrieves more descriptive names for steps based on app information:
  - Custom step name if exists
  - App display name if exists
  - Defaults to app key by default

- [ ] Pipe whose steps were tested after the for-each
  - If custom step name is set, should show custom step name
  - Otherwise, should show the default step name (e.g., New form submission, Find single row, etc.)

- [ ] Old pipes
  - If custom step name is set, should show custom step name
  - Otherwise, should show the app key (e.g., Toolbox, Tiles, Slack)
  - Click on 'Check step again'
  - Should now change to the descriptive step name (e.g., New form submission, Find single row, etc.)

**New pipe after for-each introduced `key`**

![Screenshot 2025-06-19 at 11.26.17 AM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/Wrwhm8Mmhv1Z2GwlSb7V/8f1bd88a-165f-4ff4-b2cd-832035b9e25d.png)

**Old pipe**

![Screenshot 2025-06-19 at 11.24.04 AM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/Wrwhm8Mmhv1Z2GwlSb7V/f7c57877-6926-4b20-bdd1-5f3904b1011e.png)

**Old pipe with re-tested step**

![Screenshot 2025-06-19 at 11.25.12 AM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/Wrwhm8Mmhv1Z2GwlSb7V/21a846dd-6652-4e36-83e8-fc51f72444f0.png)
This PR adds a "New" badge to actions marked with `isNew: true` and ensures they appear at the top of their respective app sections in the UI.

- Added `isNew` property to the GraphQL schema for both Action and Trigger types
- Created a reusable `NewBadge` component to display the "New" badge consistently
- Modified the app sorting logic to prioritise actions with `isNew: true` within each app
- Added the `isNew: true` flag to several actions:
  - "Find table rows" in Excel
  - "Find multiple rows" in Tiles
  - "For each" in Toolbox
- Updated the UI components to display the "New" badge next to actions and triggers when applicable

- For each action
![Screenshot 2025-07-09 at 7.30.28 PM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/Wrwhm8Mmhv1Z2GwlSb7V/57b7c962-cd09-4f4a-900a-139c3381ed4e.png)

- Find multiple rows action

![Screenshot 2025-07-09 at 7.32.03 PM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/Wrwhm8Mmhv1Z2GwlSb7V/3b2c4843-209a-4b16-a3f4-d1519450339a.png)

![Screenshot 2025-07-09 at 7.32.42 PM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/Wrwhm8Mmhv1Z2GwlSb7V/cdf051b1-e9b6-4054-9feb-d2da8483f6c2.png)
Added a new 'partial-success' status for For Each iterations to better represent scenarios where steps complete with non-critical errors. This also makes it easier for users to filter iterations for retry on the Executions page.
_Partial successes are most likely to occur in Email by Postman actions where recipient emails are blacklisted._

- Added a new 'partial-success' status type to the execution step model
- Updated the frontend to display and filter partial success states
- Added a partial success icon and updated the GroupStatusFilter component to include the new status
- Modified the iteration status detection logic to identify partial success when a step has errorDetails but still completes

1. Create a Pipe with a ForEach step that includes an action that might result in partial success (e.g., email sending with some blacklisted recipients - `[email protected]`)
2. Execute the workflow and observe the partial success status in the execution page

- [ ] Execution should still be a success
- [ ] For each step should show as partial success
- [ ] Iteration selector should show the correct iteration with the partial success state
- [ ] Status filters should filter the different status correctly

[Screen Recording 2025-07-09 at 11.14.01 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/Wrwhm8Mmhv1Z2GwlSb7V/35adf176-1bb6-45eb-892b-a798b93f4780.mov" />](https://app.graphite.dev/media/video/Wrwhm8Mmhv1Z2GwlSb7V/35adf176-1bb6-45eb-892b-a798b93f4780.mov)
### TL;DR
Allows users to replace variables in single-select mode by clicking on a variable in the input and selecting a new one from the suggestions popover.


### How to test?
- [ ] Single select inputs (For-each item)
  - [ ] Can select variable in empty input
  - [ ] Can delete variable and select a new variable
  - [ ] Can select variable and replace with another variable from the suggestions popover

- [ ] All other inputs
  - [ ] Can still select multiple variables and type freely in them
  - [ ] Can select variable and replace with another variable from the suggestions popover

### Screenshots

[Screen Recording 2025-07-09 at 10.59.31 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/Wrwhm8Mmhv1Z2GwlSb7V/14695cf9-0766-49d4-b236-c275dc4856a3.mov" />](https://app.graphite.dev/media/video/Wrwhm8Mmhv1Z2GwlSb7V/14695cf9-0766-49d4-b236-c275dc4856a3.mov)

### Why make this change?
Previously, users could replace variables by clicking on them and selecting a new one from the suggestions popover. This change prevents a regression and restores that expected behaviour.
### TL;DR
Fixed inconsistent handling of empty values in For Each action, which caused issues when cell values were `0` by standardising how empty values and string conversions are managed.

### What changed?

- Modified `getDataOutMetadata.ts` to consistently convert displayed values to strings using `String()` instead of relying on implicit conversion
- Changed empty value representation from space character (`' '`) to empty string (`''`) in tests and implementation so that checking against step parameters against test executions yielded accurate results of whether that was the latest test
- Fixed the fallback logic in `computeForEachParameters.ts` to use nullish coalescing (`??`) instead of logical OR (`||`) to properly handle falsy values like `0` or `false`
- Simplified the variable value selection in `RichTextEditor` component using nullish coalescing

### How to test?

1. Create a Pipe with a For Each action
2. Test with various data types including empty values, zeros, and boolean values

- [ ] Variable pills should show `0`
- [ ] Variable should show `false`
- [ ] Variable with empty displayedValue should be an empty variable pill with dotted outline


### Screenshots
#### Before

![Screenshot 2025-07-10 at 9.47.09 PM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/Wrwhm8Mmhv1Z2GwlSb7V/8404508c-85ee-43ec-a6f1-ca8357ea45a5.png)

#### After

![Screenshot 2025-07-10 at 9.54.31 PM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/Wrwhm8Mmhv1Z2GwlSb7V/d57d32a2-4cd8-4140-bb64-424b1f5a810e.png)

### Why make this change?

This change ensures consistent handling of empty and falsy values throughout the For Each action. The previous implementation had inconsistencies where empty values were sometimes represented as spaces and numeric values weren't properly converted to strings. Using nullish coalescing instead of logical OR prevents valid falsy values (like 0 or false) from being incorrectly replaced with empty strings.
## Problem
Pipe does not execute properly when for-each step is the second step.
Trigger step does not have metadata, which causes for-each step to fail if it is the second step in the Pipe.

_This was not caught previously as we tested with additional find multiple row step, which are action steps and have metadata by default._

## Solution
Add a default metadata at `processAction`

## How to test
Create a Pipe where:
1. FormSG with checkbox
2. For-each step using checkboxes
3. Perform some action

- [ ] Each checkbox option is correctly iterated over
- [ ] Executions page shows success with the correct number of checkbox options
### TL;DR
Use the `app_toolbox_action_forEach` LD flag to decide on the frontend whether to show the "Schedule reminders" template with or without the for-each step.

### Why make this change?
As for-each is a beta release, we only want whitelisted users to be able to view, use, and test the template. All remaining users should be able to use the existing "Schedule reminders" template.

_Using the frontend check and remove when going to GA as it is unlikely that we will have to flag templates often and we do not want to add additional parameters to the getTemplates query_


### How to test?
- [ ] Login as a non-whitelisted user, should see the original Schedule reminders template
  - [ ] Verify that template can be used
- [ ] Login as whitelisted user, should see the NEW Schedule reminders template with for-each and find multiple rows
  - [ ] Verify that template can be used
## Problem
Cannot sync `plumber-admin` to the current production build.

## Solution
Got it to work by converting the imports from `.mjs` to `.js`
Seems be safe to do that since we already specify "type": "module", and
the .js files are still treated as ES modules.

## How to test?
- [ ] Pull this and run on your local plumber-admin
- [ ] Deploy to staging and verify
### Staging
- [x] Run migration `20250606072418_add_execution_steps_key.ts`
- [x] Add whitelisted users to launchdarkly (added Plumber's tech.gov.sg
emails to verify that flag works)

### Prod
- [x] Run migration `20250606072418_add_execution_steps_key.ts`
- [x] Add whitelisted users to launchdarkly

### Sanity check
- [x] Tiles find multiple rows work as intended - previews show
correctly
- [x] Excel get table rows work as intended - previews show correctly
- [x] FormSG checkboxes work as intended
- [x] For-each action processes input data correctly - show correct
variables
- [x] For-each actions execute with the correct parameters
  - [x] parameters from Tiles row data
  - [x] parameters from Excel row data
  - [x] parameters from FormSG checkboxes
  - [x] parameters from steps within the for-each
- [x] Template creates the flow correctly
- [x] FormSG mock and actual test submission still works as intended
@kevinkim-ogp kevinkim-ogp requested a review from a team as a code owner July 17, 2025 09:00
@datadog-opengovsg
Copy link

datadog-opengovsg bot commented Jul 17, 2025

Datadog Report

Branch report: develop-v2
Commit report: d1908b8
Test service: plumber

✅ 0 Failed, 1005 Passed, 0 Skipped, 2m 45.44s Total Time
➡️ Test Sessions change in coverage: 1 no change

Copy link
Contributor

@pregnantboy pregnantboy left a comment

Choose a reason for hiding this comment

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

lfg

@kevinkim-ogp kevinkim-ogp merged commit f4799bd into production Jul 17, 2025
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants