Skip to content

Conversation

@jvega190
Copy link
Member

@jvega190 jvega190 commented Sep 26, 2025

craftercms/craftercms#7418

Summary by CodeRabbit

  • New Features

    • Responsive multi-column Checkbox Group with vertical/horizontal layouts, adaptive virtualization, Select All (with indeterminate/readonly) and loading skeleton.
  • Improvements

    • Read-only propagation to search and controls; refined option ordering and keyword filtering for column layouts; virtualization adapted for columns.
  • Other

    • Added checkbox-group minimum-selection validation and a window-width hook to enable responsive behavior.

@coderabbitai
Copy link

coderabbitai bot commented Sep 26, 2025

Warning

Rate limit exceeded

@jvega190 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 39 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 79a0e7c and c38a797.

📒 Files selected for processing (1)
  • ui/app/src/components/FormsEngine/lib/validators.ts

Walkthrough

Adds a responsive, readonly-aware CheckboxGroup control with Select All, search/filtering, multi-column and virtualized rendering; wires a checkbox-group validator enforcing minSize; adds useWindowWidth hook; and parses legacy minSize constraint into field validations.

Changes

Cohort / File(s) Summary
CheckboxGroup control
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx
Implements responsive multi-column layout (derives numColumns from window width), propagates readonly to search/options, supports listDirection, adds Select All with indeterminate state, reworks option rendering (buildOption, VirtualRow), adapts virtualization for columns, reorders/filters options, and renders a loading skeleton.
Validator implementation
ui/app/src/components/FormsEngine/lib/validators.ts
Adds checkboxGroupValidator enforcing field.validations?.minSize?.value (counts selections, pushes localized error), and registers it in validatorsMap for 'checkbox-group'. Exports the new validator function and updates the map entry.
Window width hook
ui/app/src/hooks/useWindowWidth.ts
Adds useWindowWidth() hook that tracks window.innerWidth with a resize listener and cleanup; returns current width.
Model: validation key
ui/app/src/models/ContentType.ts
Adds 'minSize' to the ValidationKeys union so ContentTypeFieldValidations can include a minSize entry.
Legacy parser: minSize
ui/app/src/services/contentTypes.ts
Parses legacy minSize constraint in parseLegacyFormDefinitionFields, storing numeric field.validations.minSize (id minSize, numeric value, level required).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant CG as CheckboxGroup
  participant HW as useWindowWidth
  participant V as Validator
  participant P as Parser

  Note over HW,CG: component mount
  HW->>CG: provide current width
  CG->>CG: compute numColumns & determine readonly

  U->>CG: type in search
  CG->>CG: filter finalOptions (label & key)

  U->>CG: click Select All
  CG->>CG: compute selections (respect readonly)
  CG-->>U: update UI (checked/indeterminate)

  U->>CG: toggle option
  CG->>CG: update selection state

  CG->>V: run checkboxGroupValidator
  V-->>CG: validity + messages

  Note over P: build-time parsing
  P->>P: parse legacy `minSize` -> field.validations.minSize
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • rart

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The pull request description only includes a link to the related issue and lacks any summary of the implementation details, features added, or context for the changes, which prevents reviewers from understanding what the PR accomplishes without inspecting the code diffs. Please expand the description to include an overview of the changes, the motivation behind them, and any relevant implementation details following the repository’s template for ticket references and PR descriptions.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title “[7418] FE2 -CheckboxGroup control” directly references the issue number and succinctly describes the primary focus of the pull request, which is adding the FE2 version of the CheckboxGroup control with its associated layout and behavior changes.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ui/app/src/components/FormsEngine/lib/validators.ts (1)

23-33: Fix validatorsMap type and message handling

  • Change from Record<BuiltInControlType, ElementType> to Record<BuiltInControlType, ValidatorFn | null> by adding
    type ValidatorFn = (field: ContentTypeField, currentValue: unknown, messages: string[]) => boolean;
    export const validatorsMap: Record<BuiltInControlType, ValidatorFn | null> = {};
  • Remove the undefined defineMessage call and push a plain string, e.g.:
    if (!isValid) messages.push(`Select at least ${minSelected} option${minSelected === 1 ? '' : 's'}.`);
🧹 Nitpick comments (2)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (2)

48-52: Remove hardcoded width on option label to avoid layout conflicts

The Grid defines the layout; forcing 50% width on FormControlLabel causes misalignment. Use full width or omit.

-  sx={{ width: '50%' }}
+  sx={{ width: '100%' }}

240-264: Legacy flex-basis styling conflicts with Grid layout

This block assumes 3 columns and can fight Grid sizing. Consider removing flexBasis and height overrides when Grid is used.

Would you like a follow-up patch to conditionally apply this only when not using the Grid container?

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f06daa4 and d2fd43c.

📒 Files selected for processing (5)
  • ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (8 hunks)
  • ui/app/src/components/FormsEngine/lib/validators.ts (1 hunks)
  • ui/app/src/hooks/useWindowWidth.ts (1 hunks)
  • ui/app/src/models/ContentType.ts (1 hunks)
  • ui/app/src/services/contentTypes.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (7)
ui/app/src/models/LookupTable.ts (1)
  • LookupTable (17-17)
ui/app/src/components/FormsEngine/controls/Checkbox.tsx (1)
  • Checkbox (26-35)
ui/app/src/hooks/useWindowWidth.ts (1)
  • useWindowWidth (19-29)
ui/app/src/hooks/useDebouncedInput.ts (1)
  • useDebouncedInput (22-32)
ui/app/src/components/FormsEngine/dataSourceHooks/useKVPLoader.ts (1)
  • useKVPLoader (30-77)
ui/app/src/components/FormsEngine/components/FormsEngineField.tsx (1)
  • FormsEngineField (91-279)
ui/app/src/components/SearchBar/SearchBar.tsx (1)
  • SearchBar (61-209)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Codacy Static Code Analysis
  • GitHub Check: Travis CI - Pull Request
🔇 Additional comments (3)
ui/app/src/models/ContentType.ts (1)

54-56: LGTM: Validation key extension

Adding 'minSize' to ValidationKeys aligns with new constraint parsing and validators.

ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (2)

29-29: No import change needed for useDebouncedInput
ui/app/src/hooks/useDebouncedInput.ts exports the hook both as a named and default export; the existing default import is valid.

Likely an incorrect or invalid review comment.


80-88: Incorrect MUI Grid props: use item + xs/sm/md/lg instead of size

@mui/material/Grid doesn’t support size={{ ... }}. Use item plus breakpoint props. Apply to both virtualized and non‑virtualized rows.

-        <Grid size={{ sm: 12, md: 6, lg: 4 }} key={option.key}>
+        <Grid item xs={12} md={6} lg={4} key={option.key}>

Apply the same change in the non‑virtualized map.

Also applies to: 279-289

⛔ Skipped due to learnings
Learnt from: jvega190
PR: craftercms/studio-ui#4580
File: ui/app/src/components/SitesGrid/SitesGrid.tsx:58-58
Timestamp: 2025-07-18T20:43:52.177Z
Learning: In MUI 7, the `size` prop is not required for Grid components. When migrating from MUI 5 to MUI 7, removing the `item` prop from Grid components without adding a `size` prop is valid and acceptable. The `size` prop is only needed when you want to explicitly control the width of the grid item.
Learnt from: jvega190
PR: craftercms/studio-ui#4580
File: ui/app/src/components/SitesGrid/SitesGrid.tsx:58-58
Timestamp: 2025-07-18T20:43:52.177Z
Learning: In MUI v7, the `size` prop on Grid components is optional, not required. Grid items can exist without explicit sizing and will use their natural dimensions. When migrating from MUI v5 to v7, simply removing the `item` prop without adding a `size` prop is a valid approach when you want the Grid items to size naturally based on their content.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ui/app/src/components/FormsEngine/lib/validators.ts (1)

28-63: Type error: validatorsMap assigns nulls but type excludes null

validatorsMap is typed as Partial<..., ValidatorFunctionDef>, so entries must be functions or undefined. Many entries are set to null, which breaks type-checking.

Simplify by only declaring implemented validators (others can be omitted), or use undefined. Recommended:

-export const validatorsMap: Partial<Record<BuiltInControlType, ValidatorFunctionDef>> = {
-  repeat: null,
-  'auto-filename': null,
-  'aws-file-upload': null,
-  'checkbox-group': (field, currentValue, messages) => { /* ... */ },
-  checkbox: null,
-  'date-time': null,
-  disabled: null,
-  dropdown: null,
-  'file-name': null,
-  forcehttps: null,
-  'image-picker': null,
-  input: null,
-  'internal-name': null,
-  label: null,
-  'link-input': null,
-  'link-textarea': null,
-  'linked-dropdown': null,
-  'locale-selector': null,
-  'node-selector': null,
-  'numeric-input': null,
-  'page-nav-order': null,
-  rte: null,
-  textarea: null,
-  time: null,
-  'transcoded-video-picker': null,
-  uuid: null,
-  'video-picker': null,
-  colorPicker: undefined
-};
+export const validatorsMap: Partial<Record<BuiltInControlType, ValidatorFunctionDef>> = {
+  'checkbox-group': (field, currentValue, messages) => {
+    const minSelected = Number(field.validations?.minSize?.value ?? 0);
+    const selectedCount = Array.isArray(currentValue) ? currentValue.length : 0;
+    const isValid = selectedCount >= minSelected;
+    if (!isValid) {
+      messages.push(defineMessage({ defaultMessage: 'Please select at least the minimum required items.' }));
+    }
+    return isValid;
+  }
+};
🧹 Nitpick comments (3)
ui/app/src/components/FormsEngine/components/FormsEngineField.tsx (1)

275-279: Avoid rendering “0” when messages array is empty

Using length in a JSX logical chain can render 0. Guard with a boolean or an explicit > 0 check.

Apply this diff:

-      {!isValid &&
-        validityData?.messages?.length &&
-        validityData.messages.map((message, key) => (
+      {!isValid &&
+        (validityData?.messages?.length ?? 0) > 0 &&
+        validityData!.messages.map((message, key) => (
           <FormHelperText key={key}>{translateIfMessageDescriptor(message, formatMessage)}</FormHelperText>
         ))}
ui/app/src/components/FormsEngine/lib/formUtils.tsx (1)

725-731: Helper looks good; adjust IntlShape import to public API

translateIfMessageDescriptor is correct and typed well. Prefer importing IntlShape from 'react-intl' rather than 'react-intl/src/types' to avoid relying on internal paths (important in react-intl 7.x).

Apply this change to the import near Line 38:

-import { IntlShape } from 'react-intl/src/types';
+import type { IntlShape } from 'react-intl';

Based on learnings

ui/app/src/components/FormsEngine/lib/validators.ts (1)

23-28: Consider exporting ValidatorFunctionDef or inlining to the exported type

If consumers need to type validatorsMap or external validators, export ValidatorFunctionDef, or inline its shape in the validatorsMap type to avoid non-exported types in public declarations.

Apply this diff:

-type ValidatorFunctionDef = (
+export type ValidatorFunctionDef = (
   field: ContentTypeField,
   currentValue: unknown,
   messages: FieldValidityState['messages']
 ) => boolean;
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d2fd43c and 509dd17.

📒 Files selected for processing (3)
  • ui/app/src/components/FormsEngine/components/FormsEngineField.tsx (2 hunks)
  • ui/app/src/components/FormsEngine/lib/formUtils.tsx (2 hunks)
  • ui/app/src/components/FormsEngine/lib/validators.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
ui/app/src/components/FormsEngine/components/FormsEngineField.tsx (2)
ui/app/src/components/FormsEngine/lib/formUtils.tsx (1)
  • translateIfMessageDescriptor (725-730)
static-assets/components/cstudio-forms/forms-engine.js (1)
  • formatMessage (786-786)
ui/app/src/components/FormsEngine/lib/formUtils.tsx (2)
static-assets/components/cstudio-common/common-api.js (3)
  • formatMessage (712-712)
  • formatMessage (847-847)
  • formatMessage (2333-2333)
static-assets/components/cstudio-forms/forms-engine.js (1)
  • formatMessage (786-786)
ui/app/src/components/FormsEngine/lib/validators.ts (3)
ui/app/src/models/ContentType.ts (1)
  • ContentTypeField (65-143)
ui/app/src/components/FormsEngine/lib/controlMap.ts (1)
  • BuiltInControlType (22-50)
ui/app/src/utils/string.ts (1)
  • isEmpty (283-286)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Codacy Static Code Analysis
  • GitHub Check: Travis CI - Pull Request
🔇 Additional comments (1)
ui/app/src/components/FormsEngine/components/FormsEngineField.tsx (1)

46-46: Good addition: translating validation messages

Importing translateIfMessageDescriptor and using formatMessage aligns the rendering path with MessageDescriptor-based validators.

@jvega190 jvega190 marked this pull request as ready for review September 26, 2025 22:16
@jvega190 jvega190 requested a review from rart as a code owner September 26, 2025 22:16
@jvega190
Copy link
Member Author

Outside diff range comment addressed in here

…o feature/7418-checkbox-group

# Conflicts:
#	ui/app/src/components/FormsEngine/components/FormsEngineField.tsx
#	ui/app/src/components/FormsEngine/lib/validators.ts
@jvega190 jvega190 marked this pull request as draft October 8, 2025 20:54
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (1)

156-162: Select All logic still doesn't handle filtered views correctly.

This issue was raised in the previous review but has not been fully addressed. The current implementation compares value.length (all selected items) with finalOptions.length (visible items), which can lead to incorrect behavior:

  1. When "Select All" is clicked in a filtered view, it clears ALL selections (including items not visible in the filter), rather than toggling only the visible items.
  2. The checked/indeterminate state at lines 186-187 will be incorrect when selections exist outside the filtered view.

Apply this diff to implement the correct logic as suggested in the previous review:

+	const visibleKeySet = useMemo(() => new Set(finalOptions.map((o) => o.key)), [finalOptions]);
+	const selectedInViewCount = useMemo(
+		() => value.reduce((acc, item) => acc + (visibleKeySet.has(item.key) ? 1 : 0), 0),
+		[value, visibleKeySet]
+	);
+	const allInViewSelected = finalOptions.length > 0 && selectedInViewCount === finalOptions.length;
+	const someInViewSelected = selectedInViewCount > 0 && !allInViewSelected;
+
 	const checkAll = () => {
-		if (value.length === finalOptions.length) {
-			setValue([]);
+		if (allInViewSelected) {
+			// Unselect only visible ones
+			setValue(value.filter((item) => !visibleKeySet.has(item.key)));
 		} else {
-			setValue(finalOptions.map((option) => ({ key: option.key, value_smv: option.value })));
+			// Select all visible (merge with existing)
+			const existing = new Set(value.map((v) => v.key));
+			const additions = finalOptions
+				.filter((o) => !existing.has(o.key) && o.value)
+				.map((o) => ({ key: o.key, value_smv: o.value }));
+			setValue(value.concat(additions));
 		}
 	};

Also update the checkbox state at lines 186-187:

-						indeterminate={value.length > 0 && value.length < finalOptions.length}
-						checked={value.length === finalOptions.length}
+						indeterminate={someInViewSelected}
+						checked={allInViewSelected}
🧹 Nitpick comments (3)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (3)

41-41: Remove unused htmlId variable.

The htmlId is declared but never used in the component. This creates unnecessary overhead and clutters the code.

Apply this diff to remove the unused variable:

-	const htmlId = useId();
 	const theme = useTheme();

47-47: Align breakpoints with MUI Grid responsive system.

The numColumns breakpoints (1200, 900) don't fully align with the MUI Grid responsive sizing used later (size={{ sm: 12, md: 6, lg: 4 }}), which follows MUI's standard breakpoints (sm: 600, md: 900, lg: 1200). This mismatch can cause inconsistent column counts between the responsive calculation and the actual grid layout, especially in the 600-900px range.

Apply this diff to align with MUI breakpoints:

-	const numColumns = windowWidth >= 1200 ? 3 : windowWidth >= 900 ? 2 : 1;
+	const numColumns = windowWidth >= 1200 ? 3 : windowWidth >= 900 ? 2 : windowWidth >= 600 ? 1 : 1;

Or simplify to:

-	const numColumns = windowWidth >= 1200 ? 3 : windowWidth >= 900 ? 2 : 1;
+	const numColumns = windowWidth >= 1200 ? 3 : windowWidth >= 600 ? 2 : 1;

This ensures consistent behavior across all viewport sizes and matches the Grid's responsive breakpoints.


251-271: Remove redundant inline width in buildOption.

Line 259 sets width: '50%' on the FormControlLabel, but this component is always rendered inside a Grid system (both in the non-virtualized path at line 235 and the virtualized path at line 291) where the Grid's responsive size prop controls the width. The inline width style is redundant and could potentially conflict with the Grid's layout calculations.

Apply this diff to remove the redundant width:

 const buildOption = (
 	option: KVPLoaderItem['items'][0],
 	onChange: CheckboxProps['onChange'],
 	checkedValuesLookup: LookupTable<boolean>,
 	readonly: boolean = false
 ) => (
 	<FormControlLabel
 		key={option.key}
-		sx={{ width: '50%' }}
 		control={
 			<Checkbox
 				disabled={readonly}
 				color="info"
 				checked={checkedValuesLookup[option.key] ?? false}
 				onChange={onChange}
 				value={option.key}
 			/>
 		}
 		label={option.value}
 	/>
 );
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d66415c and d512328.

📒 Files selected for processing (2)
  • ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (6 hunks)
  • ui/app/src/components/FormsEngine/lib/validators.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • ui/app/src/components/FormsEngine/lib/validators.ts
🧰 Additional context used
🧬 Code graph analysis (1)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (7)
ui/app/src/components/FormsEngine/types.ts (1)
  • ControlProps (23-30)
ui/app/src/hooks/useWindowWidth.ts (1)
  • useWindowWidth (19-32)
ui/app/src/components/FormsEngine/dataSourceHooks/useKVPLoader.ts (2)
  • useKVPLoader (30-77)
  • KVPLoaderItem (24-28)
ui/app/src/components/FormsEngine/components/FormsEngineField.tsx (1)
  • FormsEngineField (92-282)
ui/app/src/components/SearchBar/SearchBar.tsx (1)
  • SearchBar (61-209)
ui/app/src/components/FormsEngine/controls/Checkbox.tsx (2)
  • Checkbox (26-35)
  • CheckboxProps (22-24)
ui/app/src/models/LookupTable.ts (1)
  • LookupTable (17-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (7)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (7)

52-65: LGTM! Robust JSON parsing implemented.

The listDirection parsing now correctly handles malformed JSON and non-array values with try/catch and Array.isArray checks, as suggested in the previous review. This prevents runtime errors from calling .find on objects.


129-132: LGTM! Safe filtering for placeholder items.

The filtering logic now correctly guards against undefined values by checking typeof option.value === 'string' before calling toLowerCase(), as suggested in the previous review. This prevents runtime errors when placeholder items are present.


167-178: LGTM! Readonly state correctly propagated to SearchBar.

The readonly prop is correctly passed as disabled to the SearchBar component, which aligns with the component's API and ensures consistent behavior when the control is in readonly mode.


233-244: LGTM! Correct Grid responsive sizing.

The Grid implementation correctly uses MUI's responsive sizing (size={{ sm: 12, md: 6, lg: 4 }}) to display 1, 2, or 3 columns based on viewport width, and properly handles placeholder items with empty fragments to maintain the grid structure.


273-302: LGTM! VirtualRow correctly implements multi-column virtualization.

The VirtualRow component correctly implements multi-column virtualization by calculating the adjusted index based on numColumns and slicing the appropriate range of options. It maintains consistency with the non-virtualized rendering path by using the same Grid structure and properly handles readonly state and placeholder items.


221-231: LGTM! Virtualized rendering correctly configured.

The FixedSizeList is correctly configured with all necessary props including the new numColumns and readonly in the itemData, which are properly consumed by the VirtualRow component.


32-34: Dependency versions are up-to-date and secure. nanoid@^5.1.5 (≥5.0.9), @mui/material@^7.3.2, and react-intl@^7.1.11 have no active vulnerabilities or advisories.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (1)

124-130: Select All logic doesn't handle filtered options correctly.

The checkAll function operates on finalOptions (filtered items), but the indeterminate and checked state calculations (lines 154-155) compare value.length directly against finalOptions.length. This fails when value contains items that have been filtered out of finalOptions.

Example: User selects items A, B, C, then searches to show only items D, E. The checkbox will incorrectly show as unchecked because value.length (3) !== finalOptions.length (2), even though items might be selected.

The correct approach is to count how many items in value are actually present in the current finalOptions (visible items) and use that count for the indeterminate/checked calculations.

This is the same issue flagged in the past review. Please refer to the previous suggestion for the fix.

🧹 Nitpick comments (1)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (1)

79-92: Use numRows consistently in reordering logic.

Line 88 recalculates Math.ceil(finalOptions.length / 2), which is identical to the numRows variable computed on line 83. For clarity and maintainability, use numRows consistently.

Apply this diff:

 		for (let i = 0; i < numRows; i++) {
 			sortedOptions.push(finalOptions[i]);
 			if (finalOptions[i + numRows]) {
-				sortedOptions.push(finalOptions[i + Math.ceil(finalOptions.length / 2)]);
+				sortedOptions.push(finalOptions[i + numRows]);
 			}
 		}
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d512328 and 70f59cf.

📒 Files selected for processing (1)
  • ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (7)
ui/app/src/components/FormsEngine/types.ts (1)
  • ControlProps (23-30)
ui/app/src/hooks/useWindowWidth.ts (1)
  • useWindowWidth (19-32)
ui/app/src/components/FormsEngine/dataSourceHooks/useKVPLoader.ts (2)
  • useKVPLoader (30-77)
  • KVPLoaderItem (24-28)
ui/app/src/components/FormsEngine/components/FormsEngineField.tsx (1)
  • FormsEngineField (92-282)
ui/app/src/components/SearchBar/SearchBar.tsx (1)
  • SearchBar (61-209)
ui/app/src/components/FormsEngine/controls/Checkbox.tsx (2)
  • Checkbox (26-35)
  • CheckboxProps (22-24)
ui/app/src/models/LookupTable.ts (1)
  • LookupTable (17-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Travis CI - Pull Request
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (5)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (5)

17-46: LGTM!

The imports are appropriate, and the responsive numColumns logic using a 900px threshold aligns with MUI's default md breakpoint. The readonly state correctly combines form-level and field-level properties.


102-122: LGTM!

The checkedValuesLookup memo and handleChange logic are correct. Line 118 appropriately looks up values from the original options array rather than finalOptions, ensuring the full value is captured even when filtering is active.


189-208: LGTM!

Both rendering paths (virtualized and non-virtualized) correctly use the responsive Grid layout and pass the readonly state through. The Grid sizing with size={{ sm: 12, md: 6 }} aligns with the numColumns calculation, ensuring consistent behavior.


214-233: LGTM!

The buildOption helper is clean and correctly applies the readonly state to the Checkbox's disabled prop.


235-259: LGTM!

The VirtualRow component correctly implements multi-column virtualization by slicing the options array based on numColumns and rendering them in a Grid layout consistent with the non-virtualized path.

@jvega190 jvega190 marked this pull request as ready for review October 22, 2025 18:13
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (2)

135-141: Fix Select All to act only on visible options.

The current checkAll implementation clears all selections when all visible items are selected, which incorrectly removes selections for filtered-out items. It should only toggle visible options while preserving other selections.

Apply this diff to fix the logic:

+  const visibleKeySet = useMemo(() => new Set(finalOptions.map((o) => o.key)), [finalOptions]);
+  const selectedInViewCount = useMemo(
+    () => value.reduce((acc, item) => acc + (visibleKeySet.has(item.key) ? 1 : 0), 0),
+    [value, visibleKeySet]
+  );
+  const allInViewSelected = finalOptions.length > 0 && selectedInViewCount === finalOptions.length;
+  const someInViewSelected = selectedInViewCount > 0 && !allInViewSelected;
+
   const checkAll = () => {
-    if (value.length === finalOptions.length) {
-      setValue([]);
+    if (allInViewSelected) {
+      // Deselect only visible items
+      setValue(value.filter((item) => !visibleKeySet.has(item.key)));
     } else {
-      setValue(finalOptions.map((option) => ({ key: option.key, value_smv: option.value })));
+      // Select all visible items (merge with existing)
+      const existing = new Set(value.map((v) => v.key));
+      const additions = finalOptions
+        .filter((o) => !existing.has(o.key))
+        .map((o) => ({ key: o.key, value_smv: o.value }));
+      setValue(value.concat(additions));
     }
   };

165-166: Update Select All checkbox state to reflect visible items.

The indeterminate and checked states incorrectly compare total selections against visible options. After applying the fix for the checkAll function, update these props to use the computed allInViewSelected and someInViewSelected values.

Apply this diff:

           <Checkbox
             disabled={readonly}
             color="info"
-            indeterminate={value.length > 0 && value.length < finalOptions.length}
-            checked={value.length === finalOptions.length}
+            indeterminate={someInViewSelected}
+            checked={allInViewSelected}
             onChange={checkAll}
           />
🧹 Nitpick comments (1)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (1)

17-17: Remove unused import.

useId is imported but never used in the component.

Apply this diff:

-import React, { ChangeEvent, useId, useMemo, useState } from 'react';
+import React, { ChangeEvent, useMemo, useState } from 'react';
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2f162c9 and 7617e5e.

📒 Files selected for processing (1)
  • ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (6)
ui/app/src/components/FormsEngine/types.ts (1)
  • ControlProps (23-30)
ui/app/src/hooks/useWindowWidth.ts (1)
  • useWindowWidth (19-32)
ui/app/src/components/FormsEngine/lib/formUtils.tsx (2)
  • isFieldReadOnly (764-766)
  • getPropertyValue (749-755)
ui/app/src/components/FormsEngine/dataSourceHooks/useKVPLoader.ts (2)
  • useKVPLoader (30-77)
  • KVPLoaderItem (24-28)
ui/app/src/components/SearchBar/SearchBar.tsx (1)
  • SearchBar (61-209)
ui/app/src/models/LookupTable.ts (1)
  • LookupTable (17-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (7)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (7)

52-65: LGTM! Safe JSON parsing implemented.

The listDirection parsing now properly handles errors and validates the parsed result is an array before using it. This addresses the previous review concern.


78-102: LGTM! Reordering and filtering logic are correct.

The two-column vertical reordering correctly interleaves the first and second halves of the options array for top-down display. The filtering also safely handles undefined values with proper type checking. Both previously flagged concerns have been addressed.


110-121: LGTM! Loading skeleton provides good UX.

The loading state uses consistent Grid layout and clearly indicates the component is loading with skeleton placeholders.


50-50: LGTM! Readonly propagation is correctly implemented.

The readonly state is properly computed from both field-level and form-level settings, then consistently applied to all interactive elements (SearchBar, Select All checkbox, and individual checkboxes).

Also applies to: 147-147, 163-163, 209-209, 215-215, 235-235


212-218: LGTM! Grid-based layout is well-structured.

The non-virtualized rendering uses Material-UI Grid for responsive two-column layout, correctly delegating option rendering to the buildOption helper.


225-244: LGTM! Clean helper function for option rendering.

The buildOption helper properly encapsulates checkbox rendering with readonly support and checked state lookup.


246-270: LGTM! VirtualRow correctly implements multi-column virtualization.

The virtualized row component properly calculates item indices for multi-column layout and correctly passes all necessary props (including readonly) to the option renderer.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (1)

135-141: Critical: Select All logic still doesn't handle filtering correctly.

This issue was flagged in a previous review but remains unaddressed. When keyword filtering is active, finalOptions is a filtered subset, but the current logic:

  1. Compares value.length === finalOptions.length (total selections vs. visible items)
  2. Clears all selections when "unchecking all" (line 137)—should only clear visible selections
  3. Replaces value with only visible items (lines 139-140)—loses selections outside the current filter

Expected behavior:

  • "Select All" should select all visible items and merge with existing selections
  • "Unselect All" should unselect only visible items, preserving others

Apply the fix from the previous review:

+ const visibleKeySet = useMemo(() => new Set(finalOptions.map((o) => o.key)), [finalOptions]);
+ const selectedInViewCount = useMemo(
+   () => value.reduce((acc, item) => acc + (visibleKeySet.has(item.key) ? 1 : 0), 0),
+   [value, visibleKeySet]
+ );
+ const allInViewSelected = finalOptions.length > 0 && selectedInViewCount === finalOptions.length;
+ const someInViewSelected = selectedInViewCount > 0 && !allInViewSelected;
+
  const checkAll = () => {
-   if (value.length === finalOptions.length) {
-     setValue([]);
+   if (allInViewSelected) {
+     // Unselect only visible ones
+     setValue(value.filter((item) => !visibleKeySet.has(item.key)));
    } else {
-     setValue(finalOptions.map((option) => ({ key: option.key, value_smv: option.value })));
+     // Select all visible (merge with existing)
+     const existing = new Set(value.map((v) => v.key));
+     const additions = finalOptions
+       .filter((o) => !existing.has(o.key) && o.value)
+       .map((o) => ({ key: o.key, value_smv: o.value }));
+     setValue(value.concat(additions));
    }
  };
🧹 Nitpick comments (2)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (2)

109-121: Optional: Skeleton count could respect numColumns.

The loading skeleton hardcodes 2 items (line 113), but numColumns can be 1 on narrow screens. Consider Array.from({ length: numColumns }) for consistency. This is purely cosmetic and doesn't affect functionality.


185-187: Optional: Remove obsolete flexBasis styling.

Line 187's flexBasis: calc(32.5% - ...) suggests a 3-column layout, but the component now uses Grid-based 2-column layout (lines 212-218, 262-268). The flexBasis is overridden by Grid and should be removed to avoid confusion.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7617e5e and 5eeb1a3.

📒 Files selected for processing (1)
  • ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (6)
ui/app/src/components/FormsEngine/types.ts (1)
  • ControlProps (23-30)
ui/app/src/hooks/useWindowWidth.ts (1)
  • useWindowWidth (19-32)
ui/app/src/components/FormsEngine/lib/formUtils.tsx (2)
  • isFieldReadOnly (764-766)
  • getPropertyValue (749-755)
ui/app/src/components/FormsEngine/dataSourceHooks/useKVPLoader.ts (2)
  • useKVPLoader (30-77)
  • KVPLoaderItem (24-28)
ui/app/src/components/SearchBar/SearchBar.tsx (1)
  • SearchBar (61-209)
ui/app/src/models/LookupTable.ts (1)
  • LookupTable (17-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (4)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (4)

49-66: LGTM—Field properties are now safely extracted.

All past concerns have been addressed:

  • selectAll property key is correct (line 51)
  • listDirection parsing is properly guarded with try-catch and Array.isArray check (lines 55-60)

77-102: LGTM—Options processing is correct.

The 2-column vertical reordering logic (lines 80-92) correctly interleaves items top-down, and the keyword filtering (lines 98-101) safely handles missing or non-string values with type checking and optional chaining. Past concerns have been addressed.


225-244: LGTM—Clean helper function.

The buildOption helper is well-structured and correctly propagates the readonly state to disable checkboxes. The fallback ?? false (line 237) safely handles missing keys.


246-270: LGTM—VirtualRow correctly implements multi-column virtualization.

The adjustedIndex calculation (line 260) and slicing (line 263) correctly distribute numColumns items per row. The readonly prop is properly threaded through to buildOption.

Note: Grid sizing {{ sm: 12, md: 6 }} (line 264) relies on the implicit assumption that MUI's default md breakpoint (900px) matches the manual numColumns threshold (line 47). This works but is fragile if theme breakpoints change. Consider dynamically computing Grid size from numColumns for robustness, though this is a minor concern.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (1)

23-23: Import LookupTable as a type, not a default value.

LookupTable.ts exports a named type. Use a type-only import to avoid runtime bloat and TS error.

-import LookupTable from '../../../models/LookupTable';
+import type { LookupTable } from '../../../models/LookupTable';
♻️ Duplicate comments (4)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (4)

51-51: Correct property key usage for Select All — looks good.

Now using getPropertyValue(..., 'selectAll'). Matches schema.


52-66: Robust JSON parsing for listDirection — approved.

Safe parse with try/catch and array guard is correct.


98-101: Filter guard on placeholder items — approved.

Protects against undefined labels and missing keys.


109-116: Select All should only toggle visible options; fix empty-set and indeterminate logic.

  • checkAll replaces the entire selection and drops hidden selections.
  • allInViewSelected is true for empty filtered lists (vacuous every).
  • indeterminate should be some && !all.
+  const visibleKeySet = useMemo(() => new Set(finalOptions?.map((o) => o.key) ?? []), [finalOptions]);
+  const selectedInViewCount = useMemo(
+    () => value.reduce((acc, item) => acc + (visibleKeySet.has(item.key) ? 1 : 0), 0),
+    [value, visibleKeySet]
+  );
-  const allInViewSelected = useMemo(() => {
-    if (!finalOptions) return false;
-    return finalOptions.every((option) => checkedValuesLookup[option.key]);
-  }, [finalOptions, checkedValuesLookup]);
-  const someInViewSelected = useMemo(() => {
-    if (!finalOptions) return false;
-    return finalOptions.some((option) => checkedValuesLookup[option.key]);
-  }, [finalOptions, checkedValuesLookup]);
+  const allInViewSelected = finalOptions.length > 0 && selectedInViewCount === finalOptions.length;
+  const someInViewSelected = selectedInViewCount > 0 && !allInViewSelected;
-  const checkAll = () => {
-    if (value.length === finalOptions.length) {
-      setValue([]);
-    } else {
-      setValue(finalOptions.map((option) => ({ key: option.key, value_smv: option.value })));
-    }
-  };
+  const checkAll = () => {
+    if (allInViewSelected) {
+      // Unselect only visible ones
+      setValue(value.filter((item) => !visibleKeySet.has(item.key)));
+    } else {
+      // Select all visible (merge with existing)
+      const existing = new Set(value.map((v) => v.key));
+      const additions = finalOptions
+        .filter((o) => !existing.has(o.key))
+        .map((o) => ({ key: o.key, value_smv: o.value }));
+      setValue(value.concat(additions));
+    }
+  };
-              indeterminate={someInViewSelected}
+              indeterminate={someInViewSelected && !allInViewSelected}
               checked={allInViewSelected}

Also applies to: 144-150, 171-177

🧹 Nitpick comments (2)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (2)

103-108: Type the reduce initializer for checkedValuesLookup.

Initialize with a typed object to avoid implicit any and prototype pitfalls.

-  }, {});
+  }, Object.create(null) as Record<string, boolean>);

78-93: Tidy 2-column vertical reorder logic.

Use numRows consistently; current mix of numRows and Math.ceil(.../2) is redundant.

-      const sortedOptions = [];
+      const sortedOptions: typeof finalOptions = [];
       const numRows = Math.ceil(finalOptions.length / 2);
       for (let i = 0; i < numRows; i++) {
         sortedOptions.push(finalOptions[i]);
-        if (finalOptions[i + numRows]) {
-          sortedOptions.push(finalOptions[i + Math.ceil(finalOptions.length / 2)]);
-        }
+        const right = finalOptions[i + numRows];
+        if (right) sortedOptions.push(right);
       }
       finalOptions = sortedOptions;
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5eeb1a3 and 1fcf6a9.

📒 Files selected for processing (1)
  • ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (6)
ui/app/src/components/FormsEngine/types.ts (1)
  • ControlProps (23-30)
ui/app/src/hooks/useWindowWidth.ts (1)
  • useWindowWidth (19-32)
ui/app/src/components/FormsEngine/lib/formUtils.tsx (2)
  • isFieldReadOnly (764-766)
  • getPropertyValue (749-755)
ui/app/src/components/FormsEngine/dataSourceHooks/useKVPLoader.ts (2)
  • useKVPLoader (30-77)
  • KVPLoaderItem (24-28)
ui/app/src/components/SearchBar/SearchBar.tsx (1)
  • SearchBar (61-209)
ui/app/src/models/LookupTable.ts (1)
  • LookupTable (17-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (1)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (1)

31-31: Use MUI Grid2 for the size prop.

Grid doesn't support size={{ sm, md }}; that's Grid v2 API. Import Grid2 and use it.

-import Grid from '@mui/material/Grid';
+import Grid2 from '@mui/material/Grid2';
-        <Grid container spacing={2} sx={{ width: '100%' }}>
+        <Grid2 container spacing={2} sx={{ width: '100%' }}>
-          {Array.from({ length: 2 }).map((_, i) => (
-            <Grid size={{ sm: 12, md: 6 }} key={i}>
+          {Array.from({ length: 2 }).map((_, i) => (
+            <Grid2 size={{ sm: 12, md: 6 }} key={i}>
               ...
-            </Grid>
+            </Grid2>
           ))}
-        </Grid>
+        </Grid2>
-        <Grid container spacing={2} sx={{ width: '100%' }}>
-          {finalOptions?.map((option) => (
-            <Grid size={{ sm: 12, md: 6 }} key={option.key}>
+        <Grid2 container spacing={2} sx={{ width: '100%' }}>
+          {finalOptions?.map((option) => (
+            <Grid2 size={{ sm: 12, md: 6 }} key={option.key}>
               ...
-            </Grid>
+            </Grid2>
           ))}
-        </Grid>
+        </Grid2>
-  return (
-    <Grid container className="checkbox-group-virtual-row" spacing={2} sx={{ width: '100%' }} style={style}>
-      {options.slice(adjustedIndex, adjustedIndex + numColumns).map((option) => (
-        <Grid size={{ sm: 12, md: 6 }} key={option.key}>
+  return (
+    <Grid2 container className="checkbox-group-virtual-row" spacing={2} sx={{ width: '100%' }} style={style}>
+      {options.slice(adjustedIndex, adjustedIndex + numColumns).map((option) => (
+        <Grid2 size={{ sm: 12, md: 6 }} key={option.key}>
           ...
-        </Grid>
+        </Grid2>
       ))}
-    </Grid>
+    </Grid2>

Also applies to: 121-126, 221-227, 271-277

⛔ Skipped due to learnings
Learnt from: jvega190
PR: craftercms/studio-ui#4580
File: ui/app/src/components/SitesGrid/SitesGrid.tsx:58-58
Timestamp: 2025-07-18T20:43:52.177Z
Learning: In MUI 7, the `size` prop is not required for Grid components. When migrating from MUI 5 to MUI 7, removing the `item` prop from Grid components without adding a `size` prop is valid and acceptable. The `size` prop is only needed when you want to explicitly control the width of the grid item.
Learnt from: jvega190
PR: craftercms/studio-ui#4580
File: ui/app/src/components/SitesGrid/SitesGrid.tsx:58-58
Timestamp: 2025-07-18T20:43:52.177Z
Learning: In MUI v7, the `size` prop on Grid components is optional, not required. Grid items can exist without explicit sizing and will use their natural dimensions. When migrating from MUI v5 to v7, simply removing the `item` prop without adding a `size` prop is a valid approach when you want the Grid items to size naturally based on their content.

@jvega190
Copy link
Member Author

Outside diff range comments addressed

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (1)

110-117: Select All: scope to visible options, preserve existing selections, and fix a11y states

  • checkAll currently clears all or overwrites selections outside the filter. Toggle should only affect visible options.
  • allInViewSelected becomes true for an empty filtered list; indeterminate should exclude the all‑selected case.

Apply this consolidated patch:

-  const allInViewSelected = useMemo(() => {
-    if (!finalOptions) return false;
-    return finalOptions.every((option) => checkedValuesLookup[option.key]);
-  }, [finalOptions, checkedValuesLookup]);
-  const someInViewSelected = useMemo(() => {
-    if (!finalOptions) return false;
-    return finalOptions.some((option) => checkedValuesLookup[option.key]);
-  }, [finalOptions, checkedValuesLookup]);
+  const visibleKeySet = useMemo(() => new Set(finalOptions?.map((o) => o.key) ?? []), [finalOptions]);
+  const selectedInViewCount = useMemo(
+    () => value.reduce((acc, item) => acc + (visibleKeySet.has(item.key) ? 1 : 0), 0),
+    [value, visibleKeySet]
+  );
+  const allInViewSelected = Boolean(finalOptions?.length) && selectedInViewCount === (finalOptions?.length ?? 0);
+  const someInViewSelected = selectedInViewCount > 0 && !allInViewSelected;
@@
-  const checkAll = () => {
-    if (value.length === finalOptions.length) {
-      setValue([]);
-    } else {
-      setValue(finalOptions.map((option) => ({ key: option.key, value_smv: option.value })));
-    }
-  };
+  const checkAll = () => {
+    if (!finalOptions?.length) return; // nothing to toggle
+    if (allInViewSelected) {
+      // Unselect only visible ones
+      setValue(value.filter((item) => !visibleKeySet.has(item.key)));
+    } else {
+      // Select all visible, preserve existing selections outside the current filter
+      const existing = new Set(value.map((v) => v.key));
+      const additions = finalOptions
+        .filter((o) => !existing.has(o.key))
+        .map((o) => ({ key: o.key, value_smv: o.value }));
+      setValue(value.concat(additions));
+    }
+  };
@@
-              indeterminate={someInViewSelected}
-              checked={allInViewSelected}
+              indeterminate={someInViewSelected}
+              checked={allInViewSelected}

Also applies to: 145-151, 172-178

🧹 Nitpick comments (4)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (4)

88-92: Use numRows consistently (clarity + micro-optimization)

Replace repeated Math.ceil(finalOptions.length / 2) with numRows to avoid recomputation and improve readability.

-        if (finalOptions[i + numRows]) {
-          sortedOptions.push(finalOptions[i + Math.ceil(finalOptions.length / 2)]);
-        }
+        if (finalOptions[i + numRows]) {
+          sortedOptions.push(finalOptions[i + numRows]);
+        }

98-103: Case‑insensitive search on keys for consistency

Label comparison is case‑insensitive; key comparison is not. Normalize key too.

-    return finalOptions?.filter((option) => {
-      const label = typeof option.value === 'string' ? option.value.toLowerCase() : '';
-      return label.includes(lowerKeyword) || option.key?.includes(lowerKeyword);
-    });
+    return finalOptions?.filter((option) => {
+      const label = typeof option.value === 'string' ? option.value.toLowerCase() : '';
+      const keyLower = typeof option.key === 'string' ? option.key.toLowerCase() : '';
+      return label.includes(lowerKeyword) || keyLower.includes(lowerKeyword);
+    });

104-109: Type the reducer accumulator for checkedValuesLookup

Avoid implicit any on the accumulator; annotate as LookupTable.

-  return value.reduce((acc, item) => {
+  return value.reduce((acc, item) => {
     acc[item.key] = true;
     return acc;
-  }, {});
+  }, {} as LookupTable<boolean>);

241-243: Redundant key on inner FormControlLabel

Key belongs on the mapped Grid item; the inner key is redundant. Safe to remove.

-  <FormControlLabel
-    key={option.key}
+  <FormControlLabel
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 52b4024 and 79a0e7c.

📒 Files selected for processing (1)
  • ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (6)
ui/app/src/components/FormsEngine/types.ts (1)
  • ControlProps (23-30)
ui/app/src/hooks/useWindowWidth.ts (1)
  • useWindowWidth (19-32)
ui/app/src/components/FormsEngine/lib/formUtils.tsx (2)
  • isFieldReadOnly (764-766)
  • getPropertyValue (749-755)
ui/app/src/components/FormsEngine/dataSourceHooks/useKVPLoader.ts (2)
  • useKVPLoader (30-77)
  • KVPLoaderItem (24-28)
ui/app/src/components/SearchBar/SearchBar.tsx (1)
  • SearchBar (61-209)
ui/app/src/models/LookupTable.ts (1)
  • LookupTable (17-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (2)
ui/app/src/components/FormsEngine/controls/CheckboxGroup.tsx (2)

52-65: Good fix: robust listDirection parsing

Safe JSON parsing with try/catch and array check looks solid. Dependency is scoped to field.properties.


124-126: Ignore “invalid size prop” warning
This file imports MUI v7’s updated Grid (Grid v2), which uses size (e.g. size={{ sm: 12, md: 6 }}) for breakpoints and no longer accepts item/xs/md props (legacy GridLegacy API). No changes needed. (mui.com)

Likely an incorrect or invalid review comment.

…o feature/7418-checkbox-group

# Conflicts:
#	ui/app/src/components/FormsEngine/lib/validators.ts
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.

1 participant